diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 5cb17a44d1..940b095fe2 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -114,6 +114,7 @@ redis: # Available methods: # aid ... Short, Millisecond accuracy +# aidx ... Millisecond accuracy # meid ... Similar to ObjectID, Millisecond accuracy # ulid ... Millisecond accuracy # objectid ... This is left for backward compatibility @@ -121,7 +122,7 @@ redis: # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # ID SETTINGS AFTER THAT! -id: 'aid' +id: 'aidx' # ┌─────────────────────┐ #───┘ Other configuration └───────────────────────────────────── @@ -165,8 +166,8 @@ proxyBypassHosts: # Media Proxy #mediaProxy: https://example.com/proxy -# Proxy remote files (default: false) -#proxyRemoteFiles: true +# Proxy remote files (default: true) +proxyRemoteFiles: true # Sign to ActivityPub GET request (default: true) signToActivityPubGet: true diff --git a/.config/example.yml b/.config/example.yml index c179395966..086a6ca8fc 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -30,6 +30,10 @@ url: https://example.tld/ # The port that your Misskey server should listen on. port: 3000 +# You can also use UNIX domain socket. +# socket: /path/to/misskey.sock +# chmodSocket: '777' + # ┌──────────────────────────┐ #───┘ PostgreSQL configuration └──────────────────────────────── @@ -78,6 +82,8 @@ redis: #pass: example-pass #prefix: example-prefix #db: 1 + # You can specify more ioredis options... + #username: example-username #redisForPubsub: # host: localhost @@ -86,6 +92,8 @@ redis: # #pass: example-pass # #prefix: example-prefix # #db: 1 +# # You can specify more ioredis options... +# #username: example-username #redisForJobQueue: # host: localhost @@ -94,6 +102,8 @@ redis: # #pass: example-pass # #prefix: example-prefix # #db: 1 +# # You can specify more ioredis options... +# #username: example-username # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── @@ -104,6 +114,7 @@ redis: # apiKey: '' # ssl: true # index: '' +# scope: local # ┌───────────────┐ #───┘ ID generation └─────────────────────────────────────────── @@ -114,6 +125,7 @@ redis: # Available methods: # aid ... Short, Millisecond accuracy +# aidx ... Millisecond accuracy # meid ... Similar to ObjectID, Millisecond accuracy # ulid ... Millisecond accuracy # objectid ... This is left for backward compatibility @@ -121,7 +133,7 @@ redis: # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # ID SETTINGS AFTER THAT! -id: 'aid' +id: 'aidx' # ┌─────────────────────┐ #───┘ Other configuration └───────────────────────────────────── @@ -148,6 +160,9 @@ id: 'aid' #deliverJobMaxAttempts: 12 #inboxJobMaxAttempts: 8 +# Local address used for outgoing requests +#outgoingAddress: 127.0.0.1 + # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 @@ -172,9 +187,9 @@ proxyBypassHosts: # * 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 (default: true) # Proxy remote files by this instance or mediaProxy to prevent remote files from running in remote domains. -#proxyRemoteFiles: true +proxyRemoteFiles: true # Movie Thumbnail Generation URL # There is no reference implementation. diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a47804ab07..861b0008a0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ "features": { "ghcr.io/devcontainers-contrib/features/pnpm:2": {}, "ghcr.io/devcontainers/features/node:1": { - "version": "18.16.0" + "version": "20.5.1" } }, "forwardPorts": [3000], diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml index 824a046dc0..5dcd41599a 100644 --- a/.devcontainer/devcontainer.yml +++ b/.devcontainer/devcontainer.yml @@ -114,6 +114,7 @@ redis: # Available methods: # aid ... Short, Millisecond accuracy +# aidx ... Millisecond accuracy # meid ... Similar to ObjectID, Millisecond accuracy # ulid ... Millisecond accuracy # objectid ... This is left for backward compatibility @@ -121,7 +122,7 @@ redis: # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # ID SETTINGS AFTER THAT! -id: 'aid' +id: 'aidx' # ┌─────────────────────┐ #───┘ Other configuration └───────────────────────────────────── @@ -165,8 +166,8 @@ proxyBypassHosts: # Media Proxy #mediaProxy: https://example.com/proxy -# Proxy remote files (default: false) -#proxyRemoteFiles: true +# Proxy remote files (default: true) +proxyRemoteFiles: true # Sign to ActivityPub GET request (default: true) signToActivityPubGet: true diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 8f8c5a13ab..2809cd2ca4 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: app: - build: + build: context: . dockerfile: Dockerfile diff --git a/.editorconfig b/.editorconfig index a6f988f8d7..def7baa1a8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,6 +6,10 @@ indent_size = 2 charset = utf-8 insert_final_newline = true end_of_line = lf +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false [*.{yml,yaml}] indent_style = space diff --git a/.github/ISSUE_TEMPLATE/01_bug-report.md b/.github/ISSUE_TEMPLATE/01_bug-report.md index f6fd593c85..b889d96eb3 100644 --- a/.github/ISSUE_TEMPLATE/01_bug-report.md +++ b/.github/ISSUE_TEMPLATE/01_bug-report.md @@ -39,8 +39,22 @@ Please include errors from the developer console and/or server log files if you -Misskey version: -PostgreSQL version: -Redis version: -Your OS: -Your browser: +### 💻 Frontend +* Model and OS of the device(s): + +* Browser: + +* Server URL: + +* Misskey: + 13.x.x + +### 🛰 Backend (for server admin) + + +* Installation Method or Hosting Service: +* Misskey: 13.x.x +* Node: 20.x.x +* PostgreSQL: 15.x.x +* Redis: 7.x.x +* OS and Architecture: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 730647b086..e8b65dc3b9 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,4 @@ contact_links: - - name: 👪 Misskey Forum - url: https://forum.misskey.io/ - about: Ask questions and share knowledge - name: 💬 Misskey official Discord url: https://discord.gg/Wp8gVStHW3 about: Chat freely about Misskey diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e878e5836a..5955f6b5d9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,24 +9,24 @@ updates: directory: "/" schedule: interval: daily - open-pull-requests-limit: 0 + open-pull-requests-limit: 100 + +# Add only the root, not each workspace item +# https://github.com/dependabot/dependabot-core/issues/4993#issuecomment-1289133027 - package-ecosystem: npm directory: "/" schedule: interval: daily + # PNPM has an issue with dependabot. See: + # https://github.com/dependabot/dependabot-core/issues/7258 + # https://github.com/pnpm/pnpm/issues/6530 + # TODO: Restore this when the issue is solved open-pull-requests-limit: 0 -- package-ecosystem: npm - directory: "/packages/backend" - schedule: - interval: daily - open-pull-requests-limit: 0 -- package-ecosystem: npm - directory: "/packages/frontend" - schedule: - interval: daily - open-pull-requests-limit: 0 -- package-ecosystem: npm - directory: "/packages/sw" - schedule: - interval: daily - open-pull-requests-limit: 0 + groups: + swc: + patterns: + - "@swc/*" + storybook: + patterns: + - "storybook*" + - "@storybook/*" diff --git a/.github/misskey/test.yml b/.github/misskey/test.yml index f43f74be14..7a4aa4ae6c 100644 --- a/.github/misskey/test.yml +++ b/.github/misskey/test.yml @@ -12,4 +12,4 @@ db: redis: host: 127.0.0.1 port: 56312 -id: aid +id: aidx diff --git a/.github/reviewer-lottery.yml b/.github/reviewer-lottery.yml deleted file mode 100644 index c88e1342de..0000000000 --- a/.github/reviewer-lottery.yml +++ /dev/null @@ -1,9 +0,0 @@ -groups: - - name: devs - reviewers: 2 - internal_reviewers: 1 - usernames: - - syuilo - - acid-chicken - - EbiseLutica - - tamaina diff --git a/.github/workflows/api-misskey-js.yml b/.github/workflows/api-misskey-js.yml index ed004c78dc..4cf523a6b9 100644 --- a/.github/workflows/api-misskey-js.yml +++ b/.github/workflows/api-misskey-js.yml @@ -9,12 +9,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v4.0.0 - run: corepack enable - name: Setup Node.js - uses: actions/setup-node@v3.6.0 + uses: actions/setup-node@v3.8.1 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/check_copyright_year.yml b/.github/workflows/check_copyright_year.yml index 8daea44a83..313265f671 100644 --- a/.github/workflows/check_copyright_year.yml +++ b/.github/workflows/check_copyright_year.yml @@ -10,7 +10,7 @@ jobs: check_copyright_year: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.2.0 + - uses: actions/checkout@v4.0.0 - run: | if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then echo "Please change copyright year!" diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml index 09a2c33e0c..05bb7f77f9 100644 --- a/.github/workflows/docker-develop.yml +++ b/.github/workflows/docker-develop.yml @@ -13,24 +13,24 @@ jobs: if: github.repository == 'misskey-dev/misskey' steps: - name: Check out the repo - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v4.0.0 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2.3.0 + uses: docker/setup-buildx-action@v3.0.0 with: platforms: linux/amd64,linux/arm64 - name: Docker meta id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: misskey/misskey - name: Log in to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and Push to Docker Hub - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: builder: ${{ steps.buildx.outputs.name }} context: . diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a465d92eaf..32a98a416d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -12,15 +12,15 @@ jobs: steps: - name: Check out the repo - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v4.0.0 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2.3.0 + uses: docker/setup-buildx-action@v3.0.0 with: platforms: linux/amd64,linux/arm64 - name: Docker meta id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: misskey/misskey tags: | @@ -31,12 +31,12 @@ jobs: type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - name: Log in to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and Push to Docker Hub - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: builder: ${{ steps.buildx.outputs.name }} context: . diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml index 9b79ee54f0..d811944d61 100644 --- a/.github/workflows/dockle.yml +++ b/.github/workflows/dockle.yml @@ -14,7 +14,7 @@ jobs: env: DOCKER_CONTENT_TRUST: 1 steps: - - uses: actions/checkout@v3.2.0 + - uses: actions/checkout@v4.0.0 - run: | curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb" sudo dpkg -i dockle.deb diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0f3702f958..7c10c23e77 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,7 +11,7 @@ jobs: pnpm_install: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v4.0.0 with: fetch-depth: 0 submodules: true @@ -19,7 +19,7 @@ jobs: with: version: 8 run_install: false - - uses: actions/setup-node@v3.6.0 + - uses: actions/setup-node@v3.8.1 with: node-version-file: '.node-version' cache: 'pnpm' @@ -38,7 +38,7 @@ jobs: - sw - misskey-js steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v4.0.0 with: fetch-depth: 0 submodules: true @@ -46,7 +46,7 @@ jobs: with: version: 7 run_install: false - - uses: actions/setup-node@v3.6.0 + - uses: actions/setup-node@v3.8.1 with: node-version-file: '.node-version' cache: 'pnpm' @@ -64,7 +64,7 @@ jobs: - backend - misskey-js steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v4.0.0 with: fetch-depth: 0 submodules: true @@ -72,7 +72,7 @@ jobs: with: version: 7 run_install: false - - uses: actions/setup-node@v3.6.0 + - uses: actions/setup-node@v3.8.1 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/ok-to-test.yml b/.github/workflows/ok-to-test.yml index 87af3a6ba6..c02b980e4d 100644 --- a/.github/workflows/ok-to-test.yml +++ b/.github/workflows/ok-to-test.yml @@ -17,13 +17,13 @@ jobs: # See app.yml for an example app manifest - name: Generate token id: generate_token - uses: tibdex/github-app-token@v1 + uses: tibdex/github-app-token@v2 with: app_id: ${{ secrets.DEPLOYBOT_APP_ID }} private_key: ${{ secrets.DEPLOYBOT_PRIVATE_KEY }} - name: Slash Command Dispatch - uses: peter-evans/slash-command-dispatch@v1 + uses: peter-evans/slash-command-dispatch@v3 env: TOKEN: ${{ steps.generate_token.outputs.token }} with: diff --git a/.github/workflows/pr-preview-deploy.yml b/.github/workflows/pr-preview-deploy.yml index 9b786d34aa..702d8917e3 100644 --- a/.github/workflows/pr-preview-deploy.yml +++ b/.github/workflows/pr-preview-deploy.yml @@ -53,7 +53,7 @@ jobs: # Check out merge commit - name: Fork based /deploy checkout - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v4.0.0 with: ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' diff --git a/.github/workflows/reviewer_lottery.yml b/.github/workflows/reviewer_lottery.yml deleted file mode 100644 index 33228d7465..0000000000 --- a/.github/workflows/reviewer_lottery.yml +++ /dev/null @@ -1,13 +0,0 @@ -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/.github/workflows/storybook.yml b/.github/workflows/storybook.yml deleted file mode 100644 index 6cb1b34997..0000000000 --- a/.github/workflows/storybook.yml +++ /dev/null @@ -1,112 +0,0 @@ -name: Storybook - -on: - push: - branches: - - master - - develop - pull_request_target: - -jobs: - build: - runs-on: ubuntu-latest - - env: - NODE_OPTIONS: "--max_old_space_size=7168" - - steps: - - uses: actions/checkout@v3.3.0 - if: github.event_name != 'pull_request_target' - with: - fetch-depth: 0 - submodules: true - - uses: actions/checkout@v3.3.0 - if: github.event_name == 'pull_request_target' - with: - fetch-depth: 0 - submodules: true - ref: "refs/pull/${{ github.event.number }}/merge" - - name: Checkout actual HEAD - if: github.event_name == 'pull_request_target' - id: rev - run: | - echo "base=$(git rev-list --parents -n1 HEAD | cut -d" " -f2)" >> $GITHUB_OUTPUT - git checkout $(git rev-list --parents -n1 HEAD | cut -d" " -f3) - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 - run_install: false - - name: Use Node.js 18.x - uses: actions/setup-node@v3.6.0 - with: - node-version-file: '.node-version' - cache: 'pnpm' - - run: corepack enable - - run: pnpm i --frozen-lockfile - - name: Check pnpm-lock.yaml - run: git diff --exit-code pnpm-lock.yaml - - name: Build misskey-js - run: pnpm --filter misskey-js build - - name: Build storybook - run: pnpm --filter frontend build-storybook - - name: Publish to Chromatic - if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/master' - run: pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static - env: - CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} - - name: Publish to Chromatic - if: github.event_name != 'pull_request_target' && github.ref != 'refs/heads/master' - id: chromatic_push - run: | - DIFF="${{ github.event.before }} HEAD" - if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then - DIFF="HEAD" - fi - CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff-tree --no-commit-id --name-only -r $(echo "$DIFF") | xargs))" - if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then - echo "skip=true" >> $GITHUB_OUTPUT - fi - if pnpm --filter frontend chromatic -d storybook-static $(echo "$CHROMATIC_PARAMETER"); then - echo "success=true" >> $GITHUB_OUTPUT - else - echo "success=false" >> $GITHUB_OUTPUT - fi - env: - CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} - - name: Publish to Chromatic - if: github.event_name == 'pull_request_target' - id: chromatic_pull_request - run: | - DIFF="${{ steps.rev.outputs.base }} HEAD" - if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then - DIFF="HEAD" - fi - CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff-tree --no-commit-id --name-only -r $(echo "$DIFF") | xargs))" - if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then - echo "skip=true" >> $GITHUB_OUTPUT - fi - BRANCH="${{ github.event.pull_request.head.user.login }}:${{ github.event.pull_request.head.ref }}" - if [ "$BRANCH" = "misskey-dev:${{ github.event.pull_request.head.ref }}" ]; then - BRANCH="${{ github.event.pull_request.head.ref }}" - fi - pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name $BRANCH $(echo "$CHROMATIC_PARAMETER") - env: - CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} - - name: Notify that Chromatic detects changes - uses: actions/github-script@v6.4.0 - if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false' - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.repos.createCommitComment({ - owner: context.repo.owner, - repo: context.repo.repo, - commit_sha: context.sha, - body: 'Chromatic detects changes. Please [review the changes on Chromatic](https://www.chromatic.com/builds?appId=6428f7d7b962f0b79f97d6e4).' - }) - - name: Upload Artifacts - uses: actions/upload-artifact@v3 - with: - name: storybook - path: packages/frontend/storybook-static diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index d7be15bd4f..19496c8959 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [18.x] + node-version: [20.5.1] services: postgres: @@ -29,7 +29,7 @@ jobs: - 56312:6379 steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v4.0.0 with: submodules: true - name: Install pnpm @@ -38,7 +38,7 @@ jobs: version: 8 run_install: false - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3.6.0 + uses: actions/setup-node@v3.8.1 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index 4ea4ba4628..0618a0ef0a 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -13,10 +13,10 @@ jobs: strategy: matrix: - node-version: [18.x] + node-version: [20.5.1] steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v4.0.0 with: submodules: true - name: Install pnpm @@ -25,7 +25,7 @@ jobs: version: 8 run_install: false - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3.6.0 + uses: actions/setup-node@v3.8.1 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' @@ -51,7 +51,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [18.x] + node-version: [20.5.1] browser: [chrome] services: @@ -68,7 +68,7 @@ jobs: - 56312:6379 steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v4.0.0 with: submodules: true # https://github.com/cypress-io/cypress-docker-images/issues/150 @@ -83,7 +83,7 @@ jobs: version: 7 run_install: false - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3.6.0 + uses: actions/setup-node@v3.8.1 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' @@ -101,7 +101,7 @@ jobs: - name: Cypress install run: pnpm exec cypress install - name: Cypress run - uses: cypress-io/github-action@v5 + uses: cypress-io/github-action@v6 with: install: false start: pnpm start:test diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml index b15e704c7f..7999c183b1 100644 --- a/.github/workflows/test-misskey-js.yml +++ b/.github/workflows/test-misskey-js.yml @@ -16,17 +16,17 @@ jobs: strategy: matrix: - node-version: [18.x] + node-version: [20.5.1] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - name: Checkout - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v4.0.0 - run: corepack enable - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3.6.0 + uses: actions/setup-node@v3.8.1 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml index 5243a83777..0504f42d16 100644 --- a/.github/workflows/test-production.yml +++ b/.github/workflows/test-production.yml @@ -16,10 +16,10 @@ jobs: strategy: matrix: - node-version: [18.x] + node-version: [20.5.1] steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v4.0.0 with: submodules: true - name: Install pnpm @@ -28,7 +28,7 @@ jobs: version: 8 run_install: false - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3.6.0 + uses: actions/setup-node@v3.8.1 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.gitignore b/.gitignore index 537232d37f..a66e527db0 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,6 @@ temp *.blend3 *.blend4 *.blend5 + +# VSCode addon +.favorites.json diff --git a/.node-version b/.node-version index 6d80269a4f..7cc2069986 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -18.16.0 +20.5.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index a17c9c231b..dc99ee33fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ +## 2023.9.0 (unreleased) + +### General +- Feat: OAuth 2.0のサポート +- Feat: お知らせ機能の強化 + - ユーザー個別のお知らせを作成可能に + - お知らせのバナー表示やダイアログ表示が可能に + - お知らせのアイコンを設定可能に +- Feat: チャンネルをセンシティブ指定できるようになりました + - センシティブチャンネルのNoteのReNoteはデフォルトでHome TLに流れるようになりました + - センシティブチャンネルのノートはユーザープロフィールに表示されません +- Feat: 二要素認証のバックアップコードが生成されるようになりました + - ref. https://github.com/MisskeyIO/misskey/pull/121 +- Feat: 二要素認証でパスキーをサポートするようになりました +- Feat: 指定したユーザーが投稿したときに通知できるようになりました +- Feat: プロフィールでのリンク検証 +- Feat: 通知をテストできるようになりました +- Feat: PWAのアイコンが設定できるようになりました +- Enhance: サーバー名の略称が設定できるようになりました +- Enhance: アンテナの受信ソースに指定したユーザを除外するものを追加 +- Enhance: 二要素認証設定時のセキュリティを強化 + - パスワード入力が必要な操作を行う際、二要素認証が有効であれば確認コードの入力も必要になりました +- Enhance: manifest.jsonをオーバーライド可能に +- Enhance: 依存関係の更新 +- Enhance: ローカリゼーションの更新 + +### Client +- Feat: 任意のユーザーリストをタイムラインページにピン留めできるように + - 設定->クライアント設定->全般 から設定可能です +- Feat: Playで直接投稿フォームを埋め込めるように(`Ui:C:postForm`) +- Feat: クライアントを起動している間、デバイスの画面が自動でオフになるのを防ぐオプションを追加 +- Feat: 新しい実績を追加 +- Enhance: ノート詳細ページでリノート一覧、リアクション一覧タブを追加 + - ノートのメニューからは当該項目は消えました +- Enhance: センシティブなメディアを目立たせる設定を追加 +- Enhance: プロフィールにその人が作ったPlayの一覧出せるように +- Enhance: メニューのスイッチの動作を改善 +- Enhance: 絵文字ピッカーの検索の表示件数を100件に増加 +- Enhance: 投稿フォームのプレビューの表示状態を記憶するように +- Enhance: ユーザーメニューでスイッチでユーザーリストに追加・削除できるように +- Enhance: 自分が押したリアクションのデザインを改善 +- Enhance: ノート検索にローカルのみ検索可能なオプションの追加 +- Enhance: Renote自体を通報できるように +- Enhance: データセーバーモードの強化 +- Enhance: Renoteを管理者権限で削除可能に +- Enhance: `$[rainbow ]`記法が、動きのあるMFMが無効になっていても使用できるようになりました +- Enhance: Playの操作を行うAPI TokenをAPIコンソールから発行できるように +- Enhance: リアクションの表示サイズをより大きくできるように +- Enhance: AiScriptを0.16.0に更新 +- Enhance: AiScriptからMisskeyサーバーAPIを呼び出す際の制限を撤廃 +- Enhance: AiScriptで`LOCALE`として現在の設定言語を取得できるように +- Enhance: Mk:apiが失敗した時にエラー型の値(AiScript 0.16.0で追加)を返すように +- Enhance: ScratchpadでAsync:系関数やボタンのコールバックなどのエラーにもダイアログを出すように(試験的なためPlayなどには未実装) +- Enhance: ノート詳細ページ読み込み時のパフォーマンスが向上しました +- Enhance: タイムラインでリスト/アンテナ選択時のパフォーマンスを改善 +- Enhance: 「Moderation note」、「Add moderation note」をローカライズできるように +- Enhance: プラグインのソースコードを確認・コピーできるように +- Enhance: 細かなデザインの調整 +- Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正 +- Fix: 未読のお知らせの「わかった」をクリック・タップしてもその場で「わかった」が消えない問題を修正 +- Fix: iOSで画面を回転させるとテキストサイズが変わる問題を修正 +- Fix: word mute for sub note is not applied +- Fix: タイムラインを下にスクロールしてノート画面に移動して再び戻ったら以前のスクロール位置を失う問題を修正 +- Fix: Misskeyプラグインをインストールする際のAiScriptバージョンのチェックが0.14.0以降に対応していない問題を修正 +- Fix: 他のサーバーのユーザーへ「メッセージを送信」した時の初期テキストのメンションが間違っている問題を修正 +- Fix: 環境によってはMisskey Webが開けない問題を修正 +- Fix: プラグインの権限リストが見れない問題を修正 + +### Server +- Change: cacheRemoteFilesの初期値はfalseになりました +- Enhance: ファイルアップロード時等にファイル名の拡張子を修正する関数(correctFilename)の挙動を改善 +- Enhance: Webhookのペイロードにサーバーのurlが含まれるようになりました +- Enhance: Webhook設定でsecretを空に出来るように +- Enhance: 使われていないアンテナの自動停止を設定可能に +- Enhance: nodeinfo 2.1対応 +- Enhance: 自分へのメンション一覧を取得する際のパフォーマンスを向上 +- Enhance: Docker環境でjemallocを使用することでメモリ使用量を削減 +- Fix: MK_ONLY_SERVERオプションを指定した際にクラッシュする問題を修正 +- Fix: notes/reactionsのページネーションが機能しない問題を修正 +- Fix: ノート検索 `notes/search` にてhostを指定した際に検索結果に反映されるように +- Fix: 一部のfeatured noteを照会できない問題を修正 +- Fix: muteがapiからのuser list timeline取得で機能しない問題を修正 +- Fix: ジョブキュー管理画面の認証を回避できる問題を修正 +- Fix: 一部のサーバー内部エラーがスタックトレースを返さないように修正 +- Fix: 一部のリモートユーザーをフォローすることができない問題を修正 + +## 13.14.2 + +### Client +- リストTLで、ユーザーが追加・削除されてもTLを初期化しないように +- URL取得変数を関数に変更 CURRENT_URL -> Mk:url() +- Fix: モバイル表示のときページ下部がナビゲーションバーに隠れる問題を修正 +- Fix: 一部モーダルダイアログでスクロールできない問題を修正 +- Fix: Selecting all emojis in Custom emoji is impossible +- Fix: PhotoSwipeによるメモリリークの修正 + +### Server +- Fix: APIのオフセットが壊れていたせいで「もっと見る」でもっと見れない問題を修正 +- Fix: 外部サーバーの投稿がタイムラインに表示されないことがある問題を修正 +- Enhance: Add address bind config option (outgoingAddress) + +## 13.14.1 + +### General +- 招待機能を改善しました + * 過去に発行した招待コードを確認できるようになりました + * ロールごとに招待コードの発行数制限と制限対象期間、有効期限を設定できるようになりました + * 招待コードを作成したユーザーと使用したユーザーを確認できるようになりました +- ユーザーにロールが期限付きでアサインされている場合、その期限をユーザーのモデレーションページで確認できるようになりました +- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました +- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました + +### Client +- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように +- ドライブファイルのメニューで画像をクロップできるように +- 画像を動画と同様に簡単に隠せるように +- Enhance: ノートの埋め込みが複数画像と動画を表示されるように +- オリジナル画像を保持せずにアップロードする場合webpでアップロードされるように(Safari以外) +- 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように +- フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように +- 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように +- プロフィールURLをコピーできるボタンを追加 #11190 +- `CURRENT_URL`で現在表示中のURLを取得できるように(AiScript) +- ユーザーのContextMenuに「アンテナに追加」ボタンを追加 +- フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように +- 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように +- オフライン時の画面にリロードボタンを追加 +- Renote時に公開範囲のデフォルト設定が適用されるように +- Deckで非ルートページにアクセスした際に簡易UIで表示しない設定を追加 +- ロール設定画面でロールIDを確認できるように +- コンテキストメニュー表示時のパフォーマンスを改善 +- フォロー/フォロワー非公開時の表示を改善 +- 本文にMFMが含まれている場合に自動でたたまれる機能が、返信先や引用RNにも適用されるように + - position は対象外になりました +- AiScriptを0.15.0に更新 +- Fix: サーバーメトリクスが90度傾いている +- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正 +- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正 +- Fix: ZenUIでポップアップの表示位置がおかしい問題を修正 +- Fix: ページ遷移でスクロール位置が保持されない問題を修正 +- Fix: フォルダーのページネーションが機能しない #11180 +- Fix: 長い文章を投稿する際、プレビューが画面からはみ出る問題を修正 +- Fix: システムフォント設定が正しく反映されない問題を修正 +- Fix: アンケート終了時のプッシュ通知が正しく表示されない問題を修正 +- Fix: MasterVolumeが0の時だけでなく各通知音の音量設定が0のときも、HTMLAudioElement.playが実行されないように変更 + +### Server +- JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました +- nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように +- 連合の配送ジョブのパフォーマンスを向上(ロック機構の見直し、Redisキャッシュの活用) +- featuredノートのsignedGet回数を減らしました +- ActivityPubの署名用鍵長を2048bitに変更しパフォーマンスを向上(新規アカウントのみ) +- リモートサーバーのセンシティブなファイルのキャッシュだけを無効化できるオプションを追加 +- MeilisearchにIndexするノートの範囲を設定できるように +- Export notes with file detail +- Add unix socket support +- 設定ファイルでioredisの全てのオプションを指定可能に +- Fix: エクスポートしたカスタム絵文字のzipが大きいと読み込めない問題を修正 +- Fix: リモートサーバーに無意味なActivityPubの配信を行うことがあるのを修正 +- Fix: Remove Meilisearch index when notes are deleted +- Fix: 非英語環境でのPostgreSQLのエラーハンドリングを修正 +- Fix: インスタンスのアイコンがbase64の場合の挙動を修正 +- Fix: ローカルの `Person` を指す `acct` URI を解析するときのバグを修正しました +- Fix: 無効化されたアンテナが再度有効化されないことがある問題を修正 + +## 13.13.2 + +### General +- エラー時や項目が存在しないときなどのアイコン画像をサーバー管理者が設定できるように +- ロールが付与されているユーザーリストを非公開にできるように +- サーバーの負荷が非常に高いため、ユーザー統計表示機能を削除しました + +### Client +- Fix: タブがバックグラウンドでもstreamが切断されないように + +### Server +- Fix: キャッシュが溜まり続けないように + +## 13.13.1 + +### Client +- Fix: タブがアクティブな間はstreamが切断されないように + +### Server +- Fix: api/metaで`TypeError: JSON5.parse is not a function`エラーが発生する問題を修正 + ## 13.13.0 ### General @@ -45,8 +231,10 @@ ### Server - bullをbull-mqにアップグレードし、ジョブキューのパフォーマンスを改善 - ストリーミングのパフォーマンスを改善 +- Fix: 無効化されたアンテナにアクセスがあった際に再度有効化するように - Fix: お知らせの画像URLを空にできない問題を修正 - Fix: i/notificationsのsinceIdが機能しない問題を修正 +- Fix: pageのピン留めを解除することができない問題を修正 ## 13.12.2 @@ -86,11 +274,12 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー ## 13.12.0 ### NOTE -- Node.js 18.6.0以上が必要になりました +- Node.js 18.16.0以上が必要になりました ### General - アカウントの引っ越し(フォロワー引き継ぎ)に対応 - Meilisearchを全文検索に使用できるようになりました + * 「フォロワーのみ」の投稿は検索結果に表示されません。 - 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加 - ユーザーへの自分用メモ機能 * ユーザーに対して、自分だけが見られるメモを追加できるようになりました。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f6b3804f84..484fd99413 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -106,7 +106,7 @@ If your language is not listed in Crowdin, please open an issue. ![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg) ## Development -During development, it is useful to use the +During development, it is useful to use the ``` pnpm dev @@ -150,7 +150,7 @@ Prepare DB/Redis for testing. ``` docker compose -f packages/backend/test/docker-compose.yml up ``` -Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`. +Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`. Run all test. ``` @@ -214,30 +214,13 @@ Misskey uses [Storybook](https://storybook.js.org/) for UI development. ### Setup & Run -#### Universal - -##### Setup - -```bash -pnpm --filter misskey-js build -pnpm --filter frontend tsc -p .storybook && (node packages/frontend/.storybook/preload-locale.js & node packages/frontend/.storybook/preload-theme.js) -``` - -##### Run - -```bash -node packages/frontend/.storybook/generate.js && pnpm --filter frontend storybook dev -``` - -#### macOS & Linux - -##### Setup +#### Setup ```bash pnpm --filter misskey-js build ``` -##### Run +#### Run ```bash pnpm --filter frontend storybook-dev @@ -318,6 +301,12 @@ export const handlers = [ Don't forget to re-run the `.storybook/generate.js` script after adding, editing, or removing the above files. ## Notes + +### Misskeyのドメイン固有の概念は`Mi`をprefixする +例えばGoogleが自社サービスをMap、Earth、DriveではなくGoogle Map、Google Earth、Google Driveのように命名するのと同じ +コード上でMisskeyのドメイン固有の概念には`Mi`をprefixすることで、他のドメインの同様の概念と区別できるほか、名前の衝突を防ぐ。 +ただし、文脈上Misskeyのものを指すことが明らかであり、名前の衝突の恐れがない場合は、一時的なローカル変数に限って`Mi`を省略してもよい。 + ### How to resolve conflictions occurred at pnpm-lock.yaml? Just execute `pnpm` to fix it. @@ -447,3 +436,6 @@ marginはそのコンポーネントを使う側が設定する ## その他 ### HTMLのクラス名で follow という単語は使わない 広告ブロッカーで誤ってブロックされる + +### indexというファイル名を使うな +ESMではディレクトリインポートは廃止されているのと、ディレクトリインポートせずともファイル名が index だと何故か一部のライブラリ?でディレクトリインポートだと見做されてエラーになる diff --git a/Dockerfile b/Dockerfile index fb389659bc..d397fe01cd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.4 -ARG NODE_VERSION=18.16.0-bullseye +ARG NODE_VERSION=20.5.1-bullseye # build assets & compile TypeScript @@ -62,7 +62,8 @@ ARG GID="991" RUN apt-get update \ && apt-get install -y --no-install-recommends \ - ffmpeg tini curl \ + ffmpeg tini curl libjemalloc-dev libjemalloc2 \ + && ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so \ && corepack enable \ && groupadd -g "${GID}" misskey \ && useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \ @@ -81,6 +82,7 @@ COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/bui COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis COPY --chown=misskey:misskey . ./ +ENV LD_PRELOAD=/usr/local/lib/libjemalloc.so ENV NODE_ENV=production HEALTHCHECK --interval=5s --retries=20 CMD ["/bin/bash", "/misskey/healthcheck.sh"] ENTRYPOINT ["/usr/bin/tini", "--"] diff --git a/README.md b/README.md index 2aae4bb865..ab4388c2eb 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ Misskey logo - + **🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀** - + --- @@ -21,7 +21,7 @@ become a patron - + --- [![codecov](https://codecov.io/gh/misskey-dev/misskey/branch/develop/graph/badge.svg?token=R6IQZ3QJOL)](https://codecov.io/gh/misskey-dev/misskey) diff --git a/ROADMAP.md b/ROADMAP.md index 420f728758..3077c41e73 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -22,7 +22,7 @@ This is the phase we are at now. We need to make a high-maintenance environment Once Phase 1 is complete and an environment conducive to the development of a stable system is in place, the implementation of new functions can begin gradually. - Improve features for moderation -- OAuth2 support https://github.com/misskey-dev/misskey/issues/8262 +- ~~OAuth2 support https://github.com/misskey-dev/misskey/issues/8262~~ → Done ✔️ - GraphQL support? ## (3) Improve scalability diff --git a/assets/title_float.svg b/assets/title_float.svg index 43205ac1c4..ed1749e321 100644 Binary files a/assets/title_float.svg and b/assets/title_float.svg differ diff --git a/chart/files/default.yml b/chart/files/default.yml index e62032abfd..90b574b99f 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -135,6 +135,7 @@ redis: # Available methods: # aid ... Short, Millisecond accuracy +# aidx ... Millisecond accuracy # meid ... Similar to ObjectID, Millisecond accuracy # ulid ... Millisecond accuracy # objectid ... This is left for backward compatibility @@ -142,7 +143,7 @@ redis: # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE # ID SETTINGS AFTER THAT! -id: "aid" +id: "aidx" # ┌─────────────────────┐ #───┘ Other configuration └───────────────────────────────────── diff --git a/cypress/e2e/basic.cy.js b/cypress/e2e/basic.cy.js index 652e0c2d70..5ab07c7480 100644 --- a/cypress/e2e/basic.cy.js +++ b/cypress/e2e/basic.cy.js @@ -54,9 +54,10 @@ describe('After setup instance', () => { cy.get('[data-cy-signup]').click(); cy.get('[data-cy-signup-rules-continue]').should('be.disabled'); cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click(); + cy.get('[data-cy-modal-dialog-ok]').click(); cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled'); cy.get('[data-cy-signup-rules-continue]').click(); - + cy.get('[data-cy-signup-submit]').should('be.disabled'); cy.get('[data-cy-signup-username] input').type('alice'); cy.get('[data-cy-signup-submit]').should('be.disabled'); @@ -78,6 +79,7 @@ describe('After setup instance', () => { cy.get('[data-cy-signup]').click(); cy.get('[data-cy-signup-rules-continue]').should('be.disabled'); cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click(); + cy.get('[data-cy-modal-dialog-ok]').click(); cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled'); cy.get('[data-cy-signup-rules-continue]').click(); @@ -169,25 +171,20 @@ describe('After user signed in', () => { cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ'); // TODO: アイコン設定テスト - cy.get('[data-cy-user-setup-back]').click(); cy.get('[data-cy-user-setup-continue]').click(); // プライバシー設定 - cy.get('[data-cy-user-setup-back]').click(); cy.get('[data-cy-user-setup-continue]').click(); // フォローはスキップ - cy.get('[data-cy-user-setup-back]').click(); cy.get('[data-cy-user-setup-continue]').click(); // プッシュ通知設定はスキップ - cy.get('[data-cy-user-setup-back]').click(); cy.get('[data-cy-user-setup-continue]').click(); - cy.get('[data-cy-user-setup-back]').click(); cy.get('[data-cy-user-setup-continue]').click(); }); }); diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index 9185be344c..827a326d18 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -21,6 +21,8 @@ import './commands' Cypress.on('uncaught:exception', (err, runnable) => { if ([ + 'The source image cannot be decoded', + // Chrome 'ResizeObserver loop limit exceeded', diff --git a/docker-compose.yml.example b/docker-compose.yml.example index a0061c5c20..60ba4dc8ca 100644 --- a/docker-compose.yml.example +++ b/docker-compose.yml.example @@ -50,7 +50,7 @@ services: # meilisearch: # restart: always -# image: getmeili/meilisearch:v1.1.1 +# image: getmeili/meilisearch:v1.3.4 # environment: # - MEILI_NO_ANALYTICS=true # - MEILI_ENV=production diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index 6507aad60e..0000000000 --- a/gulpfile.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Gulp tasks - */ - -const fs = require('fs'); -const gulp = require('gulp'); -const replace = require('gulp-replace'); -const terser = require('gulp-terser'); -const cssnano = require('gulp-cssnano'); - -const locales = require('./locales'); -const meta = require('./package.json'); - -gulp.task('copy:backend:views', () => - gulp.src('./packages/backend/src/server/web/views/**/*').pipe(gulp.dest('./packages/backend/built/server/web/views')) -); - -gulp.task('copy:frontend:fonts', () => - gulp.src('./packages/frontend/node_modules/three/examples/fonts/**/*').pipe(gulp.dest('./built/_frontend_dist_/fonts/')) -); - -gulp.task('copy:frontend:tabler-icons', () => - gulp.src('./packages/frontend/node_modules/@tabler/icons-webfont/**/*').pipe(gulp.dest('./built/_frontend_dist_/tabler-icons/')) -); - -gulp.task('copy:frontend:locales', cb => { - fs.mkdirSync('./built/_frontend_dist_/locales', { recursive: true }); - - const v = { '_version_': meta.version }; - - for (const [lang, locale] of Object.entries(locales)) { - fs.writeFileSync(`./built/_frontend_dist_/locales/${lang}.${meta.version}.json`, JSON.stringify({ ...locale, ...v }), 'utf-8'); - } - - cb(); -}); - -gulp.task('build:backend:script', () => { - return gulp.src(['./packages/backend/src/server/web/boot.js', './packages/backend/src/server/web/bios.js', './packages/backend/src/server/web/cli.js']) - .pipe(replace('LANGS', JSON.stringify(Object.keys(locales)))) - .pipe(terser({ - toplevel: true - })) - .pipe(gulp.dest('./packages/backend/built/server/web/')); -}); - -gulp.task('build:backend:style', () => { - return gulp.src(['./packages/backend/src/server/web/style.css', './packages/backend/src/server/web/bios.css', './packages/backend/src/server/web/cli.css', './packages/backend/src/server/web/error.css']) - .pipe(cssnano({ - zindex: false - })) - .pipe(gulp.dest('./packages/backend/built/server/web/')); -}); - -gulp.task('build', gulp.parallel( - 'copy:frontend:locales', 'copy:backend:views', 'build:backend:script', 'build:backend:style', 'copy:frontend:fonts', 'copy:frontend:tabler-icons' -)); - -gulp.task('default', gulp.task('build')); - -gulp.task('watch', () => { - gulp.watch([ - './packages/*/src/**/*', - ], { ignoreInitial: false }, gulp.task('build')); -}); diff --git a/healthcheck.sh b/healthcheck.sh index e97a3f0636..0a36394836 100644 --- a/healthcheck.sh +++ b/healthcheck.sh @@ -1,4 +1,7 @@ #!/bin/bash +# SPDX-FileCopyrightText: syuilo and other misskey contributors +# SPDX-License-Identifier: AGPL-3.0-only + PORT=$(grep '^port:' /misskey/.config/default.yml | awk 'NR==1{print $2; exit}') curl -s -S -o /dev/null "http://localhost:${PORT}" diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 3b97f435d2..4d5872c64d 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -41,17 +41,23 @@ unfavorite: "إزالة من المفضلة" favorited: "أُضيف إلى المفضلة." alreadyFavorited: "تمت إضافته بالفعل إلى المفضلة." cantFavorite: "تعذرت الإضافة إلى المفضلة." -pin: "دبّسها على الصفحة الشخصية" -unpin: "ألغ تدبيسها من ملفك الشخصي" +pin: "ثبتها على الصفحة الشخصية" +unpin: "فكها من ملفك الشخصي" copyContent: "انسخ المحتوى" copyLink: "انسخ الرابط" delete: "حذف" deleteAndEdit: "إزالة وإعادة الصياغة" deleteAndEditConfirm: "أمتأكد من حذف الملاحظة؟ ستفقد كل مشاركاتها، والتفاعلات، والردود عليها." addToList: "أضفه إلى قائمة" +addToAntenna: "أضف إلى هوائي" sendMessage: "أرسل رسالة" copyRSS: "انسخ رابط RSS" copyUsername: "انسخ اسم المستخدم" +copyUserId: "انسخ معرف المستخدم" +copyNoteId: "انسخ معرف الملاحظة" +copyFileId: "انسخ معرّف الملف" +copyFolderId: "انسخ معرّف المجلد" +copyProfileUrl: "انسخ رابط الملف الشخصي" searchUser: "ابحث عن مستخدمين" reply: "رد" loadMore: "عرض المزيد" @@ -106,8 +112,8 @@ cantReRenote: "لا يمكنك إعادة نشر ملاحظة معاد نشره quote: "اقتبس" inChannelRenote: "إعادة نشر في قناة" inChannelQuote: "اقتباس في قناة" -pinnedNote: "ملاحظة مدبسة" -pinned: "دبّسها على الصفحة الشخصية" +pinnedNote: "ملاحظة مثبتة" +pinned: "ثبتها على الصفحة الشخصية" you: "أنت" clickToShow: "اضغط للعرض" sensitive: "محتوى حساس" @@ -134,8 +140,10 @@ unblockConfirm: "أمتأكد من إلغاء حجب هذا الحساب؟" suspendConfirm: "أمتأكد من تعليق الحساب؟" unsuspendConfirm: "أمتأكد من إلغاء تعليق؟" selectList: "اختر قائمة" +editList: "عدّل القائمة" selectChannel: "اختر قناة" selectAntenna: "اختر هوائيًا" +editAntenna: "عدّل الهوائي" selectWidget: "اختر ودجة" editWidgets: "عدّل الودجات" editWidgetsExit: "تم" @@ -206,7 +214,7 @@ blockedUsers: "الحسابات المحجوبة" noUsers: "ليس هناك مستخدمون" editProfile: "تعديل الملف التعريفي" noteDeleteConfirm: "هل تريد حذف هذه الملاحظة؟" -pinLimitExceeded: "لا يمكنك تدبيس الملاحظات بعد الآن." +pinLimitExceeded: "لا يمكنك تثبيت الملاحظات بعد الآن." intro: "لقد انتهت عملية تنصيب Misskey. الرجاء إنشاء حساب إداري." done: "تمّ" processing: "المعالجة جارية" @@ -267,8 +275,8 @@ start: "البداية" home: "الرئيسي" remoteUserCaution: "هذه المعلومات قد لا تكون مكتملة بما أن المستخدم من مثيل بعيد." activity: "النشاط" -images: "الصور" -image: "الصور" +images: "صور" +image: "صور" birthday: "تاريخ الميلاد" yearsOld: "{age} سنة" registeredDate: "انضم في" @@ -305,7 +313,7 @@ copyUrl: "انسخ الرابط" rename: "إعادة التسمية" avatar: "الصورة الرمزية" banner: "الصورة الرأسية" -nsfw: "محتوى حساس" +displayOfSensitiveMedia: "عرض المحتوى الحساس" whenServerDisconnected: "عند فقدان الاتصال بالخادم" disconnectedFromServer: "قُطِع الإتصال بالخادم" reload: "انعش" @@ -340,16 +348,15 @@ invite: "دعوة" driveCapacityPerLocalAccount: "حصة التخزين لكل مستخدم محلي" driveCapacityPerRemoteAccount: "حصة التخزين لكل مستخدم بعيد" inMb: "بالميغابايت" -iconUrl: "رابط الأيقونة" bannerUrl: "رابط صورة اللافتة" backgroundImageUrl: "رابط صورة الخلفية" basicInfo: "المعلومات الأساسية " -pinnedUsers: "المستخدمون المدبسون" -pinnedUsersDescription: "قائمة المستخدمين المدبسين في لسان \"استكشف\" ، اجعل كل اسم مستخدم في سطر لوحده." -pinnedPages: "الصفحات المدبسة" -pinnedPagesDescription: "أدخل مسار الصفحات التي تريد تدبيسها في أعلى هذا الموقع، اجعل كل مسار في سطر لوحده." -pinnedClipId: "معرّف المشبك المدبس" -pinnedNotes: "ملاحظة مدبسة" +pinnedUsers: "المستخدمون المثبتون" +pinnedUsersDescription: "قائمة المستخدمين المثبتين في لسان \"استكشف\" ، اجعل كل اسم مستخدم في سطر لوحده." +pinnedPages: "الصفحات المثبتة" +pinnedPagesDescription: "أدخل مسار الصفحات التي تريد تثبيتها في أعلى هذا الموقع، اجعل كل مسار في سطر لوحده." +pinnedClipId: "معرّف المشبك المثبت" +pinnedNotes: "ملاحظة مثبتة" hcaptcha: "hCaptcha" enableHcaptcha: "فعّل hCaptcha" hcaptchaSiteKey: "مفتاح الموقع" @@ -398,6 +405,7 @@ totp: "تطبيق استيثاق" moderator: "مشرِف" moderation: "الإشراف" nUsersMentioned: "{n} مستخدمين أُشير إليهم" +securityKeyAndPasskey: "الأمن ومفاتيح الأمان" securityKey: "مفتاح الأمان" lastUsed: "آخر استخدام" lastUsedAt: "آخر استخدام: {t}" @@ -452,6 +460,7 @@ language: "اللغة" uiLanguage: "لغة واجهة المستخدم" aboutX: "عن {x}" emojiStyle: "نمط الوجوه التعبيرية" +showNoteActionsOnlyHover: "أظهر الإجراءات عند التمرير فوق الملاحظة" noHistory: "السجل فارغ" signinHistory: "تاريخ تسجيل الدخول" doing: "انتظر لحظة" @@ -484,10 +493,12 @@ objectStoragePrefix: "البادئة" objectStoragePrefixDesc: "ستُحفظ الملفات في مجلدات تحوي اسماءها هذه البادئة." objectStorageEndpoint: "نقطة النهاية" objectStorageRegion: "المنطقة" +objectStorageRegionDesc: "حدد منطقة مثل \"xx-east-1\". إذا كانت خدمتك لا تميز بين المناطق استخدم \"us-east-1\" أو اتركها فارغة إذا كنت تستخدم متغيرات البيئة أو ملفات ضبط AWS." objectStorageUseSSL: "استخدم SSL" objectStorageUseSSLDesc: "عطل هذا الخيار إذا لم ترد استخدام API عبر HTTPS" objectStorageUseProxy: "اتصل عبر وكيل" objectStorageUseProxyDesc: "عطل هذا الخيار إذا لم ترد استخدام API عبر وكيل" +objectStorageSetPublicRead: "عينها ك\"علنية\" عند الرفع" serverLogs: "سجلات الخادم" deleteAll: "حذف الكل" showFixedPostForm: "أظهر نموذج الكتابة في أعلى الصفحة" @@ -531,6 +542,7 @@ accountDeletedDescription: "حُذف هذا الحساب." menu: "القائمة" divider: "فاصل" addItem: "إضافة عنصر" +rearrange: "أعد الترتيب" relays: "المُرَحلات" addRelay: "إضافة مُرحّل" inboxUrl: "رابط صندوق الوارد" @@ -554,6 +566,7 @@ leaveConfirm: "لديك تغييرات غير محفوظة. أتريد المت manage: "إدارة " plugins: "الإضافات" preferencesBackups: "النُسخ الاحتياطية للإعدادات" +useBlurEffectForModal: "استخدم تأثير الطمس في المشروط" useFullReactionPicker: "استخدم الحجم الكامل لمنتقي التفاعلات" width: "العرض" height: "الإرتفاع" @@ -629,7 +642,9 @@ clip: "مِشبك" createNew: "أنشِئ جديد" optional: "اختياري" createNewClip: "أنشئ مِشبكَا جديدًا" +confirmToUnclipAlreadyClippedNote: "هذه الملاحظة تنتمي للمشبك {name} سلفًا، أتريد حذفها منه⸮" public: "علني" +private: "خاص" i18nInfo: "يترجم متطوعون ميسكي إلى عدة لغات، يمكنك المساعدة عبر {link}" manageAccessTokens: "إدارة رموز الوصول" accountInfo: "معلومات الحساب" @@ -650,6 +665,7 @@ driveFilesCount: "عدد الملفات في قرص التخزين" driveUsage: "المستغل من قرص التخزين" noCrawle: "ارفض فهرسة زاحف الويب" noCrawleDescription: "يطلب من محركات البحث ألّا يُفهرسوا ملفك الشخصي وملاحظات وصفحاتك وما شابه." +lockedAccountInfo: "ستكون هذه الملاحظة مرئية للجميع مالم تحدد مرئتيها إلى \"للمتابعين فقط\"" alwaysMarkSensitive: "علّم افتراضيًا جميع ملاحظاتي كذات محتوى حساس" loadRawImages: "حمّل الصور الأصلية بدلًا من المصغرات" disableShowingAnimatedImages: "لا تشغّل الصور المتحركة" @@ -668,6 +684,8 @@ developer: "المطور" makeExplorable: "أظهر الحساب في صفحة \"استكشاف\"" makeExplorableDescription: "بتعطيل هذا الخيار لن يظهر حسابك في صفحة \"استكشاف\"" showGapBetweenNotesInTimeline: "أظهر فجوات بين المشاركات في الخيط الزمني" +left: "يسار" +center: "وسط" wide: "عريض" narrow: "رفيع" reloadToApplySetting: "سيُطبق هذا الإعداد بعد إعادة تحميل الصفحة، أتريد إعادة تحميلها الآن؟" @@ -705,6 +723,7 @@ editCode: "حرر الشفرة" apply: "تطبيق" receiveAnnouncementFromInstance: "استلم إشعارات من هذا المثيل" emailNotification: "إشعارات البريد الكتروني" +publish: "علني" inChannelSearch: "ابحث عن قناة" useReactionPickerForContextMenu: "افتح منتقي التفاعلات عند النقر بالزر الأيمن" typingUsers: "{users} يكتب(ون)..." @@ -717,7 +736,7 @@ unlikeConfirm: "أتريد إلغاء إعجابك؟" fullView: "ملء الشاشة" quitFullView: "اخرج من وضع ملء للشاشة" addDescription: "أضف وصفًا" -userPagePinTip: "لعرض ملاحظة هنا اختر \"دبسها على الصفحة الشخصية\" من قائمة تلك الملاحظة." +userPagePinTip: "لعرض ملاحظة هنا اختر \"ثبتها على الصفحة الشخصية\" من قائمة تلك الملاحظة." notSpecifiedMentionWarning: "في الملاحظة ذكر لمستخدمين لن يستلموها." info: "عن" userInfo: "معلومات المستخدم" @@ -744,6 +763,7 @@ noMaintainerInformationWarning: "لم تُضبط معلومات المدير" noBotProtectionWarning: "لم تضبط الحماية من الحسابات الآلية" configure: "اضبط" postToGallery: "انشر في المعرض" +postToHashtag: "انشر بهذا الوسم" gallery: "المعرض" recentPosts: "المشاركات الحديثة" popularPosts: "المشاركات المتداولة" @@ -776,7 +796,9 @@ translate: "ترجم" translatedFrom: "تُرجم من {x}" accountDeletionInProgress: "حذف الحساب جارٍ" usernameInfo: "الاسم الذي يميزك عن بافي مستخدمي هذا الخادم، يمكنك استخدام الحروف اللاتينية (a~z, A~Z) والأرقام (0~9) والشرطة السفلية (_). لا يمكنك تغييره بعد تسجيله." +devMode: "وضع المُطوّر" keepCw: "أبقِ على تحذيرات المحتوى" +pubSub: "حسابات Pub/Sub" lastCommunication: "آخر تواصل" resolved: "عولج" unresolved: "لم يعالج" @@ -784,6 +806,8 @@ breakFollow: "إلغاء الاشتراك" breakFollowConfirm: "أمتأكد من إزالة المتابِع ؟" itsOn: "مفعّل" itsOff: "معطّل" +on: "مفعل" +off: "معطل" emailRequiredForSignup: "عنوان البريد الإلكتروني إلزامي للتسجيل" unread: "غير مقروءة" filter: "رشّح" @@ -820,6 +844,9 @@ oneDay: "يوم" oneWeek: "أسبوع" oneMonth: "شهر" failedToFetchAccountInformation: "تعذر جلب معلومات الحساب" +cropImage: "اقتصاص الصورة" +cropImageAsk: "أتريد اقتصاص هذه الصورة" +cropYes: "اقتص" cropNo: "استخدمها كما هي" file: "الملفات" recentNHours: "آخر {n} ساعة" @@ -827,19 +854,23 @@ recentNDays: "آخر {n} أيام" noEmailServerWarning: "خادم البريد غير مضبوط." thereIsUnresolvedAbuseReportWarning: "توجد بلاغات غير معالجة." recommended: "مقترح" +check: "التحقق" driveCapOverrideLabel: "غيّر حجم قرص التخزين لهذا المستخدم" driveCapOverrideCaption: "أعد الحجم إلى القيمة الافتراضية بإدخال 0 أو أقل." requireAdminForView: "لاستعراض هذه الصفحة وجب عليك الولوج كمدير." +isSystemAccount: "حساب أنشأه النظام ويُدار من قِبله." typeToConfirm: "أدخل {x} للتأكيد" deleteAccount: "احذف الحساب" document: "التوثيق" numberOfPageCache: "عدد الصفحات المخزنة مؤقتًا" +numberOfPageCacheDescription: "رفع الرقم سيسحن تجربة المستخدم لكن سيرفع استهلاك الذاكرة." logoutConfirm: "أتريد الخروج؟" lastActiveDate: "آخر استخدام" statusbar: "شريط الحالة" pleaseSelect: "حدد خيارًا" reverse: "اقلب" colored: "ملوّن" +refreshInterval: "مهلة التحديث" label: "التسمية" type: "نوع" speed: "سرعة" @@ -847,11 +878,15 @@ slow: "بطيء" fast: "سريع" sensitiveMediaDetection: "التعرف على المحتوى الحساس" localOnly: "المحلي فقط" +remoteOnly: "بُعدي فقط" failedToUpload: "فشل الرفع" cannotUploadBecauseInappropriate: "تعذر رفع الملف لوجود محتوى حساس فيه." cannotUploadBecauseNoFreeSpace: "تعذر رفع الملف لنقص مساحة التخزين." cannotUploadBecauseExceedsFileSizeLimit: "تعذر رفع الملف بسبب تجاوز حجمه للحد المسموح" beta: "بيتا" +enableAutoSensitive: "تعيين تلقائي كمحتوى حساس NSFW" +enableAutoSensitiveDescription: "عند الاستطاعة يسمح باكتشاف المحتوى حساس NSFW تلقائيًا في الوسائط باستخدام تعلم الآلة ووسمها تبعًا لذلك. قد يكون هذا الخيار مفعلا من جهة الخادم وسيعمل حتى وان عُطل." +activeEmailValidationDescription: "يتحقق من صحة عنوان البريد الإلكتروني بشكل أكثر حزمًا وذلك عبر تحديد ما إذا كان عنوان بريد إلكتروني مؤقت وإمكانية التواصل معه. إذا لم يحدد هذا الخيار فسيتحقق من نسق عنوان البريد الإلكتروني." navbar: "شريط التنقل" shuffle: "خلط" account: "الحسابات" @@ -863,22 +898,33 @@ pushNotificationAlreadySubscribed: "إرسال الإشعارات مفعل سل pushNotificationNotSupported: "متصفحك لا يدعم إرسال الإشعارات أو المثيل لا يدعمها." sendPushNotificationReadMessage: "احذف الإشعارات فور قراءتها" sendPushNotificationReadMessageCaption: "هذا قد يزيد من معدل استهلاك الطاقة لجهازك." +windowMaximize: "املأ الشاشة" +windowRestore: "استرجاع" caption: "التعليق التوضيحي" +loggedInAsBot: "والج كآلي" tools: "أدوات" cannotLoad: "تعذر التحميل" +numberOfProfileView: "مشاهدات الملف الشخصي" like: "أعجبني" unlike: "ألغِ الإعجاب" +numberOfLikes: "الإعجابات" show: "المظهر" neverShow: "لا تظهره مجددًا" +remindMeLater: "ربما لاحقا" didYouLikeMisskey: "هل أعجبك ميسكي؟" +pleaseDonate: "يستخدم {host} البرمجية الحرة ميسكي. نتمنى أن تتبرعوا للمشروع مما سيسمح لنا متابعة تطويره!" roles: "الأدوار" role: "الدور" noRole: "لم يُعثر على دور" normalUser: "مستخدم عادي" undefined: "غير معرّف" +assign: "أسند" +unassign: "ألغ الإسناد" color: "اللون" manageCustomEmojis: "إدارة الإيموجي المخصصة" +youCannotCreateAnymore: "وصلت لسقف الإنشاء." cannotPerformTemporary: "غير متاح مؤقتاً" +invalidParamError: "معاملات غير صالحة" permissionDeniedError: "رُفضة العملية" preset: "إعدادات مسبقة" selectFromPresets: "اختر من الإعدادات المسبقة" @@ -900,6 +946,8 @@ cannotBeChangedLater: "لا يمكن تغييره لاحقًا." reactionAcceptance: "قبول التفاعلات" rolesAssignedToMe: "الأدوار المسندة إلي" resetPasswordConfirm: "هل تريد إعادة تعيين كلمة السر؟" +license: "الرخصة" +unfavoriteConfirm: "أتريد إزالتها من المفضلة؟" noteIdOrUrl: "معرف الملاحظة أو رابطها" video: "فيديو" videos: "فيديوهات" @@ -908,6 +956,13 @@ accountMoved: "نقل هذا المستخدم حسابه:" accountMovedShort: "رُحل هذا الحساب." operationForbidden: "عملية ممنوعة" forceShowAds: "أظهر الإعلانات التجارية دائما" +reactionsList: "التفاعلات" +renotesList: "إعادات النشر" +leftTop: "أعلى اليسار" +rightTop: "أعلى اليمين" +leftBottom: "أسفل اليسار" +rightBottom: "أسفل اليمين" +stackAxis: "اتجاه التكديس" vertical: "عمودي" horizontal: "جانبي" position: "الموضع" @@ -917,9 +972,111 @@ pleaseAgreeAllToContinue: "للمتابعة وافق على الحقول أعل continue: "متابعة" preservedUsernames: "أسماء المستخدمين المحجوزة" preservedUsernamesDescription: "قائمة بأسماء المستخدمين المحجوزة كلٌ في سطر. لن يُقبل التسجيل بهذه الأسماء وستبقى محصورة على التسجيل اليدوي بواسطة المديرين. لن يتأثر المستخدمون الذين يملكون هذه الأسماء سلفًا." +createNoteFromTheFile: "أنشئ ملاحظة من هذا الملف" archive: "الأرشيف" +channelArchiveConfirmTitle: "أتريد أرشفت {name}؟" +channelArchiveConfirmDescription: "لن يمكنك نشر ملاحظات في القناة المأرشفة ولن تظهر في قائمة القنوات ولا في نتائج البحث." +thisChannelArchived: "أُرشفت هذه القناة." +displayOfNote: "عرض الملاحظة" +initialAccountSetting: "إعداد الملف الشخصي" youFollowing: "متابَع" options: "خيارات" +specifyUser: "مستخدم محدد" +failedToPreviewUrl: "تتعذر المعاينة" +update: "حدِّث" +rolesThatCanBeUsedThisEmojiAsReaction: "الأدوار التي يُسمح لأصحابها استخدام هذا اإيموجي في اللتفاعل" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "إذا لم تحدد دورًا يمكن للجميع استخدام هذا الإيموجي في التفاعل." +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "يجب أن تكون الأدوار علنية." +cancelReactionConfirm: "أتريد حذف تفاعلك؟" +changeReactionConfirm: "أتريد تعديل تفاعلك؟" +later: "لاحقاً" +goToMisskey: "لميسكي" +additionalEmojiDictionary: "قواميس إيموجي إضافية" +installed: "مُثبت" +expirationDate: "تاريخ انتهاء الصلاحية" +unused: "غير مستعمَل" +expired: "منتهية صلاحيته" +icon: "الصورة الرمزية" +replies: "رد" +renotes: "أعد النشر" +_initialAccountSetting: + accountCreated: "نجح إنشاء حسابك!" + letsStartAccountSetup: "إذا كنت جديدًا لنعدّ حسابك الشخصي." + letsFillYourProfile: "أولًا لنعد ملفك الشخصي." + profileSetting: "إعدادات الملف الشخصي" + privacySetting: "إعدادات الخصوصية" + theseSettingsCanEditLater: "يمكنك تغيير هذه الإعدادات لاحقًا." + skipAreYouSure: "أتريد تخطي إعداد الملف الشخصي؟" + laterAreYouSure: "أتريد إعداد الملف الشخصي لاحقًا؟" +_serverRules: + description: "مجموعة من القواعد لعرضها عند التسجيل، من المستحسن كتابة ملخصٍ للشروط الخدمة." +_accountMigration: + moveFrom: "انقل حسابًا آخر لهذا الحساب" + moveFromLabel: "الحساب الأصلي #{n}" + moveTo: "انقل هذا الحساب لحساب آخر" + moveToLabel: "الحساب الوجهة:" + moveCannotBeUndone: "لا يمكن التراجع عن نقل الحساب." + movedTo: "الحساب الوجهة:" +_achievements: + _types: + _notes1: + description: "انشر ملاحظتك الأولى" + flavor: "تمتع باستخدام ميسكي!" + _notes10: + title: "بعض الملاحظات" + description: "انشر 10 ملاحظات" + _notes100: + title: "كثير من الملاحظات" + description: "انشر 100 ملاحظة" + _notes500: + description: "انشر 500 ملاحظة" + _notes1000: + title: "جبل ملاحظات" + description: "انشر 1000 ملاحظة" + _notes5000: + description: "انشر 5000 ملاحظة" + _notes10000: + description: "انشر 10000 ملاحظة" + _notes20000: + title: "أريد...ملاحظات...أكثر" + description: "انشر 20000 ملاحظة" + _notes30000: + title: "ملاحظات وملاحظات وملاحظات" + description: "انشر 30000 ملاحظة" + _notes40000: + title: "مصنع ملاحظات" + description: "انشر 40000 ملاحظة" + _notes50000: + title: "كوكب ملاحظات" + description: "انشر 50000 ملاحظة" + _notes60000: + title: "نجم ملاحظات" + description: "انشر 60000 ملاحظة" + _notes70000: + title: "ثقب أسود للملاحظات" + description: "انشر 70000 ملاحظة" + _notes80000: + title: "مجرة ملاحظات" + description: "انشر 80000 ملاحظة" + _notes90000: + title: "كوْن ملاحظات" + description: "انشر 90000 ملاحظة" + _notes100000: + title: "كل ملاحظاتك لنا" + description: "انشر 100000 ملاحظة" + flavor: "حقًا لديك الكثير من القصص" + _login3: + title: "مبتدأ I" + _noteFavorited1: + description: "فضًِل ملاحظتك الأولى" + _myNoteFavorited1: + title: "ساعٍ للنجوم" + description: "أعجب شخص آخر بإحدى ملاحظاتك" + _profileFilled: + title: "مستعد" + description: "أعدّ حسابك" + _markedAsCat: + title: "أنا قط" _role: new: "دور جديد" edit: "حرر الأدوار" @@ -927,6 +1084,7 @@ _role: description: "وصف الدور" permission: "أذونات الدور" assignTarget: "نوع الإسناد" + condition: "الشرط" options: "خيارات" policies: "السياسة العامة" priority: "الأولوية" @@ -936,6 +1094,7 @@ _role: high: "عالية" _options: canManageCustomEmojis: "إدارة الإيموجي المخصصة" + pinMax: "حد عدد الملاحظات المثبتة" _condition: isLocal: "مستخدم محلي" isRemote: "مستخدم بعيد" @@ -981,6 +1140,9 @@ _plugin: install: "ثبّت إضافات" installWarn: "رجاءً لا تثبت إضافات غير موثوقة." manage: "إدارة الإضافات" +_preferencesBackups: + createdAt: "تم إنشاؤه: {date} {time}" + updatedAt: "آخر تحديث: {date} {time}" _registry: scope: "الحيّز" key: "مفتاح" @@ -996,10 +1158,6 @@ _aboutMisskey: donate: "تبرع لميسكي" morePatrons: "نحن نقدر الدعم الذي قدمه العديد من الأشخاص الذين لم نذكرهم. شكرًا لكم 🥰" patrons: "الداعمون" -_nsfw: - respect: "اخف الوسائط ذات المحتوى الحساس" - ignore: "اعرض الوسائط ذات المحتوى الحساس" - force: "اخف كل الوسائط" _instanceTicker: none: "لا تظهره بتاتًا" remote: "أظهر للمستخدمين البِعاد" @@ -1109,6 +1267,9 @@ _time: minute: "د" hour: "سا" day: "ي" +_timelineTutorial: + title: "كيف تستخدم Misskey" + step3_1: "هل نشرت ملاحظتك الأولى؟" _2fa: alreadyRegistered: "سجلت سلفًا جهازًا للاستيثاق بعاملين." step1: "أولًا ثبّت تطبيق استيثاق على جهازك (مثل {a} و{b})." @@ -1316,7 +1477,7 @@ _pages: url: "رابط الصفحة" summary: "ملخص الصفحة" alignCenter: "توسيط العناصر" - hideTitleWhenPinned: "اخف عنوان الصفحة عند تدبيسها في ملف الشخصي" + hideTitleWhenPinned: "اخف عنوان الصفحة عند تثبيتها في ملف الشخصي" font: "الخط" fontSerif: "Serif" fontSansSerif: "Sans Serif" @@ -1331,7 +1492,7 @@ _pages: text: "نص" textarea: "حقل نصي" section: "قسم" - image: "الصور" + image: "صور" button: "زرّ" note: "ملاحظة مضمّنة" _note: @@ -1346,22 +1507,22 @@ _notification: fileUploaded: "نجح رفع الملف" youGotMention: "{name} أشار إليك" youGotReply: "ردّ عليك {name}" - youGotQuote: "اقتبس منك {name}" - youRenoted: "إعادت نشر من {name}" + youGotQuote: "اقتبس {name} منشورك" + youRenoted: "أعاد {name} نشر منشورك" youWereFollowed: "يتابعك" youReceivedFollowRequest: "تلقيتَ طلب متابعة" yourFollowRequestAccepted: "قُبل طلب المتابعة" - pollEnded: "ظهرت نتائج الاستطلاع" + pollEnded: "انتهى الاستطلاع" unreadAntennaNote: "هوائي {name}" _types: all: "الكل" follow: "متابِعون جدد" mention: "الإشارات" reply: "الردود" - renote: "أعد النشر" + renote: "أعاد النشر" quote: "الاقتباسات" - reaction: "التفاعلات" - receiveFollowRequest: "طلبات المتابعة المتلقاة" + reaction: "التفاعل" + receiveFollowRequest: "طلبات المتابعة" followRequestAccepted: "طلبات المتابعة المقبولة" app: "إشعارات التطبيقات المرتبطة" _actions: @@ -1369,26 +1530,28 @@ _notification: reply: "رد" renote: "أعد النشر" _deck: - alwaysShowMainColumn: "أظهر العمود الرئيسي دائمًا" - columnAlign: "حاذِ الأعمدة" - addColumn: "أضف عمودًا" - swapLeft: "حرّك لليسار" - swapRight: "حرّك لليمين" - swapUp: "حرّك لأعلى" - swapDown: "حرّك لأسفل" - profile: "الملف الشخصي" + alwaysShowMainColumn: "أظهر العمود الأساسي دائمًا" + columnAlign: "محاذاة الأعمدة" + addColumn: "إضافة عمود" + swapLeft: "التحريك إلى اليسار" + swapRight: "التحريك إلى اليمين" + swapUp: "التحريك إلى الأعلى" + swapDown: "التحريك إلى الأسفل" + profile: "حسابي الشخصي" + newProfile: "ملف تعريفي جديد" + deleteProfile: "حذف الملف التعريفي" _columns: - main: "الرئيسي" - widgets: "الودجات" + main: "الرئيسية" + widgets: "التطبيقات المُصغّرة" notifications: "الإشعارات" - tl: "الخيط الزمني" + tl: "الخط الزمني" antenna: "الهوائيات" list: "القوائم" channel: "القنوات" mentions: "الإشارات" direct: "مباشرة" _webhookSettings: - name: "الإسم" - active: "مفعّل" + name: "الاسم" + active: "مُفعّل" _events: - reaction: "عند تلقي تفاعل" + reaction: "عند التفاعل" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 4b1a9cb758..f3475de225 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -294,7 +294,6 @@ copyUrl: "URL কপি করুন" rename: "পুনঃনামকরণ" avatar: "প্রোফাইল ছবি" banner: "ব্যানার" -nsfw: "সংবেদনশীল বিষয়বস্তু" whenServerDisconnected: "সার্ভারের সাথে সংযোগ বিচ্ছিন্ন হয়ে গেলে" disconnectedFromServer: "সার্ভার থেকে সংযোগ বিচ্ছিন্ন হয়েছে" reload: "আবার লোড করুন" @@ -329,7 +328,6 @@ invite: "আমন্ত্রণ" driveCapacityPerLocalAccount: "প্রত্যেক স্থানীয় ব্যাবহারকারীর জন্য ড্রাইভের জায়গা" driveCapacityPerRemoteAccount: "প্রত্যেক রিমোট ব্যাবহারকারীর জন্য ড্রাইভের জায়গা" inMb: "মেগাবাইটে লিখুন" -iconUrl: "আইকনের URL (ফ্যাভিকন, ইত্যাদি)" bannerUrl: "ব্যানার ছবির URL" backgroundImageUrl: "পটভূমির চিত্রের URL" basicInfo: "আপনার ব্যক্তিগত তথ্য" @@ -629,6 +627,7 @@ createNew: "নতুন" optional: "প্রয়োজনীয় নয়" createNewClip: "নতুন ক্লিপ তৈরি করুন" public: "সর্বজনীন" +private: "ব্যাক্তিগত" i18nInfo: "Misskey স্বেচ্ছাসেবকদের দ্বারা বিভিন্ন ভাষায় অনুবাদ করা হচ্ছে। আপনি {link} এ গিয়ে অনুবাদে সহযোগিতা করতে পারেন।" manageAccessTokens: "অ্যাক্সেস টোকেন পরিচালনা করুন" accountInfo: "অ্যাকাউন্টের তথ্য" @@ -838,6 +837,9 @@ show: "প্রদর্শন" color: "রং" horizontal: "পাশে" youFollowing: "অনুসরণ করা হচ্ছে" +icon: "প্রোফাইল ছবি" +replies: "জবাব" +renotes: "রিনোট" _role: priority: "অগ্রাধিকার" _priority: @@ -902,10 +904,6 @@ _aboutMisskey: donate: "Misskey তে দান করুন" morePatrons: "আরও অনেকে আমাদের সাহায্য করছেন। তাদের সবাইকে ধন্যবাদ 🥰" patrons: "সমর্থনকারী" -_nsfw: - respect: "স্পর্শকাতর মিডিয়া লুকান" - ignore: "স্পর্শকাতর মিডিয়া লুকাবেন না" - force: "সকল মিডিয়া লুকান" _instanceTicker: none: "দেখাবেন না" remote: "রিমোট ব্যাবহারকারীদের জন্য দেখান" @@ -1045,7 +1043,6 @@ _2fa: alreadyRegistered: "আপনি ইতিমধ্যে একটি 2-ফ্যাক্টর অথেনটিকেশন ডিভাইস নিবন্ধন করেছেন৷" step1: "প্রথমে, আপনার ডিভাইসে {a} বা {b} এর মতো একটি অথেনটিকেশন অ্যাপ ইনস্টল করুন৷" step2: "এরপরে, অ্যাপের সাহায্যে প্রদর্শিত QR কোডটি স্ক্যান করুন।" - step2Url: "ডেস্কটপ অ্যাপে, নিম্নলিখিত URL লিখুন:" step3: "অ্যাপে প্রদর্শিত টোকেনটি লিখুন এবং আপনার কাজ শেষ।" step4: "আপনাকে এখন থেকে লগ ইন করার সময়, এইভাবে টোকেন লিখতে হবে।" securityKeyInfo: "আপনি একটি হার্ডওয়্যার সিকিউরিটি কী ব্যবহার করে লগ ইন করতে পারেন যা FIDO2 বা ডিভাইসের ফিঙ্গারপ্রিন্ট সেন্সর বা পিন সমর্থন করে৷" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 588a07fd87..ec52c57610 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -13,12 +13,14 @@ fetchingAsApObject: "Cercant en el Fediverse..." ok: "OK" gotIt: "Ho he entès!" cancel: "Cancel·lar" +noThankYou: "No, gràcies" enterUsername: "Introdueix el teu nom d'usuari" renotedBy: "Impulsat per {usuari}" noNotes: "Cap nota" noNotifications: "Cap notificació" instance: "Servidor" settings: "Preferències" +notificationSettings: "Paràmetres de notificacions" basicSettings: "Configuració bàsica" otherSettings: "Configuració avançada" openInWindow: "Obrir en una nova finestra" @@ -47,8 +49,15 @@ delete: "Elimina" deleteAndEdit: "Elimina i edita" deleteAndEditConfirm: "Segur que vols eliminar aquesta publicació i editar-la? Perdràs totes les reaccions, impulsos i respostes." addToList: "Afegir a una llista" +addToAntenna: "Afegir a l'antena" sendMessage: "Enviar un missatge" +copyRSS: "Copiar RSS" copyUsername: "Copiar nom d'usuari" +copyUserId: "Copiar ID d'usuari" +copyNoteId: "Copiar ID de nota" +copyFileId: "Copiar ID d'arxiu" +copyFolderId: "Copiar ID de carpeta" +copyProfileUrl: "Copiar URL del perfil" searchUser: "Cercar un usuari" reply: "Respondre" loadMore: "Carregar més" @@ -128,6 +137,7 @@ suspendConfirm: "Estàs segur que vols suspendre aquest compte?" unsuspendConfirm: "Estàs segur que vols treure la suspensió d'aquest compte?" selectList: "Tria una llista" selectAntenna: "Tria una antena" +editAntenna: "Modificar antena" selectWidget: "Triar un giny" editWidgets: "Editar ginys" editWidgetsExit: "Fet" @@ -263,7 +273,6 @@ emptyFolder: "La carpeta està buida" unableToDelete: "No es pot eliminar" copyUrl: "Copia l'URL" rename: "Canvia el nom" -nsfw: "NSFW" reload: "Actualitza" doNothing: "Ignora" accept: "Accepta" @@ -299,8 +308,10 @@ manageAntennas: "Gestiona les antenes" antennaSource: "Font de l'antena" antennaKeywords: "Paraules clau a seguir" antennaExcludeKeywords: "Paraules clau a excloure" +antennaKeywordsDescription: "Separar amb espais per la condició AND o amb salts de línia per la condició OR." notifyAntenna: "Notifica'm les publicacions noves" withFileAntenna: "Només les publicacions amb fitxers" +antennaUsersDescription: "Llistar un nom d'usuari per línia" notesAndReplies: "Amb respostes" silence: "Silencia" silenceConfirm: "Segur que vols silenciar aquest usuari?" @@ -370,6 +381,11 @@ user: "Usuaris" global: "Global" searchByGoogle: "Cercar" file: "Fitxers" +replies: "Respondre" +renotes: "Impulsa" +_role: + _options: + antennaMax: "Nombre màxim d'antenes" _email: _follow: title: "t'ha seguit" @@ -385,7 +401,7 @@ _sfx: chat: "Xat" antenna: "Antenes" _2fa: - step2Url: "També pots inserir aquest enllaç i utilitzes una aplicació d'escriptori:" + renewTOTPCancel: "No, gràcies" _antennaSources: all: "Totes les publicacions" homeTimeline: "Publicacions dels usuaris seguits" @@ -431,6 +447,7 @@ _pages: _notification: youRenoted: "Impulsat per {name}" youWereFollowed: "t'ha seguit" + unreadAntennaNote: "Antena {name}" _types: all: "Tots" follow: "Seguint" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index d055fde38f..9a751abc78 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -2,6 +2,7 @@ _lang_: "Čeština" headlineMisskey: "Síť propojená poznámkami" introMisskey: "Vítejte! Misskey je otevřený a decentralizovaný microblogový servis.\n\"Poznámkami\" můžete sdílet co se zrovna děje se všemi ve Vašem okolí. 📡\nPomocí \"reakcí\" můžete sdílet své názory a pocity na ostatní poznámky. 👍\nPojďte objevovat nový svět! 🚀" +poweredByMisskeyDescription: "{name} je jeden ze serverů využívající open source platformu Misskey (nazývaná \"Misskey instance\")." monthAndDay: "{day}. {month}." search: "Vyhledávání" notifications: "Oznámení" @@ -48,8 +49,15 @@ delete: "Smazat" deleteAndEdit: "Smazat a upravit" deleteAndEditConfirm: "Jste si jistí že chcete smazat tuto poznámku a editovat ji? Ztratíte tím všechny reakce, sdílení a odpovědi na ni." addToList: "Přidat do seznamu" +addToAntenna: "Přidat do antény" sendMessage: "Odeslat zprávu" +copyRSS: "Kopírovat RSS" copyUsername: "Kopírovat uživatelské jméno" +copyUserId: "Kopírovat ID uživatele" +copyNoteId: "Kopírovat ID poznámky" +copyFileId: "Kopírovat ID souboru" +copyFolderId: "Kopírovat ID složky" +copyProfileUrl: "Kopírovat URL profilu" searchUser: "Vyhledat uživatele" reply: "Odpovědět" loadMore: "Zobrazit více" @@ -60,6 +68,7 @@ receiveFollowRequest: "Žádost o sledování přijata" followRequestAccepted: "Žádost o sledování přijata" mention: "Zmínění" mentions: "Zmínění" +directNotes: "Přímé poznámky" importAndExport: "Import a export" import: "Importovat" export: "Exportovat" @@ -82,6 +91,7 @@ error: "Chyba" somethingHappened: "Jejda. Něco se nepovedlo." retry: "Opakovat" pageLoadError: "Nepodařilo se načíst stránku" +pageLoadErrorDescription: "Tohle je obvykle způsobeno chybou sítě nebo mezipaměti prohlížeče. Zkuste vymazat mezipaměť a po chvíli čekání to zkuste znovu." serverIsDead: "Server neodpovídá. Počkejte chvíli a zkuste to znovu." youShouldUpgradeClient: "Pro zobrazení této stránky obnovte stránku pro aktualizaci klienta." enterListName: "Jméno seznamu" @@ -100,6 +110,8 @@ renoted: "Přeposláno" cantRenote: "Tento příspěvek nelze přeposlat." cantReRenote: "Odpověď nemůže být odstraněna." quote: "Citovat" +inChannelRenote: "Přeposlání v kanálu" +inChannelQuote: "Citace v kanálu" pinnedNote: "Připnutá poznámka" pinned: "Připnout" you: "Vy" @@ -108,6 +120,7 @@ sensitive: "NSFW" add: "Přidat" reaction: "Reakce" reactions: "Reakce" +reactionSetting: "Reakce zobrazené ve výběru reakcí" reactionSettingDescription2: "Přetažením změníte pořadí, kliknutím smažete, zmáčkněte \"+\" k přidání" rememberNoteVisibility: "Zapamatovat nastavení zobrazení poznámky" attachCancel: "Odstranit přílohu" @@ -116,6 +129,8 @@ unmarkAsSensitive: "Odznačit jako NSFW" enterFileName: "Zadejte název souboru" mute: "Ztlumit" unmute: "Odmlčet" +renoteMute: "Ztlumit poznámky" +renoteUnmute: "Zrušit ztlumení poznámek" block: "Zablokovat" unblock: "Odblokovat" suspend: "Zmrazit" @@ -125,7 +140,10 @@ unblockConfirm: "Jste si jistí že chcete odblokovat tento účet?" suspendConfirm: "Jste si jistí že chcete suspendovat tenhle účet?" unsuspendConfirm: "Jste si jistí že chcete obnovit tenhle účet?" selectList: "Vybrat seznam" +editList: "Upravit seznam" +selectChannel: "Vybrat kanál" selectAntenna: "Vyberte Anténu" +editAntenna: "Upravit anténu" selectWidget: "Zvolte widget" editWidgets: "Upravit widget" editWidgetsExit: "Hotovo" @@ -138,6 +156,8 @@ addEmoji: "Přidat emoji" settingGuide: "Doporučené nastavení" cacheRemoteFiles: "Ukládání vzdálených souborů do mezipaměti" cacheRemoteFilesDescription: "Zakázání tohoto nastavení způsobí, že vzdálené soubory budou odkazovány přímo, místo aby byly ukládány do mezipaměti. Tím se ušetří úložiště na serveru, ale zvýší se provoz, protože se negenerují miniatury." +cacheRemoteSensitiveFiles: "Uložit do mezipaměti vzdálené citlivé soubory" +cacheRemoteSensitiveFilesDescription: "Když je tohle nastavení zrušeno, tak jsou vzdálené citlivé soubory načítány přímo ze vzdálených instancí bez uložení do mezipaměti." flagAsBot: "Tento účet je bot" flagAsBotDescription: "Pokud je tento účet kontrolován programem zaškrtněte tuto možnost. To označí tento účet jako bot pro ostatní vývojáře a zabrání tak nekonečným interakcím s ostatními boty a upraví Misskey systém aby se choval k tomuhle účtu jako bot." flagAsCat: "Tenhle účet je kočka" @@ -146,6 +166,7 @@ flagShowTimelineReplies: "Zobrazovat odpovědi na časové ose" flagShowTimelineRepliesDescription: "Je-li zapnuto, zobrazí odpovědi uživatelů na poznámky jiných uživatelů na vaší časové ose." autoAcceptFollowed: "Automaticky akceptovat následování od účtů které sledujete" addAccount: "Přidat účet" +reloadAccountsList: "Obnovit list účtů" loginFailed: "Přihlášení se nezdařilo." showOnRemote: "Více na původním profilu" general: "Obecně" @@ -186,17 +207,27 @@ instanceInfo: "Informace o instanci" statistics: "Statistiky" clearQueue: "Vyčistit frontu" clearQueueConfirmTitle: "Jste si jisti že zrušit všechny úlohy ve frontě?" +clearQueueConfirmText: "Jakékoliv nedoručené poznámky ve frontě nebudou sdružovány. Většinou tahle operace není zapotřebí." clearCachedFiles: "Vyprázdnit mezipaměť" +clearCachedFilesConfirm: "Jste jistí že chcete smazat všechny vzdálené soubory v mezipaměti?" blockedInstances: "Blokované instance" +blockedInstancesDescription: "Vypište názvy hostitelů instancí, které chcete blokovat odděleně řádkovými zlomky. Uvedené instance již nebudou moci s touto instancí komunikovat." +muteAndBlock: "Ztlumení a blokování" +mutedUsers: "Zltumení uživatelé" +blockedUsers: "Blokovaní uživatelé" noUsers: "Žádní uživatelé" editProfile: "Upravit můj profil" +noteDeleteConfirm: "Jste si jistí že chcete smazat tuhle poznámku?" pinLimitExceeded: "Nemůžete připnout další poznámky." intro: "Instalace Misskey byla dokončena! Prosím vytvořte admina." done: "Hotovo" processing: "Zpracovávám" preview: "Náhled" default: "Výchozí" +defaultValueIs: "Základní hodnota: {value}" noCustomEmojis: "Bez Emoji" +noJobs: "Žádné úlohy" +federating: "Sdružování" blocked: "Blokováno" suspended: "Suspendováno" all: "Vše" @@ -217,6 +248,7 @@ more: "Více!" featured: "Oblíbené poznámky" usernameOrUserId: "Uživatelské jméno nebo uživatelské id" noSuchUser: "Uživatel nebyl nalezen" +lookup: "Vyhledat" announcements: "Oznámení" imageUrl: "URL obrázku" remove: "Smazat" @@ -227,10 +259,13 @@ resetAreYouSure: "Opravdu resetovat?" saved: "Uloženo" messaging: "Zprávy" upload: "Nahrát soubory" +keepOriginalUploading: "Ponechat originální obrázek" +keepOriginalUploadingDescription: "Uloží původní nahraný obrázek jak je. Pokud je to vypnuté, vygeneruje se zobrazení verze na webu při nahrátí." fromDrive: "Z disku" fromUrl: "Z URL" uploadFromUrl: "Nahrát z URL adresy" uploadFromUrlDescription: "URL adresa souboru, který chcete nahrát" +uploadFromUrlRequested: "Upload zažádán" uploadFromUrlMayTakeTime: "Může trvat nějakou dobu, dokud nebude dokončeno nahrávání." explore: "Objevovat" messageRead: "Přečtené" @@ -238,6 +273,10 @@ noMoreHistory: "To je vše" startMessaging: "Zahájit chat" nUsersRead: "přečteno {n} uživateli" agreeTo: "Souhlasím s {0}" +agree: "Souhlasím" +agreeBelow: "Souhlasím s následným" +basicNotesBeforeCreateAccount: "Důležité poznámky" +termsOfService: "Podmínky užívání" start: "Začít" home: "Domů" remoteUserCaution: "Tyto informace nemusí být aktuální jelikož uživatel je ze vzdálené instance." @@ -268,18 +307,24 @@ createFolder: "Vytvořit složku" renameFolder: "Přejmenovat složku" deleteFolder: "Odstranit složku" addFile: "Přidat soubor" +emptyDrive: "Váš disk je prázdný" emptyFolder: "Tato složka je prázdná" unableToDelete: "Nelze smazat" inputNewFileName: "Zadejte nový název" +inputNewDescription: "Zadejte nový popisek" inputNewFolderName: "Zadejte název nové složky" +circularReferenceFolder: "Koncová složka je podsložka složky, kterou chcete přesunout." +hasChildFilesOrFolders: "Nemůžete odstranit složku, která není prázdná." copyUrl: "Kopírovat URL" rename: "Přejmenovat" avatar: "Avatar" banner: "Baner" -nsfw: "NSFW" +displayOfSensitiveMedia: "Zobrazit citlivé média" +whenServerDisconnected: "Když ztratíte spojení se serverem" disconnectedFromServer: "Spojení bylo přerušeno" reload: "Aktualizovat" doNothing: "Ignorovat" +reloadConfirm: "Chcete obnovit časovou osu?" watch: "Sledovat" unwatch: "Přestat sledovat" accept: "Souhlasím" @@ -302,15 +347,21 @@ connectService: "Připojit" disconnectService: "Odpojit" enableLocalTimeline: "Povolit lokální čas" enableGlobalTimeline: "Povolit globální čas" +disablingTimelinesInfo: "Administrátoři a Moderátoři budou mít stálý přístup ke všem časovým osám i přes to že nejsou zapnuté." registration: "Registrace" enableRegistration: "Povolit registraci novým uživatelům" invite: "Pozvat" +driveCapacityPerLocalAccount: "Kapacita disku na lokálního uživatele" +driveCapacityPerRemoteAccount: "Kapacita disku na vzdáleného uživatele" inMb: "V megabajtech" -iconUrl: "Favicon URL" bannerUrl: "Baner URL" backgroundImageUrl: "Adresa URL obrázku pozadí" basicInfo: "Základní informace" pinnedUsers: "Připnutí uživatelé" +pinnedUsersDescription: "Seznam uživatelských přezdívek oddělených řádkami bude připnutý v záložce \"Objevit\"." +pinnedPages: "Připnutý stránky" +pinnedPagesDescription: "Zadejte cesty stránek oddělené řádkami, které si přejete mít přípnutý na vrcholu téhle instance." +pinnedClipId: "ID připnutého klipu" pinnedNotes: "Připnutá poznámka" hcaptcha: "hCaptcha" enableHcaptcha: "Aktivovat hCaptchu" @@ -320,30 +371,56 @@ recaptcha: "reCAPTCHA" enableRecaptcha: "Zapnout ReCAPTCHu" recaptchaSiteKey: "Klíč stránky" recaptchaSecretKey: "Tajný Klíč (Secret Key)" +turnstile: "Turnstile" +enableTurnstile: "Povolit Turnstile" turnstileSiteKey: "Klíč stránky" turnstileSecretKey: "Tajný Klíč (Secret Key)" +avoidMultiCaptchaConfirm: "Používání několik Captcha systému může způsobit konflikt mezi nimi. Chtěli byste vypnout ostatní aktivní Captcha systémy? Pokud je chcete nechat zapnuté, stiskněte zrušit." antennas: "Antény" manageAntennas: "Spravovat Antény" name: "Jméno" antennaSource: "Zdroj Antény" +antennaKeywords: "Klíčová slova na poslech" +antennaExcludeKeywords: "Vyloučená klíčová slova" +antennaKeywordsDescription: "Oddělte mezerami pro AND kondice nebo řádkami pro OR kondice." +notifyAntenna: "Upozornit na nové poznámky" +withFileAntenna: "Poznámky jenom se souborama" enableServiceworker: "Povolit ServiceWorker" +antennaUsersDescription: "Vypsat jednoho uživatele na řádek" caseSensitive: "Rozlišuje malá a velká písmena" +withReplies: "Zahrnout odpovědi" connectedTo: "Následující účty jsou připojeny" notesAndReplies: "Poznámky a odpovědi" withFiles: "Včetně souborů" +silence: "Ztlumení" +silenceConfirm: "Jste si jistí že chcete ztlumit tohoto uživatele?" +unsilence: "Zrušit ztlumení" +unsilenceConfirm: "Jste jistí že chcete vrátit zltumení tohoto uživatele?" popularUsers: "Populární uživatelé" recentlyUpdatedUsers: "Nedávno aktívni uživatelé" +recentlyRegisteredUsers: "Nově připojený uživatelé" +recentlyDiscoveredUsers: "Nově objevený uživatelé" +exploreUsersCount: "Existuje {count} uživatelů" +exploreFediverse: "Objevovat Fediverse" popularTags: "Populární tagy" userList: "Seznamy" about: "Informace" aboutMisskey: "O Misskey" administrator: "Administrátor" token: "Token" +2fa: "Dvoufázové ověření" +totp: "Ověřovací aplikace" +totpDescription: "Použít ověřovací aplikaci pro použití jednorázových hesel" moderator: "Moderátor" +moderation: "Moderování" nUsersMentioned: "{n} uživatelů zmínilo" +securityKeyAndPasskey: "Bezpečnostní klíče a tokeny" securityKey: "Bezpečnostní klíč" lastUsed: "Naposledy použito" +lastUsedAt: "Naposledy použito: {t}" unregister: "Odstranit" +passwordLessLogin: "Přihlášení bez hesla" +passwordLessLoginDescription: "Umožní bez-heslové přihlášení pomocí bezpečnostního klíče či tokenu" resetPassword: "Resetovat heslo" newPasswordIs: "Nové heslo je \"{password}\"" reduceUiAnimation: "Snížit UI animace" @@ -392,14 +469,25 @@ or: "Nebo" language: "Jazyk" uiLanguage: "Jazyk uživatelského rozhraní" aboutX: "O {x}" +emojiStyle: "Styl emoji" +native: "Výchozí" +disableDrawer: "Nepoužívat šuplíkové menu" +showNoteActionsOnlyHover: "Zobrazit akce poznámky jenom při naběhnutí myši" noHistory: "Žádná historie" signinHistory: "Historie přihlášení" +enableAdvancedMfm: "Zapnout pokročilé MFM" +enableAnimatedMfm: "Zapnout animované MFM" +doing: "Procesuju..." category: "Kategorie" tags: "Štítky" +docSource: "Zdroj tohoto dokumentu" createAccount: "Vytvořit účet" existingAccount: "Existující účet" regenerate: "Obnovit" fontSize: "Velikost písma" +mediaListWithOneImageAppearance: "Výška seznamu médií s jedním obrázkem" +limitTo: "Omezeno na {x}" +noFollowRequests: "Nemáte žádné žádosti o sledování" openImageInNewTab: "Otevřít obrázek v novém panelu" dashboard: "Přehled" local: "Lokální" @@ -413,15 +501,35 @@ accountSettings: "Nastavení účtu" promotion: "Propagace" promote: "Propagovat" numberOfDays: "Počet dní" +hideThisNote: "Skrýt tuto poznámku" +showFeaturedNotesInTimeline: "Zobrazit významné poznámky v časové ose" +objectStorage: "Úložiště objektů" +useObjectStorage: "Použít úložiště objektů" objectStorageBaseUrl: "Base URL" +objectStorageBaseUrlDesc: "URL použitá jako reference. Upřesněte URL vlastní CDN nebo Proxy pokud používáte jeden z nich. Pro S3 použijte 'https://.s3.amazonaws.com' a pro GCS nebo ekvivalentní služby použijte 'https://storage.googleapis.com/', apd." objectStorageBucket: "Bucket" +objectStorageBucketDesc: "Prosím upřesněte název bucketu používaný poskytovatelem." objectStoragePrefix: "Předpona" +objectStoragePrefixDesc: "Soubory budou ukládány pod složkama s tímhle prefixem." objectStorageEndpoint: "Endpoint" +objectStorageEndpointDesc: "Ponechte tohle prázdné pokud používáte AWS S3, jinak upřesněte endpoint jako \"\" nebo \":\", podle toho jakou službu používáte." objectStorageRegion: "Región" +objectStorageRegionDesc: "Upřesněte region jako například \"xx-east-1\". Pokud vlastní služba nerozlišuje mezi regiony, zadejte \"us-east-1\". Zanechte prázdné pokud používáte AWS konfiguraci či proměnné veličiny." objectStorageUseSSL: "Použít SSL" +objectStorageUseSSLDesc: "Vypněte to pokud nebudete používat HTTPS pro API připojení" +objectStorageUseProxy: "Připojení skrze Proxy" +objectStorageUseProxyDesc: "Vypněte to pokud nebudete používat Proxy pro API připojení." +objectStorageSetPublicRead: "Při nahrátí nastavit na \"public-read\"" +s3ForcePathStyleDesc: "Pokud je povolena funkce s3ForcePathStyle, musí být název Bucketu zahrnut do cesty k adrese URL, nikoli do názvu hostitele adresy URL. Toto nastavení může být nutné povolit při používání služeb, jako je například samostatně hostovaná instance Minio." +serverLogs: "Logy serveru" deleteAll: "Smazat vše" showFixedPostForm: "Zobrazit formulář pro nové příspěvky nad časovou osou" +showFixedPostFormInChannel: "Zobrazit vkládací formulář na vrcholu časové osy (Kanály)" +newNoteRecived: "Jsou k dispozici nové poznámky" +sounds: "Zvuky" +sound: "Zvuky" listen: "Poslouchat" +none: "Žádný" showInPage: "Zobrazit na stránce" popout: "Pop-out" volume: "Hlasitost" @@ -434,29 +542,61 @@ install: "Nainstalovat" uninstall: "Odinstalovat" installedApps: "Autorizované aplikace" nothing: "Nic nebylo nalezeno" +installedDate: "Datum autorizace" lastUsedDate: "Poslední použití" state: "Stav" sort: "Seřadit" ascendingOrder: "Vzestupně" descendingOrder: "Sestupně" scratchpad: "Zápisník" +scratchpadDescription: "Scratchpad poskytuje rozhraní pro AiScript experimenty. Můžete psát, spustit či zkontrolovat výsledky jeho interakce s Misskey." output: "Výstup" script: "Skript" +disablePagesScript: "Vypnout AiScript na stránkách" updateRemoteUser: "Aktualizovat informace o vzdáleném účtu" deleteAllFiles: "Smazat všechny soubory" deleteAllFilesConfirm: "Jste si jistí že chcete smazat všechny soubory?" +removeAllFollowing: "Přestat sledovat všechny sledované uživatele" +removeAllFollowingDescription: "Spuštěním přestanete sledovat všechny účty z {host}. Prosíme spustěte tohle v případě že instance už neexistuje. " userSuspended: "Tomuto uživateli byl pozastaven účet." +userSilenced: "Tenhle uživatel je umlčen." +yourAccountSuspendedTitle: "Tenhle účet je zmrazený" +yourAccountSuspendedDescription: "Tenhle účet byl zmrazen z důvodu porušení smluvní podmínky serveru. Pro přesnější informace kontaktujte administrátora. Prosíme nezakládejte si nový účet." +tokenRevoked: "Nesprávný token" +tokenRevokedDescription: "Tenhle token vyprchal. Prosíme přihlašte se znova." +accountDeleted: "Účet smazán" +accountDeletedDescription: "Tenhle účet byl smazán." menu: "Menu" divider: "Dělící čára" addItem: "Přidat položku" +rearrange: "Přeřadit" relays: "Relay" addRelay: "Přidat Relay" inboxUrl: "Inbox URL" +addedRelays: "Přidané přenosy" +serviceworkerInfo: "Musí být zapnut pro push notifikace." deletedNote: "Odstraněné příspěvky" invisibleNote: "Skryté příspěvky" +enableInfiniteScroll: "Automaticky načítat více" +visibility: "Viditelnost" +poll: "Anketa" +useCw: "Schovat obsah" +enablePlayer: "Otevřít video přehrávač" +disablePlayer: "Zavřít video přehrávač" +expandTweet: "Rozbalit tweet" +themeEditor: "Editor témat" description: "Popis" +describeFile: "Přidat popisek" +enterFileDescription: "Vložit popisek" author: "Autor" +leaveConfirm: "Máte neuložené změny. Opravdu je chcete zahodit?" manage: "Administrace" +plugins: "Pluginy" +preferencesBackups: "Zálohy nastavení" +deck: "Deck" +undeck: "Opustit Deck" +useBlurEffectForModal: "Použít efekt rozostření na okna" +useFullReactionPicker: "Používat plnou velikost výběru emoji" width: "Šířka" height: "Výška" large: "Velké" @@ -466,10 +606,13 @@ generateAccessToken: "Vygenerovat přístupový token" permission: "Oprávnění" enableAll: "Povolit vše" disableAll: "Vypnout vše" +tokenRequested: "Povolit přístup k účtu" +pluginTokenRequestedDescription: "Tenhle plugin bude moct používat oprávnění nastavená zde." notificationType: "Typy oznámení" edit: "Upravit" emailServer: "Mailový server" enableEmail: "Zapnout email dystribuci" +emailConfigInfo: "Používá se na ověření emailové adresy během registrace nebo při zapomenutí hesla." email: "Email" emailAddress: "Emailová adresa" smtpConfig: "Konfigurace SMTP serveru" @@ -477,8 +620,15 @@ smtpHost: "Hostitel" smtpPort: "Port" smtpUser: "Uživatelské jméno" smtpPass: "Heslo" +emptyToDisableSmtpAuth: "Zanechte uživatelské jméno a heslo prázdné pro vypnutí SMTP verifikace." +smtpSecure: "Použít implicitní SSL/TLS pro SMTP připojení" smtpSecureInfo: "Toto vypněte pokud používáte STARTTLS" testEmail: "Otestovat doručení emailů" +wordMute: "Ztlumené slova" +regexpError: "Chyba v regulérním výrazu" +regexpErrorDescription: "Došlo k chybě v regulérním výrazu v řádku {line} tabulky {tab} ztlumených slov:" +instanceMute: "Ztlumené instance" +userSaysSomething: "{name} řekl/a něco" makeActive: "Aktivovat" display: "Zobrazit" copy: "Kopírovat" @@ -490,42 +640,107 @@ database: "Databáze" channel: "Kanály" create: "Vytvořit" notificationSetting: "Nastavení oznámení" +notificationSettingDesc: "Vyberte typy oznámení k zobrazení." useGlobalSetting: "Použít globální nastavení" +useGlobalSettingDesc: "Pokud je to zapnuté, tak nastavení oznámení účtu bude použito. Pokud je to vypnuté, tak se bude moct použít jednotlivá nastavení." other: "Ostatní" +regenerateLoginToken: "Přegenerovat přihlašovací token" +regenerateLoginTokenDescription: "Přegeneruje token interně používaný během přihlášení. Běžně tahle akce není nutná. Pokud bude token přegenerovaný, tak se všechna přihlášená zařízení odhlásí." +setMultipleBySeparatingWithSpace: "Oddělení více položek mezerami." fileIdOrUrl: "ID nebo URL souboru" behavior: "Chování" sample: "Ukázka" +abuseReports: "Nahlášení" +reportAbuse: "Nahlášení" +reportAbuseOf: "Nahlásit {name}" +fillAbuseReportDescription: "Prosíme vyplňte všechny detaily ohledně tohodle nahlášení. Pokud jde o specifickou poznámku, prosíme o přiložení její URL." +abuseReported: "Nahlášení bylo odesláno. Děkujeme převelice." +reporter: "Nahlásil" +reporteeOrigin: "Původ nahlášení" +reporterOrigin: "Původ nahlasovače" +forwardReport: "Přeposlat nahlášení do vzdálené instance" +forwardReportIsAnonymous: "Místo vašeho účtu se ve vzdálené instanci zobrazí anonymní systémový účet jako nahlašovač." send: "Odeslat" +abuseMarkAsResolved: "Označit nahlášení jako vyřešené" openInNewTab: "Otevřít v nové kartě" +openInSideView: "Otevřít v bočním panelu" +defaultNavigationBehaviour: "Výchozí chování navigace" +editTheseSettingsMayBreakAccount: "Uprávou těchto nastavení si můžete poškodit účet." +instanceTicker: "Informace instance o poznámkách" +waitingFor: "Čeká se na {x}" random: "Náhodně" system: "Systém" +switchUi: "Přepnout UI" desktop: "Plocha" clip: "Oříznout" createNew: "Vytvořit nový" optional: "Volitelné" +createNewClip: "Vytvořit nový klip" +unclip: "Odepnout" +confirmToUnclipAlreadyClippedNote: "Tahle poznámku je už součásti \"{name}\" klipu. Chcete ji místo toho odepnout z tohodle klipu?" +public: "Veřejný" +private: "Soukromý" +i18nInfo: "Misskey je překládán do jiných jazyků dobrovolníkama. Můžete pomoci na {link}." +manageAccessTokens: "Spravovat přístupové tokeny" +accountInfo: "Informace o účtu" +notesCount: "Počet poznámek" +repliesCount: "Počet odeslaných odpovědí" +renotesCount: "Počet přeposlaných poznámek" +repliedCount: "Počet přijatých odpovědí" +renotedCount: "Počet přijatých přeposlaných poznámek" +followingCount: "Počet sledovaných účtů" +followersCount: "Počet sledujících" +sentReactionsCount: "Počet odeslaných reakcí" +receivedReactionsCount: "Počet přijatých reakcí" +pollVotesCount: "Počet odeslaných anketových hlasů" +pollVotedCount: "Počet přijatých anketových hlasů" yes: "Ano" no: "Ne" +driveFilesCount: "Počet souborů na disku" +driveUsage: "Využití disku" +noCrawle: "Odmítat indexování crawleru" +noCrawleDescription: "Požádat vyhledávače aby neindexovali váš profil, poznámky, stránky, atd." +lockedAccountInfo: "Pokud nenastavíte viditelnost poznámek na \"Pouze pro sledující\", budou poznámky viditelné všem i přesto že vyžadujete manuální potvrzení pro sledování." +alwaysMarkSensitive: "Výchozně označovat jako citlivý" +loadRawImages: "Načítat originální obrázky místo náhledů" +disableShowingAnimatedImages: "Nepřehrávat animované obrázky" +verificationEmailSent: "Ověřovací email byl zaslán. Ověření dokončíte kliknutím na odkaz v emailu." notSet: "Není nastaveno" emailVerified: "Váš e-mail byl ověřen" +noteFavoritesCount: "Počet oblíbených poznámek" +pageLikesCount: "Počet oblíbených stránek" +pageLikedCount: "Počet přijatých \"Libí se mi\"" contact: "Kontakt" useSystemFont: "Použít výchozí font systému" clips: "Oříznout" experimentalFeatures: "Experimentální funkce" +experimental: "Experimentální" +thisIsExperimentalFeature: "Tohle je experimentální funkce. Její funkce se může změnit a nemusí fungovat tak, jak bylo zamýšleno." developer: "Vývojář" +makeExplorable: "Udělat účet viditelný v \"Objevit\"" +makeExplorableDescription: "Pokud tohle vypnete, tak se účet přestane zobrazovat v sekci \"Objevit\"." +showGapBetweenNotesInTimeline: "Zobrazit mezeru mezi příspěvkama na časové ose" duplicate: "Duplikovat" left: "Vlevo" center: "Uprostřed" wide: "Široké" narrow: "Úzké" +reloadToApplySetting: "Tohle nastavení se použije až po obnovení stránky. Obnovit teď?" +needReloadToApply: "K projevení nastavení je zapotřebí obnovit stránku." +showTitlebar: "Zobrazit řádek s nadpisem" clearCache: "Vyprázdnit mezipaměť" +onlineUsersCount: "{n} uživatelů je online" nUsers: "{n} užívatelů" nNotes: "{n} poznámek" +sendErrorReports: "Odesílat chybové záznamy" +sendErrorReportsDescription: "Pokud je tato funkce zapnutá, budou se při výskytu problému sdílet podrobné informace o chybách se službou Misskey, což pomůže zlepšit kvalitu služby Misskey. Tyto informace budou zahrnovat například verzi operačního systému, jaký prohlížeč používáte, vaši aktivitu v Misskey atd." myTheme: "Moje vzhledy" backgroundColor: "Pozadí" accentColor: "Akcent" textColor: "Barva textu" saveAs: "Uložit jako…" advanced: "Pokročilé" +advancedSettings: "Pokročilá nastavení" value: "Hodnota" createdAt: "Vytvořeno" updatedAt: "Upraveno" @@ -533,7 +748,35 @@ saveConfirm: "Uložit změny?" deleteConfirm: "Opravdu smazat?" invalidValue: "Neplatná hodnota." registry: "Registr" +closeAccount: "Uzavřít účet" +currentVersion: "Aktuální verze" +latestVersion: "Nejnovější verze" +youAreRunningUpToDateClient: "Používáte nejnovější verzi klienta." +newVersionOfClientAvailable: "Nová verze klienta je k dispozici." +usageAmount: "Využití" +capacity: "Kapacita" +inUse: "Používáno" +editCode: "Upravit kód" +apply: "Potvrdit" +receiveAnnouncementFromInstance: "Dostávat oznámení z téhle instance" +emailNotification: "Emailové oznámení" +publish: "Zveřejnit" +inChannelSearch: "Vyhledat v kanálech" +useReactionPickerForContextMenu: "Otevřít výběr reakce na kliknutí pravého tlačítka myši" +typingUsers: "{users} píše..." +jumpToSpecifiedDate: "Skočit do konkrétního datumu" +showingPastTimeline: "Právě je zobrazována stará časová osa" +clear: "Vrátit" +markAllAsRead: "Označit všechno jako přečtené" +goBack: "Zpět" +unlikeConfirm: "Opravdu chcete odstranit like?" +fullView: "Plné zobrazení" +quitFullView: "Odejít z plného zobrazení" +addDescription: "Přidat popis" +userPagePinTip: "Zde můžete zobrazovat poznámky vybráním \"Připnout na profil\" z menu jednotlivých poznámek." +notSpecifiedMentionWarning: "Tahle poznámka zmiňuje uživatele, které nejsou mezi adresáty" info: "Informace" +userInfo: "Informace o uživateli" unknown: "Neznámý" onlineStatus: "Online status" hideOnlineStatus: "Skrýt Váš online status" @@ -553,10 +796,18 @@ user: "Uživatelé" administration: "Administrace" accounts: "Účty" switch: "Přepnout" +noMaintainerInformationWarning: "Informace o správci nejsou nastavené" +noBotProtectionWarning: "Ochrana proti botům není nastavena" configure: "Nastavit" +postToGallery: "Vytvořit nový příspěvek v galerii" +postToHashtag: "Přidat příspěvek k tomuhle hastagu" gallery: "Galerie" recentPosts: "Poslední příspěvky" +popularPosts: "Populární příspěvky" +shareWithNote: "Sdílet s poznámkou" ads: "Reklamy" +expiration: "Ukončit hlasování" +startingperiod: "Začátek" memo: "Memo" priority: "Priorita" high: "Vysoká" @@ -564,63 +815,701 @@ middle: "Střední" low: "Nízká" emailNotConfiguredWarning: "E-mailová adresa není nastavena." ratio: "Poměr" +previewNoteText: "Zobrazit náhled" +customCss: "Vlastní CSS" +customCssWarn: "Tohle nastavení by mělo být použito pouze v případě pokud víte co děláte. Vložením nesprávných hodnot může způsobit nefunkčnost klienta." global: "Globální" +squareAvatars: "Zobrazovat čtvercové avatary" sent: "Odeslat" +received: "Přijaté" +searchResult: "Výsledky hledání" hashtags: "Hashtagy" troubleshooting: "Poradce při potížích" +useBlurEffect: "Použít efekt rozostření v UI" +learnMore: "Zjistit více" +misskeyUpdated: "Misskey byl aktualizován!" whatIsNew: "Zobrazit změny" translate: "Přeložit" +translatedFrom: "Přeloženo z {x}" +accountDeletionInProgress: "Smazání účtu právě probíhá" +usernameInfo: "Jméno které identifikuje váš účet od jiných na tomhle serveru. Můžete použít abecedu (a~z, A~Z), čísla (0~9) nebo podtržítka (_). Uživatelské jména nemůžou být změněna později." +aiChanMode: "Režim Ai" +devMode: "Vývojářský režim" +keepCw: "Zachovat varování o obsahu" +pubSub: "Pub/Sub účty" +lastCommunication: "Poslední komunikace" +resolved: "Vyřešeno" +unresolved: "Nevyřešené" +breakFollow: "Odstranit sledujícího" +breakFollowConfirm: "Opravdu chcete odstranit tohodle sledujícího?" +itsOn: "Zapnuto" +itsOff: "Vypnuto" +on: "Zapnuto" +off: "Vypnuto" +emailRequiredForSignup: "Vyžadovat email pro registraci" +unread: "Nepřečtený" +filter: "Filtr" +controlPanel: "Ovládací panel" +manageAccounts: "Spravovat účty" +makeReactionsPublic: "Nastavit historii reakcí jako veřejnou" +makeReactionsPublicDescription: "Tohle zviditelný seznam vašich předchozích reakcí veřejně." +classic: "Klasický" +muteThread: "Ztlumit vlákno" +unmuteThread: "Zrušit ztlumení vlákna" +ffVisibility: "Viditelnost Sledovaných/Sledujících" +ffVisibilityDescription: "Umožní vám nastavit kdo uvidí koho sledujete a kdo vás sleduje." +continueThread: "Zobrazit pokračování vlákna" +deleteAccountConfirm: "Tohle nenávratně smaže váš účet, chcete pokračovat?" +incorrectPassword: "Nesprávné heslo." +voteConfirm: "Potvrdit hlas pro \"{choice}\"?" hide: "Skrýt" +useDrawerReactionPickerForMobile: "Zobrazit výběr reakcí jako šuplík na mobilním zařízení" +welcomeBackWithName: "Vítejte zpět, {name}" +clickToFinishEmailVerification: "Prosíme klikněte na [{ok}] pro dokončení ověření emailu." +overridedDeviceKind: "Typ zařízení" smartphone: "Telefon" tablet: "Tablet" auto: "Auto" +themeColor: "Barva motivu" size: "Velikost" numberOfColumn: "Počet sloupců" searchByGoogle: "Vyhledávání" +instanceDefaultLightTheme: "Výchozí světlý motiv instance" +instanceDefaultDarkTheme: "Výhozí tmavý motiv instance" +instanceDefaultThemeDescription: "Zadejte kód motivu v objektovém formátu" +mutePeriod: "Délka ztlumení" +period: "Časový limit" indefinitely: "Navždy" tenMinutes: "10 minut" oneHour: "1 hodina" oneDay: "1 den" oneWeek: "1 týden" +oneMonth: "1 měsíc" reflectMayTakeTime: "Může trvat nějakou dobu, než se projeví změny." +failedToFetchAccountInformation: "Nepodařily se načíst informace o účtě" +rateLimitExceeded: "Překročení rychlostního limitu" cropImage: "Oříznout obrázek" +cropImageAsk: "Chcete oříznout tenhle obrázek?" +cropYes: "Uříznout" +cropNo: "Použít tak jak je" file: "Soubor(ů)" recentNHours: "Posledních {n} hodin" recentNDays: "Posledních {n} dnů" +noEmailServerWarning: "Emailový server není nastavený" +thereIsUnresolvedAbuseReportWarning: "Jsou k dispozici nevyřešené nahlášení zneužití" recommended: "Doporučeno" +check: "Zkontrolovat" +driveCapOverrideLabel: "Změnit velikost disku pro tohoto uživatele" +driveCapOverrideCaption: "K vyresetování velikosti na výchozí hodnotu zadejte hodnotu 0 nebo nižší." +requireAdminForView: "Pro zobrazení se musíte přihlásit administrátorským účtem." +isSystemAccount: "Účet automaticky vytvořený a ovládaný serverem." +typeToConfirm: "Prosíme zadejte {x} pro potvrzení" deleteAccount: "Odstranit účet" document: "Dokumentace" +numberOfPageCache: "Počet stránek uložených v mezipaměti" +numberOfPageCacheDescription: "Zvýšením čísla zlepšíte pohodlí pro uživatele ale může to způsobit větší zátěž na server a na paměť." logoutConfirm: "Opravdu se chcete odhlásit?" +lastActiveDate: "Naposledy použito" +statusbar: "Stavový řádek" pleaseSelect: "Vybrat možnost" reverse: "Otočit" colored: "Barevné" +refreshInterval: "Interval obnovení" +label: "Popisek" type: "Typ" speed: "Rychlost" slow: "Pomalá" fast: "Rychlá" +sensitiveMediaDetection: "Detekce citlivého média" +localOnly: "Jenom lokální" +remoteOnly: "Jenom vzdáleně" +failedToUpload: "Nahrání se nezdařilo" +cannotUploadBecauseInappropriate: "Tenhle soubor se nenahrál, protože některé části byly detekovány jako nevhodné." +cannotUploadBecauseNoFreeSpace: "Nahrání se nezdařilo z důvodu nedostatku místa na disku." +cannotUploadBecauseExceedsFileSizeLimit: "Tenhle soubor nemůže být nahráný protože překračuje velikostní limit." +beta: "Beta verze" +enableAutoSensitive: "Automaticky označovat jako citlivé" +enableAutoSensitiveDescription: "Umožňuje automatickou detekci a označování citlivého média skrze strojového účení všude kde je možno. I pokud je tahle možnost vypnutá, může být povolena instancí." +activeEmailValidationDescription: "Umožňuje striktní validaci emailové adresy, která zahrnuje kontrolu pro jednorázové adresy a pokud je možno s ní komunikovat. Pokud je to vypnuté, bude se kontrolovat pouze formát emailu." +navbar: "Navigační panel" +shuffle: "Zamíchat" account: "Účty" +move: "Přesunout" +pushNotification: "Push oznámení" +subscribePushNotification: "Povolit push oznamení" +unsubscribePushNotification: "Vypnout push oznámení" +pushNotificationAlreadySubscribed: "Push oznámení jsou už zapnuté" +pushNotificationNotSupported: "Tenhle prohlížeč nepodporuje push oznámení" +sendPushNotificationReadMessage: "Odstraněnit oznámení push po jejich přečtení" +sendPushNotificationReadMessageCaption: "Tohle může zvýšit spotřebu energie vašeho zařízení." +windowMaximize: "Maximalizovat" +windowMinimize: "Minimalizovat" +windowRestore: "Obnovit" +caption: "Titulek" +loggedInAsBot: "Právě jste přihlášen jako bot" +tools: "Nástroje" +cannotLoad: "Načtení se nezdařilo" +numberOfProfileView: "Počet zobrazení profilu" +like: "To se mi líbí" +unlike: "Už se mi to nelíbí" +numberOfLikes: "Počet \"To se mi líbí\"" show: "Zobrazit" +neverShow: "Znovu nezobrazovat" +remindMeLater: "Možná později" +didYouLikeMisskey: "Oblíbili jste si Misskey?" +pleaseDonate: "{host} používá bezplatný software Misskey. Velmi bychom ocenili vaše dary, aby mohl vývoj Misskey pokračovat!" +roles: "Role" +role: "Role" +noRole: "Role nenalezena" +normalUser: "Normální uživatel" +undefined: "Neurčeno" +assign: "Přiřadit" +unassign: "Zrušit přirazení" color: "Barva" +manageCustomEmojis: "Spravovat vlastní emoji" +youCannotCreateAnymore: "Narazili jste na limit pro vytváření." +cannotPerformTemporary: "Dočasně nedostupné" +cannotPerformTemporaryDescription: "Tuto akci nelze dočasně provést z důvodu překročení limitu provedení. Chvíli počkejte a zkuste to znovu." +invalidParamError: "Neplatné parametry" +invalidParamErrorDescription: "Parametry požadavku jsou neplatné. Obvykle je to způsobeno chybou, ale může to být také způsobeno překročením limitů velikosti vstupů nebo podobně." +permissionDeniedError: "Operace zamítnuta" +permissionDeniedErrorDescription: "Tento účet nemá oprávnění k provedení této akce." +preset: "Předvolba" +selectFromPresets: "Vybrat z předvoleb" +achievements: "Úspěchy" +gotInvalidResponseError: "Neplatná odpověď serveru" +gotInvalidResponseErrorDescription: "Server může být nedostupný nebo na něm probíhá údržba. Zkuste to prosím později." +thisPostMayBeAnnoying: "Tato poznámka může ostatní obtěžovat." +thisPostMayBeAnnoyingHome: "Zveřejnit na domovskou časovou osu" +thisPostMayBeAnnoyingCancel: "Zrušit" +thisPostMayBeAnnoyingIgnore: "I přesto zveřejnit" +collapseRenotes: "Sbalit poznámky, které jste již viděli" +internalServerError: "Interní chyba serveru" +internalServerErrorDescription: "Server narazil na neočekávanou chybu." +copyErrorInfo: "Zkopírovat detaily erroru" +joinThisServer: "Zaregistrovat se v této instanci" +exploreOtherServers: "Podívat se na ostatní instance" +letsLookAtTimeline: "Podívejte se na časovou osu" +disableFederationConfirm: "Chcete opravdu vypnout federace?" +disableFederationConfirmWarn: "I v případě defederace budou příspěvky nadále veřejné, pokud nebude nastaveno jinak. Obvykle to není nutné." +disableFederationOk: "Vypnout" +invitationRequiredToRegister: "Tahle instance je pouze na pozvánku. Musíte zadat validní kód pozvánky." +emailNotSupported: "Tahle instance nepodporuje zasílání emailů" +postToTheChannel: "Vložit do kanálu" +cannotBeChangedLater: "Tohle nemůže být změněno později." +reactionAcceptance: "Přijímání reakcí" +likeOnly: "Jenom \"oblíbené\"" +likeOnlyForRemote: "Všechny (Pouze \"oblíbené\" pro vzdálenou instanci)" +nonSensitiveOnly: "Pouze bez citlivých medií" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "Pouze bez citlivých medií (Pouze vzdálený \"oblíbený\")" +rolesAssignedToMe: "Přiřazené role ke mně" +resetPasswordConfirm: "Opravdu chcete resetovat heslo?" +sensitiveWords: "Citlivá slova" +sensitiveWordsDescription: "Viditelnost všech poznámek obsahujících některé z nakonfigurovaných slov bude automaticky nastavena na \"Domů\". Můžete jich uvést více tak, že je oddělíte pomocí řádků." +sensitiveWordsDescription2: "Použití mezer vytvoří výrazy AND a obklopení klíčových slov lomítky je změní na regulární výraz." +notesSearchNotAvailable: "Vyhledávání poznámek je nedostupné." +license: "Licence" +unfavoriteConfirm: "Opravdu chcete odstranit z oblíbených?" +myClips: "Moje klipy" +drivecleaner: "Čistič disku" +retryAllQueuesNow: "Obnovit všechny běžící fronty" +retryAllQueuesConfirmTitle: "Opravdu chcete obnovit všechno?" +retryAllQueuesConfirmText: "Tohle dočasně zvýší zatěž na server." +enableChartsForRemoteUser: "Vygenerovat grafy dat vzdálených uživatelů" +enableChartsForFederatedInstances: "Vygenerovat grafy dat vzdálených instancí" +showClipButtonInNoteFooter: "Přidat \"Připnout\" do akčního menu poznámky" +noteIdOrUrl: "ID nebo URL poznámky" +video: "Video" +videos: "Videa" +dataSaver: "Spořič dat" +accountMigration: "Migrace účtu" +accountMoved: "Tenhle uživatel se přesunul na nový účet:" +accountMovedShort: "Tenhle účet byl migrován." +operationForbidden: "Zakázaná operace" +forceShowAds: "Vždycky zobrazovat reklamy" +addMemo: "Přidat memo" +editMemo: "Upravit memo" +reactionsList: "Reakce" +renotesList: "Poznámky" +notificationDisplay: "Oznámení" +leftTop: "Vlevo nahoře" +rightTop: "Vpravo nahoře" +leftBottom: "Vlevo dole" +rightBottom: "Vpravo dole" +stackAxis: "Směr ukládání" +vertical: "Svisle" +horizontal: "Vodorovně" +position: "Pozice" +serverRules: "Pravidla serveru" +pleaseConfirmBelowBeforeSignup: "Abyste se mohli přihlásit na server, musíte souhlasit s následujícím." +pleaseAgreeAllToContinue: "Musíte souhlasit se vším abyste mohli pokračovat." +continue: "Pokračovat" +preservedUsernames: "Rezervované uživatelské jména" +preservedUsernamesDescription: "Seznam uživatelských jmén na rezervaci oddělené mezerama. Tyhle jména se potom nebudou moc použít při normálním procesu vytvoření účtu ale můžou být použiti manuálně administratorém. Existujících účtů se to nedotkne." +createNoteFromTheFile: "Vytvořit poznámku z tohodle souboru" +archive: "Archiv" +channelArchiveConfirmTitle: "Opravdu chcete archivovat {name}?" +channelArchiveConfirmDescription: "Archivovaný kanál se objeví v seznamu kanálů nebo ve výsledcích hledání. Nové poznámky se nedají vložit do seznamu." +thisChannelArchived: "Tenhle kanál je archivovaný" +displayOfNote: "Zobrazit poznámku" +initialAccountSetting: "Nastavení profilu" +youFollowing: "Sleduji" +preventAiLearning: "Odmítnout použití v strojovém učení (Generative AI)" +preventAiLearningDescription: "Požaduje, aby prohlížeče nepoužívaly zveřejněný textový nebo obrazový materiál atd. v datových sadách pro strojové učení (prediktivní / generativní umělá inteligence). Toho se dosáhne přidáním příznaku \"noai\" HTML-Response k příslušnému obsahu. Úplné prevence však tímto příznakem nelze dosáhnout, protože může být jednoduše ignorován." +options: "Možnosti" +specifyUser: "Upřesnit uživatele" +failedToPreviewUrl: "Náhled se nezdařil" +update: "Aktualizovat" +rolesThatCanBeUsedThisEmojiAsReaction: "Role, které můžou tuhle emoji použít jako reakci" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Pokud nejsou určena role, tak pak každý může použít tenhle emoji." +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Role musí být veřejné." +cancelReactionConfirm: "Opravdu chcete odstranit vaší reakci?" +changeReactionConfirm: "Opravdu chcete změnit vaši reakci?" +later: "Později" +goToMisskey: "Jít na Misskey" +additionalEmojiDictionary: "Další slovníky emoji" +installed: "Nainstalováno" +branding: "Značka" +enableServerMachineStats: "Zveřejněnit statistiky hardwaru serveru" +enableIdenticonGeneration: "Povolit generování identicon uživatele" +turnOffToImprovePerformance: "Vypnutí této funkce může zvýšit výkon." +createInviteCode: "Vygenerovat pozvánku" +createWithOptions: "Vygenerovat s nastavením" +createCount: "Počet vytvořených pozvánek" +inviteCodeCreated: "Pozvánka vygenerována" +inviteLimitExceeded: "Překročili jste limit pozvánek, které můžete vygenerovat." +createLimitRemaining: "Limit pozvánek: {limit} zbývá" +inviteLimitResetCycle: "Tento limit se obnoví na hodnotu {limit} v {time}." +expirationDate: "Datum expirace" +noExpirationDate: "Bez expirace" +inviteCodeUsedAt: "Kód pozvánky použitý na" +registeredUserUsingInviteCode: "Pozvánku používá" +waitingForMailAuth: "Čeká se na ověření emailu" +inviteCodeCreator: "Pozvánku vytvořil" +usedAt: "Používá se v" +unused: "Nepoužívaná" +used: "Používaná" +expired: "Prošlá" +doYouAgree: "Souhlasíte?" +beSureToReadThisAsItIsImportant: "Přečtěte si prosím tyto důležité informace." +iHaveReadXCarefullyAndAgree: "Přečetl jsem si text \"{x}\" a souhlasím s ním." +icon: "Avatar" +replies: "Odpovědět" +renotes: "Přeposlat" +_initialAccountSetting: + accountCreated: "Váš účet byl úspěšně vytvořen!" + letsStartAccountSetup: "Pro začátek si nastavte svůj profil." + letsFillYourProfile: "Nejprve si nastavte svůj profil." + profileSetting: "Nastavení profilu" + privacySetting: "Nastavení soukromí" + theseSettingsCanEditLater: "Tato nastavení můžete vždy později změnit." + youCanEditMoreSettingsInSettingsPageLater: "Na stránce \"Nastavení\" můžete nakonfigurovat mnoho dalších nastavení. Nezapomeňte ji navštívit později." + followUsers: "Zkuste sledovat některé uživatele, kteří vás zajímají pro vystavění časový osy." + pushNotificationDescription: "Povolení push oznámení vám umožní přijímat oznámení od {name} přímo ve vašem zařízení." + initialAccountSettingCompleted: "Nastavení profilu dokončeno!" + haveFun: "Užívejte {name}!" + ifYouNeedLearnMore: "Pokud se chcete dozvědět více o tom, jak používat {name} (Misskey), navštivte {link}." + skipAreYouSure: "Opravdu chcete přeskočit nastavení profilu?" + laterAreYouSure: "Opravdu chcete provést nastavení profilu později?" +_serverRules: + description: "Soubor pravidel, která se zobrazí před registrací. Doporučuje se nastavit shrnutí podmínek služby." +_serverSettings: + iconUrl: "URL ikony" +_accountMigration: + moveFrom: "Migrace jiného účtu na tento účet" + moveFromSub: "Vytvořit alias na jiný účet" + moveFromLabel: "Původní účet #{n}" + moveFromDescription: "Pro účet, ze kterého se chcete přesunout, musíte vytvořit alias na tomto účtu.\nZadejte účet, ze kterého chcete přejít, v následujícím formátu: @username@server.example.com\nChcete-li alias odstranit, ponechte pole prázdné (nedoporučuje se)." + moveTo: "Přesunout tenhle účet do jiného" + moveToLabel: "Cílový účet pro přesunutí:" + moveCannotBeUndone: "Migrace účtu nemůže být vrácena." + moveAccountDescription: "Tím dojde k migraci vašeho účtu na jiný účet.\n ・Sledovatelé z tohoto účtu budou automaticky převedeni na nový účet.\n ・Tento účet zruší sledování všech uživatelů, které aktuálně sleduje.\n ・Na tomto účtu nebude možné vytvářet nové poznámky atd.\n\nZatímco migrace sledovaných uživatelů probíhá automaticky, pro migraci seznamu sledovaných uživatelů je nutné připravit některé kroky ručně. Za tímto účelem proveďte export sledovaných, který později naimportujete na nový účet v nabídce nastavení. Stejný postup platí pro seznamy i pro ztlumené a zablokované uživatele.\n\n(Tento výklad platí pro Misskey v13.12.0 a novější. Jiný software ActivityPub, například Mastodon, může fungovat jinak.)" + moveAccountHowTo: "Chcete-li migrovat, vytvořte nejprve alias tohoto účtu na účtu, na který chcete přejít.\nPo vytvoření aliasu zadejte účet, na který chcete přejít, v následujícím formátu: @username@server.example.com" + startMigration: "Migrovat" + migrationConfirm: "Opravdu chcete migrovat tento účet na {account}? Jednou zahájený proces nelze zastavit ani vrátit zpět a tento účet již nebudete moci používat v původním stavu." + movedAndCannotBeUndone: "\nTento účet byl převeden.\nMigraci nelze vrátit zpět." + postMigrationNote: "Tento účet zruší sledování všech účtů, které aktuálně sleduje, 24 hodin po dokončení migrace.\nPočet sledujících i následovníků se poté vynuluje. Aby se zabránilo tomu, že vaši sledující nebudou moci vidět příspěvky tohoto účtu určené pouze pro sledující, budou však tento účet sledovat i nadále." + movedTo: "Cílový účet pro přesunutí:" +_achievements: + earnedAt: "Odemčeno v" + _types: + _notes1: + title: "Dobrý den Misskey!" + description: "Zveřejněte vaší první poznámku" + flavor: "Užijte si to s Misskey!" + _notes10: + title: "Pár poznámek" + description: "Zveřejněte 10 poznámek" + _notes100: + title: "Hodně poznámek" + description: "Zveřejněte 100 poznámek" + _notes500: + title: "Zahlcen poznámkama" + description: "Zveřejněte 500 poznámek" + _notes1000: + title: "Hora poznámek" + description: "Zveřejněte 1000 poznámek" + _notes5000: + title: "Přetékající poznámky" + description: "Zveřejněte 5000 poznámek" + _notes10000: + title: "Super poznámka" + description: "Zveřejněte 10 000 poznámek" + _notes20000: + title: "Potřebuju... více... poznámek..." + description: "Zveřejněte 20 000 poznámek" + _notes30000: + title: "Poznámky, poznámky, POZNÁMKY!" + description: "Zveřejněte 30 000 poznámek" + _notes40000: + title: "Továrna na poznámky" + description: "Zveřejněte 40 000 poznámek" + _notes50000: + title: "Planeta poznámek" + description: "Zveřejněte 50 000 poznámek" + _notes60000: + title: "Poznámkový kvasar" + description: "Zveřejněte 60 000 poznámek" + _notes70000: + title: "Černá díra poznámek" + description: "Zveřejněte 70 000 poznámek" + _notes80000: + title: "Galaxie poznámek" + description: "Zveřejněte 80 000 poznámek" + _notes90000: + title: "Vesmír poznámek" + description: "Zveřejněte 90 000 poznámek" + _notes100000: + title: "ALL YOUR NOTE ARE BELONG TO US" + description: "Zveřejněte 100 000 poznámek" + flavor: "Máte toho hodně co říct." + _login3: + title: "Začátečník I" + description: "Přihlaste se celkově za 3 dny" + flavor: "Ode dneška mi říkejte Misskista." + _login7: + title: "Začátečník II" + description: "Přihlaste se celkově za 7 dní" + flavor: "Máte pocit, že už jste se v tom vyznali?" + _login15: + title: "Začátečník III" + description: "Přihlaste se celkově za 15 dní" + _login30: + title: "Misskista I" + description: "Přihlaste se celkově za 30 dní" + _login60: + title: "Misskista II" + description: "Přihlaste se celkově za 60 dní" + _login100: + title: "Misskista III" + description: "Přihlaste se celkově za 100 dní" + flavor: "Violent Misskista" + _login200: + title: "Stálý zákazník I" + description: "Přihlaste se celkově za 200 dní" + _login300: + title: "Stálý zákazník II" + description: "Přihlaste se celkově za 300 dní" + _login400: + title: "Stálý zákazník III" + description: "Přihlaste se celkově za 400 dní" + _login500: + title: "Expert I" + description: "Přihlaste se celkově za 500 dní" + flavor: "Moji přátelé, často se říká, že mám rád poznámky." + _login600: + title: "Expert II" + description: "Přihlaste se celkově za 600 dní" + _login700: + title: "Expert III" + description: "Přihlaste se celkově za 700 dní" + _login800: + title: "Mistr poznámek I" + description: "Přihlaste se celkově za 800 dní" + _login900: + title: "Mistr poznámek II" + description: "Přihlaste se celkově za 900 dní" + _login1000: + title: "Mistr poznámek III" + description: "Přihlaste se celkově za 1000 dní" + flavor: "Děkujeme, že používáte Misskey!" + _noteClipped1: + title: "Musím... připnout..." + description: "Připněte si první poznámku" + _noteFavorited1: + title: "Hvězdář" + description: "Oblíbena první poznámka" + _myNoteFavorited1: + title: "Hledání hvězd" + description: "Někdo si oblíbil jednu z vašich poznámek" + _profileFilled: + title: "Dobře připravený" + description: "Nastavte si profil" + _markedAsCat: + title: "Já jsem kočka" + description: "Označte váš účet \"jako kočka\"" + flavor: "Jméno ti dám později." + _following1: + title: "Sledujte prvního uživatele" + description: "Sledujte uživatele" + _following10: + title: "Drž se... drž se..." + description: "Sledujte 10 uživatelů" + _following50: + title: "Hodně přátel" + description: "Sledujte 50 uživatelů" + _following100: + title: "100 přátel" + description: "Sledujte 100 uživatelů" + _following300: + title: "Přetížení přátel" + description: "Sledujte 300 účtů" + _followers1: + title: "První sledující" + description: "Získejte 1 sledujícího" + _followers10: + title: "Sledujte mě!" + description: "Získejte 10 sledujících" + _followers50: + title: "Přicházejí davy" + description: "Získejte 50 sledujících" + _followers100: + title: "Populární" + description: "Získejte 100 sledujících" + _followers300: + title: "Prosíme srovnejte se do jedné řady!" + description: "Získejte 300 sledujících" + _followers500: + title: "Rádiová věž" + description: "Získejte 500 sledujících" + _followers1000: + title: "Influencer" + description: "Získejte 1000 sledujících" + _collectAchievements30: + title: "Sběratel úspěchů" + description: "Získejte 30 úspěchů" + _viewAchievements3min: + title: "Máš rád úspěchy" + description: "Koukejte na váš seznam úspěchů alespoň po dobu 3 minut" + _iLoveMisskey: + title: "Miluju Misskey" + description: "Zveřejněte \" I ❤ #Misskey\"" + flavor: "Vývojový tým Misskey si velmi váží vaší podpory!" + _foundTreasure: + title: "Hon za pokladem" + description: "Našli jste schovaný poklad!" + _client30min: + title: "Krátká pauza" + description: "Mějte otevřený Misskey alespoň po dobu 30 minut" + _client60min: + title: "Žádný \"Miss\" v Misskey" + description: "Mějte otevřený Misskey alespoň po dobu 60 minut" + _noteDeletedWithin1min: + title: "Ups, nevadí" + description: "Vymažte poznámku během minuty co ji zveřejníte" + _postedAtLateNight: + title: "Noční typ" + description: "Zveřejněte poznámku pozdě v noci" + flavor: "Je nejvyšší čas jít spát." + _postedAt0min0sec: + title: "Mluvící hodiny" + description: "Zveřejněte poznámku přesně v 00:00" + flavor: "Klik Klik Klik Bum" + _selfQuote: + title: "Sebereference" + description: "Citujte vlastní poznámku" + _htl20npm: + title: "Plynoucí časová osa" + description: "Mějte rychlost vaší domovské časové osy vyšší než 20 pzm (poznámek za minutu)." + _viewInstanceChart: + title: "Analytik" + description: "Zobrazte graf instance" + _outputHelloWorldOnScratchpad: + title: "Hello, world!" + description: "Dostaňte výpis \"hello world\" do Scratchpadu" + _open3windows: + title: "Splitscreen" + description: "Mějte otevřená alespoň 3 okna zároveň" + _driveFolderCircularReference: + title: "Okružní reference" + description: "Pokuste se o vytvoření rekurzivně vnořené složky v disku" + _reactWithoutRead: + title: "Opravdu jste to četl/a?" + description: "Reagujte na poznámku, která má více než 100 znaků, do 3 sekund od jejího zveřejnění." + _clickedClickHere: + title: "Klikněte sem" + description: "Kliknul si tam" + _justPlainLucky: + title: "Čisté štěstí" + description: "Mějte šanci na získání s pravděpodobností 0,005 % každých 10 sekund." + _setNameToSyuilo: + title: "Boží komplex" + description: "Nastavte si jméno na \"syuilo\"" + _passedSinceAccountCreated1: + title: "Roční výročí" + description: "Od vytvoření vašeho účtu uplynul jeden rok" + _passedSinceAccountCreated2: + title: "Dvouleté výročí" + description: "Od vytvoření vašeho účtu uplynuly dva roky" + _passedSinceAccountCreated3: + title: "Tříleté výročí" + description: "Od vytvoření vašeho účtu uplynuly tři roky" + _loggedInOnBirthday: + title: "Všechno nejlepší!" + description: "Přihlašte se v den vašich narozenin" + _loggedInOnNewYearsDay: + title: "Štastný nový rok!" + description: "Přihlašte se v den nového roku" + flavor: "Na další skvělý rok v této instanci" + _cookieClicked: + title: "Hra, ve které klikáte na sušenky" + description: "Klikněte na soubor cookie" + flavor: "Počkejte, jste na správné webové stránce?" + _brainDiver: + title: "Brain Diver" + description: "Zveřejněte odkaz na Brain Diver" + flavor: "Misskey-Misskey La-Tu-Ma" _role: + new: "Nová role" + edit: "Upravit roli" + name: "Název role" + description: "Popis role" + permission: "Oprávnění role" + descriptionOfPermission: "Moderators může provádět základní operace moderování.\nAdministrators může měnit všechna nastavení instance." + assignTarget: "Přiřadit" + descriptionOfAssignTarget: "Manual ručně změnit, kdo je součástí této role a kdo ne.\nConditional mít uživatelé automaticky přiřazováni a odebíráni z této role na základě podmínky." + manual: "Dokumentace" + conditional: "Podmíněné" + condition: "Podmínky" + isConditionalRole: "Tato role je podmíněná." + isPublic: "Veřejná role" + descriptionOfIsPublic: "Tato role se zobrazí v profilech přiřazených uživatelů." + options: "Nastavení" + policies: "Zásady" + baseRole: "Šablona role" + useBaseValue: "Použít hodnotu šablony role" + chooseRoleToAssign: "Vyberte roli, kterou chcete přiřadit" + iconUrl: "URL ikony" + asBadge: "Zobrazovat jako odznak" + descriptionOfAsBadge: "Ikona této role se zobrazí vedle uživatelského jména uživatelů s touto rolí, pokud je zapnuta." + isExplorable: "Udělat roli objevitelnou" + descriptionOfIsExplorable: "Časová osa této role a seznam uživatelů s touto rolí budou zveřejněny, pokud jsou povoleny." + displayOrder: "Pozice" + descriptionOfDisplayOrder: "Čím vyšší číslo, tím vyšší pozice v uživatelském rozhraní." + canEditMembersByModerator: "Umožnit moderátorům upravovat seznam členů pro tuto roli" + descriptionOfCanEditMembersByModerator: "Po zapnutí této role budou moci moderátoři i administrátoři přiřazovat a odebírat uživatele do této role. Pokud je tato funkce vypnutá, budou moci uživatele přiřazovat pouze správci." priority: "Priorita" _priority: low: "Nízká" middle: "Střední" high: "Vysoká" + _options: + gtlAvailable: "Může zobrazit globální časovou osu" + ltlAvailable: "Může zobrazit místní časovou osu" + canPublicNote: "Může posílat veřejné poznámky" + canInvite: "Může vytvářet kódy pozvánek instance" + inviteLimit: "Limit pozvánek" + inviteLimitCycle: "Limit mezi pozvánkama" + inviteExpirationTime: "Interval vypršení platnosti pozvánky" + canManageCustomEmojis: "Spravovat vlastní emoji" + driveCapacity: "Velikost disku" + alwaysMarkNsfw: "Vždy označovat soubory jako NSFW" + pinMax: "Maximální počet připnutých poznámek" + antennaMax: "Maximální počet antén" + wordMuteMax: "Maximální počet znaků povolených v ztlumených slovech" + webhookMax: "Maximální počet Webhooků" + clipMax: "Maximální počet připnutí" + noteEachClipsMax: "Maximální počet poznámek v připnutí" + userListMax: "Maximální počet seznamů uživatelů" + userEachUserListsMax: "Maximální počet uživatelů v seznamu uživatelů" + rateLimitFactor: "Limit rychlosti" + descriptionOfRateLimitFactor: "Nižší limity rychlosti jsou méně omezující, vyšší více omezující. " + canHideAds: "Může schovat reklamy" + canSearchNotes: "Použití vyhledávání poznámek" + _condition: + isLocal: "Místní uživatel" + isRemote: "Vzdálený uživatel" + createdLessThan: "Od vytvoření účtu uplynulo méně než X" + createdMoreThan: "Od vytvoření účtu uplynulo více než X" + followersLessThanOrEq: "Má X nebo méně sledujících" + followersMoreThanOrEq: "Má X nebo více sledujících" + followingLessThanOrEq: "Sleduje X nebo méně účtů" + followingMoreThanOrEq: "Sleduje X nebo více účtů" + notesLessThanOrEq: "Počet příspěvků je menší než/rovná se" + notesMoreThanOrEq: "Počet příspěvků je větší než/rovná se" + and: "AND kondice" + or: "OR kondice" + not: "NOT kondice" +_sensitiveMediaDetection: + description: "Snižuje náročnost moderování serveru díky automatickému rozpoznávání citlivých médií pomocí strojového učení. Tím se mírně zvýší zatížení serveru." + sensitivity: "Detekce citlivosti" + sensitivityDescription: "Snížení citlivosti povede k menšímu počtu chybných detekcí (falešně pozitivních), zatímco její zvýšení povede k menšímu počtu chybných detekcí (falešně negativních)." + setSensitiveFlagAutomatically: "Označit jako citlivé" + setSensitiveFlagAutomaticallyDescription: "Výsledky interní detekce se zachovají, i když je tato možnost vypnutá." + analyzeVideos: "Povolit analýzy videí" + analyzeVideosDescription: "Kromě obrázků analyzuje i videa. Tím se mírně zvýší zatížení serveru." +_emailUnavailable: + used: "Tato emailová adresa se již používá" + format: "Formát této emailové adresy je neplatný" + disposable: "Jednorázové emailové adresy se nesmí používat" + mx: "Tento e-mailový server je neplatný" + smtp: "Tento emailový server neodpovídá" +_ffVisibility: + public: "Zveřejnit" + followers: "Viditelné pouze pro sledující" + private: "Soukromý" +_signup: + almostThere: "Už to skoro je" + emailAddressInfo: "Zadejte prosím svou emailovou adresu. Nebude zveřejněna." + emailSent: "Na vaši e-mailovou adresu ({email}) byl odeslán potvrzovací e-mail. Kliknutím na přiložený odkaz dokončete vytvoření účtu." +_accountDelete: + accountDelete: "Smazat účet" + mayTakeTime: "Vzhledem k tomu, že odstranění účtu je proces náročný na zdroje, může jeho dokončení trvat určitou dobu v závislosti na tom, kolik obsahu jste vytvořili a kolik souborů jste nahráli." + sendEmail: "Po dokončení odstranění účtu bude na emailovou adresu registrovanou k tomuto účtu zaslán email." + requestAccountDelete: "Žádost o smazání účtu" + started: "Bylo zahájeno mazání." + inProgress: "V současné době probíhá mazání" _ad: back: "Zpět" + reduceFrequencyOfThisAd: "Zobrazovat tuto reklamu méně" + hide: "Schovat" + timezoneinfo: "Den v týdnu se určuje podle časového pásma serveru." +_forgotPassword: + enterEmail: "Zadejte emailovou adresu, kterou jste použili při registraci. Na ni vám pak bude zaslán odkaz, pomocí kterého si můžete obnovit heslo." + ifNoEmail: "Pokud jste při registraci nepoužili email, obraťte se na správce instance." + contactAdmin: "Tato instance nepodporuje používání emailových adres, pro obnovení hesla se obraťte na správce instance." _gallery: my: "Moje galerie" + liked: "Oblíbené příspěvky" + like: "To se mi líbí" + unlike: "Už se mi to nelíbí" _email: _follow: title: "Máte nového následovníka" + _receiveFollowRequest: + title: "Obdrželi jste žádost o sledování" _plugin: install: "Instalovat plugin" + installWarn: "Neinstalujte nedůvěryhodné pluginy." manage: "Správce pluginů" _preferencesBackups: list: "Vytvořit backup" + saveNew: "Uložit novou zálohu" loadFile: "Načíst ze souboru" + apply: "Použít pro toto zařízení" save: "Uložit změny" + inputName: "Zadejte prosím název pro tuto zálohu" + cannotSave: "Uložení selhalo" + nameAlreadyExists: "Záloha s názvem \"{name}\" již existuje. Zadejte prosím jiný název." + applyConfirm: "Opravdu chcete na toto zařízení použít zálohu \"{name}\"? Stávající nastavení tohoto zařízení bude přepsáno." + saveConfirm: "Uložit zálohu jako {name}?" + deleteConfirm: "Odstranit zálohu {name}?" + renameConfirm: "Přejmenovat tuto zálohu z \"{old}\" na \"{new}\"?" + noBackups: "Neexistují žádné zálohy. Nastavení klienta na tomto serveru můžete zálohovat pomocí \"Vytvořit novou zálohu\"." + createdAt: "Vytvořeno v: {date} {time}" + updatedAt: "Aktualizováno: {date} {time}" + cannotLoad: "Načítání selhalo" + invalidFile: "Neplatný typ souboru" _registry: scope: "Rozsah" key: "Klíč" @@ -628,46 +1517,235 @@ _registry: domain: "Doména" createKey: "Vytvořit klíč" _aboutMisskey: + about: "Misskey je open-source software vyvíjený syuilo od roku 2014." + contributors: "Hlavní přispěvatelé" allContributors: "Všichni přispěvatelé" source: "Zdrojový kód" + translation: "Přeložit Misskey" + donate: "Přispějte na Misskey" + morePatrons: "Vážíme si také podpory mnoha dalších pomocníků, kteří zde nejsou uvedeni. Děkujeme! 🥰" + patrons: "Patroni" +_displayOfSensitiveMedia: + respect: "Skrýt média označená jako citlivá" + ignore: "Zobrazit média označená jako citlivá" + force: "Skrýt všechna média" +_instanceTicker: + none: "Nikdy nezobrazovat" + remote: "Zobrazit pro vzdálené uživatelé" + always: "Vždy zobrazovat" +_serverDisconnectedBehavior: + reload: "Automatické znovunačtení" + dialog: "Zobrazení dialogového okna s varováním" + quiet: "Zobrazit nerušivé upozornění" _channel: + create: "Vytvořit kanál" + edit: "Upravit kanál" + setBanner: "Nastavit banner" + removeBanner: "Odstranit banner" featured: "Trendy" + owned: "Vlastněný" + following: "Sledovaný" + usersCount: "{n} Účastníků" + notesCount: "{n} Poznámek" + nameAndDescription: "Název a popis" + nameOnly: "Pouze název" _menuDisplay: + sideFull: "Postranně" + sideIcon: "Postranně (Ikony)" top: "Nahoru" hide: "Skrýt" +_wordMute: + muteWords: "Ztlumená slova" + muteWordsDescription: "Podmínku AND oddělujte mezerami, podmínku OR oddělujte řádkovými zlomy." + muteWordsDescription2: "Chcete-li použít regulární výrazy, obklopte klíčová slova lomítky." + softDescription: "Skrýt poznámky, které splňují nastavené podmínky, z časové osy." + hardDescription: "Zabrání přidání poznámek splňujících nastavené podmínky na časovou osu. Kromě toho nebudou tyto poznámky přidány na časovou osu, ani když se podmínky změní." + soft: "Měkký" + hard: "Tvrdý" + mutedNotes: "Ztlumené poznámky" +_instanceMute: + instanceMuteDescription: "Tímhle se ztlumí všechny poznámky/poznámky z uvedených instancí, včetně poznámek uživatelů, kteří odpovídají uživateli ze ztlumené instance." + instanceMuteDescription2: "Oddělte novými řádky" + title: "Skryje poznámky z uvedených případů." + heading: "Seznam instancí, které mají být ztlumeny" _theme: + explore: "Objevit témata" install: "Nainstalovat vzhled" manage: "Správa vzhledů" code: "Kód vzhledu" description: "Popis" + installed: "{name} byl nainstalován" installedThemes: "Nainstalované vzhledy" + builtinThemes: "Vestavěné temáta" + alreadyInstalled: "Tento vzhled je již nainstalován." + invalid: "Formát tohoto tématu je neplatný" + make: "Vytvořit téma" + base: "Základ" + addConstant: "Přidat konstantu" constant: "Konstanta" defaultValue: "Výchozí hodnota" color: "Barva" + refProp: "Odkázat na vlastnost" + refConst: "Odkázat na konstantu" key: "Klíč" func: "Funkce " + funcKind: "Typ funkce" + argument: "Argument" + basedProp: "Odkazovaná vlastnost" + alpha: "Průhlednost" + darken: "Ztmavit" + lighten: "Zesvětlit" + inputConstantName: "Zadejte název pro tuto konstantu" + importInfo: "Pokud zde zadáte kód motivu, můžete jej importovat do editoru motivu." + deleteConstantConfirm: "Opravdu chcete odstranit konstantu {const}?" keys: + accent: "Akcent" + bg: "Pozadí" + fg: "Text" + focus: "Fokus" + indicator: "Indikátor" + panel: "Panely" shadow: "Stín" header: "Nadpis" + navBg: "Pozadí postranního panelu" + navFg: "Text na postranním panelu" + navHoverFg: "Text na postranním panelu (Hover)" + navActive: "Text na postranním panelu (Aktivní)" + navIndicator: "Indikátor na postranním panelu" link: "Odkaz" hashtag: "Hashtag" mention: "Zmínění" + mentionMe: "Zmínky (mě)" renote: "Přeposlat" + modalBg: "Pozadí Modalu" divider: "Dělící čára" + scrollbarHandle: "Rukojeť posuvníku" + scrollbarHandleHover: "Rukojeť posuvníku (Hover)" + dateLabelFg: "Text štítku s datem" + infoBg: "Pozadí informací" + infoFg: "Text informací" + infoWarnBg: "Pozadí varování" + infoWarnFg: "Text varování" + cwBg: "Pozadí CW tlačítka" + cwFg: "Text CW tlačítka" + cwHoverBg: "Pozadí CW tlačítka (Hover)" + toastBg: "Pozadí oznámení" + toastFg: "Text oznámení" + buttonBg: "Pozadí tlačítka" + buttonHoverBg: "Pozadí tlačítka (Hover)" + inputBorder: "Ohraničení vstupního pole" + listItemHoverBg: "Pozadí položky seznamu (Hover)" + driveFolderBg: "Pozadí složky disku" + wallpaperOverlay: "Překrytí tapety" + badge: "Odznak" + messageBg: "Pozadí chatu" + accentDarken: "Akcent (Ztmavený)" + accentLighten: "Akcent (Zesvětlený)" + fgHighlighted: "Zvýrazněný text" _sfx: note: "Poznámky" + noteMy: "Moje poznámka" notification: "Oznámení" chat: "Zprávy" + chatBg: "Chat (Pozadí)" + antenna: "Antény" + channel: "Oznámení kanálu" _ago: future: "Budoucí" justNow: "Teď" + secondsAgo: "Před {n}s" + minutesAgo: "Před {n}min" + hoursAgo: "Před {n}h" + daysAgo: "Před {n}d" + weeksAgo: "Před {n}t" + monthsAgo: "Před {n}m" + yearsAgo: "Před {n}r" invalid: "Nic nebylo nalezeno" _time: second: "Sekund" minute: "Minut" hour: "Hodin" + day: "Dnů" +_timelineTutorial: + title: "Jak používat Misskey" + step1_1: "Toto je \"časová osa\". Zde se chronologicky zobrazují všechny \"poznámky\" odeslané na {name}." + step1_2: "Existuje několik různých časových plánů. Například \"Domácí časová osa\" bude obsahovat poznámky uživatelů, které sledujete, a \"Místní časová osa\" bude obsahovat poznámky všech uživatelů {name}." + step2_1: "Zkusme zveřejnit poznámku. Můžete tak učinit stisknutím tlačítka s ikonou tužky." + step2_2: "Co takhle napsat sebepředstavení, nebo jen \"Ahoj {name}!\", pokud se vám nechce?" + step3_1: "Dokončil jsi svou první poznámku?" + step3_2: "Na časové ose by se nyní měla zobrazit vaše první poznámka." + step4_1: "K poznámkám můžete také připojit \"Reakce\"." + step4_2: "Chcete-li připojit reakci, stiskněte na poznámce znaménko \"+\" a vyberte emoji, kterým chcete reagovat." _2fa: + alreadyRegistered: "Již jste zaregistrovali dvoufaktorové ověřovací zařízení." + registerTOTP: "Registrovat aplikaci autentizátoru" + step1: "Nejprve si do zařízení nainstalujte aplikaci pro ověřování (například {a} nebo {b})." + step2: "Poté naskenujte QR kód zobrazený na této obrazovce." + step2Click: "Kliknutím na tento QR kód můžete zaregistrovat 2FA do bezpečnostního klíče nebo aplikace autentizace telefonu." + step3Title: "Zadejte ověřovací kód" + step3: "Pro dokončení nastavení zadejte token poskytnutý vaší aplikací." + step4: "Od této chvíle budou všechny budoucí pokusy o přihlášení vyžadovat tento přihlašovací token." + securityKeyNotSupported: "Váš prohlížeč nepodporuje bezpečnostní klíče." + registerTOTPBeforeKey: "Nastavte aplikaci autentizátoru pro registraci bezpečnostního nebo přístupového klíče." + securityKeyInfo: "Kromě ověřování otiskem prstu nebo PIN můžete nastavit také ověřování pomocí hardwarových bezpečnostních klíčů, které podporují FIDO2, a svůj účet tak dále zabezpečit." + registerSecurityKey: "Registrace bezpečnostního nebo přístupového klíče" + securityKeyName: "Zadejte název klíče" + tapSecurityKey: "Při registraci bezpečnostního nebo přístupového klíče postupujte podle svého prohlížeče." + removeKey: "Odstranit bezpečnostní klíč" + removeKeyConfirm: "Opravdu chcete odstranit klíč {name}?" + whyTOTPOnlyRenew: "Aplikaci autentizátoru nelze odstranit, dokud je zaregistrován bezpečnostní klíč." + renewTOTP: "Překonfigurování aplikace autentizátor" + renewTOTPConfirm: "Tohle způsobí, že ověřovací kódy z předchozí aplikace přestanou fungovat." + renewTOTPOk: "Přenastavit" renewTOTPCancel: "Ne děkuji" +_permissions: + "read:account": "Zobrazit informace o účtu" + "write:account": "Upravit informace o účtu" + "read:blocks": "Zobrazit seznam blokovaných uživatelů" + "write:blocks": "Upravit seznam blokovaných uživatelů" + "read:drive": "Přístup k souborům a složkám na disku" + "write:drive": "Úprava nebo odstranění souborů a složek na disku" + "read:favorites": "Zobrazit seznam oblíbených" + "write:favorites": "Upravit seznam oblíbených" + "read:following": "Zobrazit informace o tom, koho sledujete" + "write:following": "Sledování nebo zrušení sledování jiných účtů" + "read:messaging": "Zobrazit chat" + "write:messaging": "Sestavit nebo mazat zprávy chatu" + "read:mutes": "Zobrazit seznam ztlumených uživatelů" + "write:mutes": "Upravit seznam ztlumených uživatelů" + "write:notes": "Sestavit nebo odstranit poznámky" + "read:notifications": "Zobrazit oznámení" + "write:notifications": "Spravit oznámení" + "read:reactions": "Zobrazit vaše reakce" + "write:reactions": "Upravit své reakce" + "write:votes": "Hlasovat v anketě" + "read:pages": "Zobrazit své stránky" + "write:pages": "Upravit nebo odstranit stránky" + "read:page-likes": "Zobrazit to se mi líbí na stránkách" + "write:page-likes": "Upravit to se mi líbí na stránkách" + "read:user-groups": "Zobrazit skupiny uživatelů" + "write:user-groups": "Upravit nebo odstranit skupiny uživatelů" + "read:channels": "Zobrazit své kanály" + "write:channels": "Upravit kanály" + "read:gallery": "Zobrazit galerii" + "write:gallery": "Upravit galerii" + "read:gallery-likes": "Zobrazit seznam to se mi líbí příspěvků v galerii" + "write:gallery-likes": "Upravit seznam to se mi líbí příspěvků v galerii" +_auth: + shareAccessTitle: "Udělovat oprávnění k aplikacím" + shareAccess: "Chcete autorizovat \"{name}\" pro přístup k tomuto účtu?" + shareAccessAsk: "Opravdu chcete této aplikaci povolit přístup k vašemu účtu?" + permission: "{jméno} požaduje tato oprávnění" + permissionAsk: "Tato aplikace požaduje následující oprávnění" + pleaseGoBack: "Vraťte se prosím zpět do aplikace" + callback: "Návrat k aplikaci" + denied: "Přístup odepřen" + pleaseLogin: "Pro autorizaci aplikací se prosím přihlaste." +_antennaSources: + all: "Všechny poznámky" + homeTimeline: "Poznámky sledovaných uživatelů" + users: "Poznámky konkrétních uživatelů" + userList: "Poznámky z určitého seznamu uživatelů" _weekday: sunday: "Neděle" monday: "Pondělí" @@ -679,38 +1757,81 @@ _weekday: _widgets: profile: "Váš profil" instanceInfo: "Informace o instanci" + memo: "Přilepené poznámky" notifications: "Oznámení" timeline: "Časová osa" calendar: "Kalendář" trends: "Trendy" clock: "Hodiny" rss: "RSS čtečka" + rssTicker: "RSS Ticker" activity: "Aktivita" photos: "Fotky" digitalClock: "Digitální hodiny" + unixClock: "Hodiny UNIX" federation: "Federace" + instanceCloud: "Cloud instance" + postForm: "Formulář pro odeslání" slideshow: "Prezentace" button: "Tlačítko" onlineUsers: "Online uživatelé" jobQueue: "Fronta úloh" + serverMetric: "Metriky serveru" aiscript: "AiScript conzole" + aiscriptApp: "Aplikace AiScript" aichan: "Ai" + userList: "Seznam uživatelů" _userList: chooseList: "Vybrat seznam" + clicker: "Clicker" _cw: hide: "Skrýt" show: "Zobrazit více" + chars: "{count} charakterů" + files: "{count} souborů" _poll: + noOnlyOneChoice: "Jsou zapotřebí alespoň dvě možnosti" + choiceN: "Volba {n}" noMore: "Více už přidat nemůžete" + canMultipleVote: "Umožnit výběr více možností" + expiration: "Ukončení ankety" infinite: "Nikdy" + at: "Ukončit v" + after: "Ukončit po" deadlineDate: "Datum ukončení" deadlineTime: "Hodin" duration: "Trvání" + votesCount: "{n} hlasů" + totalVotes: "{n} hlasů celkově" + vote: "Hlasovat v anketě" + showResult: "Zobrazit výsledky" + voted: "Odhlasováno" + closed: "Uzavřeno" + remainingDays: "Zbývá {d} den/dní a {h} hodin/a" + remainingHours: "Zbývá {h} hodin/a a {m} minut/a" + remainingMinutes: "Zbývá {m} minut/a a {s} sekund/a" + remainingSeconds: "Zbývá {s} sekund/a" _visibility: + public: "Veřejný" + publicDescription: "Vaše poznámka bude viditelná pro všechny uživatele" home: "Domů" + homeDescription: "Zveřejnit příspěvek pouze na domovskou časovou osu" followers: "Sledující" + followersDescription: "Zviditelnit pouze pro své sledující" + specified: "Přímý" + specifiedDescription: "Zviditelnit pouze pro určité uživatele" + disableFederation: "Defederace" + disableFederationDescription: "Nepřenášet do jiných instancí" _postForm: + replyPlaceholder: "Odpovědět na tuto poznámku..." + quotePlaceholder: "Citovat tuto poznámku..." + channelPlaceholder: "Zveřejnit příspěvek do kanálu..." _placeholders: + a: "Co máte v plánu?" + b: "Co se děje kolem vás?" + c: "Co máte na mysli?" + d: "Co chcete říct?" + e: "Začít psát..." f: "Čekám, až něco napíšete..." _profile: name: "Jméno" @@ -718,36 +1839,100 @@ _profile: description: "O mně" youCanIncludeHashtags: "V popisku o Vás můžete použít i hastagy." metadata: "Doplňující informace" + metadataEdit: "Upravit doplňující informace" + metadataDescription: "Pomocí nich můžete ve svém profilu zobrazit doplňující informační pole." + metadataLabel: "Popisek" metadataContent: "Obsah" + changeAvatar: "Změnit avatara" + changeBanner: "Změnit banner" _exportOrImport: allNotes: "Všechny poznámky" + favoritedNotes: "Oblíbené poznámky" followingList: "Sledovaní" muteList: "Ztlumit" blockingList: "Zablokovat" userLists: "Seznamy" + excludeMutingUsers: "Vyloučit ztlumené uživatele" + excludeInactiveUsers: "Vyloučit neaktivní uživatele" _charts: federation: "Federace" apRequest: "Požadavek" + usersIncDec: "Rozdíl v počtech uživatelů" usersTotal: "Celkem uživatelů" activeUsers: "Aktivní uživatelé" + notesIncDec: "Rozdíl v počtu poznámek" + localNotesIncDec: "Rozdíl v počtu místních poznámek" + remoteNotesIncDec: "Rozdíl v počtu vzdálených poznámek" notesTotal: "Celkový počet poznámek" + filesIncDec: "Rozdíl v počtu souborů" + filesTotal: "Celkový počet souborů" + storageUsageIncDec: "Rozdíl ve využití úložiště" + storageUsageTotal: "Celkové využití úložiště" +_instanceCharts: + requests: "Požadavky" + users: "Rozdíl v počtech uživatelů" + usersTotal: "Kumulativní počet uživatelů" + notes: "Rozdíl v počtu poznámek" + notesTotal: "Kumulativní počet poznámek" + ff: "Rozdíl v počtu sledovaných uživatelů / sledujících" + ffTotal: "Kumulativní počet sledovaných uživatelů / sledujících" + cacheSize: "Rozdíl ve velikosti mezipaměti" + cacheSizeTotal: "Kumulativní celková velikost mezipaměti" + files: "Rozdíl v počtu souborů" + filesTotal: "Kumulativní počet souborů" _timelines: home: "Domů" + local: "Místní" + social: "Sociální síť" global: "Globální" _play: + new: "Vytvořit Play" + edit: "Upravit Play" + created: "Play vytvořen" + updated: "Play upraven" + deleted: "Play smazán" + pageSetting: "Nastavení Play" + editThisPage: "Upravit tenhle Play" + viewSource: "Zobrazit zdroj" + my: "Moje Plays" + liked: "To se mi líbí Plays" + featured: "Populární" + title: "Titulek" script: "Skript" summary: "Popis" _pages: newPage: "Vytvořit novou stránku" editPage: "Upravit stránku" + readPage: "Prohlížení zdroje této stránky" created: "Stránka byla úspěšně vytvořena" updated: "Stránka byla úspěšně aktualizována" deleted: "Stránka byla úspěšně smazána" pageSetting: "Nastavení stránky" + nameAlreadyExists: "Zadaná adresa URL stránky již existuje" + invalidNameTitle: "Zadaná adresa URL stránky je neplatná" invalidNameText: "Ujistěte se že jméno stránky je vyplněno" + editThisPage: "Upravit tuto stránku" + viewSource: "Zobrazit zdroj" + viewPage: "Zobrazit své stránky" + like: "To se mi líbí" + unlike: "Už se mi to nelíbí" + my: "Moje stránky" + liked: "To se mi líbí stránky" + featured: "Populární" + inspector: "Inspektor" contents: "Obsah" + content: "Blok stránky" + variables: "Proměnné" + title: "Titulek" + url: "URL stránky" + summary: "Přehled stránky" + alignCenter: "Vycentrovat prvky" + hideTitleWhenPinned: "Skrytí názvu stránky při připnutí k profilu" + font: "Písmo" fontSerif: "Serif" fontSansSerif: "Sans Serif" + eyeCatchingImageSet: "Nastavení miniatury" + eyeCatchingImageRemove: "Smazání miniatury" chooseBlock: "Přidat blok" selectType: "Vyberte typ" contentBlocks: "Obsah" @@ -759,8 +1944,28 @@ _pages: section: "Sekce" image: "Obrázky" button: "Tlačítko" + note: "Vestavěná poznámka" + _note: + id: "ID poznámky" + idDescription: "Adresu URL poznámky můžete vložit také sem." + detailed: "Podrobné zobrazení" +_relayStatus: + requesting: "Čeká se" + accepted: "Schváleno" + rejected: "Odmítnuto" _notification: + fileUploaded: "Soubor úspěšně nahrán" + youGotMention: "{name} vás zmínil" + youGotReply: "{name} vám odpověděl" + youGotQuote: "{name} vás citoval" + youRenoted: "Poznámka od {jméno}" youWereFollowed: "Máte nového následovníka" + youReceivedFollowRequest: "Obdrželi jste žádost o sledování" + yourFollowRequestAccepted: "Vaše žádost o sledování byla přijata" + pollEnded: "Výsledky ankety jsou k dispozici" + unreadAntennaNote: "Anténa {name}" + emptyPushNotificationMessage: "Push oznámení byla aktualizována" + achievementEarned: "Úspěch odemčen" _types: all: "Vše" follow: "Sledovaní" @@ -769,17 +1974,64 @@ _notification: renote: "Přeposlat" quote: "Citovat" reaction: "Reakce" + pollEnded: "Anketa končí" + receiveFollowRequest: "Obdržené žádosti o sledování" + followRequestAccepted: "Přijaté žádosti o sledování" + achievementEarned: "Úspěch odemčen" + app: "Oznámení z propojených aplikací" _actions: + followBack: "vás začal sledovat zpět" reply: "Odpovědět" renote: "Přeposlat" _deck: + alwaysShowMainColumn: "Vždy zobrazovat hlavní sloupec" + columnAlign: "Zarovnat sloupce" + addColumn: "Přidat sloupec" + configureColumn: "Nastavení sloupců" + swapLeft: "Prohodit s levým sloupcem" + swapRight: "Prohodit s pravým sloupcem" + swapUp: "Prohodit s výše uvedeným sloupcem" + swapDown: "Prohodit s níže uvedeným sloupcem" + stackLeft: "Nahromadit v levém sloupci" + popRight: "Popnout sloupec na pravou stranu" + profile: "Profil" + newProfile: "Nový profil" + deleteProfile: "Smazat profil" + introduction: "Vytvořte si dokonalé rozhraní volným uspořádáním sloupců!" + introduction2: "Kliknutím na tlačítko + v pravé části obrazovky můžete kdykoli přidat nové sloupce." + widgetsIntroduction: "V nabídce sloupce vyberte možnost \"Upravit widgety\" a přidejte widget." + useSimpleUiForNonRootPages: "Použít zjednodušené uživatelské rozhraní pro navigaci na stránkách" _columns: + main: "Hlavní" + widgets: "Widgety" notifications: "Oznámení" tl: "Časová osa" antenna: "Antény" list: "Seznamy" channel: "Kanály" mentions: "Zmínění" + direct: "Přímý" + roleTimeline: "Časová osa role" +_dialog: + charactersExceeded: "Překročili jste maximální počet znaků! V současné době je na hodnotě {current} z {max}." + charactersBelow: "Nedosahujete minimálního limitu znaků! V současné době je na {current} z {min}." +_disabledTimeline: + title: "Časová osa vypnuta" + description: "Tuto časovou osu nemůžete používat v rámci svých současných rolí." +_drivecleaner: + orderBySizeDesc: "Sestupná velikost souborů" + orderByCreatedAtAsc: "Vzestupné datumy" _webhookSettings: + createWebhook: "Vytvořit Webhook" name: "Jméno" + secret: "Tajné" + events: "Události Webhook" active: "Zapnuto" + _events: + follow: "Při sledování uživatele" + followed: "Při sledování" + note: "Při zveřejňování poznámky" + reply: "Při obdržení odpovědi" + renote: "Při renotaci poznámky" + reaction: "Při obdržení reakce" + mention: "Při zmínce" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index c4c12cb1aa..596a6e5fd8 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -45,15 +45,20 @@ pin: "An dein Profil anheften" unpin: "Von deinem Profil lösen" copyContent: "Inhalt kopieren" copyLink: "Link kopieren" +copyLinkRenote: "Renote-Link kopieren" delete: "Löschen" deleteAndEdit: "Löschen und Bearbeiten" deleteAndEditConfirm: "Möchtest du diese Notiz wirklich löschen und bearbeiten? Alle Reaktionen, Renotes und Antworten dieser Notiz werden verloren gehen." addToList: "Zu Liste hinzufügen" +addToAntenna: "Zu Antenne hinzufügen" sendMessage: "Nachricht senden" copyRSS: "RSS kopieren" copyUsername: "Benutzernamen kopieren" copyUserId: "Benutzer-ID kopieren" copyNoteId: "Notiz-ID kopieren" +copyFileId: "Datei-ID kopieren" +copyFolderId: "Ordner-ID kopieren" +copyProfileUrl: "Profil-URL kopieren" searchUser: "Nach einem Benutzer suchen" reply: "Antworten" loadMore: "Mehr laden" @@ -112,7 +117,7 @@ pinnedNote: "Angeheftete Notiz" pinned: "Angeheftet" you: "Du" clickToShow: "Zum Anzeigen anklicken" -sensitive: "NSFW" +sensitive: "Sensibel" add: "Hinzufügen" reaction: "Reaktionen" reactions: "Reaktionen" @@ -120,8 +125,8 @@ reactionSetting: "In der Reaktionsauswahl anzuzeigende Reaktionen" reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drücke „+“ um hinzuzufügen" rememberNoteVisibility: "Notizsichtbarkeit merken" attachCancel: "Anhang entfernen" -markAsSensitive: "Als NSFW markieren" -unmarkAsSensitive: "Als nicht NSFW markieren" +markAsSensitive: "Als sensibel markieren" +unmarkAsSensitive: "Als nicht sensibel markieren" enterFileName: "Dateinamen eingeben" mute: "Stummschalten" unmute: "Stummschaltung aufheben" @@ -136,8 +141,10 @@ unblockConfirm: "Möchtest du diese Blockierung wirklich aufheben?" suspendConfirm: "Möchtest du diesen Benutzer wirklich sperren?" unsuspendConfirm: "Möchtest du diesen Benutzer wirklich entsperren?" selectList: "Liste auswählen" +editList: "Liste bearbeiten" selectChannel: "Kanal auswählen" selectAntenna: "Antenne auswählen" +editAntenna: "Antenne bearbeiten" selectWidget: "Widget auswählen" editWidgets: "Widgets bearbeiten" editWidgetsExit: "Fertig" @@ -150,6 +157,9 @@ addEmoji: "Emoji hinzufügen" settingGuide: "Empfohlene Einstellung" cacheRemoteFiles: "Dateien von fremden Instanzen im Cache speichern" cacheRemoteFilesDescription: "Ist diese Einstellung deaktiviert, so werden Dateien fremder Instanzen direkt von dort geladen. Hierdurch wird Speicherplatz auf diesem Server gespart, aber durch fehlende Generierung von Vorschaubildern mehr Bandbreite verwendet." +youCanCleanRemoteFilesCache: "Klicke auf den 🗑️-Knopf der Dateiverwaltungsansicht, um den Cache zu leeren." +cacheRemoteSensitiveFiles: "Sensitive Dateien von fremden Instanzen im Cache speichern" +cacheRemoteSensitiveFilesDescription: "Ist diese Einstellung deaktiviert, so werden sensitive Dateien fremder Instanzen direkt von dort ohne Zwischenspeicherung geladen." flagAsBot: "Als Bot markieren" flagAsBotDescription: "Aktiviere diese Option, falls dieses Benutzerkonto durch ein Programm gesteuert wird. Falls aktiviert, agiert es als Flag für andere Entwickler zur Verhinderung von endlosen Kettenreaktionen mit anderen Bots und lässt Misskeys interne Systeme dieses Benutzerkonto als Bot behandeln." flagAsCat: "Als Katze markieren" @@ -222,7 +232,7 @@ noJobs: "Keine Jobs vorhanden" federating: "Wird föderiert" blocked: "Blockiert" suspended: "Gesperrt" -all: "Alles" +all: "Alle" subscribing: "Wird abonniert" publishing: "Wird veröffentlicht" notResponding: "Antwortet nicht" @@ -311,7 +321,7 @@ copyUrl: "URL kopieren" rename: "Umbenennen" avatar: "Profilbild" banner: "Banner" -nsfw: "NSFW" +displayOfSensitiveMedia: "Darstellung sensibler Medien" whenServerDisconnected: "Bei Verbindungsverlust zum Server" disconnectedFromServer: "Die Verbindung zum Server wurde getrennt" reload: "Aktualisieren" @@ -346,7 +356,6 @@ invite: "Einladen" driveCapacityPerLocalAccount: "Drive-Kapazität pro lokalem Benutzerkonto" driveCapacityPerRemoteAccount: "Drive-Kapazität pro Benutzer fremder Instanzen" inMb: "In Megabytes" -iconUrl: "Icon-URL (favicon etc)" bannerUrl: "Banner-URL" backgroundImageUrl: "Hintergrundbild-URL" basicInfo: "Grundlegende Informationen" @@ -402,13 +411,16 @@ aboutMisskey: "Über Misskey" administrator: "Administrator" token: "Token" 2fa: "Zwei-Faktor-Authentifizierung" +setupOf2fa: "Zweifaktorauthentifizierung einrichten" totp: "Authentifizierungs-App" totpDescription: "Logge dich via Authentifizierungs-App mit Einmalpasswort ein" moderator: "Moderator" moderation: "Moderation" +moderationNote: "Moderationsnotiz" +addModerationNote: "Moderationsnotiz hinzufügen" nUsersMentioned: "Von {n} Benutzern erwähnt" -securityKeyAndPasskey: "Security-Tokens und Passkeys" -securityKey: "Sicherheitsschlüssel" +securityKeyAndPasskey: "Hardware-Sicherheitsschlüssel und Passkeys" +securityKey: "Hardware-Sicherheitsschlüssel" lastUsed: "Zuletzt benutzt" lastUsedAt: "Zuletzt verwendet: {t}" unregister: "Deaktivieren" @@ -532,7 +544,7 @@ chooseEmoji: "Emoji auswählen" unableToProcess: "Der Vorgang konnte nicht abgeschlossen werden" recentUsed: "Vor kurzem verwendet" install: "Installieren" -uninstall: "Uninstallieren" +uninstall: "Deinstallieren" installedApps: "Authorisierte Anwendungen" nothing: "Hier gibt es nichts zu sehen" installedDate: "Authorisiert am" @@ -623,7 +635,7 @@ regexpErrorDescription: "Im regulären Ausdruck deiner {tab}en Wortstummschaltun instanceMute: "Instanzstummschaltungen" userSaysSomething: "{name} hat etwas gesagt" makeActive: "Aktivieren" -display: "Anzeigen" +display: "Anzeigeart" copy: "Kopieren" metrics: "Metriken" overview: "Übersicht" @@ -645,6 +657,7 @@ behavior: "Verhalten" sample: "Beispiel" abuseReports: "Meldungen" reportAbuse: "Melden" +reportAbuseRenote: "Renote melden" reportAbuseOf: "{name} melden" fillAbuseReportDescription: "Bitte gib zusätzliche Informationen zu dieser Meldung an. Falls es sich um eine spezielle Notiz handelt, bitte gib dessen URL an." abuseReported: "Deine Meldung wurde versendet. Vielen Dank." @@ -672,6 +685,7 @@ createNewClip: "Neuen Clip erstellen" unclip: "Aus Clip entfernen" confirmToUnclipAlreadyClippedNote: "Diese Notiz ist bereits im \"{name}\" Clip enthalten. Möchtest du sie aus diesem Clip entfernen?" public: "Öffentlich" +private: "Privat" i18nInfo: "Misskey wird durch freiwillige Helfer in viele verschiedene Sprachen übersetzt. Auf {link} kannst du mithelfen." manageAccessTokens: "Zugriffstokens verwalten" accountInfo: "Benutzerkonto-Informationen" @@ -693,7 +707,7 @@ driveUsage: "Drive-Auslastung" noCrawle: "Crawler-Indexierung ablehnen" noCrawleDescription: "Suchmaschinen bitten, die eigene Profilseite, Notizen, Seiten usw. nicht zu indexieren." lockedAccountInfo: "Auch wenn du Follow-Anfragen auf manuelle Bestätigung setzt, wird jede deiner Notizen öffentlich sichtbar sein, sofern du ihre Notizsichtbarkeit nicht auf \"Nur Follower\" setzt." -alwaysMarkSensitive: "Medien standardmäßig als NSFW markieren" +alwaysMarkSensitive: "Medien standardmäßig als sensibel markieren" loadRawImages: "Anstatt Vorschaubilder immer Originalbilder anzeigen" disableShowingAnimatedImages: "Animierte Bilder nicht abspielen" verificationEmailSent: "Eine Bestätigungsmail wurde an deine Email-Adresse versendet. Besuche den dort enthaltenen Link, um die Verifizierung abzuschließen." @@ -912,16 +926,16 @@ type: "Art" speed: "Geschwindigkeit" slow: "Langsam" fast: "Schnell" -sensitiveMediaDetection: "Erkennung von NSFW-Medien" +sensitiveMediaDetection: "Erkennung von sensiblen Medien" localOnly: "Nur Lokal" remoteOnly: "Nur für fremde Instanzen" failedToUpload: "Hochladen fehlgeschlagen" -cannotUploadBecauseInappropriate: "Diese Datei kann nicht hochgeladen werden, da Anteile der Datei als möglicherweise NSFW festgestellt wurden." +cannotUploadBecauseInappropriate: "Diese Datei kann nicht hochgeladen werden, da Anteile der Datei als möglicherweise unangebracht festgestellt wurden." cannotUploadBecauseNoFreeSpace: "Die Datei konnte nicht hochgeladen werden, da dein Drive-Speicherplatz aufgebraucht ist." cannotUploadBecauseExceedsFileSizeLimit: "Diese Datei kann wegen Überschreitung der Maximalgröße nicht hochgeladen werden." beta: "Beta" -enableAutoSensitive: "NSFW-Automarkierung" -enableAutoSensitiveDescription: "Setzt soweit möglich durch Verwendung von Machine Learning automatisch NSFW-Markierungen für Medien, die NSFW-Anteile beinhalten. Auch wenn du diese Option deaktiviert hast, ist sie möglicherweise auf Instanzebene aktiviert." +enableAutoSensitive: "Automarkierung sensibler Medien" +enableAutoSensitiveDescription: "Setzt soweit möglich durch Verwendung von Machine Learning automatisch Markierungen für sensible Medien. Auch wenn du diese Option deaktiviert hast, ist sie möglicherweise auf Instanzebene aktiviert." activeEmailValidationDescription: "Aktivert strengere Überprüfung von E-Mail-Adressen, d.h. Testen auf Wegwerfadressen und darauf, ob mit der Adresse tatsächlich kommuniziert werden kann. Ist dies deaktiviert, so wird nur das Format der E-Mail überprüft." navbar: "Navigationsleiste" shuffle: "Mischen" @@ -991,7 +1005,7 @@ postToTheChannel: "In Kanal senden" cannotBeChangedLater: "Kann später nicht mehr geändert werden." reactionAcceptance: "Reaktionsannahme" likeOnly: "Nur \"Gefällt mir\"" -likeOnlyForRemote: "Nur \"Gefällt mir\" für fremde Instanzen" +likeOnlyForRemote: "Alle (Nur \"Gefällt mir\" für fremde Instanzen)" nonSensitiveOnly: "Keine Sensitiven" nonSensitiveOnlyForLocalLikeOnlyForRemote: "Keine Sensitiven (Nur \"Gefällt mir\" von fremden Instanzen)" rolesAssignedToMe: "Mir zugewiesene Rollen" @@ -1010,7 +1024,7 @@ retryAllQueuesConfirmText: "Dies wird zu einer temporären Erhöhung der Serverl enableChartsForRemoteUser: "Diagramme für Nutzer fremder Instanzen erstellen" enableChartsForFederatedInstances: "Diagramme für fremde Instanzen erstellen" showClipButtonInNoteFooter: "\"Clip\" zum Notizmenu hinzufügen" -largeNoteReactions: "Reaktionen vergrößert anzeigen" +reactionsDisplaySize: "Reaktionsanzeigegröße" noteIdOrUrl: "Notiz-ID oder URL" video: "Video" videos: "Videos" @@ -1044,7 +1058,7 @@ archive: "Archivieren" channelArchiveConfirmTitle: "{name} wirklich archivieren?" channelArchiveConfirmDescription: "Ein archivierter Kanal taucht nicht mehr in der Kanalliste oder in Suchergebnissen auf. Zudem können ihm keine Beiträge mehr hinzugefügt werden." thisChannelArchived: "Dieser Kanal wurde archiviert." -displayOfNote: "Anzeige von Notizen" +displayOfNote: "Darstellung von Notizen" initialAccountSetting: "Kontoeinrichtung" youFollowing: "Gefolgt" preventAiLearning: "Verwendung in machinellem Lernen (Generative bzw. Prediktive AI/KI) ablehnen" @@ -1062,6 +1076,55 @@ later: "Später" goToMisskey: "Zu Misskey" additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher" installed: "Installiert" +branding: "Branding" +enableServerMachineStats: "Hardwareinformationen des Servers veröffentlichen" +enableIdenticonGeneration: "Generierung von Benutzer-Identicons aktivieren" +turnOffToImprovePerformance: "Deaktivierung kann zu höherer Leistung führen." +createInviteCode: "Einladung erstellen" +createWithOptions: "Einladung mit Optionen erstellen" +createCount: "Einladungsanzahl" +inviteCodeCreated: "Einladung erstellt" +inviteLimitExceeded: "Du hast das Maximum an erstellbaren Einladungen erreicht." +createLimitRemaining: "Erstellbare Einladungen: Noch {limit}" +inviteLimitResetCycle: "Am {time} wird dies auf {limit} zurückgesetzt." +expirationDate: "Ablaufdatum" +noExpirationDate: "Keins" +inviteCodeUsedAt: "Einladung verwendet am" +registeredUserUsingInviteCode: "Einladung verwendet von" +waitingForMailAuth: "Bestätigungsemail ausstehend" +inviteCodeCreator: "Einladung erstellt von" +usedAt: "Benutzt am" +unused: "Unbenutzt" +used: "Benutzt" +expired: "Abgelaufen" +doYouAgree: "Zustimmen?" +beSureToReadThisAsItIsImportant: "Lies bitte diese wichtige Informationen." +iHaveReadXCarefullyAndAgree: "Ich habe den Text \"{x}\" gelesen und stimme zu." +dialog: "Dialogfeld" +icon: "Symbol" +forYou: "Für dich" +currentAnnouncements: "Aktuelle Ankündigungen" +pastAnnouncements: "Alte Ankündigungen" +youHaveUnreadAnnouncements: "Es gibt neue Ankündigungen." +useSecurityKey: "Folge bitten den Anweisungen deines Browsers bzw. Gerätes und verwende deinen Hardware-Sicherheitsschlüssel oder Passkey." +replies: "Antworten" +renotes: "Renotes" +loadReplies: "Antworten anzeigen" +loadConversation: "Unterhaltung anzeigen" +pinnedList: "Angeheftete Liste" +keepScreenOn: "Bildschirm angeschaltet lassen" +verifiedLink: "Link-Besitz wurde verifiziert" +notifyNotes: "Über neue Notizen benachrichtigen" +unnotifyNotes: "Nicht über neue Notizen benachrichtigen" +_announcement: + forExistingUsers: "Nur für existierende Nutzer" + forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt." + needConfirmationToRead: "Separate Lesebestätigung erfordern" + needConfirmationToReadDescription: "Ist dies aktiviert, so wird beim Markieren dieser Ankündigung als gelesen ein separates Bestätigungsfenster angezeigt. Auch wird sie von der \"Alle als gelesen markieren\"-Funktion ausgenommen." + end: "Ankündigung archivieren" + tooManyActiveAnnouncementDescription: "Zu viele aktive Ankündigungen können die Benutzerfreundlichkeit verschlechtern. Es wird empfohlen, veraltete Ankündigungen zu archivieren." + readConfirmTitle: "Als gelesen markieren?" + readConfirmText: "Dies markiert den Inhalt von \"{title}\" als gelesen." _initialAccountSetting: accountCreated: "Dein Konto wurde erfolgreich erstellt!" letsStartAccountSetup: "Lass uns nun dein Konto einrichten." @@ -1079,6 +1142,13 @@ _initialAccountSetting: laterAreYouSure: "Die Kontoeinrichtung wirklich später erledigen?" _serverRules: description: "Eine Reihe von Regeln, die vor der Registrierung angezeigt werden. Eine Zusammenfassung der Nutzungsbedingungen anzuzeigen ist empfohlen." +_serverSettings: + iconUrl: "Icon-URL" + appIconDescription: "Gibt das zu verwendende Icon bei der Anzeige von {host} als App an." + appIconUsageExample: "Beispielsweise als PWA, oder bei Lesezeichen auf dem Startbildschirm von Smartphones" + appIconStyleRecommendation: "Da das Icon zu einem Kreis oder Quadrat zugeschnitten wird, wird ein Icon mit gefülltem Margin um den Inhalt herum empfohlen." + appIconResolutionMustBe: "Die Mindestauflösung ist {resolution}." + manifestJsonOverride: "Überschreiben von manifest.json" _accountMigration: moveFrom: "Von einem anderen Konto zu diesem migrieren" moveFromSub: "Alias für ein anderes Konto erstellen" @@ -1093,7 +1163,7 @@ _accountMigration: migrationConfirm: "Dieses Konto wirklich zu {account} umziehen? Sobald der Umzug beginnt, kann er nicht rückgängig gemacht werden, und dieses Konto nicht wieder im ursprünglichen Zustand verwendet werden." movedAndCannotBeUndone: "\nDieses Konto wurde migriert.\nDiese Aktion ist unwiderruflich." postMigrationNote: "Dieses Konto wird 24 Stunden nach Abschluss der Migration allen Konten, denen es derzeit folgt, nicht mehr folgen.\n\nSowohl die Anzahl der Follower als auch die der Konten, denen dieses Konto folgt, wird dann auf Null gesetzt. Um zu vermeiden, dass Follower dieses Kontos dessen Beiträge, welche nur für Follower bestimmt sind, nicht mehr sehen können, werden sie diesem Konto jedoch weiterhin folgen." - movedTo: "Umzugsziel:" + movedTo: "Neues Konto:" _achievements: earnedAt: "Freigeschaltet am" _types: @@ -1333,6 +1403,9 @@ _achievements: title: "Brain Diver" description: "Sende den Link zu Brain Diver" flavor: "Misskey-Misskey La-Tu-Ma" + _smashTestNotificationButton: + title: "Testüberfluss" + description: "Betätige den Benachrichtigungstest mehrfach innerhalb einer extrem kurzen Zeitspanne" _role: new: "Rolle erstellen" edit: "Rolle bearbeiten" @@ -1347,7 +1420,7 @@ _role: condition: "Bedingung" isConditionalRole: "Dies ist eine konditionale Rolle." isPublic: "Öffentliche Rolle" - descriptionOfIsPublic: "Ist dies aktiviert, so kann jeder die Liste der Benutzer, die dieser Rolle zugewiesen sind, einsehen. Zusätzlich wird diese Rolle im Profil zugewiesener Benutzer angezeigt." + descriptionOfIsPublic: "Diese Rolle wird im Profil zugewiesener Benutzer angezeigt." options: "Optionen" policies: "Richtlinien" baseRole: "Rollenvorlage" @@ -1356,8 +1429,8 @@ _role: iconUrl: "Icon-URL" asBadge: "Als Abzeichen anzeigen" descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt." - isExplorable: "Rollenchronik veröffentlichen" - descriptionOfIsExplorable: "Ist dies aktiviert, so ist die Rollenchronik dieser Rolle frei zugänglich. Die Chronik von Rollen, welche nicht öffentlich sind, wird auch bei Aktivierung nicht veröffentlicht." + isExplorable: "Benutzerliste veröffentlichen" + descriptionOfIsExplorable: "Ist dies aktiviert, so ist die Chronik dieser Rolle, sowie eine Liste der Benutzer mit dieser Rolle, frei zugänglich." displayOrder: "Position" descriptionOfDisplayOrder: "Je höher die Nummer, desto höher die UI-Position." canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen" @@ -1372,6 +1445,9 @@ _role: ltlAvailable: "Kann auf die lokale Chronik zugreifen" canPublicNote: "Kann öffentliche Notizen erstellen" canInvite: "Erstellung von Einladungscodes für diese Instanz" + inviteLimit: "Maximalanzahl an Einladungen" + inviteLimitCycle: "Zyklus des Einladungslimits" + inviteExpirationTime: "Gültigkeitsdauer von Einladungen" canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten" driveCapacity: "Drive-Kapazität" alwaysMarkNsfw: "Dateien immer als NSFW markieren" @@ -1402,10 +1478,10 @@ _role: or: "ODER-Bedingung" not: "NICHT-Bedingung" _sensitiveMediaDetection: - description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht." + description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von sensiblen Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht." sensitivity: "Erkennungssensitivität" sensitivityDescription: "Durch das Senken der Sensitivität kann die Anzahl an Fehlerkennungen (sog. false positives) reduziert werden. Durch ein Erhöhen dieser kann die Anzahl an verpassten Erkennungen (sog. false negatives) reduziert werden." - setSensitiveFlagAutomatically: "Als NSFW markieren" + setSensitiveFlagAutomatically: "Als sensibel markieren" setSensitiveFlagAutomaticallyDescription: "Die Resultate der internen Erkennung werden beibehalten, auch wenn diese Option deaktiviert ist." analyzeVideos: "Videoanalyse aktivieren" analyzeVideosDescription: "Analysiert zusätzlich zu Bildern auch Videos. Die Last des Servers wird hierdurch etwas erhöht." @@ -1434,6 +1510,7 @@ _ad: back: "Zurück" reduceFrequencyOfThisAd: "Diese Werbung weniger anzeigen" hide: "Ausblenden" + timezoneinfo: "Der Wochentag wird durch die Serverzeitzone bestimmt." _forgotPassword: enterEmail: "Gib die Email-Adresse ein, mit der du dich registriert hast. An diese wird ein Link gesendet, mit dem du dein Passwort zurücksetzen kannst." ifNoEmail: "Solltest du bei der Registrierung keine Email-Adresse angegeben haben, wende dich bitte an den Administrator." @@ -1485,9 +1562,9 @@ _aboutMisskey: donate: "An Misskey spenden" morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter Personen sehr. Danke! 🥰" patrons: "UnterstützerInnen" -_nsfw: - respect: "Als NSFW markierte Bilder verbergen" - ignore: "Als NSFW markierte Bilder nicht verbergen" +_displayOfSensitiveMedia: + respect: "Sensible Medien verbergen" + ignore: "Sensible Medien anzeigen" force: "Alle Medien verbergen" _instanceTicker: none: "Nie anzeigen" @@ -1639,19 +1716,18 @@ _timelineTutorial: _2fa: alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung registriert." 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:" + step2Uri: "Nutzt du ein Desktopprogramm, gib folgende URI eingeben" step3Title: "Authentifizierungsscode eingeben" step3: "Gib zum Abschluss den Token ein, der von deiner App angezeigt wird." + setupCompleted: "Einrichtung abgeschlossen" step4: "Alle folgenden Anmeldeversuche werden ab sofort die Eingabe eines solchen Tokens benötigen." - securityKeyNotSupported: "Dein Browser unterstützt keine Security-Tokens." + securityKeyNotSupported: "Dein Browser unterstützt keine Hardware-Sicherheitsschlüssel." 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" + registerSecurityKey: "Hardware-Sicherheitsschlüssel oder Passkey registrieren" securityKeyName: "Schlüsselname eingeben" tapSecurityKey: "Bitten folge den Anweisungen deines Browsers zur Registrierung" removeKey: "Sicherheitsschlüssel entfernen" @@ -1661,6 +1737,11 @@ _2fa: renewTOTPConfirm: "Codes der bisherigen App werden hierdurch nutzlos" renewTOTPOk: "Neu einrichten" renewTOTPCancel: "Abbrechen" + checkBackupCodesBeforeCloseThisWizard: "Notiere bitte deine Backup-Codes, bevor du dieses Fenster schließt." + backupCodes: "Backup-Codes" + backupCodesDescription: "Verwende diese Codes, falls du nicht mehr auf deine App zur Zweifaktorauthentifizierung zugreifen kannst. Jeder Code kann nur einmal verwendet werden. Bewahre sie an einem sicheren Ort auf." + backupCodeUsedWarning: "Ein Backup-Code wurde verwendet. Falls du den Zugriff zu deiner Zweifaktorauthentifizierungsapp verloren hast, konfiguriere diese bitte möglichst bald erneut." + backupCodesExhaustedWarning: "Alle Backup-Codes wurden verwendet. Falls du den Zugang zu deiner Zweifaktorauthentifizierungsapp verlierst, wirst du dich nicht mehr in dieses Konto einloggen können. Bitte konfiguriere diese App erneut." _permissions: "read:account": "Deine Benutzerkontoinformationen lesen" "write:account": "Deine Benutzerkontoinformationen bearbeiten" @@ -1694,6 +1775,10 @@ _permissions: "write:gallery": "Deine Galerie bearbeiten" "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" + "read:flash": "Deine Plays lesen" + "write:flash": "Deine Plays bearbeiten oder löschen" + "read:flash-likes": "Liste der Plays, die mir gefallen, lesen" + "write:flash-likes": "Liste der Plays, die mir gefallen, bearbeiten" _auth: shareAccessTitle: "Verteilung von App-Berechtigungen" shareAccess: "Möchtest du „{name}“ authorisieren, auf dieses Benutzerkonto zugreifen zu können?" @@ -1808,6 +1893,7 @@ _profile: metadataContent: "Inhalt" changeAvatar: "Profilbild ändern" changeBanner: "Banner ändern" + verifiedLinkDescription: "Gibst du hier eine URL ein, die einen Link zu deinem Profile enthält, wird neben diesem Feld ein Icon zur Besitzbestätigung angezeigt." _exportOrImport: allNotes: "Alle Notizen" favoritedNotes: "Als Favorit markierte Notizen" @@ -1926,9 +2012,14 @@ _notification: youReceivedFollowRequest: "Du hast eine Follow-Anfrage erhalten" yourFollowRequestAccepted: "Deine Follow-Anfrage wurde akzeptiert" pollEnded: "Umfrageergebnisse sind verfügbar" + newNote: "Neue Notiz" unreadAntennaNote: "Antenne {name}" emptyPushNotificationMessage: "Push-Benachrichtigungen wurden aktualisiert" achievementEarned: "Errungenschaft freigeschaltet" + testNotification: "Testbenachrichtigung" + checkNotificationBehavior: "Aussehen von Benachrichtigungen überprüfen" + sendTestNotification: "Testbenachrichtigung senden" + notificationWillBeDisplayedLikeThis: "Benachrichtigungen sehen so aus" _types: all: "Alle" follow: "Neue Follower" @@ -1963,6 +2054,9 @@ _deck: introduction: "Erstelle eine auf dich zugeschneiderte Benutzeroberfläche durch das Aneinanderreihen von Spalten!" introduction2: "Klicke auf das + rechts um wann immer du möchtest neue Spalten hinzuzufügen." widgetsIntroduction: "Drücke bitte \"Widgets bearbeiten\" im Spaltenmenü und füge ein Widget hinzu." + useSimpleUiForNonRootPages: "Simple Benutzeroberfläche für navigierte Seiten verwenden" + usedAsMinWidthWhenFlexible: "Ist \"Automatische Breitenanpassung\" aktiviert, wird hierfür die minimale Breite verwendet" + flexible: "Automatische Breitenanpassung" _columns: main: "Hauptspalte" widgets: "Widgets" diff --git a/locales/el-GR.yml b/locales/el-GR.yml index 41b1ea7c65..e8ed6f118e 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -287,6 +287,9 @@ searchByGoogle: "Αναζήτηση" file: "Αρχεία" recommended: "Προτεινόμενα" cannotUploadBecauseNoFreeSpace: "Το ανέβασμα απέτυχε λόγω ανεπαρκούς Αποθηκευτικού Χώρου" +icon: "Εικονίδιο" +replies: "Απάντηση" +renotes: "Κοινοποίηση σημειώματος" _email: _follow: title: "Έχετε ένα νέο ακόλουθο" diff --git a/locales/en-US.yml b/locales/en-US.yml index 0f1c7c89fe..527c68d839 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -45,15 +45,20 @@ pin: "Pin to profile" unpin: "Unpin from profile" copyContent: "Copy contents" copyLink: "Copy link" +copyLinkRenote: "Copy renote link" delete: "Delete" deleteAndEdit: "Delete and edit" -deleteAndEditConfirm: "Are you sure you want to delete this note and edit it? You will lose all reactions, renotes and replies to it." +deleteAndEditConfirm: "Are you sure you want to redraft this note? This means you will lose all reactions, renotes, and replies to it." addToList: "Add to list" +addToAntenna: "Add to antenna" sendMessage: "Send a message" copyRSS: "Copy RSS" copyUsername: "Copy username" copyUserId: "Copy user ID" copyNoteId: "Copy note ID" +copyFileId: "Copy file ID" +copyFolderId: "Copy folder ID" +copyProfileUrl: "Copy profile URL" searchUser: "Search for a user" reply: "Reply" loadMore: "Load more" @@ -70,7 +75,7 @@ import: "Import" export: "Export" files: "Files" download: "Download" -driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? It will also vanish from all contents that use it." +driveFileDeleteConfirm: "Do you want to remove the file \"{name}\"? Some content using this file will also be removed." unfollowConfirm: "Are you sure you want to unfollow {name}?" exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed." importRequested: "You've requested an import. This may take a while." @@ -101,7 +106,7 @@ unfollow: "Unfollow" followRequestPending: "Follow request pending" enterEmoji: "Enter an emoji" renote: "Renote" -unrenote: "Take back renote" +unrenote: "Remove renote" renoted: "Renoted." cantRenote: "This post can't be renoted." cantReRenote: "A renote can't be renoted." @@ -112,7 +117,7 @@ pinnedNote: "Pinned note" pinned: "Pin to profile" you: "You" clickToShow: "Click to show" -sensitive: "NSFW" +sensitive: "Sensitive" add: "Add" reaction: "Reactions" reactions: "Reactions" @@ -120,8 +125,8 @@ reactionSetting: "Reactions to show in the reaction picker" reactionSettingDescription2: "Drag to reorder, click to delete, press \"+\" to add." rememberNoteVisibility: "Remember note visibility settings" attachCancel: "Remove attachment" -markAsSensitive: "Mark as NSFW" -unmarkAsSensitive: "Unmark as NSFW" +markAsSensitive: "Mark as sensitive" +unmarkAsSensitive: "Unmark as sensitive" enterFileName: "Enter filename" mute: "Mute" unmute: "Unmute" @@ -136,8 +141,10 @@ unblockConfirm: "Are you sure that you want to unblock this account?" suspendConfirm: "Are you sure that you want to suspend this account?" unsuspendConfirm: "Are you sure that you want to unsuspend this account?" selectList: "Select a list" +editList: "Edit list" selectChannel: "Select a channel" selectAntenna: "Select an antenna" +editAntenna: "Edit antenna" selectWidget: "Select a widget" editWidgets: "Edit widgets" editWidgetsExit: "Done" @@ -150,6 +157,9 @@ addEmoji: "Add an emoji" settingGuide: "Recommended settings" cacheRemoteFiles: "Cache remote files" cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote instance. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated." +youCanCleanRemoteFilesCache: "You can clear the cache by clicking the 🗑️ button in the file management view." +cacheRemoteSensitiveFiles: "Cache sensitive remote files" +cacheRemoteSensitiveFilesDescription: "When this setting is disabled, sensitive remote files are loaded directly from the remote instance without caching." flagAsBot: "Mark this account as a bot" flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Misskey's internal systems to treat this account as a bot." flagAsCat: "Mark this account as a cat" @@ -311,7 +321,7 @@ copyUrl: "Copy URL" rename: "Rename" avatar: "Avatar" banner: "Banner" -nsfw: "NSFW" +displayOfSensitiveMedia: "Display of sensitive media" whenServerDisconnected: "When losing connection to the server" disconnectedFromServer: "Connection to server has been lost" reload: "Refresh" @@ -346,7 +356,6 @@ invite: "Invite" driveCapacityPerLocalAccount: "Drive capacity per local user" driveCapacityPerRemoteAccount: "Drive capacity per remote user" inMb: "In megabytes" -iconUrl: "Icon URL" bannerUrl: "Banner image URL" backgroundImageUrl: "Background image URL" basicInfo: "Basic info" @@ -402,10 +411,13 @@ aboutMisskey: "About Misskey" administrator: "Administrator" token: "Token" 2fa: "Two-factor authentication" +setupOf2fa: "Setup two-factor authentification" totp: "Authenticator App" totpDescription: "Use an authenticator app to enter one-time passwords" moderator: "Moderator" moderation: "Moderation" +moderationNote: "Moderation note" +addModerationNote: "Add moderation note" nUsersMentioned: "Mentioned by {n} users" securityKeyAndPasskey: "Security- and passkeys" securityKey: "Security key" @@ -571,7 +583,7 @@ serviceworkerInfo: "Must be enabled for push notifications." deletedNote: "Deleted note" invisibleNote: "Invisible note" enableInfiniteScroll: "Automatically load more" -visibility: "Visiblility" +visibility: "Visibility" poll: "Poll" useCw: "Hide content" enablePlayer: "Open video player" @@ -645,6 +657,7 @@ behavior: "Behavior" sample: "Sample" abuseReports: "Reports" reportAbuse: "Report" +reportAbuseRenote: "Report renote" reportAbuseOf: "Report {name}" fillAbuseReportDescription: "Please fill in details regarding this report. If it is about a specific note, please include its URL." abuseReported: "Your report has been sent. Thank you very much." @@ -672,6 +685,7 @@ createNewClip: "Create new clip" unclip: "Unclip" confirmToUnclipAlreadyClippedNote: "This note is already part of the \"{name}\" clip. Do you want to remove it from this clip instead?" public: "Public" +private: "Private" i18nInfo: "Misskey is being translated into various languages by volunteers. You can help at {link}." manageAccessTokens: "Manage access tokens" accountInfo: "Account Info" @@ -693,7 +707,7 @@ driveUsage: "Drive space usage" noCrawle: "Reject crawler indexing" noCrawleDescription: "Ask search engines to not index your profile page, notes, Pages, etc." lockedAccountInfo: "Unless you set your note visiblity to \"Followers only\", your notes will be visible to anyone, even if you require followers to be manually approved." -alwaysMarkSensitive: "Mark as NSFW by default" +alwaysMarkSensitive: "Mark as sensitive by default" loadRawImages: "Load original images instead of showing thumbnails" disableShowingAnimatedImages: "Don't play animated images" verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification." @@ -912,16 +926,16 @@ type: "Type" speed: "Speed" slow: "Slow" fast: "Fast" -sensitiveMediaDetection: "Detection of NSFW media" +sensitiveMediaDetection: "Detection of sensitive media" localOnly: "Local only" remoteOnly: "Remote only" failedToUpload: "Upload failed" -cannotUploadBecauseInappropriate: "This file could not be uploaded because parts of it have been detected as potentially NSFW." +cannotUploadBecauseInappropriate: "This file could not be uploaded because parts of it have been detected as potentially inappropriate." cannotUploadBecauseNoFreeSpace: "Upload failed due to lack of Drive capacity." cannotUploadBecauseExceedsFileSizeLimit: "This file cannot be uploaded as it exceeds the file size limit." beta: "Beta" -enableAutoSensitive: "Automatic NSFW-Marking" -enableAutoSensitiveDescription: "Allows automatic detection and marking of NSFW media through Machine Learning where possible. Even if this option is disabled, it may be enabled instance-wide." +enableAutoSensitive: "Automatic marking as sensitive" +enableAutoSensitiveDescription: "Allows automatic detection and marking of sensitive media through Machine Learning where possible. Even if this option is disabled, it may be enabled instance-wide." activeEmailValidationDescription: "Enables stricter validation of email addresses, which includes checking for disposable addresses and by whether it can actually be communicated with. When unchecked, only the format of the email is validated." navbar: "Navigation bar" shuffle: "Shuffle" @@ -991,7 +1005,7 @@ postToTheChannel: "Post to channel" cannotBeChangedLater: "This cannot be changed later." reactionAcceptance: "Reaction Acceptance" likeOnly: "Only likes" -likeOnlyForRemote: "Only likes for remote instances" +likeOnlyForRemote: "All (Only likes for remote instances)" nonSensitiveOnly: "Non-sensitive only" nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non-sensitive only (Only likes from remote)" rolesAssignedToMe: "Roles assigned to me" @@ -1010,7 +1024,7 @@ retryAllQueuesConfirmText: "This will temporarily increase the server load." enableChartsForRemoteUser: "Generate remote user data charts" enableChartsForFederatedInstances: "Generate remote instance data charts" showClipButtonInNoteFooter: "Add \"Clip\" to note action menu" -largeNoteReactions: "Enlargen displayed reactions" +reactionsDisplaySize: "Reaction display size" noteIdOrUrl: "Note ID or URL" video: "Video" videos: "Videos" @@ -1034,7 +1048,7 @@ vertical: "Vertical" horizontal: "Horizontal" position: "Position" serverRules: "Server rules" -pleaseConfirmBelowBeforeSignup: "Please confirm the below before signing up." +pleaseConfirmBelowBeforeSignup: "To register on this server, you must review and agree to the following:" pleaseAgreeAllToContinue: "You must agree to all above fields to continue." continue: "Continue" preservedUsernames: "Reserved usernames" @@ -1062,6 +1076,55 @@ later: "Later" goToMisskey: "To Misskey" additionalEmojiDictionary: "Additional emoji dictionaries" installed: "Installed" +branding: "Branding" +enableServerMachineStats: "Publish server hardware stats" +enableIdenticonGeneration: "Enable user identicon generation" +turnOffToImprovePerformance: "Turning this off can increase performance." +createInviteCode: "Generate invite" +createWithOptions: "Generate with options" +createCount: "Invite count" +inviteCodeCreated: "Invite generated" +inviteLimitExceeded: "You've exceeded the limit of invites you can generate." +createLimitRemaining: "Invite limit: {limit} remaining" +inviteLimitResetCycle: "This limit will reset to {limit} at {time}." +expirationDate: "Expiration date" +noExpirationDate: "No expiration" +inviteCodeUsedAt: "Invite code used at" +registeredUserUsingInviteCode: "Invite used by" +waitingForMailAuth: "Email verification pending" +inviteCodeCreator: "Invite created by" +usedAt: "Used at" +unused: "Unused" +used: "Used" +expired: "Expired" +doYouAgree: "Agree?" +beSureToReadThisAsItIsImportant: "Please read this important information." +iHaveReadXCarefullyAndAgree: "I have read the text \"{x}\" and agree." +dialog: "Dialog" +icon: "Icon" +forYou: "For you" +currentAnnouncements: "Current announcements" +pastAnnouncements: "Past announcements" +youHaveUnreadAnnouncements: "There are unread announcements." +useSecurityKey: "Please follow your browser's or device's instructions to use your security- or passkey." +replies: "Reply" +renotes: "Renotes" +loadReplies: "Show replies" +loadConversation: "Show conversation" +pinnedList: "Pinned list" +keepScreenOn: "Keep screen on" +verifiedLink: "Link ownership has been verified" +notifyNotes: "Notify about new notes" +unnotifyNotes: "Stop notifying about new notes" +_announcement: + forExistingUsers: "Existing users only" + forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it." + needConfirmationToRead: "Require separate read confirmation" + needConfirmationToReadDescription: "A separate prompt to confirm marking this announcement as read will be displayed if enabled. This announcement will also be excluded from any \"Mark all as read\" functionality." + end: "Archive announcement" + tooManyActiveAnnouncementDescription: "Having too many active announcements may worsen the user experience. Please consider archiving announcements that have become obsolete." + readConfirmTitle: "Mark as read?" + readConfirmText: "This will mark the contents of \"{title}\" as read." _initialAccountSetting: accountCreated: "Your account was successfully created!" letsStartAccountSetup: "For starters, let's set up your profile." @@ -1079,6 +1142,13 @@ _initialAccountSetting: laterAreYouSure: "Really do profile setup later?" _serverRules: description: "A set of rules to be displayed before registration. Setting a summary of the Terms of Service is recommended." +_serverSettings: + iconUrl: "Icon URL" + appIconDescription: "Specifies the icon to use when {host} is displayed as an app." + appIconUsageExample: "E.g. As PWA, or when displayed as a home screen bookmark on a phone" + appIconStyleRecommendation: "As the icon may be cropped to a square or circle, an icon with colored margin around the content is recommended." + appIconResolutionMustBe: "The minimum resolution is {resolution}." + manifestJsonOverride: "manifest.json Override" _accountMigration: moveFrom: "Migrate another account to this one" moveFromSub: "Create alias to another account" @@ -1093,7 +1163,7 @@ _accountMigration: migrationConfirm: "Really migrate this account to {account}? Once started, this process cannot be stopped or taken back, and you will not be able to use this account in its original state anymore." movedAndCannotBeUndone: "\nThis account has been migrated.\nMigration cannot be reversed." postMigrationNote: "This account will unfollow all accounts it is currently following 24 hours after migration finishes.\nBoth the number of follows and followers will then become zero. To avoid your followers from being unable to see followers only posts of this account, they will however continue following this account." - movedTo: "Account to move to:" + movedTo: "New account:" _achievements: earnedAt: "Unlocked at" _types: @@ -1333,6 +1403,9 @@ _achievements: title: "Brain Diver" description: "Post the link to Brain Diver" flavor: "Misskey-Misskey La-Tu-Ma" + _smashTestNotificationButton: + title: "Test overflow" + description: "Trigger the notification test repeatedly within an extremely short time" _role: new: "New role" edit: "Edit role" @@ -1347,7 +1420,7 @@ _role: condition: "Condition" isConditionalRole: "This is a conditional role." isPublic: "Public role" - descriptionOfIsPublic: "Anyone will be able to view a list of users assigned to this role. In addition, this role will be displayed in the profiles of assigned users." + descriptionOfIsPublic: "This role will be displayed in the profiles of assigned users." options: "Options" policies: "Policies" baseRole: "Role template" @@ -1356,8 +1429,8 @@ _role: iconUrl: "Icon URL" asBadge: "Show as badge" descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on." - isExplorable: "Role timeline is public" - descriptionOfIsExplorable: "This role's timeline will become publicly accessible if enabled. Timelines of non-public roles will not be made public even if set." + isExplorable: "Make role explorable" + descriptionOfIsExplorable: "This role's timeline and the list of users with this will be made public if enabled." displayOrder: "Position" descriptionOfDisplayOrder: "The higher the number, the higher its UI position." canEditMembersByModerator: "Allow moderators to edit the list of members for this role" @@ -1372,6 +1445,9 @@ _role: ltlAvailable: "Can view the local timeline" canPublicNote: "Can send public notes" canInvite: "Can create instance invite codes" + inviteLimit: "Invite limit" + inviteLimitCycle: "Invite limit cooldown" + inviteExpirationTime: "Invite expiration interval" canManageCustomEmojis: "Can manage custom emojis" driveCapacity: "Drive capacity" alwaysMarkNsfw: "Always mark files as NSFW" @@ -1402,10 +1478,10 @@ _role: or: "OR-Condition" not: "NOT-Condition" _sensitiveMediaDetection: - description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server." + description: "Reduces the effort of server moderation through automatically recognizing sensitive media via Machine Learning. This will slightly increase the load on the server." sensitivity: "Detection sensitivity" sensitivityDescription: "Reducing the sensitivity will lead to fewer misdetections (false positives) whereas increasing it will lead to fewer missed detections (false negatives)." - setSensitiveFlagAutomatically: "Mark as NSFW" + setSensitiveFlagAutomatically: "Mark as sensitive" setSensitiveFlagAutomaticallyDescription: "The results of the internal detection will be retained even if this option is turned off." analyzeVideos: "Enable analysis of videos" analyzeVideosDescription: "Analyzes videos in addition to images. This will slightly increase the load on the server." @@ -1434,6 +1510,7 @@ _ad: back: "Back" reduceFrequencyOfThisAd: "Show this ad less" hide: "Hide" + timezoneinfo: "The day of the week is determined from the server's timezone." _forgotPassword: enterEmail: "Enter the email address you used to register. A link with which you can reset your password will then be sent to it." ifNoEmail: "If you did not use an email during registration, please contact the instance administrator instead." @@ -1485,9 +1562,9 @@ _aboutMisskey: donate: "Donate to Misskey" morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰" patrons: "Patrons" -_nsfw: - respect: "Hide NSFW media" - ignore: "Don't hide NSFW media" +_displayOfSensitiveMedia: + respect: "Hide media marked as sensitive" + ignore: "Display media marked as sensitive" force: "Hide all media" _instanceTicker: none: "Never show" @@ -1639,18 +1716,17 @@ _timelineTutorial: _2fa: alreadyRegistered: "You have already registered a 2-factor authentication device." 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:" + step2Uri: "Enter the following URI if you are using a desktop program" step3Title: "Enter an authentication code" step3: "Enter the token provided by your app to finish setup." + setupCompleted: "Setup complete" 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" @@ -1661,6 +1737,11 @@ _2fa: renewTOTPConfirm: "This will cause verification codes from your previous app to stop working" renewTOTPOk: "Reconfigure" renewTOTPCancel: "Cancel" + checkBackupCodesBeforeCloseThisWizard: "Before you close this window, please note the following backup codes." + backupCodes: "Backup codes" + backupCodesDescription: "You can use these codes to gain access to your account in case of becoming unable to use your two-factor authentificator app. Each can only be used once. Please keep them in a safe place." + backupCodeUsedWarning: "A backup code has been used. Please reconfigure two-factor authentification as soon as possible if you are no longer able to use it." + backupCodesExhaustedWarning: "All backup codes have been used. Should you lose access to your two-factor authentification app, you will be unable to access this account. Please reconfigure two-factor authentification." _permissions: "read:account": "View your account information" "write:account": "Edit your account information" @@ -1682,10 +1763,10 @@ _permissions: "read:reactions": "View your reactions" "write:reactions": "Edit your reactions" "write:votes": "Vote on a poll" - "read:pages": "View your pages" - "write:pages": "Edit or delete your pages" - "read:page-likes": "View your likes on pages" - "write:page-likes": "Edit your likes on pages" + "read:pages": "View your Pages" + "write:pages": "Edit or delete your Pages" + "read:page-likes": "View list of liked Pages" + "write:page-likes": "Edit list of liked Pages" "read:user-groups": "View your user groups" "write:user-groups": "Edit or delete your user groups" "read:channels": "View your channels" @@ -1694,6 +1775,10 @@ _permissions: "write:gallery": "Edit your gallery" "read:gallery-likes": "View your list of liked gallery posts" "write:gallery-likes": "Edit your list of liked gallery posts" + "read:flash": "View Play" + "write:flash": "Edit Plays" + "read:flash-likes": "View list of liked Plays" + "write:flash-likes": "Edit list of liked Plays" _auth: shareAccessTitle: "Granting application permissions" shareAccess: "Would you like to authorize \"{name}\" to access this account?" @@ -1808,6 +1893,7 @@ _profile: metadataContent: "Content" changeAvatar: "Change avatar" changeBanner: "Change banner" + verifiedLinkDescription: "By entering an URL that contains a link to your profile here, an ownership verification icon can be displayed next to the field." _exportOrImport: allNotes: "All notes" favoritedNotes: "Favorite notes" @@ -1926,9 +2012,14 @@ _notification: youReceivedFollowRequest: "You've received a follow request" yourFollowRequestAccepted: "Your follow request was accepted" pollEnded: "Poll results have become available" + newNote: "New note" unreadAntennaNote: "Antenna {name}" emptyPushNotificationMessage: "Push notifications have been updated" achievementEarned: "Achievement unlocked" + testNotification: "Test notification" + checkNotificationBehavior: "Check notification appearance" + sendTestNotification: "Send test notification" + notificationWillBeDisplayedLikeThis: "Notifications look like this" _types: all: "All" follow: "New followers" @@ -1963,6 +2054,9 @@ _deck: introduction: "Create the perfect interface for you by arranging columns freely!" introduction2: "Click on the + on the right of the screen to add new colums whenever you want." widgetsIntroduction: "Please select \"Edit widgets\" in the column menu and add a widget." + useSimpleUiForNonRootPages: "Use simplified UI to navigated pages" + usedAsMinWidthWhenFlexible: "Minimum width will be used for this when the \"Auto-adjust width\" option is enabled" + flexible: "Auto-adjust width" _columns: main: "Main" widgets: "Widgets" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index a5dd18e3fc..bfa779d78a 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -8,10 +8,10 @@ search: "Buscar" notifications: "Notificaciones" username: "Nombre de usuario" password: "Contraseña" -forgotPassword: "Olvidé mi Contraseña" +forgotPassword: "Olvidé mi contraseña" fetchingAsApObject: "Buscando en el fediverso" ok: "OK" -gotIt: "¡Lo tengo!" +gotIt: "Entendido" cancel: "Cancelar" noThankYou: "No gracias" enterUsername: "Introduce el nombre de usuario" @@ -20,8 +20,8 @@ noNotes: "No hay notas" noNotifications: "No hay notificaciones" instance: "Instancia" settings: "Configuración" -notificationSettings: "Configurar las notificaciones" -basicSettings: "Configuración Básica" +notificationSettings: "Ajustes de notificaciones" +basicSettings: "Configuración básica" otherSettings: "Configuración avanzada" openInWindow: "Abrir en una ventana" profile: "Perfil" @@ -45,13 +45,20 @@ pin: "Fijar al perfil" unpin: "Desfijar" copyContent: "Copiar contenido" copyLink: "Copiar enlace" +copyLinkRenote: "Copiar enlace de renota" delete: "Borrar" deleteAndEdit: "Borrar y editar" deleteAndEditConfirm: "¿Estás seguro de que quieres borrar esta nota y editarla? Perderás todas las reacciones, renotas y respuestas." addToList: "Agregar a lista" +addToAntenna: "Añadir a la antena" sendMessage: "Enviar un mensaje" copyRSS: "Copiar RSS" copyUsername: "Copiar nombre de usuario" +copyUserId: "Copiar ID del usuario" +copyNoteId: "Copiar ID de la nota" +copyFileId: "Copiar ID del archivo" +copyFolderId: "Copiar ID de carpeta" +copyProfileUrl: "Copiar la URL del perfil" searchUser: "Buscar un usuario" reply: "Responder" loadMore: "Ver más" @@ -134,8 +141,10 @@ unblockConfirm: "¿Quiere dejar de bloquear esta cuenta?" suspendConfirm: "¿Quiere suspender esta cuenta?" unsuspendConfirm: "¿Quiere dejar de suspender esta cuenta?" selectList: "Seleccione una lista" +editList: "Editar lista" selectChannel: "Seleccionar canal" selectAntenna: "Seleccionar antena" +editAntenna: "Editar antena" selectWidget: "Seleccionar widget" editWidgets: "Editar widgets" editWidgetsExit: "Terminar edición" @@ -148,6 +157,9 @@ addEmoji: "Agregar emoji" settingGuide: "Configuración sugerida" cacheRemoteFiles: "Mantener en cache los archivos remotos" cacheRemoteFilesDescription: "Si desactiva esta configuración, Los archivos remotos se cargarán desde el link directo sin usar la caché. Con eso se puede ahorrar almacenamiento del servidor, pero eso aumentará el tráfico al no crear miniaturas." +youCanCleanRemoteFilesCache: "Puedes vaciar la caché pulsando en el botón 🗑️ en el administrador de archivos." +cacheRemoteSensitiveFiles: "Cachear archivos remotos sensibles" +cacheRemoteSensitiveFilesDescription: "Cuando esta opción está desactivada, los archivos remotos sensibles son cargador directamente de la instancia origen sin ser cacheados." flagAsBot: "Esta cuenta es un bot" flagAsBotDescription: "En caso de que esta cuenta fuera usada por un programa, active esta opción. Al hacerlo, esta opción servirá para otros desarrolladores para evitar cadenas infinitas de reacciones, y ajustará los sistemas internos de Misskey para que trate a esta cuenta como un bot." flagAsCat: "Esta cuenta es un gato" @@ -229,10 +241,10 @@ instanceFollowers: "Seguidores de la instancia" instanceUsers: "Usuarios de la instancia" changePassword: "Cambiar contraseña" security: "Seguridad" -retypedNotMatch: "No hay coincidencia" +retypedNotMatch: "La información no coincide." currentPassword: "Contraseña actual" newPassword: "Contraseña nueva" -newPasswordRetype: "Contraseña nueva (repetir)" +newPasswordRetype: "Reescribe contraseña nueva" attachFile: "Añadir archivo" more: "¡Más!" featured: "Destacados" @@ -309,7 +321,7 @@ copyUrl: "Copiar URL" rename: "Renombrar" avatar: "Avatar" banner: "Banner" -nsfw: "Marcado como sensible" +displayOfSensitiveMedia: "Mostrar contenido sensible" whenServerDisconnected: "Cuando se pierda la conexión con el servidor" disconnectedFromServer: "Desconectado del servidor" reload: "Recargar" @@ -344,7 +356,6 @@ invite: "Invitar" driveCapacityPerLocalAccount: "Capacidad del drive por usuario local" driveCapacityPerRemoteAccount: "Capacidad del drive por usuario remoto" inMb: "En megabytes" -iconUrl: "URL de la imagen del avatar" bannerUrl: "URL de la imagen del banner" backgroundImageUrl: "URL de la imagen de fondo" basicInfo: "Información básica" @@ -400,10 +411,13 @@ aboutMisskey: "Sobre Misskey" administrator: "Administrador" token: "Token" 2fa: "Autenticación de doble factor" +setupOf2fa: "Configurar la autenticación de dos factores" 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" +moderationNote: "Nota de moderación" +addModerationNote: "Añadir nota de moderación" nUsersMentioned: "{n} usuarios mencionados" securityKeyAndPasskey: "Clave de seguridad / clave de paso" securityKey: "Clave de seguridad" @@ -433,7 +447,7 @@ title: "Título" text: "Texto" enable: "Activar" next: "Siguiente" -retype: "Intentar de nuevo" +retype: "Ingrese de nuevo" noteOf: "Notas de {user}" quoteAttached: "Cita añadida" quoteQuestion: "¿Quiere añadir una cita?" @@ -453,7 +467,7 @@ weakPassword: "Contraseña débil" normalPassword: "Buena contraseña" strongPassword: "Muy buena contraseña" passwordMatched: "Correcto" -passwordNotMatched: "Las contraseñas no son las mismas" +passwordNotMatched: "Las contraseñas no coinciden" signinWith: "Inicie sesión con {x}" signinFailed: "Autenticación fallida. Asegúrate de haber usado el nombre de usuario y contraseña correctos." or: "O" @@ -643,6 +657,7 @@ behavior: "Comportamiento" sample: "Muestra" abuseReports: "Reportes" reportAbuse: "Reportar" +reportAbuseRenote: "Reportar renota" reportAbuseOf: "Reportar a {name}" fillAbuseReportDescription: "Ingrese los detalles del reporte. Si hay una nota en particular, ingrese la URL de esta." abuseReported: "Se ha enviado el reporte. Muchas gracias." @@ -670,6 +685,7 @@ createNewClip: "Crear clip nuevo" unclip: "Quitar clip" confirmToUnclipAlreadyClippedNote: "Esta nota ya está incluida en el clip \"{name}\". ¿Quiere quitar la nota del clip?" public: "Público" +private: "Privado" i18nInfo: "Misskey está siendo traducido a varios idiomas gracias a voluntarios. Se puede colaborar traduciendo en {link}" manageAccessTokens: "Administrar tokens de acceso" accountInfo: "Información de la Cuenta" @@ -790,8 +806,9 @@ noMaintainerInformationWarning: "No se ha establecido la información del admini noBotProtectionWarning: "La protección contra los bots no está configurada" configure: "Configurar" postToGallery: "Crear una nueva publicación en la galería" +postToHashtag: "Publicar a este hashtag" gallery: "Galería" -recentPosts: "Posts recientes" +recentPosts: "Publicaciones recientes" popularPosts: "Más vistos" shareWithNote: "Compartir con una nota" ads: "Anuncios" @@ -823,6 +840,7 @@ translatedFrom: "Traducido de {x}" accountDeletionInProgress: "La eliminación de la cuenta está en curso" usernameInfo: "Un nombre que identifique su cuenta de otras en este servidor. Puede utilizar el alfabeto (a~z, A~Z), dígitos (0~9) o guiones bajos (_). Los nombres de usuario no se pueden cambiar posteriormente." aiChanMode: "Modo Ai" +devMode: "Modo de desarrollador" keepCw: "Mantener la advertencia de contenido" pubSub: "Cuentas Pub/Sub" lastCommunication: "Última comunicación" @@ -832,15 +850,17 @@ 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" +on: "Activado" +off: "Desactivado" +emailRequiredForSignup: "Se requiere una dirección de correo electrónico para el registro de la cuenta" unread: "No leído" -filter: "Filtro" +filter: "Filtrar" controlPanel: "Panel de control" manageAccounts: "Administrar cuenta" makeReactionsPublic: "Hacer el historial de reacciones público" makeReactionsPublicDescription: "Todas las reacciones que hayas hecho serán públicamente visibles." classic: "Clásico" -muteThread: "Ocultar hilo" +muteThread: "Silenciar hilo" unmuteThread: "Mostrar hilo" ffVisibility: "Visibilidad de seguidores y seguidos" ffVisibilityDescription: "Puedes configurar quien puede ver a quienes sigues y quienes te siguen" @@ -912,6 +932,7 @@ remoteOnly: "Sólo remoto" failedToUpload: "La subida falló" cannotUploadBecauseInappropriate: "Este archivo no se puede subir debido a que algunas partes han sido detectadas comoNSFW." cannotUploadBecauseNoFreeSpace: "La subida falló debido a falta de espacio libre en la unidad del usuario." +cannotUploadBecauseExceedsFileSizeLimit: "Este archivo supera el peso máximo y no puede ser subido." beta: "Beta" enableAutoSensitive: "Marcar automáticamente contenido NSFW" enableAutoSensitiveDescription: "Permite la detección y marcado automático de contenido NSFW usando 'Machine Learning' cuando sea posible. Incluso si esta opción está desactivada, puede ser activado para toda la instancia." @@ -942,18 +963,23 @@ show: "Apariencia" neverShow: "No mostrar de nuevo" remindMeLater: "Recordar después" didYouLikeMisskey: "¿Te gusta Misskey?" -pleaseDonate: "Misskey es software libre, y es usado por {host} . Por favor, ¡considera donar al proyecto principal para que podamos continuar!" +pleaseDonate: "{host} usa el software gratuito Misskey. Por favor ¡Considera donar al proyecto principal para que podamos continuar!" roles: "Roles" -role: "Roles" +role: "Rol" +noRole: "Rol no encontrado" normalUser: "Usuario normal" undefined: "Indefinido" assign: "Asignar" unassign: "Quitar" color: "Color" manageCustomEmojis: "Administrar emojis personalizados" -youCannotCreateAnymore: "Se alcanzó el límite de creación" -cannotPerformTemporary: "Indisponible temporalmente" +youCannotCreateAnymore: "Has llegado al límite de creaciones." +cannotPerformTemporary: "Temporalmente no disponible" cannotPerformTemporaryDescription: "Esta acción no se puede realizar porque se excedió el límite de ejecución. Espera un poco y prueba de nuevo." +invalidParamError: "Parámetros inválidos" +invalidParamErrorDescription: "Los parámetros de la solicitud son inválidos. Normalmente se trata de un error, pero también puede haberse excedido algún límite o similares." +permissionDeniedError: "Operación denegada" +permissionDeniedErrorDescription: "Esta cuenta no tiene permisos para hacer esa acción." preset: "Predefinido" selectFromPresets: "Escoger desde predefinidos" achievements: "Logros" @@ -969,7 +995,7 @@ 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" +letsLookAtTimeline: "Mira la línea de tiempo" disableFederationConfirm: "¿Estas seguro que quieres desactivar la federación?" disableFederationConfirmWarn: "Aunque no exista federación los posts no serán marcados como privados. En la mayoría de los casos, no es necesario hacer los posts no federar." disableFederationOk: "Desactivar." @@ -980,10 +1006,13 @@ cannotBeChangedLater: "Esto no podrá ser cambiado después." reactionAcceptance: "Aceptación de reacciones" likeOnly: "Sólo 'me gusta'" likeOnlyForRemote: "Sólo reacciones de instancias remotas" +nonSensitiveOnly: "Solo no sensible" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "Sólo no contenido sensible (sólo me gusta en remote)" rolesAssignedToMe: "Roles asignados a mí" resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?" sensitiveWords: "Palabras sensibles" sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de línea" +sensitiveWordsDescription2: "Si se usan espacios se crearán expresiones AND y las palabras subsecuentes con barras inclinadas se convertirán en expresiones regulares." notesSearchNotAvailable: "No se puede buscar una nota" license: "Licencia" unfavoriteConfirm: "¿Desea quitar de favoritos?" @@ -995,24 +1024,134 @@ retryAllQueuesConfirmText: "La carga del servidor está incrementándose tempora enableChartsForRemoteUser: "Generar gráficas de usuarios remotos." enableChartsForFederatedInstances: "Generar gráficos de servidores remotos" showClipButtonInNoteFooter: "Añadir \"Clip\" al menú de notas" -largeNoteReactions: "Agrandar las reacciones de las notas" +reactionsDisplaySize: "Tamaño de las reacciones" noteIdOrUrl: "ID o URL de la nota" +video: "Video" +videos: "Video" +dataSaver: "Ahorro de datos" accountMigration: "Migración de cuenta" -accountMoved: "Este usuario se ha mudado a una nueva cuenta:" +accountMoved: "Este usuario se movió a una nueva cuenta:" accountMovedShort: "Esta cuenta ha sido migrada." +operationForbidden: "Operación prohibida" +forceShowAds: "Siempre mostrar anuncios" +addMemo: "Añadir nota" +editMemo: "Editar nota" +reactionsList: "Lista de reacciones" +renotesList: "Renotas" +notificationDisplay: "Notificaciones" +leftTop: "Arriba a la izquierda" +rightTop: "Arriba a la derecha" +leftBottom: "Abajo a la izquierda" +rightBottom: "Abajo a la derecha" +stackAxis: "Dirección de apilado" +vertical: "Vertical" horizontal: "Horizontal" +position: "Posición" +serverRules: "Reglas del servidor" +pleaseConfirmBelowBeforeSignup: "Por favor confirma antes de continuar el registro" +pleaseAgreeAllToContinue: "Tienes que estar de acuerdo con los campos anteriores para contnuar." +continue: "Continuar" +preservedUsernames: "Nombre de usuario reservado" +preservedUsernamesDescription: "La lista de nombres de usuario para reservar tienen que separarse con saltos de línea.\nEstos estarán indisponibles durante la creación de cuentas, pero pueden ser usados para que los administradores puedan crear esas cuentas manualmente. Las cuentas existentes con esos nombres de usuario no se verán afectadas." +createNoteFromTheFile: "Componer una nota desde éste archivo" +archive: "Archivo" +channelArchiveConfirmTitle: "¿Seguro de archivar {name}?" +channelArchiveConfirmDescription: "Un canal archivado no aparecerá en la lista de canales ni en los resultados. Las nuevas publicaciones tampoco serán añadidas." +thisChannelArchived: "El canal ha sido archivado." +displayOfNote: "Mostrar notas" +initialAccountSetting: "Configración inicial de su cuenta\nか\nConfigración de inicio" youFollowing: "Siguiendo" +preventAiLearning: "Rechazar el uso en el Aprendizaje de Máquinas. (IA Generativa)" +preventAiLearningDescription: "Pedirle a las arañas (crawlers) no usar los textos publicados o imágenes en el aprendizaje automático (IA Predictiva / Generativa). Ésto se logra añadiendo una marca respuesta HTML con la cadena \"noai\" al cantenido. Una prevención total no podría lograrse sólo usando ésta marca, ya que puede ser simplemente ignorada." options: "Opción" +specifyUser: "Especificar usuario" +failedToPreviewUrl: "No se pudo generar la vista previa" +update: "Actualizar" +rolesThatCanBeUsedThisEmojiAsReaction: "Roles que pueden usar este emoji como reacción" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Si no se especifican roles, cualquiera podrá usar éste emoji como reacción." +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Éstos roles deben ser públicos." +cancelReactionConfirm: "¿Realmente quieres eliminar la reacción?" +changeReactionConfirm: "¿Realmente quieres cambiar la reacción?" +later: "Ahora no" +goToMisskey: "ir a Misskey" +additionalEmojiDictionary: "Diccionario adicional de Emoji" +installed: "Instalado" +branding: "Marca" +enableServerMachineStats: "Publicar estadísticas de hardware del servidor" +enableIdenticonGeneration: "Activar generación de identicon por usuario" +turnOffToImprovePerformance: "Desactivar esto puede aumentar el rendimiento." +createInviteCode: "Generar invitación" +createWithOptions: "Generar con opciones" +createCount: "Conteo de invitaciones" +inviteCodeCreated: "Invitación generada" +inviteLimitExceeded: "Has excedido el límite de invitaciones que puedes generar." +createLimitRemaining: "Límite de invitaciones: quedan {limit}" +inviteLimitResetCycle: "El límite ha sido reiniciado a {limit} por {time}." +expirationDate: "Fecha de caducidad" +noExpirationDate: "Sin caducidad" +inviteCodeUsedAt: "Código de invitación usado el" +registeredUserUsingInviteCode: "Invitación usada por" +waitingForMailAuth: "Verificación de correo pendiente" +inviteCodeCreator: "Invitación creada por" +usedAt: "Usada el" +unused: "Sin usar" +used: "Usada" +expired: "Caducada" +doYouAgree: "¿Está de acuerdo?" +beSureToReadThisAsItIsImportant: "Por favor lea esto que es importante" +iHaveReadXCarefullyAndAgree: "He leído el texto {x} y estoy de acuerdo" +dialog: "Diálogo" +icon: "Avatar" +forYou: "Para ti" +currentAnnouncements: "Anuncios actuales" +pastAnnouncements: "Anuncios anteriores" +youHaveUnreadAnnouncements: "Hay anuncios sin leer" +useSecurityKey: "Por favor, sigue las instrucciones de tu dispositivo o navegador para usar tu clave de seguridad o tu clave de paso." +replies: "Responder" +renotes: "Renotar" +_announcement: + forExistingUsers: "Solo para usuarios registrados" + forExistingUsersDescription: "Este anuncio solo se mostrará a aquellos usuarios registrados en el momento de su publicación. Si se deshabilita esta opción, aquellos usuarios que se registren tras su publicación también lo verán." + needConfirmationToRead: "Requerir confirmación de lectura aparte" + needConfirmationToReadDescription: "Si se habilita esta opción, se pedirá una confirmación de lectura aparte. Además, este anuncio será excluido de cualquier funcionalidad de \"Marcar todos como leídos\"." + end: "Anuncios archivados" + tooManyActiveAnnouncementDescription: "Tener demasiados anuncios activos empeora la experiencia de usuario. Por favor, considera archivar aquellos anuncios que hayan quedado obsoletos." + readConfirmTitle: "¿Marcar como leído?" + readConfirmText: "Esto marcará el contenido de \"{title}\" como leído." _initialAccountSetting: accountCreated: "¡La cuenta ha sido creada!" + letsStartAccountSetup: "Para empezar, creemos tu perfil." + letsFillYourProfile: "Primero, creemos tu perfil." + profileSetting: "Configuración del perfil" + privacySetting: "Configuración de privacidad" + theseSettingsCanEditLater: "Puedes cambiar estos ajustes más tarde." + youCanEditMoreSettingsInSettingsPageLater: "Desde la pestaña de \"Configuración\" puedes modificar más ajustes. Asegúrate de visitarla después." + followUsers: "Comienza a seguir a usuarios que te interesen para construir tu línea de tiempo." + pushNotificationDescription: "Habilitar las notificaciones push te permitirá recibir notificaciones de {name} directamente en tu dispositivo." + initialAccountSettingCompleted: "¡Configuración del perfil completada!" + haveFun: "¡Disfruta de {name}!" + ifYouNeedLearnMore: "Si quieres aprender cómo usar {name} (Misskey), por favor, visita {link}." + skipAreYouSure: "¿Realmente quieres saltarte la configuración del perfil?" + laterAreYouSure: "¿Realmente quieres configurar tu perfil después?" +_serverRules: + description: "Un conjunto de reglas que serán mostradas antes del registro. Configurar un sumario de términos de servicio es recomendado." +_serverSettings: + iconUrl: "URL del ícono" + manifestJsonOverride: "Sobreescribir manifest.json" _accountMigration: moveFrom: "Trasladar de otra cuenta a ésta" + moveFromSub: "Crear un alias para otra cuenta." moveFromLabel: "Cuenta desde la que se realiza el traslado:" moveFromDescription: "Si quieres transferir seguidores de otra cuenta a esta cuenta y trasladarlos, tendrás que crear un alias aquí. Asegúrate de crearlo antes de realizar el traslado. Introduce la cuenta desde la que estás moviendo los seguidores así: @person@instance.com" moveTo: "Mover esta cuenta a una nueva" moveToLabel: "Cuenta destino:" + moveCannotBeUndone: "La migración de la cuenta no puede ser revertida." moveAccountDescription: "Esta operación no puede deshacerse. En primer lugar, asegúrese de haber creado un alias para esta cuenta en la cuenta a la que se va a trasladar. Después de crear el alias, introduzca la cuenta a la que se está trasladando de la siguiente manera: @person@instance.com" + moveAccountHowTo: "Para migrar, primero crea un alias para ésta cuenta en la cuenta a donde te moverás.\nDespués de crear el alias, ingresa la cuenta a mover de la siguiente forma:\n@usuario@servidor.ejempo.com" + startMigration: "Migrar" migrationConfirm: "¿Estás seguro de que quieres mover esta cuenta a {account}? Una vez trasladada, no podrás deshacer el traslado y no podrás volver a utilizar la cuenta original.\n\nAdemás, compruebe que ha configurado un alias en el destino del traslado." + movedAndCannotBeUndone: "\nLa migración decuenta ha sido completada.\nNo se puede revertir éste proceso." + postMigrationNote: "Ésta cuenta dejará de seguir a todas las cuentas en las siguientes 24 horas después de que finalice la migración.\nEl número de seguidos y seguidores serán 0. Para evitar que Para evitar que tus seguidores dejen de ver las publicaciones, todas serán marcadas como \"sólo seguidores\"." movedTo: "Cuenta destino:" _achievements: earnedAt: "Desbloqueado el" @@ -1187,6 +1326,7 @@ _achievements: description: "30 minutos dedicados a Misskey" _client60min: title: "Viendo mucho Misskey." + description: "Dejar abierto Misskey por al menos 60 minutos" _noteDeletedWithin1min: title: "Ah... Mejor no..." description: "Borrar una nota antes que de pase 1 minuto" @@ -1252,6 +1392,9 @@ _achievements: title: "Brain Diver" description: "Publicaste un vínculo a \"Brain Diver\"" flavor: "Misskey-Misskey La-Tu-Ma" + _smashTestNotificationButton: + title: "Sobrecarga de pruebas" + description: "Envía muchas notificaciones de prueba en un corto espacio de tiempo" _role: new: "Crear rol" edit: "Editar rol" @@ -1275,6 +1418,8 @@ _role: iconUrl: "URL del ícono" asBadge: "Mostrar como emblema" descriptionOfAsBadge: "Este ícono de rol se mostrará a lado del nombre de usuario cuando este rol se encuentre activo." + isExplorable: "Hacer el rol explorable" + descriptionOfIsExplorable: "La línea de tiempo de éste rol y la lista de usuarios serán públicos si se activa.." displayOrder: "Posición" descriptionOfDisplayOrder: "Entre más alto el número, mayor es la posición en la interfaz." canEditMembersByModerator: "Permitir a los moderadores editar los miembros" @@ -1289,8 +1434,12 @@ _role: ltlAvailable: "Explorar la línea de tiempo local" canPublicNote: "Permitir la publicación" canInvite: "Puede crear códigos de invitación" + inviteLimit: "Límite de invitaciones" + inviteLimitCycle: "Enfriamiento del límite de invitaciones" + inviteExpirationTime: "Intervalo de caducidad de invitaciones" canManageCustomEmojis: "Administrar emojis personalizados" - driveCapacity: "Capacidad de almacenamiento" + driveCapacity: "Capacidad del drive" + alwaysMarkNsfw: "Siempre marcar archivos como NSFW" pinMax: "Máximo de notas fijadas" antennaMax: "Máximo de antenas" wordMuteMax: "Máximo de caracteres en palabras silenciadas" @@ -1350,6 +1499,7 @@ _ad: back: "Deseleccionar" reduceFrequencyOfThisAd: "Mostrar menos este anuncio." hide: "No mostrar" + timezoneinfo: "El día de la semana está determidado por la zona horaria del servidor." _forgotPassword: enterEmail: "Ingrese el correo usado para registrar la cuenta. Se enviará un link para resetear la contraseña." ifNoEmail: "Si no utilizó un correo para crear la cuenta, contáctese con el administrador." @@ -1401,10 +1551,10 @@ _aboutMisskey: donate: "Donar a Misskey" morePatrons: "Muchas más personas nos apoyan. Muchas gracias🥰" patrons: "Patrocinadores" -_nsfw: - respect: "Ocultar medios NSFW" - ignore: "No esconder medios NSFW " - force: "Ocultar todos los medios" +_displayOfSensitiveMedia: + respect: "Esconder medios marcados como sensibles" + ignore: "Mostrar medios marcados como sensibles" + force: "Esconder todala multimedia" _instanceTicker: none: "No mostrar" remote: "Mostrar a usuarios remotos" @@ -1423,6 +1573,8 @@ _channel: following: "Siguiendo" usersCount: "{n} participantes" notesCount: "{n} notas" + nameAndDescription: "Nombre y descripción" + nameOnly: "Sólo nombre" _menuDisplay: sideFull: "Horizontal" sideIcon: "Horizontal (ícono)" @@ -1526,7 +1678,7 @@ _sfx: channel: "Notificaciones del canal" _ago: future: "Futuro" - justNow: "Recién ahora" + justNow: "Justo ahora" secondsAgo: "Hace {n} segundos" minutesAgo: "Hace {n} minutos" hoursAgo: "Hace {n} horas" @@ -1540,21 +1692,30 @@ _time: minute: "Minutos" hour: "Horas" day: "Días" +_timelineTutorial: + title: "Cómo usar Misskey" + step1_1: "Ésta es la \"línea de tiempo\". Todas las \"notas\" que sean publicadas en {name} serán mostradas cronológicamente aquí." + step1_2: "Hay varias líneas de tiempo. Por ejemplo, la línea temporal \"Inicio\" contiene las notas de otros usuarios que sigues, y la línea \"Local\" contandrá las notas de todos los usuarios de {name}." + step2_1: "Ahora probemos publicar una nota. Puedes hacerlo presionando el botón que tiene un ícono de lápiz." + step2_2: "¿Qué tal si escribimos una introducción? o sólo un \"¡Hola {name}!\" ¿No te apetece?" + step3_1: "¿Terminaste de publicar tu primera nota?" + step3_2: "Tu primera nota ahora se mostrará en tu línea de tiempo." + step4_1: "También puedes añadir \"Reacciones\" a notas." + step4_2: "Para añadir una reacción selecciona el botón \"+\" en la nota y escoge el emoji que quieras para reaccionar." _2fa: alreadyRegistered: "Ya has completado la configuración." 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:" + step2Uri: "Si usas una aplicación de escritorio, introduce en ella la siguiente URL." step3Title: "Ingresa un código de autenticación" step3: "Para terminar, ingrese el token mostrado en la aplicación." + setupCompleted: "Configuración completada" 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" @@ -1565,6 +1726,11 @@ _2fa: 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" + checkBackupCodesBeforeCloseThisWizard: "Por favor, copia los siguientes códigos de respaldo antes de finalizar el asistente." + backupCodes: "Códigos de Respaldo" + backupCodesDescription: "En caso de que no puedas usar tu aplicación de autenticación, podrás usar los códigos de respaldo que figuran abajo para acceder a tu cuenta. Asegúrate de guardar en lugar seguro los códigos de respaldo. Cada uno de los códigos de respaldo es de un solo uso." + backupCodeUsedWarning: "Has usado todos los códigos de respaldo. Si dejas de tener acceso a tu aplicación de autenticación, no podrás volver a iniciar sesión en tu cuenta. Por favor, reconfigura tu aplicación de autenticación lo antes posible." + backupCodesExhaustedWarning: "Has usado todos los códigos de respaldo. Si dejas de tener acceso a tu aplicación de autenticación, no podrás volver a iniciar sesión en la cuenta que figura arriba. Por favor, reconfigura tu aplicación de autenticación lo antes posible." _permissions: "read:account": "Ver información de la cuenta" "write:account": "Editar información de la cuenta" @@ -1598,6 +1764,10 @@ _permissions: "write:gallery": "Editar galería" "read:gallery-likes": "Ver favoritos de la galería" "write:gallery-likes": "Editar favoritos de la galería" + "read:flash": "Ver Play" + "write:flash": "Editar Plays" + "read:flash-likes": "Ver los Play que me gustan" + "write:flash-likes": "Editar lista de Play que me gustan" _auth: shareAccessTitle: "Permisos de la aplicación" shareAccess: "¿Desea permitir el acceso a la cuenta \"{name}\"?" @@ -1685,14 +1855,14 @@ _visibility: homeDescription: "Visible sólo en la linea de tiempo de inicio" followers: "Seguidores" followersDescription: "Visible sólo para tus seguidores" - specified: "Mensaje directo" + specified: "Nota directa" specifiedDescription: "Visible sólo para los usuarios elegidos" disableFederation: "No federado" disableFederationDescription: "No enviar a otras instancias" _postForm: replyPlaceholder: "Responder a esta nota" quotePlaceholder: "Citar esta nota" - channelPlaceholder: "Postear en el canal" + channelPlaceholder: "Publicar en el canal" _placeholders: a: "¿Qué haces?" b: "¿Te pasó algo?" @@ -1833,6 +2003,10 @@ _notification: unreadAntennaNote: "Antena {name}" emptyPushNotificationMessage: "Se han actualizado las notificaciones push" achievementEarned: "Logro desbloqueado" + testNotification: "Notificación de prueba" + checkNotificationBehavior: "Comprobar comportamiento de la notificación" + sendTestNotification: "Enviar notificación de prueba" + notificationWillBeDisplayedLikeThis: "Las notificaciones tendrán este aspecto" _types: all: "Todo" follow: "Siguiendo" @@ -1867,6 +2041,9 @@ _deck: introduction: "¡Crea la interfaz perfecta para tí organizando las columnas libremente!" introduction2: "Presiona en la + de la derecha de la pantalla para añadir nuevas columnas donde quieras." widgetsIntroduction: "Por favor selecciona \"Editar Widgets\" en el menú columna y agrega un widget." + useSimpleUiForNonRootPages: "Mostrar páginas no pertenecientes a la raíz con la interfaz simple" + usedAsMinWidthWhenFlexible: "Se usará el ancho mínimo cuando la opción \"Autoajustar ancho\" esté habilitada" + flexible: "Autoajustar ancho" _columns: main: "Principal" widgets: "Widgets" @@ -1876,7 +2053,8 @@ _deck: list: "Listas" channel: "Canal" mentions: "Menciones" - direct: "Mensaje directo" + direct: "Notas directas" + roleTimeline: "Linea de tiempo del rol" _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}." @@ -1884,8 +2062,8 @@ _disabledTimeline: title: "Línea de tiempo deshabilitada" description: "No puedes usar esta línea de tiempo con tus roles actuales." _drivecleaner: - orderBySizeDesc: "Más grandes" - orderByCreatedAtAsc: "Más antiguos" + orderBySizeDesc: "Tamaño descendiente" + orderByCreatedAtAsc: "Fecha ascendente" _webhookSettings: createWebhook: "Crear Webhook" name: "Nombre" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 173380805c..64a59522f2 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -47,15 +47,21 @@ copyContent: "Copier le contenu" copyLink: "Copier le lien" delete: "Supprimer" deleteAndEdit: "Supprimer et réécrire" -deleteAndEditConfirm: "Êtes-vous sûr·e de vouloir supprimer cette note et la reformuler ? Vous perdrez toutes les réactions, renotes et réponses y afférentes." +deleteAndEditConfirm: "Êtes-vous sûr de vouloir effacer cette note et la modifier ? Vous perdrez toutes les réactions, renotes et réponses." addToList: "Ajouter à une liste" +addToAntenna: "Ajouter à l’antenne" sendMessage: "Envoyer un message" copyRSS: "Copier le RSS" copyUsername: "Copier le nom d’utilisateur·rice" +copyUserId: "Copier l'identifiant de l'utilisateur" +copyNoteId: "Copier l'identifiant de la note" +copyFileId: "Copier l'identifiant du fichier" +copyFolderId: "Copier l'identifiant du dossier" +copyProfileUrl: "Copier l'URL du profil" searchUser: "Chercher un·e utilisateur·rice" reply: "Répondre" loadMore: "Afficher plus …" -showMore: "Afficher plus …" +showMore: "Voir plus" showLess: "Fermer" youGotNewFollower: "Vous suit" receiveFollowRequest: "Demande d’abonnement reçue" @@ -68,13 +74,13 @@ import: "Importer" export: "Exporter" files: "Fichiers" download: "Télécharger" -driveFileDeleteConfirm: "Êtes-vous sûr·e de vouloir supprimer le fichier \"{name}\" ? Les notes liées à ce fichier seront aussi supprimées." +driveFileDeleteConfirm: "Êtes-vous sûr de vouloir supprimer le fichier \"{name}\" ? Les notes liées à ce fichier seront aussi supprimées." unfollowConfirm: "Désirez-vous vous désabonner de {name} ?" -exportRequested: "Vous avez demandé une exportation. L’opération pourrait prendre un peu de temps. Une terminée, le fichier résultant sera ajouté au Drive." +exportRequested: "Vous avez demandé une exportation. L’opération pourrait prendre un peu de temps. Une fois terminée, le fichier sera ajouté au Drive." importRequested: "Vous avez initié un import. Cela pourrait prendre un peu de temps." lists: "Listes" noLists: "Vous n’avez aucune liste" -note: "Notes" +note: "Note" notes: "Notes" following: "Abonnements" followers: "Abonné·e·s" @@ -116,7 +122,7 @@ reaction: "Réactions" reactions: "Réactions" reactionSetting: "Réactions à afficher dans le sélecteur de réactions" reactionSettingDescription2: "Déplacer pour réorganiser, cliquer pour effacer, utiliser « + » pour ajouter." -rememberNoteVisibility: "Activer l'option \" se souvenir de la visibilité des notes \" vous permet de réutiliser automatiquement la visibilité utilisée lors de la publication de votre note précédente." +rememberNoteVisibility: "Se souvenir de la visibilité des notes" attachCancel: "Supprimer le fichier attaché" markAsSensitive: "Marquer comme sensible" unmarkAsSensitive: "Supprimer le marquage comme sensible" @@ -132,8 +138,10 @@ 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" +editList: "Modifier la liste" selectChannel: "Sélectionner un canal" selectAntenna: "Sélectionner une antenne" +editAntenna: "Modifier l'antenne" selectWidget: "Sélectionner un widget" editWidgets: "Modifier les widgets" editWidgetsExit: "Valider les modifications" @@ -146,6 +154,8 @@ addEmoji: "Ajouter un émoji" settingGuide: "Configuration proposée" cacheRemoteFiles: "Mise en cache des fichiers distants" cacheRemoteFilesDescription: "Lorsque cette option est désactivée, les fichiers distants sont chargés directement depuis l’instance distante. La désactiver diminuera certes l’utilisation de l’espace de stockage local mais augmentera le trafic réseau puisque les miniatures ne seront plus générées." +cacheRemoteSensitiveFiles: "Mettre en cache les fichiers distants sensibles" +cacheRemoteSensitiveFilesDescription: "Si vous désactivez ce paramètre, les fichiers sensibles distants ne seront pas mis en cache et un lien direct sera utilisé à la place" flagAsBot: "Ce compte est un robot" flagAsBotDescription: "Si ce compte est géré de manière automatisée, choisissez cette option. Si elle est activée, elle agira comme un marqueur pour les autres développeurs afin d'éviter des chaînes d'interaction sans fin avec d'autres robots et d'ajuster les systèmes internes de Misskey pour traiter ce compte comme un robot." flagAsCat: "Ce compte est un chat" @@ -154,6 +164,7 @@ flagShowTimelineReplies: "Afficher les réponses dans le fil" flagShowTimelineRepliesDescription: "Affiche les réponses des utilisateurs aux notes des autres utilisateurs dans la timeline si cette option est activée." autoAcceptFollowed: "Accepter automatiquement les demandes d’abonnement venant d’utilisateur·rice·s que vous suivez" addAccount: "Ajouter un compte" +reloadAccountsList: "Rafraichir la liste des comptes" loginFailed: "Échec de la connexion" showOnRemote: "Voir sur l’instance distante" general: "Général" @@ -240,7 +251,7 @@ announcements: "Annonces" imageUrl: "URL de l’image" remove: "Supprimer" removed: "Supprimé" -removeAreYouSure: "Êtes-vous sûr·e de vouloir supprimer「{x}」?" +removeAreYouSure: "Êtes-vous sûr·e de vouloir supprimer « {x} » ?" deleteAreYouSure: "Êtes-vous sûr·e de vouloir supprimer「{x}」?" resetAreYouSure: "Voulez-vous réinitialiser ?" saved: "Enregistré" @@ -260,6 +271,9 @@ noMoreHistory: "Il n’y a plus d’historique" startMessaging: "Commencer à discuter" nUsersRead: "Lu par {n} personnes" agreeTo: "J’accepte {0}" +agree: "Accepter" +basicNotesBeforeCreateAccount: "Notes importantes" +termsOfService: "Conditions d'utilisation" start: "Commencer" home: "Principal" remoteUserCaution: "Les informations de ce compte risqueraient d’être incomplètes du fait que l’utilisateur·rice provient d’une instance distante." @@ -302,7 +316,7 @@ copyUrl: "Copier l’URL" rename: "Renommer" avatar: "Avatar" banner: "Bannière" -nsfw: "Contenu sensible" +displayOfSensitiveMedia: "Afficher les médias sensibles" whenServerDisconnected: "Lorsque la connexion au serveur est perdue" disconnectedFromServer: "Déconnecté·e du serveur" reload: "Rafraîchir" @@ -337,7 +351,6 @@ invite: "Inviter" driveCapacityPerLocalAccount: "Volume du Drive par utilisateur local" driveCapacityPerRemoteAccount: "Volume du Drive par utilisateur distant" inMb: "en mégaoctets" -iconUrl: "URL de l'icône" bannerUrl: "URL de l’image de la bannière" backgroundImageUrl: "URL de l'image d'arrière-plan" basicInfo: "Informations basiques" @@ -392,11 +405,17 @@ about: "Informations" aboutMisskey: "À propos de Misskey" administrator: "Administrateur" token: "Jeton" +2fa: "Authentification à deux facteurs" +totp: "Application d'authentification" +totpDescription: "Entrez un mot de passe à usage unique à l'aide d'une application d'authentification" moderator: "Modérateur·rice·s" moderation: "Modérations" +moderationNote: "Note de modération" +addModerationNote: "Ajouter une note de modération" nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s" securityKey: "Clé de sécurité" lastUsed: "Dernier utilisé" +lastUsedAt: "Dernière utilisation : {t}" unregister: "Se désinscrire" passwordLessLogin: "Se connecter sans mot de passe" resetPassword: "Réinitialiser le mot de passe" @@ -461,6 +480,7 @@ createAccount: "Créer un compte" existingAccount: "Compte existant" regenerate: "Générer à nouveau" fontSize: "Taille de la police" +limitTo: "Limiter à {x}" noFollowRequests: "Vous n’avez aucune demande d’abonnement en attente" openImageInNewTab: "Ouvrir les images dans un nouvel onglet" dashboard: "Tableau de bord" @@ -534,9 +554,14 @@ userSuspended: "Cet·te utilisateur·rice a été suspendu·e." userSilenced: "Cette utilisateur·trice a été mis·e en sourdine." yourAccountSuspendedTitle: "Ce compte est suspendu" yourAccountSuspendedDescription: "Ce compte est suspendu car vous avez enfreint les conditions d'utilisation de l'instance, ou pour un motif similaire. Si vous souhaitez connaître en détail les raisons de cette suspension, renseignez-vous auprès de l'administrateur·rice de votre instance. Merci de ne pas créer de nouveau compte." +tokenRevoked: "Ce jeton est invalide." +tokenRevokedDescription: "Votre jeton de connexion a expiré. Veuillez vous reconnecter." +accountDeleted: "Compte supprimé" +accountDeletedDescription: "Ce compte a été supprimé." menu: "Menu" divider: "Séparateur" addItem: "Ajouter un élément" +rearrange: "Trier par" relays: "Relais" addRelay: "Ajouter un relais" inboxUrl: "Inbox URL" @@ -577,7 +602,7 @@ tokenRequested: "Autoriser l'accès au compte" pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici." notificationType: "Type de notifications" edit: "Editer" -emailServer: "Serveur mail" +emailServer: "Serveur de messagerie" enableEmail: "Activer la distribution de courriel" emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation de votre mot de passe en cas d’oubli." email: "E-mail " @@ -644,6 +669,7 @@ createNew: "Créer nouveau" optional: "Facultatif" createNewClip: "Créer un nouveau clip" public: "Public" +private: "Privé" i18nInfo: "Misskey est traduit dans différentes langues par des bénévoles. Vous pouvez contribuer à {link}." manageAccessTokens: "Gérer les jetons d'accès" accountInfo: " Informations du compte " @@ -678,6 +704,8 @@ contact: "Contact" useSystemFont: "Utiliser la police par défaut du système" clips: "Clips" experimentalFeatures: "Fonctionnalités expérimentales" +experimental: "Expérimental" +thisIsExperimentalFeature: "Ceci est une fonctionnalité expérimentale. Il y a une possibilité que les spécifications changent ou qu'elle ne fonctionne pas correctement." developer: "Développeur" makeExplorable: "Rendre le compte visible sur la page \"Découvrir\"." makeExplorableDescription: "Si vous désactivez cette option, votre compte n'apparaîtra pas sur la page \"Découvrir\"." @@ -762,6 +790,7 @@ noMaintainerInformationWarning: "Informations administrateur non configurées." noBotProtectionWarning: "La protection contre les bots n'est pas configurée." configure: "Configurer" postToGallery: "Publier dans la galerie" +postToHashtag: "Publier avec ce hashtag" gallery: "Galerie" recentPosts: "Les plus récentes" popularPosts: "Les plus consultées" @@ -794,18 +823,22 @@ translatedFrom: "Traduit depuis {x}" accountDeletionInProgress: "La suppression de votre compte est en cours" usernameInfo: "C'est un nom qui identifie votre compte sur l'instance de manière unique. Vous pouvez utiliser des lettres de l'alphabet (minuscules et majuscules), des chiffres (de 0 à 9), ou bien le tiret « _ ». Vous ne pourrez pas modifier votre nom d'utilisateur·rice par la suite." aiChanMode: "Mode Ai" +devMode: "Mode développement" keepCw: "Garder le CW" pubSub: "Comptes Pub/Sub" lastCommunication: "Dernière communication" resolved: "Résolu" unresolved: "En attente" breakFollow: "Ne plus suivre" +breakFollowConfirm: "Êtes-vous sûr de vouloir vous désabonner ?" itsOn: "Activé" itsOff: "Désactivé" +on: "Activé" +off: "Désactivé" emailRequiredForSignup: "Une adresse e-mail est nécessaire pour créer un compte" unread: "Non lu" filter: "Filtre" -controlPanel: "Panneau de contrôle" +controlPanel: "Panneau de configuration" manageAccounts: "Gérer les comptes" makeReactionsPublic: "Rendre les réactions publiques" makeReactionsPublicDescription: "Ceci rendra la liste de toutes vos réactions données publique." @@ -910,20 +943,31 @@ roles: "Rôles" role: "Rôles" noRole: "Aucun rôle" normalUser: "Simple utilisateur·rice" +undefined: "Non défini" assign: "Attribuer" color: "Couleur" manageCustomEmojis: "Gestion des émojis personnalisés" preset: "Préréglage" selectFromPresets: "Sélectionner à partir des préréglages" +achievements: "Accomplissements" thisPostMayBeAnnoying: "Cette note peut gêner d'autres personnes." +thisPostMayBeAnnoyingHome: "Publier vers le fil principal" thisPostMayBeAnnoyingCancel: "Annuler" +thisPostMayBeAnnoyingIgnore: "Publier quand-même" +internalServerError: "Erreur interne du serveur" +copyErrorInfo: "Copier les détails de l’erreur" +exploreOtherServers: "Trouver une autre instance" +disableFederationOk: "Désactiver" license: "Licence" video: "Vidéo" videos: "Vidéos" dataSaver: "Économiseur de données" accountMigration: "Migration de compte" accountMoved: "Cet·te utilisateur·rice a migré son compte vers :" +accountMovedShort: "Ce compte a migré" +operationForbidden: "Opération non autorisée" addMemo: "Ajouter un mémo" +reactionsList: "Réactions" notificationDisplay: "Style des notifications" leftTop: "En haut à gauche" rightTop: "En haut à droite" @@ -932,12 +976,37 @@ rightBottom: "En bas à droite" vertical: "Vertical" horizontal: "Latéral" serverRules: "Règles du serveur" +archive: "Archive" youFollowing: "Abonné·e" +later: "Plus tard" +goToMisskey: "Retour vers Misskey" +expirationDate: "Date d’expiration" +usedAt: "Utilisé le" +unused: "Non-utilisé" +used: "Utilisé" +expired: "Expiré" +doYouAgree: "Êtes-vous d’accord ?" +icon: "Avatar" +forYou: "Pour vous" +replies: "Répondre" +renotes: "Renoter" +_announcement: + readConfirmTitle: "Marquer comme lu ?" +_initialAccountSetting: + profileSetting: "Paramètres du profil" + privacySetting: "Paramètres de confidentialité" +_accountMigration: + moveToLabel: "Compte vers lequel vous migrez :" + startMigration: "Migrer" + movedTo: "Compte vers lequel vous migrez :" _achievements: _types: _notes1: + title: "Je viens tout juste de configurer mon msky" description: "Publiez votre première note" flavor: "Passez un bon moment avec Misskey !" + _notes10: + title: "Quelques notes" _notes100: title: "Beaucoup de notes" _notes100000: @@ -952,16 +1021,23 @@ _achievements: title: "Débutant Ⅲ" description: "Se connecter pour un total de 15 jours" _login30: + title: "Misskeynaute I" description: "Se connecter pour un total de 30 jours" _login60: + title: "Misskeynaute II" description: "Se connecter pour un total de 60 jours" _login100: + title: "Misskeynaute III" description: "Se connecter pour un total de 100 jours" + flavor: "Misskeynaute acharné·e" _login200: + title: "Régulier I" description: "Se connecter pour un total de 200 jours" _login300: + title: "Régulier II" description: "Se connecter pour un total de 300 jours" _login400: + title: "Régulier III" description: "Se connecter pour un total de 400 jours" _login500: description: "Se connecter pour un total de 500 jours" @@ -975,6 +1051,8 @@ _achievements: description: "Se connecter pour un total de 900 jours" _login1000: flavor: "Merci d'utiliser Misskey !" + _profileFilled: + description: "Configuration de votre profil" _markedAsCat: title: "Je suis un chat" flavor: "Je n'ai pas encore de nom" @@ -984,6 +1062,16 @@ _achievements: title: "Abonnez-moi !" _iLoveMisskey: title: "J’adore Misskey" + description: "Publication « J’❤ #Misskey »" + _foundTreasure: + title: "Chasse au trésor" + description: "Vous avez trouvé le trésor caché" + _postedAtLateNight: + flavor: "C’est l’heure d’aller au lit." + _postedAt0min0sec: + title: "Horloge parlante" + description: "Publication d’une note à 00:00" + flavor: "Tic tac, tic tac, tic tac, ding !" _viewInstanceChart: title: "Analyste" _loggedInOnBirthday: @@ -993,7 +1081,11 @@ _achievements: _cookieClicked: flavor: "Attendez une minute, vous êtes sur le mauvais site web ?" _role: + name: "Nom du rôle" + description: "Description du rôle" + permission: "Rôle et autorisations" assignTarget: "Attribuer" + condition: "Condition" priority: "Priorité" _priority: low: "Basse" @@ -1085,10 +1177,8 @@ _aboutMisskey: donate: "Soutenir Misskey" morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes non mentionnées ici. Merci à toutes et à tous ! 🥰" patrons: "Contributeurs" -_nsfw: - respect: "Cacher les médias marqués comme contenu sensible" - ignore: "Afficher les médias sensibles" - force: "Cacher tous les médias" +_displayOfSensitiveMedia: + force: "Masquer tous les médias" _instanceTicker: none: "Cacher " remote: "Montrer pour les utilisateur·ice·s distant·e·s" @@ -1107,6 +1197,8 @@ _channel: following: "Abonné·e" usersCount: "{n} Participant·e·s" notesCount: "{n} Notes" + nameAndDescription: "Nom et description" + nameOnly: "Nom seulement" _menuDisplay: sideFull: "Latéral" sideIcon: "Latéral (icônes)" @@ -1224,16 +1316,24 @@ _time: minute: "min" hour: "h" day: "j" +_timelineTutorial: + title: "Comment utiliser Misskey" + step3_1: "Avez-vous publié votre première note ?" _2fa: alreadyRegistered: "Configuration déjà achevée." 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 :" + step3Title: "Veuillez saisir le code d’authentification" step3: "Entrez le jeton affiché sur votre application pour compléter la configuration." + setupCompleted: "Configuration terminée avec succès !" step4: "À partir de maintenant, ce même jeton vous sera demandé à chacune de vos connexions." + securityKeyNotSupported: "Votre navigateur ne prend pas en charge les clés de sécurité." securityKeyInfo: "Vous pouvez configurer l'authentification WebAuthN pour sécuriser davantage le processus de connexion grâce à une clé de sécurité matérielle qui prend en charge FIDO2, ou bien en configurant l'authentification par empreinte digitale ou par code PIN sur votre appareil." + securityKeyName: "Nom de la clé" removeKeyConfirm: "Voulez-vous supprimer {name} ?" + renewTOTPOk: "Reconfigurer" renewTOTPCancel: "Pas maintenant" + backupCodes: "Codes de Secours" _permissions: "read:account": "Afficher les informations du compte" "write:account": "Mettre à jour les informations de votre compte" @@ -1450,7 +1550,7 @@ _pages: fontSerif: "Serif" fontSansSerif: "Sans Serif" eyeCatchingImageSet: "Définir une image attractive" - eyeCatchingImageRemove: "Supprimer l'image attractive" + eyeCatchingImageRemove: "Supprimer la miniature" chooseBlock: "Ajouter un bloc" selectType: "Choisir un type" contentBlocks: "Contenu" @@ -1483,6 +1583,7 @@ _notification: pollEnded: "Les résultats du sondage sont disponibles" unreadAntennaNote: "Antenne {name}" emptyPushNotificationMessage: "Les notifications push ont été mises à jour" + achievementEarned: "Accomplissement" _types: all: "Toutes" follow: "Nouvel·le abonné·e" @@ -1494,6 +1595,7 @@ _notification: pollEnded: "Sondages se cloturant" receiveFollowRequest: "Demande d'abonnement reçue" followRequestAccepted: "Demande d'abonnement acceptée" + achievementEarned: "Accomplissement" app: "Notifications provenant des apps" _actions: followBack: "Suivre" @@ -1515,6 +1617,7 @@ _deck: deleteProfile: "Supprimer le profil" introduction: "Créez l’interface parfaite qui vous sied en arrangeant librement les colonnes !" introduction2: "Cliquez sur le + à droite de l'écran pour ajouter de nouvelles colonnes quand vous le souhaitez." + flexible: "Ajuster automatiquement la largeur" _columns: main: "Principale" widgets: "Widgets" diff --git a/locales/generateDTS.js b/locales/generateDTS.js index 5949aee7cd..7af773f3b1 100644 --- a/locales/generateDTS.js +++ b/locales/generateDTS.js @@ -1,6 +1,11 @@ -const fs = require('fs'); -const yaml = require('js-yaml'); -const ts = require('typescript'); +import * as fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import * as yaml from 'js-yaml'; +import ts from 'typescript'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); function createMembers(record) { return Object.entries(record) @@ -14,7 +19,7 @@ function createMembers(record) { )); } -module.exports = function generateDTS() { +export default function generateDTS() { const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8')); const members = createMembers(locale); const elements = [ @@ -51,11 +56,7 @@ module.exports = function generateDTS() { ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags, ), ), - ts.factory.createExportAssignment( - undefined, - true, - ts.factory.createIdentifier('locales'), - ), + ts.factory.createExportDefault(ts.factory.createIdentifier('locales')), ]; const printed = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, diff --git a/locales/hu-HU.yml b/locales/hu-HU.yml new file mode 100644 index 0000000000..023a91494d --- /dev/null +++ b/locales/hu-HU.yml @@ -0,0 +1,104 @@ +--- +_lang_: "Japán" +monthAndDay: "{month}.{day}." +search: "Keresés" +notifications: "Értesítések" +username: "Felhasználónév" +password: "Jelszó" +forgotPassword: "Elfelejtett jelszó" +ok: "OK" +gotIt: "Rendben" +cancel: "Mégse" +noThankYou: "Nem, köszönöm" +enterUsername: "Felhasználónév megadása" +renotedBy: "{user} Renotolta" +noNotes: "Nincs Note" +noNotifications: "Nincs értesítés" +instance: "Szerver" +settings: "Beállítások" +notificationSettings: "Értesítés beállításai" +basicSettings: "Alapbeállítás" +otherSettings: "Egyéb beállítások" +openInWindow: "Megnyitás ablakban" +profile: "Saját profil" +timeline: "Idővonal" +noAccountDescription: "Nincs leírás" +login: "Bejelentkezés" +loggingIn: "Belépés" +logout: "Kijelentkezés" +signup: "Regisztráció" +uploading: "Feltöltés" +save: "Mentés" +users: "Felhasználók" +addUser: "Felhasználó hozzáadása" +favorite: "Kedvencek" +favorites: "Kedvencek" +unfavorite: "Törlés a kedvencek közül." +favorited: "Kedvencek közé rakva." +alreadyFavorited: "Már a kedvencek között van." +cantFavorite: "Nem sikerült a kedvencek közé rakni." +pin: "Rögzítés" +unpin: "Rögzítés feloldása" +copyContent: "Tartalom másolása" +copyLink: "Hivatkozás Másolása" +delete: "Törlés" +deleteAndEdit: "Törlés és szerkesztés" +deleteAndEditConfirm: "Biztosan törlöd ezt a jegyzetet és újrafogalmazza? Így eveszíted az összes reakciót, renote-ot és választ." +addToList: "Hozzáadás a listákhoz" +privacy: "Adatvédelem" +makeFollowManuallyApprove: "Csak jóváhagyással követhetnek" +defaultNoteVisibility: "Alapértelmezett láthatóság" +follow: "Követés" +followRequest: "Követés kérése" +followRequests: "Követési kérések" +unfollow: "Követés visszavonása" +followRequestPending: "Függőben levő követési kérés" +enterEmoji: "Írj egy emoji-t" +renote: "Renote" +unrenote: "Renote visszavonása" +renoted: "Renotolva" +cantRenote: "Nem lehet Renotolni" +cantReRenote: "A Renote nem renotálható" +quote: "Idézet" +inChannelRenote: "Csak csatornán bellüli Renote" +inChannelQuote: "Csak csatornán bellüli idézet" +pinnedNote: "Csatolt jegyzet" +pinned: "Rögzítés" +you: "Te" +clickToShow: "Kattints ide" +sensitive: "Érzékeny" +add: "Hozzáad" +reaction: "Reakciók" +reactions: "Reakciók" +instances: "Szerver" +remove: "Törlés" +pinnedNotes: "Csatolt jegyzet" +smtpUser: "Felhasználónév" +smtpPass: "Jelszó" +user: "Felhasználók" +searchByGoogle: "Keresés" +renotes: "Renote" +_theme: + keys: + renote: "Renote" +_sfx: + notification: "Értesítések" +_2fa: + renewTOTPCancel: "Nem, köszönöm" +_widgets: + profile: "Saját profil" + notifications: "Értesítések" + timeline: "Idővonal" +_profile: + username: "Felhasználónév" +_notification: + _types: + renote: "Renote" + quote: "Idézet" + reaction: "Reakciók" + _actions: + renote: "Renote" +_deck: + _columns: + notifications: "Értesítések" + tl: "Idővonal" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 70217caa27..e39b49774d 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -5,7 +5,7 @@ introMisskey: "Selamat datang! Misskey adalah perangkat mikroblog tercatu bersif poweredByMisskeyDescription: "{name} adalah sebuah layanan (instance) yang menggunakan platform sumber terbuka Misskey." monthAndDay: "{day} {month}" search: "Penelusuran" -notifications: "Pemberitahuan" +notifications: "Notifikasi" username: "Nama Pengguna" password: "Kata sandi" forgotPassword: "Lupa Kata Sandi" @@ -17,7 +17,7 @@ noThankYou: "Tidak sekarang." enterUsername: "Masukkan nama pengguna" renotedBy: "direnote oleh {user}" noNotes: "Tidak ada catatan" -noNotifications: "Tidak ada pemberitahuan" +noNotifications: "Tidak ada notifikasi" instance: "Instansi" settings: "Pengaturan" notificationSettings: "Atur Notifikasi" @@ -25,7 +25,7 @@ basicSettings: "Pengaturan umum" otherSettings: "Pengaturan lainnya" openInWindow: "Buka di jendela" profile: "Profil" -timeline: "Linimasa" +timeline: "Lini masa" noAccountDescription: "Pengguna ini belum menulis bio" login: "Masuk" loggingIn: "Sedang masuk" @@ -49,9 +49,15 @@ delete: "Hapus" deleteAndEdit: "Hapus dan sunting" deleteAndEditConfirm: "Apakah kamu yakin ingin menghapus note ini dan menyuntingnya? Kamu akan kehilangan semua reaksi, renote dan balasan di note ini." addToList: "Tambahkan ke daftar" +addToAntenna: "Tambahkan ke Antena" sendMessage: "Kirim pesan" copyRSS: "Salin RSS" copyUsername: "Salin nama pengguna" +copyUserId: "Salin ID pengguna" +copyNoteId: "Salin ID catatan" +copyFileId: "Salin Berkas" +copyFolderId: "Salin Folder" +copyProfileUrl: "Salin Alamat Web Profil" searchUser: "Cari pengguna" reply: "Balas" loadMore: "Selebihnya" @@ -90,7 +96,7 @@ serverIsDead: "Tidak ada respon dari peladen. Mohon tunggu dan coba beberapa saa youShouldUpgradeClient: "Untuk melihat halaman ini, mohon muat ulang untuk memutakhirkan klienmu." enterListName: "Masukkan nama daftar" privacy: "Privasi" -makeFollowManuallyApprove: "Permintaan mengikuti membutuhkan persetujuan" +makeFollowManuallyApprove: "Permintaan mengikuti butuh persetujuan" defaultNoteVisibility: "Privasi bawaan catatan" follow: "Ikuti" followRequest: "Permintaan mengikuti" @@ -115,7 +121,7 @@ add: "Tambahkan" reaction: "Reaksi" reactions: "Reaksi" reactionSetting: "Reaksi untuk dimunculkan di bilah reaksi" -reactionSettingDescription2: "Geser untuk memindah urutkan, klik untuk menghapus, tekan \"+\" untuk menambahkan" +reactionSettingDescription2: "Geser untuk memindah urutan emoji, klik untuk menghapus, tekan \"+\" untuk menambahkan" rememberNoteVisibility: "Ingat pengaturan visibilitas catatan" attachCancel: "Hapus lampiran" markAsSensitive: "Tandai sebagai konten sensitif" @@ -127,15 +133,17 @@ renoteMute: "Matikan renote" renoteUnmute: "Batal mematikan renote" block: "Blokir" unblock: "Buka blokir" -suspend: "Bekukan" -unsuspend: "Buka pembekuan" +suspend: "Tangguhkan" +unsuspend: "Batalkan penangguhan" blockConfirm: "Apakah kamu yakin ingin memblokir akun ini?" 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?" +suspendConfirm: "Apakah kamu yakin ingin menangguhkan akun ini?" +unsuspendConfirm: "Apakah kamu yakin ingin membatalkan penangguhan akun ini?" selectList: "Pilih daftar" +editList: "Sunting daftar" selectChannel: "Pilih kanal" selectAntenna: "Pilih Antena" +editAntenna: "Sunting antena" selectWidget: "Pilih gawit" editWidgets: "Sunting gawit" editWidgetsExit: "Selesai" @@ -146,16 +154,19 @@ emojiName: "Nama emoji" emojiUrl: "URL Emoji" addEmoji: "Tambahkan emoji" settingGuide: "Pengaturan rekomendasi" -cacheRemoteFiles: "Tembolokkan berkas remote" -cacheRemoteFilesDescription: "Ketika pengaturan ini dinonaktifkan, berkas luar akan dimuat langsung dari instansi luar. Menonaktifkan ini akan mengurangi penggunaan penyimpanan, namun dapat menyebabkan meningkatkan lalu lintas bandwidth, karena thumbnail tidak dihasilkan." +cacheRemoteFiles: "Tembolokkan berkas dari instansi luar" +cacheRemoteFilesDescription: "Ketika pengaturan ini dinonaktifkan, berkas dari instansi luar akan dimuat langsung. Menonaktifkan ini akan mengurangi penggunaan penyimpanan peladen, namun dapat menyebabkan peningkatan lalu lintas bandwidth, karena keluku tidak dihasilkan." +cacheRemoteSensitiveFiles: "Tembolokkan berkas dari instansi luar" +cacheRemoteSensitiveFilesDescription: "Menonaktifkan pengaturan ini menyebabkan berkas sensitif dari instansi luar ditautkan secara langsung, bukan ditembolok." flagAsBot: "Atur akun ini sebagai Bot" flagAsBotDescription: "Jika akun ini dikendalikan oleh program, tetapkanlah opsi ini. Jika diaktifkan, ini akan berfungsi sebagai tanda bagi pengembang lain untuk mencegah interaksi berantai dengan bot lain dan menyesuaikan sistem internal Misskey untuk memperlakukan akun ini sebagai bot." flagAsCat: "Atur akun ini sebagai kucing" flagAsCatDescription: "Nyalakan tanda ini untuk menandai akun ini sebagai kucing." -flagShowTimelineReplies: "Tampilkan balasan di linimasa" -flagShowTimelineRepliesDescription: "Menampilkan balasan pengguna dari note pengguna lain di linimasa apabila dinyalakan." +flagShowTimelineReplies: "Tampilkan balasan di lini masa" +flagShowTimelineRepliesDescription: "Menampilkan balasan pengguna dari catatan pengguna lain di lini masa apabila dinyalakan." autoAcceptFollowed: "Setujui otomatis permintaan mengikuti dari pengguna yang kamu ikuti" addAccount: "Tambahkan akun" +reloadAccountsList: "Muat ulang daftar akun" loginFailed: "Gagal untuk masuk" showOnRemote: "Lihat profil asli" general: "Umum" @@ -166,7 +177,7 @@ searchWith: "Cari: {q}" youHaveNoLists: "Kamu tidak memiliki daftar apapun" followConfirm: "Apakah kamu yakin ingin mengikuti {name}?" proxyAccount: "Akun proksi" -proxyAccountDescription: "Akun proksi merupakan sebuah akun yang bertindak sebagai pengikut luar untuk pengguna dalam kondisi tertentu. Sebagai contoh, ketika pengguna menambahkan seorang pengguna luar ke dalam daftar, aktivitas dari pengguna luar tidak akan disampaikan ke instansi apabila tidak ada pengguna lokal yang mengikuti pengguna tersebut, dengan begitu akun proksilah yang akan mengikutinya." +proxyAccountDescription: "Akun proksi merupakan sebuah akun yang bertindak sebagai pengikut instansi luar untuk pengguna dalam kondisi tertentu. Sebagai contoh, ketika pengguna menambahkan seorang pengguna instansi luar ke dalam daftar, aktivitas dari pengguna instansi luar tidak akan disampaikan ke instansi apabila tidak ada pengguna lokal yang mengikuti pengguna tersebut, dengan begitu akun proksilah yang akan mengikutinya." host: "Host" selectUser: "Pilih pengguna" recipient: "Penerima" @@ -198,7 +209,7 @@ clearQueue: "Bersihkan antrian" clearQueueConfirmTitle: "Apakah kamu yakin ingin membersihkan antrian?" clearQueueConfirmText: "Seluruh sisa catatan yang tidak tersampaikan di dalam antrian tidak akan difederasi. Biasanya operasi ini TIDAK dibutuhkan." clearCachedFiles: "Hapus tembolok" -clearCachedFilesConfirm: "Apakah kamu yakin ingin menghapus seluruh tembolok berkas remote?" +clearCachedFilesConfirm: "Apakah kamu yakin ingin menghapus seluruh tembolok berkas instansi luar?" blockedInstances: "Instansi terblokir" blockedInstancesDescription: "Daftar nama host dari instansi yang diperlukan untuk diblokir. Instansi yang didaftarkan tidak akan dapat berkomunikasi dengan instansi ini." muteAndBlock: "Bisukan / Blokir" @@ -218,14 +229,14 @@ noCustomEmojis: "Tidak ada emoji kustom" noJobs: "Tidak ada kerja" federating: "memfederasi" blocked: "Diblokir" -suspended: "Diberhentikan" +suspended: "Ditangguhkan" all: "Semua" subscribing: "Berlangganan" publishing: "Sedang menyiarkan langsung" notResponding: "Tidak ada respon" -instanceFollowing: "Mengikuti instance" -instanceFollowers: "Pengikut instance" -instanceUsers: "Pengguna pada instance ini" +instanceFollowing: "Mengikuti instansi" +instanceFollowers: "Pengikut instansi" +instanceUsers: "Pengguna pada instansi ini" changePassword: "Ubah kata sandi" security: "Keamanan" retypedNotMatch: "Input tidak sama" @@ -233,11 +244,11 @@ currentPassword: "Kata sandi saat ini" newPassword: "Kata sandi baru" newPasswordRetype: "Ulangi kata sandi baru" attachFile: "Lampirkan berkas" -more: "Lagi !" +more: "Lainnya" featured: "Sorotan" usernameOrUserId: "Nama pengguna atau User ID" noSuchUser: "Pengguna tidak ditemukan" -lookup: "Mencari" +lookup: "Cari" announcements: "Pengumuman" imageUrl: "URL Gambar" remove: "Hapus" @@ -268,7 +279,7 @@ basicNotesBeforeCreateAccount: "Catatan penting" termsOfService: "Syarat dan ketentuan" start: "Mulai" home: "Beranda" -remoteUserCaution: "Informasi ini mungkin tidak mutakhir, karena pengguna ini berasal dari instansi luar." +remoteUserCaution: "Informasi ini mungkin tidak mutakhir, karena pengguna ini berasal dari peladen instansi luar." activity: "Aktivitas" images: "Gambar" image: "Gambar" @@ -308,19 +319,19 @@ copyUrl: "Salin tautan" rename: "Ubah nama" avatar: "Avatar" banner: "Banner" -nsfw: "Konten sensitif" +displayOfSensitiveMedia: "Tampilkan media NSFW" whenServerDisconnected: "Ketika kehilangan koneksi dengan peladen" disconnectedFromServer: "Terputus koneksi dari peladen" reload: "Muat ulang" doNothing: "Abaikan" -reloadConfirm: "Apakah kamu ingin memuat ulang linimasa?" +reloadConfirm: "Apakah kamu ingin memuat ulang lini masa?" watch: "Tonton" unwatch: "Batal tonton" accept: "Terima" reject: "Tolak" normal: "Normal" -instanceName: "Nama instance" -instanceDescription: "Tentang instance" +instanceName: "Nama instansi" +instanceDescription: "Tentang instansi" maintainerName: "Pengelola" maintainerEmail: "Surel pengelola" tosUrl: "URL Syarat dan Ketentuan" @@ -334,16 +345,15 @@ pages: "Halaman" integration: "Integrasi" connectService: "Sambungkan" disconnectService: "Putuskan" -enableLocalTimeline: "Nyalakan linimasa lokal" -enableGlobalTimeline: "Nyalakan linimasa global" -disablingTimelinesInfo: "Admin dan Moderator akan selalu memiliki akses ke semua linimasa meskipun linimasa tersebut tidak diaktifkan." +enableLocalTimeline: "Nyalakan lini masa lokal" +enableGlobalTimeline: "Nyalakan lini masa global" +disablingTimelinesInfo: "Admin dan Moderator akan selalu memiliki akses ke semua lini masa meskipun lini masa tersebut tidak diaktifkan." registration: "Pendaftaran" enableRegistration: "Nyalakan pendaftaran pengguna baru" invite: "Undang" driveCapacityPerLocalAccount: "Kapasitas drive per pengguna lokal" driveCapacityPerRemoteAccount: "Kapasitas drive per pengguna remote" inMb: "dalam Megabytes" -iconUrl: "URL Gambar ikon" bannerUrl: "URL Banner" backgroundImageUrl: "URL Gambar latar" basicInfo: "Informasi Umum" @@ -379,13 +389,13 @@ enableServiceworker: "Aktifkan ServiceWorker" antennaUsersDescription: "Tuliskan satu nama pengguna per baris" caseSensitive: "Peka huruf besar dan huruf kecil" withReplies: "Termasuk balasan" -connectedTo: "Akun yang mengikuti telah terhubung" +connectedTo: "Akun berikut terhubung" notesAndReplies: "Catatan dan balasan" withFiles: "Media" -silence: "Bungkam" -silenceConfirm: "Apakah kamu yakin ingin membungkam pengguna ini?" -unsilence: "Hapus bungkam" -unsilenceConfirm: "Apakah kamu ingin untuk batal membungkam pengguna ini?" +silence: "Senyapkan" +silenceConfirm: "Apakah kamu yakin ingin menyenyapkan pengguna ini?" +unsilence: "Batalkan senyap" +unsilenceConfirm: "Apakah kamu ingin untuk batal menyenyapkan pengguna ini?" popularUsers: "Pengguna populer" recentlyUpdatedUsers: "Pengguna dengan aktivitas terkini" recentlyRegisteredUsers: "Pengguna baru saja bergabung" @@ -398,6 +408,7 @@ about: "Informasi" aboutMisskey: "Tentang Misskey" administrator: "Admin" token: "Token" +2fa: "Autentikasi 2-faktor" totp: "Aplikasi autentikator" totpDescription: "Gunakan aplikasi autentikator untuk mendapatkan kata sandi sekali pakai" moderator: "Moderator" @@ -409,6 +420,7 @@ lastUsed: "Terakhir digunakan" lastUsedAt: "Penggunaan terakhir: {t}" unregister: "Batalkan pendaftaran" passwordLessLogin: "Setel login tanpa kata sandi" +passwordLessLoginDescription: "Bolehkan masuk tanpa kata sandi dengan menggunakan hanya security-key atau passkey" resetPassword: "Atur ulang kata sandi" newPasswordIs: "Kata sandi baru adalah \"{password}\"" reduceUiAnimation: "Kurangi animasi antarmuka" @@ -417,7 +429,7 @@ notFound: "Tidak dapat ditemukan" notFoundDescription: "Tidak ada halaman sesuai dengan URL yang ditentukan." uploadFolder: "Lokasi unggah folder bawaan" cacheClear: "Bersihkan tembolok" -markAsReadAllNotifications: "Tandai semua pemberitahuan telah dibaca" +markAsReadAllNotifications: "Tandai semua notifikasi telah dibaca" markAsReadAllUnreadNotes: "Tandai semua catatan telah dibaca" markAsReadAllTalkMessages: "Tandai semua pesan telah dibaca" help: "Bantuan" @@ -460,6 +472,7 @@ aboutX: "Tentang {x}" emojiStyle: "Gaya emoji" native: "Native" disableDrawer: "Jangan gunakan menu bergaya laci" +showNoteActionsOnlyHover: "Hanya tampilkan aksi catatan saat ditunjuk" noHistory: "Tidak ada riwayat" signinHistory: "Riwayat masuk" enableAdvancedMfm: "Nyalakan MFM tingkat lanjut" @@ -472,6 +485,8 @@ createAccount: "Buat akun" existingAccount: "Akun yang ada" regenerate: "Buat ulang" fontSize: "Ukuran huruf" +mediaListWithOneImageAppearance: "Tinggi daftar media dengan satu gambar saja" +limitTo: "Batasi pada {x}" noFollowRequests: "Kamu tidak memiliki permintaan mengikuti yang menunggu" openImageInNewTab: "Buka gambar di tab baru" dashboard: "Dasbor" @@ -487,7 +502,7 @@ promotion: "Promosi" promote: "Promosikan" numberOfDays: "Jumlah hari" hideThisNote: "Sembunyikan catatan ini" -showFeaturedNotesInTimeline: "Tampilkan catatan yang diunggulkan di linimasa" +showFeaturedNotesInTimeline: "Tampilkan catatan yang diunggulkan di lini masa" objectStorage: "Object Storage" useObjectStorage: "Gunakan object storage" objectStorageBaseUrl: "Base URL" @@ -505,9 +520,11 @@ objectStorageUseSSLDesc: "Matikan ini jika kamu tidak akan menggunakan HTTPS unt objectStorageUseProxy: "Hubungkan melalui Proxy" objectStorageUseProxyDesc: "Matikan ini jika kamu tidak akan menggunakan Proxy untuk koneksi ObjectStorage" objectStorageSetPublicRead: "Setel \"public-read\" disaat mengunggah" +s3ForcePathStyleDesc: "Jika s3ForcePathStyle dinyalakan, nama bucket harus dimasukkan dalam path URL dan bukan URL nama host tersebut. Kamu perlu menyalakan pengaturan ini jika menggunakan layanan seperti instansi Minio yang self-hosted." serverLogs: "Log Peladen" deleteAll: "Hapus semua" -showFixedPostForm: "Tampilkan form posting di atas linimasa." +showFixedPostForm: "Tampilkan form posting di atas lini masa." +showFixedPostFormInChannel: "Tampilkan form posting di atas lini masa (Kanal)" newNoteRecived: "Kamu mendapat catatan baru" sounds: "Bunyi" sound: "Bunyi" @@ -536,23 +553,28 @@ scratchpadDescription: "Scratchpad menyediakan lingkungan eksperimen untuk AiScr output: "Keluaran" script: "Script" disablePagesScript: "Nonaktifkan script pada halaman" -updateRemoteUser: "Perbaharui informasi pengguna luar" +updateRemoteUser: "Perbaharui informasi pengguna instansi luar" deleteAllFiles: "Hapus semua berkas" deleteAllFilesConfirm: "Apakah kamu yakin ingin menghapus semua berkas?" -removeAllFollowing: "Tahan semua mengikuti" +removeAllFollowing: "Batalkan mengikuti semua pengguna" removeAllFollowingDescription: "Batal mengikuti semua akun dari {host}. Mohon jalankan ini ketika instansi sudah tidak ada lagi." -userSuspended: "Pengguna ini telah dibekukan." -userSilenced: "Pengguna ini telah dibungkam." -yourAccountSuspendedTitle: "Akun ini dibekukan" -yourAccountSuspendedDescription: "Akun ini dibekukan karena melanggar ketentuan penggunaan layanan peladen atau semacamnya. Hubungi admin apabila ingin tahu alasan lebih lanjut. Mohon untuk tidak membuat akun baru." +userSuspended: "Pengguna ini telah ditangguhkan" +userSilenced: "Pengguna ini telah disenyapkan." +yourAccountSuspendedTitle: "Akun ini ditangguhkan" +yourAccountSuspendedDescription: "Akun ini ditangguhkan karena melanggar ketentuan penggunaan layanan peladen atau semacamnya. Hubungi admin apabila ingin mengetahui alasan lebih lanjut. Mohon untuk tidak membuat akun baru." +tokenRevoked: "Token tidak valid" +tokenRevokedDescription: "Token ini telah kedaluwarsa. Mohon masuk lagi." +accountDeleted: "Akun telah dihapus" +accountDeletedDescription: "Akun ini telah dihapus." menu: "Menu" divider: "Pembagi" addItem: "Tambahkan item" +rearrange: "Tata ulang" relays: "Relay" addRelay: "Tambahkan relay" inboxUrl: "URL Kotak masuk" addedRelays: "Relay yang ditambahkan" -serviceworkerInfo: "Harus diaktifkan untuk pemberitahuan push." +serviceworkerInfo: "Harus diaktifkan untuk notifikasi dorong." deletedNote: "Catatan yang dihapus" invisibleNote: "Catatan yang disembunyikan" enableInfiniteScroll: "Aktifkan gulir tak terbatas" @@ -580,13 +602,13 @@ height: "Tinggi" large: "Besar" medium: "Sedang" small: "Kecil" -generateAccessToken: "Buat access token" +generateAccessToken: "Buat token akses" permission: "Izin" enableAll: "Aktifkan semua" disableAll: "Nonaktifkan semua" tokenRequested: "Berikan ijin akses ke akun" pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini." -notificationType: "Jenis pemberitahuan" +notificationType: "Jenis notifikasi" edit: "Sunting" emailServer: "Peladen surel" enableEmail: "Nyalakan distribusi surel" @@ -617,10 +639,10 @@ delayed: "Terlambat" database: "Basis data" channel: "Kanal" create: "Buat" -notificationSetting: "Pengaturan Pemberitahuan" -notificationSettingDesc: "Pilih tipe pemberitahuan untuk ditampilkan" +notificationSetting: "Pengaturan Notifikasi" +notificationSettingDesc: "Pilih tipe notifikasi untuk ditampilkan" useGlobalSetting: "Gunakan setelan global" -useGlobalSettingDesc: "Jika dinyalakan, setelan pemberitahuan akun kamu akan digunakan. Jika dimatikan, konfigurasi secara individu dapat dibuat." +useGlobalSettingDesc: "Jika dinyalakan, setelan notifikasi akun kamu akan digunakan. Jika dimatikan, pengaturan secara individu dapat dibuat." other: "Lainnya" regenerateLoginToken: "Perbarui token login" regenerateLoginTokenDescription: "Perbarui token yang digunakan secara internal saat login. Normalnya aksi ini tidak diperlukan. Jika diperbarui, semua perangkat akan dilogout." @@ -657,8 +679,9 @@ createNewClip: "Buat klip baru" unclip: "Batalkan klip" confirmToUnclipAlreadyClippedNote: "Catatan ini sudah disertakan di klip \"{name}\". Yakin ingin membatalkan catatan dari klip ini?" public: "Publik" -i18nInfo: "Misskey diterjemahkan ke dalam banyak bahasa oleh sukarelawan. Kamu dapat ikut membantu di {link}." -manageAccessTokens: "Kelola access token" +private: "Tersembunyi" +i18nInfo: "Misskey diterjemahkan ke dalam banyak bahasa oleh sukarelawan. Kamu juga dapat ikut membantu menerjemahkannya di {link}." +manageAccessTokens: "Kelola token akses" accountInfo: "Informasi akun" notesCount: "Jumlah catatan" repliesCount: "Jumlah balasan terkirim" @@ -675,7 +698,7 @@ yes: "Iya" no: "Tidak" driveFilesCount: "Jumlah berkas drive" driveUsage: "Penggunaan ruang penyimpanan drive" -noCrawle: "Tolak pengindeksan crawler" +noCrawle: "Tolak pengindeksan perayap web" noCrawleDescription: "Meminta mesin pencari untuk tidak mengindeks halaman profil kamu, catatan, Halaman, dll." lockedAccountInfo: "Kecuali kamu menyetel visibilitas catatan milikmu ke \"Hanya pengikut\", catatan milikmu akan dapat dilihat oleh siapa saja, bahkan jika kamu memerlukan pengikut untuk disetujui secara manual." alwaysMarkSensitive: "Tandai media dalam catatan sebagai media sensitif" @@ -691,10 +714,12 @@ contact: "Kontak" useSystemFont: "Gunakan font bawaan sistem operasi" clips: "Klip" experimentalFeatures: "Fitur eksperimental" +experimental: "Eksperimental" +thisIsExperimentalFeature: "Fitur ini eksperimental. Fungsionalitas dari fitur ini dapat berubah sewaktu-waktu dan mungkin tidak bekerja sesuai semestinya." developer: "Pengembang" makeExplorable: "Buat akun tampil di \"Jelajahi\"" -makeExplorableDescription: "Jika kamu mematikan ini, akun kamu tidak akan muncul di bagian \"Jelajahi:" -showGapBetweenNotesInTimeline: "Tampilkan jarak diantara catatan pada linimasa" +makeExplorableDescription: "Jika kamu mematikan ini, akun kamu tidak akan muncul di menu \"Jelajahi\"" +showGapBetweenNotesInTimeline: "Tampilkan jarak diantara catatan pada lini masa" duplicate: "Duplikat" left: "Kiri" center: "Tengah" @@ -733,14 +758,14 @@ capacity: "Kapasitas" inUse: "Digunakan" editCode: "Sunting kode" apply: "Terapkan" -receiveAnnouncementFromInstance: "Terima pemberitahuan surel dari instansi ini" -emailNotification: "Pemberitahuan surel" +receiveAnnouncementFromInstance: "Terima pengumuman dari instansi ini" +emailNotification: "Notifikasi surel" publish: "Terbitkan" inChannelSearch: "Cari di kanal" useReactionPickerForContextMenu: "Buka pemilih reaksi dengan klik-kanan" typingUsers: "{users} sedang mengetik..." jumpToSpecifiedDate: "Loncat ke tanggal spesifik" -showingPastTimeline: "Sedang menampilkan linimasa lama" +showingPastTimeline: "Sedang menampilkan lini masa lama" clear: "Bersihkan" markAllAsRead: "Tandai semua telah dibaca" goBack: "Kembali" @@ -755,7 +780,7 @@ userInfo: "Informasi pengguna" unknown: "Tidak diketahui" onlineStatus: "Status daring" hideOnlineStatus: "Sembunyikan status daring" -hideOnlineStatusDescription: "Menyembunyikan status daring kamu umengurangi kenyamanan untuk beberapa fungsi seperti contohnya pencarian." +hideOnlineStatusDescription: "Menyembunyikan status daring kamu akan mengurangi kenyamanan untuk beberapa fungsi, seperti contohnya pencarian." online: "Daring" active: "Aktif" offline: "Luring" @@ -775,12 +800,14 @@ noMaintainerInformationWarning: "Informasi pengelola belum disetel." noBotProtectionWarning: "Proteksi bot belum disetel." configure: "Setel" postToGallery: "Posting ke galeri" +postToHashtag: "Catat ke tagar ini" gallery: "Galeri" recentPosts: "Postingan terbaru" popularPosts: "Postingan populer" shareWithNote: "Bagikan dengan catatan" ads: "Iklan" expiration: "Batas akhir" +startingperiod: "Mulai" memo: "Memo" priority: "Prioritas" high: "Tinggi" @@ -807,14 +834,18 @@ translatedFrom: "Terjemahkan dari {x}" accountDeletionInProgress: "Penghapusan akun sedang dalam proses" usernameInfo: "Nama yang mengidentifikasikan akun kamu dari yang lain pada peladen ini. Kamu dapat menggunakan alfabet (a~z, A~Z), digit (0~9) atau garis bawah (_). Username tidak dapat diubah setelahnya." aiChanMode: "Mode Ai" -keepCw: "Biarkan Peringatan Konten" +devMode: "Mode pengembang" +keepCw: "Biarkan peringatan konten" pubSub: "Akun Pub/Sub" lastCommunication: "Komunikasi terakhir" resolved: "Selesai" unresolved: "Belum selesai" -breakFollow: "Batalkan mengikuti" +breakFollow: "Hapus pengikut" +breakFollowConfirm: "Yakin untuk menghapus pengikut ini?" itsOn: "Aktif" itsOff: "Nonaktif" +on: "Nyala" +off: "Mati" emailRequiredForSignup: "Membutuhkan alamat surel untuk mendaftar" unread: "Belum dibaca" filter: "Saring" @@ -891,25 +922,26 @@ slow: "Lambat" fast: "Cepat" sensitiveMediaDetection: "Deteksi media NSFW" localOnly: "Hanya lokal" -remoteOnly: "Hanya remot" +remoteOnly: "Hanya luar instansi" failedToUpload: "Gagal mengunggah" cannotUploadBecauseInappropriate: "Berkas ini tidak dapat diunggah karena sebagian dari berkas terdeteksi berpotensi NSFW." cannotUploadBecauseNoFreeSpace: "Gagal mengunggah karena kekurangan kapasitas Drive." +cannotUploadBecauseExceedsFileSizeLimit: "Berkas ini tidak dapat diunggah karena melebihi batas ukuran berkas." beta: "Beta" enableAutoSensitive: "Penandaan NSFW otomatis" -enableAutoSensitiveDescription: "Mendeteksi otomatis dan menandai media NSFW menggunakan Machine Learning jika memungkinkan. Meskipun opsi ini dimatikan, ada kemungkinan dinyalakan secara menyeluruh pada instansi peladen." +enableAutoSensitiveDescription: "Mendeteksi otomatis dan menandai media NSFW menggunakan Pembelajaran Mesin jika memungkinkan. Meskipun opsi ini dimatikan, ada kemungkinan dinyalakan secara menyeluruh pada instansi peladen." activeEmailValidationDescription: "Membolehkan validasi alamat surel ketat dengan mengecek apakah alamat surel tersebut temporer dan bisa berkomunikasi dengan surel tersebut. Ketidak tidak dicentang, hanya format surel yang divalidasi." navbar: "Bilah navigasi" shuffle: "Acak" account: "Akun" move: "Pindah" -pushNotification: "Pemberitahuan push" -subscribePushNotification: "Nyalakan pemberitahuan push" -unsubscribePushNotification: "Matikan pemberitahuan push" -pushNotificationAlreadySubscribed: "Pemberitahuan push telah dinyalakan" -pushNotificationNotSupported: "Browser atau instansi kamu tidak mendukung pemberitahuan push" -sendPushNotificationReadMessage: "Hapus pemberitahuan push ketika pemberitahuan relevan atau pesan telah dibaca" -sendPushNotificationReadMessageCaption: "Pemberitahuan berisi teks「{emptyPushNotificationMessage}」akan ditampilkan dalam waktu pendek. Ini mungkin dapat menambah pemakaian baterai pada perangkat kamu." +pushNotification: "Notifikasi dorong" +subscribePushNotification: "Nyalakan notifikasi dorong" +unsubscribePushNotification: "Matikan notifikasi dorong" +pushNotificationAlreadySubscribed: "Notifikasi dorong telah dinyalakan" +pushNotificationNotSupported: "Browser atau instansi kamu tidak mendukung notifikasi dorong" +sendPushNotificationReadMessage: "Hapus notifikasi dorong ketika notifikasi relevan atau pesan telah dibaca" +sendPushNotificationReadMessageCaption: "Notifikasi berisi teks「{emptyPushNotificationMessage}」akan ditampilkan dalam waktu pendek. Ini mungkin dapat menambah pemakaian baterai pada perangkat kamu." windowMaximize: "Maksimalkan" windowMinimize: "Minimalkan" windowRestore: "Kembalikan" @@ -928,6 +960,7 @@ 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" +noRole: "Peran tidak temukan" normalUser: "Pengguna umum" undefined: "Tak terdefinisi" assign: "Tetapkan\n" @@ -937,28 +970,170 @@ 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." +invalidParamError: "Parameter tidak valid" +invalidParamErrorDescription: "Parameter permintaan tidak valid. Hal ini biasanya disebabkan oleh bug, namun juga dapat terjadi karena input melebihi batas ukuran atau semacamnya." +permissionDeniedError: "Operasi ditolak" +permissionDeniedErrorDescription: "Akun ini tidak memiliki izin untuk melakukan aksi ini." 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" +thisPostMayBeAnnoyingHome: "Catat ke lini masa 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" -joinThisServer: "Gabung server ini" -exploreOtherServers: "Cari server lain" -letsLookAtTimeline: "LIhat timeline" +joinThisServer: "Gabung peladen ini" +exploreOtherServers: "Cari peladen lain" +letsLookAtTimeline: "LIhat lini masa" disableFederationConfirm: "Matikan federasi?" disableFederationConfirmWarn: "Mematikan federasi tidak membuat kiriman menjadi privat. Umumnya, mematikan federasi tidak diperlukan." disableFederationOk: "Matikan federasi" +invitationRequiredToRegister: "Instansi ini dalam mode undangan-saja. Kamu harus memasukkan kode undangan yang valid untuk mendaftar." +emailNotSupported: "Instansi ini tidak mendukung mengirim surel" +postToTheChannel: "Catat ke kanal" +cannotBeChangedLater: "Hal ini nantinya tidak dapat diubah lagi." +reactionAcceptance: "Penerimaan reaksi" +likeOnly: "Hanya suka" +likeOnlyForRemote: "Semua (Hanya suka dari instansi luar)" +nonSensitiveOnly: "Hanya non-sensitif" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "Hanya non-sensitif (Hanya suka dari instansi luar)" +rolesAssignedToMe: "Peran yang ditugaskan ke saya" +resetPasswordConfirm: "Yakin untuk mereset kata sandimu?" +sensitiveWords: "Kata sensitif" +sensitiveWordsDescription: "Visibilitas dari semua catatan mengandung kata yang telah diatur akan dijadikan \"Beranda\" secara otomatis. Kamu dapat mendaftarkan kata tersebut lebih dari satu dengan menuliskannya di baris baru." +sensitiveWordsDescription2: "Menggunakan spasi akan membuat ekspresi AND dan kata kunci disekitarnya dengan garis miring akan mengubahnya menjadi ekspresi reguler." +notesSearchNotAvailable: "Pencarian catatan tidak tersedia." +license: "Lisensi" +unfavoriteConfirm: "Yakin ingin menghapusnya dari favorit?" +myClips: "Klip saya" +drivecleaner: "Pembersih Drive" +retryAllQueuesNow: "Coba jalankan lagi semua antrian" +retryAllQueuesConfirmTitle: "Yakin ingin mencoba lagi semuanya?" +retryAllQueuesConfirmText: "Hal ini akan meningkatkan beban sementara ke peladen." +enableChartsForRemoteUser: "Buat bagan data pengguna instansi luar" +enableChartsForFederatedInstances: "Buat bagan data peladen instansi luar" +showClipButtonInNoteFooter: "Tambahkan \"Klip\" ke menu aksi catatan" +noteIdOrUrl: "ID catatan atau URL" +video: "Video" +videos: "Video" +dataSaver: "Penghemat data" +accountMigration: "Pemindahan akun" +accountMoved: "Pengguna ini telah berpindah ke akun baru:" +accountMovedShort: "Akun ini telah dipindahkan." +operationForbidden: "Operasi dilarang" +forceShowAds: "Selalu tampilkan iklan" +addMemo: "Tambahkan memo" +editMemo: "Sunting memo" +reactionsList: "Reaksi" +renotesList: "Renote" +notificationDisplay: "Notifikasi" +leftTop: "Kiri atas" +rightTop: "Kanan atas" +leftBottom: "Kiri bawah" +rightBottom: "Kanan bawah" +stackAxis: "Arah tumpukan" +vertical: "Vertikal" horizontal: "Horisontal" +position: "Posisi" +serverRules: "Aturan peladen" +pleaseConfirmBelowBeforeSignup: "Mohon konfirmasi di bawah ini sebelum mendaftar." +pleaseAgreeAllToContinue: "Kamu harus menyetujui semua kolom di atas untuk melanjutkan." +continue: "Lanjutkan" +preservedUsernames: "Nama pengguna tercadangkan" +preservedUsernamesDescription: "Daftar nama pengguna yang dicadangkan dipisah dengan baris baru. Nama pengguna berikut akan tidak dapat dipakai pada pembuatan akun normal, namun dapat digunakan oleh admin untuk membuat akun baru. Akun yang sudah ada dengan menggunakan nama pengguna ini tidak akan terpengaruh." +createNoteFromTheFile: "Buat catatan dari berkas ini" +archive: "Arsipkan" +channelArchiveConfirmTitle: "Yakin untuk mengarsipkan {name}?" +channelArchiveConfirmDescription: "Kanal yang diarsipkan tidak akan muncul pada daftar kanal atau hasil pencarian. Postingan baru juga tidak dapat ditambahkan lagi." +thisChannelArchived: "Kanal ini telah diarsipkan." +displayOfNote: "Tampilan catatan" +initialAccountSetting: "Atur profil" youFollowing: "Mengikuti" +preventAiLearning: "Tolak penggunaan Pembelajaran Mesin (AI Generatif)" +preventAiLearningDescription: "Minta perayap web untuk tidak menggunakan materi teks atau gambar yang telah diposting ke dalam set data Pembelajaran Mesin (Prediktif / Generatif). Hal ini dicapai dengan menambahkan flag HTML-Response \"noai\" ke masing-masing konten. Pencegahan penuh mungkin tidak dapat dicapai dengan flag ini, karena juga dapat diabaikan begitu saja." options: "Opsi peran" +specifyUser: "Pengguna spesifik" +failedToPreviewUrl: "Tidak dapat dipratinjau" +update: "Perbarui" +rolesThatCanBeUsedThisEmojiAsReaction: "Peran yang dapat menggunakan emoji ini sebagai reaksi" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Jika peran tidak ditentukan, semua pengguna dapat menggunakan emoji ini sebagai reaksi." +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Peran ini harus publik." +cancelReactionConfirm: "Yakin untuk menghapus reaksimu?" +changeReactionConfirm: "Yakin untuk mengganti reaksimu?" +later: "Nanti saja" +goToMisskey: "Ke Misskey" +additionalEmojiDictionary: "Kamus emoji tambahan" +installed: "Terpasang" +branding: "Merek" +enableServerMachineStats: "Tampilkan informasi mesin peladen menjadi publik" +enableIdenticonGeneration: "Nyalakan pembuatan Identicon per pengguna" +turnOffToImprovePerformance: "Matikan untuk tingkatkan performa." +createInviteCode: "Buat kode undangan" +createWithOptions: "Buat dengan opsi" +createCount: "Jumlah undangan" +inviteCodeCreated: "Kode undangan dibuat" +inviteLimitExceeded: "Kamu telah mencapai jumlah maksimum kode undangan yang dapat dibuat." +createLimitRemaining: "Kode undangan yang dapat dibuat: tersisa {limit}" +inviteLimitResetCycle: "Kamu dapat membuat hingga {limit} kode undangan dalam {time}." +expirationDate: "Tanggal kedaluwarsa" +noExpirationDate: "tidak ada tanggal kedaluwarsa" +inviteCodeUsedAt: "Kode undangan digunakan pada" +registeredUserUsingInviteCode: "Undangan digunakan oleh" +waitingForMailAuth: "Menunggu verifikasi surel" +inviteCodeCreator: "Undangan dibuat oleh" +usedAt: "Digunakan pada" +unused: "Tidak digunakan" +used: "Digunakan" +expired: "Kedaluwarsa" +doYouAgree: "Apa kamu setuju?" +beSureToReadThisAsItIsImportant: "Mohon baca informasi penting berikut." +iHaveReadXCarefullyAndAgree: "Saya telah membaca \"{x}\" dan menyetujui." +dialog: "Dialog" +icon: "Avatar" +forYou: "Untuk Anda" +currentAnnouncements: "Pengumuman Saat Ini" +pastAnnouncements: "Pengumuman Terdahulu" +replies: "Balas" +renotes: "Renote" +_initialAccountSetting: + accountCreated: "Akun kamu telah sukses dibuat!" + letsStartAccountSetup: "Untuk pemula, ayo atur profilmu dulu." + letsFillYourProfile: "Pertama, ayo atur profilmu dulu." + profileSetting: "Pengaturan profil" + privacySetting: "Pengaturan privasi" + theseSettingsCanEditLater: "Kamu selalu bisa mengganti pengaturan ini lain kali." + youCanEditMoreSettingsInSettingsPageLater: "Ada banyak pengaturan yang dapat kamu atur dari halaman \"Pengaturan\". Pastikan untuk mengunjungi halaman tersebut nanti." + followUsers: "Coba ikuti beberapa pengguna yang menarik bagimu untuk membangun lini masa akunmu." + pushNotificationDescription: "Menyalakan notifikasi dorong akan membuatmu menerima notifikasi dari {name} secara langsung ke perangkatmu." + initialAccountSettingCompleted: "Pengaturan profil selesai!" + haveFun: "Selamat menikmati, {name}!" + ifYouNeedLearnMore: "Kalau kamu ingin mempelajari lebih lanjut bagaimana cara menggunakan {name} (Misskey), silahkan kunjungi {link}." + skipAreYouSure: "Yakin melewati atur profil?" + laterAreYouSure: "Yakin banget untuk atur profil nanti?" +_serverRules: + description: "Daftar peraturan akan ditampilkan sebelum pendaftaran. Mengatur ringkasan dari Syarat dan Ketentuan sangat direkomendasikan." +_serverSettings: + iconUrl: "URL ikon" +_accountMigration: + moveFrom: "Pindahkan akun lain ke akun ini" + moveFromSub: "Buat alias ke akun lain" + moveFromLabel: "Akun asli #{n}" + moveFromDescription: "Kamu harus membuat alias untuk akun asal kamu berpindah ke akun ini\nMasukkan alias akun asal kamu berpindah ke dalam format berikut: @namapengguna@nama.server.com\nUntuk menghapus alias, kosongkan kolom ini (tidak direkomendasikan)." + moveTo: "Pindahkan akun ini ke akun lain" + moveToLabel: "Akun tujuan pindah:" + moveCannotBeUndone: "Pemindahan akun tidak dapat diurungkan." + moveAccountDescription: "Hal ini akan memindahkan akun kamu ke akun lain.\n ・Pengikut dari akun ini akan secara otomatis dipindahkan ke akun baru\n ・Akun ini akan berhenti mengikuti dari semua pengguna yang sedang kamu ikuti\n ・Kamu akan tidak dapat membuat catatan baru dan lain-lain pada akun ini\n\nMeskipun pemindahan pengikut dilakukan secara otomatis, kamu harus mempersiapkan beberapa langkah secara manual untuk memindahkan daftar pengguna yang sedang kamu ikuti. Untuk melakukan tersebut, lakukan ekspor daftar ikuti yang nantinya dapat kamu impor pada menu pengaturan di akun baru kamu. Prosedur yang sama juga dapat diterapkan pada daftar seperti pengguna yang kamu bisukan atau blokir.\n\n(Penjelasan ini hanya berlaku pada Misskey versi 13.12.0 dan setelahnya. Perangkat lunak ActivityPub lainnya seperti Mastodon berkemungkinan befungsi berbeda.)" + moveAccountHowTo: "Untuk pindah, pertama buat alias untuk akun ini pada akun tujuan kamu berpindah.\nSetelah kamu membuat alias, masukkan akun tujuan kamu berpindah ke dalam format berikut:\n@namapengguna@nama.server.com" + startMigration: "Pindahkan" + migrationConfirm: "Yakin untuk memindahkan akun ini ke {account}? Sekali dimulai, proses ini tidak dapat dihentikan atau ditarik kembali, dan kamu tidak dapat menggunakan akun ini lagi dalam keadaan asli semula." + movedAndCannotBeUndone: "\nAkun ini telah dipindahkan.\nPemindahan tidak dapat diurungkan." + postMigrationNote: "24 jam setelah pemindahan akun selesai, akun ini akan berhenti mengikuti semua akun yang sedang diikuti. Angka mengikut dan pengikut akan menjadi nol. Untuk menghindari pengikut kamu tidak dapat melihat postingan hanya pengikut saja dalam postingan ini, mereka akan tetap mengikuti akun ini." + movedTo: "Akun baru tujuan pindah:" _achievements: earnedAt: "Terbuka pada" _types: @@ -1130,6 +1305,9 @@ _achievements: _client30min: title: "Istirahat pendek" description: "Habiskan waktu 30 menit di Misskey" + _client60min: + title: "Tidak ada \"Miss\" dalam Misskey" + description: "Biarkan Misskey tetap terbuka setidaknya selama 60 menit" _noteDeletedWithin1min: title: "Eh, salah coy!" description: "Hapus catatan kurang dari semenit kamu catat" @@ -1145,8 +1323,8 @@ _achievements: title: "Rujukan mandiri" description: "Kutip catatanmu sendiri" _htl20npm: - title: "Linimasa mengalir" - description: "Memiliki linimasa beranda dengan kecepatan melebihi 20 cpm (catatan per menit)" + title: "Lini masa mengalir" + description: "Memiliki lini masa beranda dengan kecepatan melebihi 20 cpm (catatan per menit)" _viewInstanceChart: title: "Analis" description: "Lihat bagan instansimu" @@ -1218,6 +1396,10 @@ _role: iconUrl: "URL ikon" asBadge: "Tampilkan sebagai lencana" descriptionOfAsBadge: "Ikon peran ini akan ditampilkan bersebelahan dengan username pengguna yang memiliki peran ini jika dinyalakan." + isExplorable: "Buat peran dapat terjelajahi" + descriptionOfIsExplorable: "Lini masa peran ini dan daftar pengguna dengan peran ini akan dibuat publik apabila dinyalakan." + displayOrder: "Urutan" + descriptionOfDisplayOrder: "Semakin tinggi angka, semakin tinggi posisi antarmukanya." 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" @@ -1226,12 +1408,16 @@ _role: middle: "Sedang" high: "Tinggi" _options: - gtlAvailable: "Dapat melihat linimasa global" - ltlAvailable: "Dapat melihat linimasa lokal" + gtlAvailable: "Dapat melihat lini masa global" + ltlAvailable: "Dapat melihat lini masa lokal" canPublicNote: "Dapat mengirim catatan publik" canInvite: "Dapat membuat kode undangan instansi" + inviteLimit: "Batas jumlah undangan" + inviteLimitCycle: "Interval Penerbitan Kode Undangan" + inviteExpirationTime: "Interval kedaluwarsa undangan" canManageCustomEmojis: "Dapat mengelola Emoji kustom" driveCapacity: "Kapasitas Drive" + alwaysMarkNsfw: "Selalu tandai berkas sebagai NSFW" pinMax: "Jumlah maksimal catatan yang disematkan" antennaMax: "Jumlah maksimum antena" wordMuteMax: "Jumlah maksimum karakter yang diperbolehkan dalam membisukan kata" @@ -1243,6 +1429,7 @@ _role: rateLimitFactor: "Batas kecepatan" descriptionOfRateLimitFactor: "Batas kecepatan yang rendah tidak begitu membatasi, batas kecepatan tinggi lebih membatasi. " canHideAds: "Dapat menyembunyikan iklan" + canSearchNotes: "Penggunaan pencarian catatan" _condition: isLocal: "Pengguna lokal" isRemote: "Pengguna remote" @@ -1252,11 +1439,13 @@ _role: followersMoreThanOrEq: "Memiliki pengikut X atau lebih dari tersebut" followingLessThanOrEq: "Mengikuti X pengguna atau kurang dari itu" followingMoreThanOrEq: "Mengikuti X pengguna atau lebih dari itu" + notesLessThanOrEq: "Jumlah postingan kurang dari sama dengan" + notesMoreThanOrEq: "Jumlah postingan lebih dari sama dengan" 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." + description: "Mengurangi usaha moderasi peladen dengan mengenali media NSFW secara otomatis menggunakan Pembelajaran Mesin. 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" @@ -1288,6 +1477,7 @@ _ad: back: "Kembali" reduceFrequencyOfThisAd: "Tampilkan iklan ini lebih sedikit" hide: "Jangan tampilkan" + timezoneinfo: "Hari dalam satu minggu ditentukan dari zona waktu peladen." _forgotPassword: enterEmail: "Masukkan alamat surel yang kamu gunakan pada saat mendaftar. Sebuah tautan untuk mengatur ulang kata sandi kamu akan dikirimkan ke alamat surel tersebut." ifNoEmail: "Apabila kamu tidak menggunakan surel pada saat pendaftaran, mohon hubungi admin segera." @@ -1339,21 +1529,21 @@ _aboutMisskey: donate: "Donasi ke Misskey" morePatrons: "Kami sangat mengapresiasi dukungan dari banyak penolong lain yang tidak tercantum disini. Terima kasih! 🥰" patrons: "Pendukung" -_nsfw: - respect: "Sembunyikan media NSFW" - ignore: "Jangan sembunyikan media NSFW" +_displayOfSensitiveMedia: + respect: "Sembunyikan media yang ditandai sensitif" + ignore: "Tampilkan media yang ditandai sensitif" force: "Sembunyikan semua media" _instanceTicker: none: "Jangan tampilkan" - remote: "Tampilkan untuk pengguna luar" + remote: "Tampilkan untuk pengguna instansi luar" always: "Selalu tampilkan" _serverDisconnectedBehavior: reload: "Muat ulang otomatis" dialog: "Tampilkan dialog peringatan" quiet: "Tampilkan peringatan tidak mengganggu" _channel: - create: "Buat saluran" - edit: "Sunting saluran" + create: "Buat Kanal" + edit: "Sunting Kanal" setBanner: "Setel banner" removeBanner: "Hapus banner" featured: "Tren" @@ -1361,6 +1551,8 @@ _channel: following: "Mengikuti" usersCount: "{n} Partisipan" notesCount: "terdapat {n} catatan" + nameAndDescription: "Nama dan deskripsi" + nameOnly: "Hanya nama" _menuDisplay: sideFull: "Horisontal" sideIcon: "Horisontal (Ikon)" @@ -1369,9 +1561,9 @@ _menuDisplay: _wordMute: muteWords: "Kata yang dibisukan" muteWordsDescription: "Pisahkan dengan spasi untuk kondisi AND. Pisahkan dengan baris baru untuk kondisi OR." - muteWordsDescription2: "Kurung kata kunci dengan garis miring untuk menggunakan regular expressions." - softDescription: "Sembunyikan catatan yang memenuhi aturan kondisi dari linimasa." - hardDescription: "Cegah catatan memenuhi aturan kondisi dari ditambahkan ke linimasa. Dengan tambahan, catatan berikut tidak akan ditambahkan ke linimasa meskipun jika kondisi tersebut diubah." + muteWordsDescription2: "Kurung kata kunci dengan garis miring untuk menggunakan ekspresi reguler." + softDescription: "Sembunyikan catatan yang memenuhi aturan kondisi dari lini masa." + hardDescription: "Cegah catatan memenuhi aturan kondisi dari ditambahkan ke lini masa. Dengan tambahan, catatan berikut tidak akan ditambahkan ke lini masa meskipun jika kondisi tersebut diubah." soft: "Lembut" hard: "Keras" mutedNotes: "Catatan yang dibisukan" @@ -1441,8 +1633,8 @@ _theme: cwBg: "Latar belakang tombol Sembunyikan Konten" cwFg: "Teks tombol Sembunyikan Konten" cwHoverBg: "Latar belakang tombol Sembunyikan Konten (Mengambang)" - toastBg: "Latar belakang pemberitahuan" - toastFg: "Teks pemberitahuan" + toastBg: "Latar belakang notifikasi" + toastFg: "Teks notifikasi" buttonBg: "Latar belakang tombol" buttonHoverBg: "Latar belakang tombol (Mengambang)" inputBorder: "Batas bidang masukan" @@ -1457,11 +1649,11 @@ _theme: _sfx: note: "Catatan" noteMy: "Catatan (Saya)" - notification: "Pemberitahuan" + notification: "Notifikasi" chat: "Pesan" chatBg: "Obrolan (Latar Belakang)" antenna: "Penerimaan Antenna" - channel: "Pemberitahuan saluran" + channel: "Notifikasi Kanal" _ago: future: "Masa depan" justNow: "Baru saja" @@ -1478,16 +1670,39 @@ _time: minute: "menit" hour: "jam" day: "hari" +_timelineTutorial: + title: "Bagaimana cara menggunakan Misskey" + step1_1: "Ini adalah \"lini masa\". Semua \"catatan\" yang dikirimkan oleh {name} akan dimunculkan secara kronologis di sini." + step1_2: "Ada beberapa lini masa yang berbeda. Seperti contoh, \"Lini masa Beranda\" berisi catatan dari pengguna yang kamu ikuti, dan \"Lini masa lokal\" berisi catatan dari semua pengguna dari {name}." + step2_1: "Selanjutnya, mari kita coba memposting sebuah catatan. Kamu dapat melakukanya dengan menekan tombol dengan ikon pensil." + step2_2: "Bagaimana dengan menuliskan sedikit perkenalan diri, atau hanya \"Hello {name}\" kalau kamu lagi ngga feeling?" + step3_1: "Udah selesai memposting catatan pertamamu?" + step3_2: "Catatan pertamamu seharusnya sekarang sudah tampil di lini masa kamu." + step4_1: "Kamu dapat menyisipkan \"Reaksi\" ke dalam catatan." + step4_2: "Untuk menyisipkan reaksi, tekan tanda \"+\" dalam catatan dan pilih emoji yang kamu suka untuk mereaksi catatan tersebut." _2fa: alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor." + registerTOTP: "Daftarkan aplikasi autentikator" 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:" + step2Click: "Mengeklik kode QR ini akan membolehkanmu untuk mendaftarkan 2FA ke security-key atau aplikasi autentikator ponsel." + step3Title: "Masukkan kode autentikasi" 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." + securityKeyNotSupported: "Peramban kamu tidak mendukung security key." + registerTOTPBeforeKey: "Mohon atur aplikasi autentikator untuk mendaftarkan security key atau passkey." 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." + registerSecurityKey: "Daftarkan security key atau passkey." + securityKeyName: "Masukkan nama key." + tapSecurityKey: "Mohon ikuti peramban kamu untuk mendaftarkan security key atau passkey" + removeKey: "Hapus security key" removeKeyConfirm: "Hapus cadangan {name}?" + whyTOTPOnlyRenew: "Aplikasi autentikator tidak dapat dihapus selama security key masih terdaftar." + renewTOTP: "Atur ulang aplikasi autentikator" + renewTOTPConfirm: "Hal ini akan menyebabkan kode verifikasi dari aplikasi autentikator sebelumnya berhenti bekerja" + renewTOTPOk: "Atur ulang" renewTOTPCancel: "Tidak sekarang." + backupCodes: "Kode Pencadangan" _permissions: "read:account": "Lihat informasi akun" "write:account": "Sunting informasi akun" @@ -1504,8 +1719,8 @@ _permissions: "read:mutes": "Lihat daftar orang yang dibisukan" "write:mutes": "Sunting daftar orang yang dibisukan" "write:notes": "Buat atau hapus catatan" - "read:notifications": "Lihat pemberitahuan" - "write:notifications": "Sunting pemberitahuan" + "read:notifications": "Lihat notifikasi" + "write:notifications": "Sunting notifikasi" "read:reactions": "Lihat reaksi" "write:reactions": "Sunting reaksi" "write:votes": "Beri suara" @@ -1515,8 +1730,8 @@ _permissions: "write:page-likes": "Sunting suka pada Halaman" "read:user-groups": "Lihat grup pengguna" "write:user-groups": "Sunting atau hapus grup pengguna" - "read:channels": "Lihat saluran" - "write:channels": "Sunting saluran" + "read:channels": "Lihat Kanal" + "write:channels": "Sunting Kanal" "read:gallery": "Lihat galeri" "write:gallery": "Sunting galeri" "read:gallery-likes": "Lihat daftar postingan galeri yang disukai" @@ -1548,8 +1763,8 @@ _widgets: profile: "Profil" instanceInfo: "Informasi Instansi" memo: "Catatan memo" - notifications: "Pemberitahuan" - timeline: "Linimasa" + notifications: "Notifikasi" + timeline: "Lini masa" calendar: "Kalender" trends: "Tren" clock: "Jam" @@ -1603,13 +1818,15 @@ _poll: remainingSeconds: "Berakhir dalam {s} detik" _visibility: public: "Publik" - publicDescription: "Catat ke linimasa global" + publicDescription: "Catat ke lini masa global" home: "Beranda" - homeDescription: "Catat ke linimasa beranda saja" + homeDescription: "Catat ke lini masa beranda saja" followers: "Pengikut" followersDescription: "Catat ke pengikut saja" specified: "Langsung" specifiedDescription: "Catat ke pengguna yang ditentukan saja" + disableFederation: "Matikan federasi" + disableFederationDescription: "Jangan kirimkan ke instansi lain" _postForm: replyPlaceholder: "Balas ke catatan ini..." quotePlaceholder: "Kutip catatan ini..." @@ -1650,7 +1867,7 @@ _charts: activeUsers: "Pengguna aktif" notesIncDec: "Perbedaan # dalam catatan" localNotesIncDec: "Perbedaan # dalam catatan lokal" - remoteNotesIncDec: "Perbedaan # dalam catatan luar" + remoteNotesIncDec: "Perbedaan # dalam catatan instansi luar" notesTotal: "Total # catatan" filesIncDec: "Perbedaan # dalam berkas" filesTotal: "Jumlah # berkas" @@ -1765,7 +1982,8 @@ _notification: pollEnded: "Jajak pendapat berakhir" receiveFollowRequest: "Permintaan mengikuti diterima" followRequestAccepted: "Permintaan mengikuti disetujui" - app: "Pemberitahuan dari aplikasi" + achievementEarned: "Pencapaian didapatkan" + app: "Notifikasi dari aplikasi tertaut" _actions: followBack: "Ikuti Kembali" reply: "Balas" @@ -1787,16 +2005,38 @@ _deck: introduction: "Buat antarmuka sempurna untukmu dengan menata kolom secara bebas!" introduction2: "Klik \"+\" pada kanan layar untuk menambahkan kolom baru kapanpun yang kamu mau." widgetsIntroduction: "Mohon pilih \"Sunting gawit\" pada menu kolom dan tambahkan gawit." + useSimpleUiForNonRootPages: "Gunakan antarmuka sederhana ke halaman yang dituju" _columns: main: "Utama" widgets: "Widget" - notifications: "Pemberitahuan" - tl: "Linimasa" + notifications: "Notifikasi" + tl: "Lini masa" antenna: "Antena" list: "Daftar" channel: "Kanal" mentions: "Sebutan" direct: "Langsung" + roleTimeline: "Lini masa peran" +_dialog: + charactersExceeded: "Kamu telah melebihi batas karakter maksimum! Saat ini pada {current} dari {max}." + charactersBelow: "Kamu berada di bawah batas minimum karakter! Saat ini pada {current} dari {min}." +_disabledTimeline: + title: "Lini masa dinonaktifkan" + description: "Saat ini kamu tidak dapat menggunakan lini masa ini karena peran kamu saat ini." +_drivecleaner: + orderBySizeDesc: "Ukuran berkas (Turun)" + orderByCreatedAtAsc: "Tanggal (Naik)" _webhookSettings: + createWebhook: "Buat Webhook" name: "Nama" + secret: "Secret" + events: "Webhook Events" active: "Aktif" + _events: + follow: "Ketika mengikuti pengguna" + followed: "Ketika diikuti pengguna" + note: "Ketika memposting catatan" + reply: "Ketika menerima balasan" + renote: "Ketika direnote" + reaction: "Ketika menerima reaksi" + mention: "Ketika sedang disebut" diff --git a/locales/index.d.ts b/locales/index.d.ts index 7047f42eff..da60550193 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -48,15 +48,20 @@ export interface Locale { "unpin": string; "copyContent": string; "copyLink": string; + "copyLinkRenote": string; "delete": string; "deleteAndEdit": string; "deleteAndEditConfirm": string; "addToList": string; + "addToAntenna": string; "sendMessage": string; "copyRSS": string; "copyUsername": string; "copyUserId": string; "copyNoteId": string; + "copyFileId": string; + "copyFolderId": string; + "copyProfileUrl": string; "searchUser": string; "reply": string; "loadMore": string; @@ -139,8 +144,10 @@ export interface Locale { "suspendConfirm": string; "unsuspendConfirm": string; "selectList": string; + "editList": string; "selectChannel": string; "selectAntenna": string; + "editAntenna": string; "selectWidget": string; "editWidgets": string; "editWidgetsExit": string; @@ -153,6 +160,9 @@ export interface Locale { "settingGuide": string; "cacheRemoteFiles": string; "cacheRemoteFilesDescription": string; + "youCanCleanRemoteFilesCache": string; + "cacheRemoteSensitiveFiles": string; + "cacheRemoteSensitiveFilesDescription": string; "flagAsBot": string; "flagAsBotDescription": string; "flagAsCat": string; @@ -314,7 +324,7 @@ export interface Locale { "rename": string; "avatar": string; "banner": string; - "nsfw": string; + "displayOfSensitiveMedia": string; "whenServerDisconnected": string; "disconnectedFromServer": string; "reload": string; @@ -349,7 +359,6 @@ export interface Locale { "driveCapacityPerLocalAccount": string; "driveCapacityPerRemoteAccount": string; "inMb": string; - "iconUrl": string; "bannerUrl": string; "backgroundImageUrl": string; "basicInfo": string; @@ -405,10 +414,13 @@ export interface Locale { "administrator": string; "token": string; "2fa": string; + "setupOf2fa": string; "totp": string; "totpDescription": string; "moderator": string; "moderation": string; + "moderationNote": string; + "addModerationNote": string; "nUsersMentioned": string; "securityKeyAndPasskey": string; "securityKey": string; @@ -648,6 +660,7 @@ export interface Locale { "sample": string; "abuseReports": string; "reportAbuse": string; + "reportAbuseRenote": string; "reportAbuseOf": string; "fillAbuseReportDescription": string; "abuseReported": string; @@ -675,6 +688,7 @@ export interface Locale { "unclip": string; "confirmToUnclipAlreadyClippedNote": string; "public": string; + "private": string; "i18nInfo": string; "manageAccessTokens": string; "accountInfo": string; @@ -699,6 +713,7 @@ export interface Locale { "alwaysMarkSensitive": string; "loadRawImages": string; "disableShowingAnimatedImages": string; + "highlightSensitiveMedia": string; "verificationEmailSent": string; "notSet": string; "emailVerified": string; @@ -1013,7 +1028,7 @@ export interface Locale { "enableChartsForRemoteUser": string; "enableChartsForFederatedInstances": string; "showClipButtonInNoteFooter": string; - "largeNoteReactions": string; + "reactionsDisplaySize": string; "noteIdOrUrl": string; "video": string; "videos": string; @@ -1065,6 +1080,58 @@ export interface Locale { "goToMisskey": string; "additionalEmojiDictionary": string; "installed": string; + "branding": string; + "enableServerMachineStats": string; + "enableIdenticonGeneration": string; + "turnOffToImprovePerformance": string; + "createInviteCode": string; + "createWithOptions": string; + "createCount": string; + "inviteCodeCreated": string; + "inviteLimitExceeded": string; + "createLimitRemaining": string; + "inviteLimitResetCycle": string; + "expirationDate": string; + "noExpirationDate": string; + "inviteCodeUsedAt": string; + "registeredUserUsingInviteCode": string; + "waitingForMailAuth": string; + "inviteCodeCreator": string; + "usedAt": string; + "unused": string; + "used": string; + "expired": string; + "doYouAgree": string; + "beSureToReadThisAsItIsImportant": string; + "iHaveReadXCarefullyAndAgree": string; + "dialog": string; + "icon": string; + "forYou": string; + "currentAnnouncements": string; + "pastAnnouncements": string; + "youHaveUnreadAnnouncements": string; + "useSecurityKey": string; + "replies": string; + "renotes": string; + "loadReplies": string; + "loadConversation": string; + "pinnedList": string; + "keepScreenOn": string; + "verifiedLink": string; + "notifyNotes": string; + "unnotifyNotes": string; + "authentication": string; + "authenticationRequiredToContinue": string; + "_announcement": { + "forExistingUsers": string; + "forExistingUsersDescription": string; + "needConfirmationToRead": string; + "needConfirmationToReadDescription": string; + "end": string; + "tooManyActiveAnnouncementDescription": string; + "readConfirmTitle": string; + "readConfirmText": string; + }; "_initialAccountSetting": { "accountCreated": string; "letsStartAccountSetup": string; @@ -1084,6 +1151,16 @@ export interface Locale { "_serverRules": { "description": string; }; + "_serverSettings": { + "iconUrl": string; + "appIconDescription": string; + "appIconUsageExample": string; + "appIconStyleRecommendation": string; + "appIconResolutionMustBe": string; + "manifestJsonOverride": string; + "shortName": string; + "shortNameDescription": string; + }; "_accountMigration": { "moveFrom": string; "moveFromSub": string; @@ -1413,6 +1490,10 @@ export interface Locale { "description": string; "flavor": string; }; + "_smashTestNotificationButton": { + "title": string; + "description": string; + }; }; }; "_role": { @@ -1455,6 +1536,9 @@ export interface Locale { "ltlAvailable": string; "canPublicNote": string; "canInvite": string; + "inviteLimit": string; + "inviteLimitCycle": string; + "inviteExpirationTime": string; "canManageCustomEmojis": string; "driveCapacity": string; "alwaysMarkNsfw": string; @@ -1525,6 +1609,7 @@ export interface Locale { "back": string; "reduceFrequencyOfThisAd": string; "hide": string; + "timezoneinfo": string; }; "_forgotPassword": { "enterEmail": string; @@ -1549,6 +1634,7 @@ export interface Locale { "install": string; "installWarn": string; "manage": string; + "viewSource": string; }; "_preferencesBackups": { "list": string; @@ -1586,7 +1672,7 @@ export interface Locale { "morePatrons": string; "patrons": string; }; - "_nsfw": { + "_displayOfSensitiveMedia": { "respect": string; "ignore": string; "force": string; @@ -1753,18 +1839,17 @@ export interface Locale { "_2fa": { "alreadyRegistered": string; "registerTOTP": string; - "passwordToTOTP": string; "step1": string; "step2": string; "step2Click": string; - "step2Url": string; + "step2Uri": string; "step3Title": string; "step3": string; + "setupCompleted": string; "step4": string; "securityKeyNotSupported": string; "registerTOTPBeforeKey": string; "securityKeyInfo": string; - "chromePasskeyNotSupported": string; "registerSecurityKey": string; "securityKeyName": string; "tapSecurityKey": string; @@ -1775,6 +1860,11 @@ export interface Locale { "renewTOTPConfirm": string; "renewTOTPOk": string; "renewTOTPCancel": string; + "checkBackupCodesBeforeCloseThisWizard": string; + "backupCodes": string; + "backupCodesDescription": string; + "backupCodeUsedWarning": string; + "backupCodesExhaustedWarning": string; }; "_permissions": { "read:account": string; @@ -1809,6 +1899,10 @@ export interface Locale { "write:gallery": string; "read:gallery-likes": string; "write:gallery-likes": string; + "read:flash": string; + "write:flash": string; + "read:flash-likes": string; + "write:flash-likes": string; }; "_auth": { "shareAccessTitle": string; @@ -1826,6 +1920,7 @@ export interface Locale { "homeTimeline": string; "users": string; "userList": string; + "userBlacklist": string; }; "_weekday": { "sunday": string; @@ -1934,6 +2029,7 @@ export interface Locale { "metadataContent": string; "changeAvatar": string; "changeBanner": string; + "verifiedLinkDescription": string; }; "_exportOrImport": { "allNotes": string; @@ -2062,11 +2158,17 @@ export interface Locale { "youReceivedFollowRequest": string; "yourFollowRequestAccepted": string; "pollEnded": string; + "newNote": string; "unreadAntennaNote": string; "emptyPushNotificationMessage": string; "achievementEarned": string; + "testNotification": string; + "checkNotificationBehavior": string; + "sendTestNotification": string; + "notificationWillBeDisplayedLikeThis": string; "_types": { "all": string; + "note": string; "follow": string; "mention": string; "reply": string; @@ -2102,6 +2204,9 @@ export interface Locale { "introduction": string; "introduction2": string; "widgetsIntroduction": string; + "useSimpleUiForNonRootPages": string; + "usedAsMinWidthWhenFlexible": string; + "flexible": string; "_columns": { "main": string; "widgets": string; @@ -2147,4 +2252,4 @@ export interface Locale { declare const locales: { [lang: string]: Locale; }; -export = locales; +export default locales; diff --git a/locales/index.js b/locales/index.js index 2248bb6ac9..7801f1275b 100644 --- a/locales/index.js +++ b/locales/index.js @@ -2,8 +2,8 @@ * Languages Loader */ -const fs = require('fs'); -const yaml = require('js-yaml'); +import * as fs from 'node:fs'; +import * as yaml from 'js-yaml'; const merge = (...args) => args.reduce((a, c) => ({ ...a, @@ -51,9 +51,9 @@ const primaries = { // 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), ''); -const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8'))) || {}, a), {}); +const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {}); -module.exports = Object.entries(locales) +export default Object.entries(locales) .reduce((a, [k ,v]) => (a[k] = (() => { const [lang] = k.split('-'); switch (k) { diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 3c1a26e85c..9810e6015a 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -21,20 +21,20 @@ noNotifications: "Nessuna notifica" instance: "Istanza" settings: "Impostazioni" notificationSettings: "Preferenze di notifica" -basicSettings: "Impostazioni generali" +basicSettings: "Impostazioni base" otherSettings: "Altre impostazioni" openInWindow: "Apri in una finestra" profile: "Profilo" timeline: "Timeline" -noAccountDescription: "L'utente non ha ancora scritto niente nella biografia di profilo." +noAccountDescription: "La persona non ha ancora scritto alcuna autobiografia." login: "Accedi" loggingIn: "Accesso in corso..." logout: "Uscita" signup: "Iscriviti" uploading: "Caricamento..." save: "Salva" -users: "Utente" -addUser: "Aggiungi utente" +users: "Profili" +addUser: "Aggiungi profilo" favorite: "Preferiti" favorites: "Preferiti" unfavorite: "Rimuovi nota dai preferiti" @@ -45,21 +45,28 @@ pin: "Fissa sul profilo" unpin: "Non fissare sul profilo" copyContent: "Copia il contenuto" copyLink: "Copia il link" +copyLinkRenote: "Copia collegamento alla Rinota" delete: "Elimina" deleteAndEdit: "Elimina e modifica" deleteAndEditConfirm: "Vuoi davvero cancellare questa nota e scriverla di nuovo? Verranno eliminate anche tutte le reazioni, rinote e risposte collegate." addToList: "Aggiungi alla lista" +addToAntenna: "Aggiungi all'antenna" sendMessage: "Invia messaggio" copyRSS: "Copia RSS" copyUsername: "Copia nome utente" -searchUser: "Cerca utente" +copyUserId: "Copia ID del profilo" +copyNoteId: "Copia ID della Nota" +copyFileId: "Copia ID del file" +copyFolderId: "Copia ID della cartella" +copyProfileUrl: "Copia URL del profilo" +searchUser: "Cerca profilo" reply: "Rispondi" loadMore: "Mostra di più" showMore: "Espandi" showLess: "Comprimi" -youGotNewFollower: "Ha iniziato a seguirti" +youGotNewFollower: "Ti sta seguendo" receiveFollowRequest: "Hai ricevuto una richiesta di follow" -followRequestAccepted: "Richiesta di follow accettata" +followRequestAccepted: "Ha accettato la tua richiesta di follow" mention: "Menzioni" mentions: "Menzioni" directNotes: "Note dirette" @@ -68,8 +75,8 @@ import: "Importa" export: "Esporta" files: "Allegati" download: "Scarica" -driveFileDeleteConfirm: "Vuoi davvero eliminare il file \"{name}\"? Anche gli allegati verranno eliminati." -unfollowConfirm: "Vuoi smettere di seguire {name}?" +driveFileDeleteConfirm: "Vuoi davvero eliminare il file \"{name}\", e le Note a cui è stato allegato?" +unfollowConfirm: "Vuoi davvero smettere di seguire {name}?" exportRequested: "Hai richiesto un'esportazione, e potrebbe volerci tempo. Quando sarà compiuta, il file verrà aggiunto direttamente al Drive." importRequested: "Hai richiesto un'importazione. Può volerci tempo. " lists: "Liste" @@ -78,7 +85,7 @@ note: "Nota" notes: "Note" following: "Follow" followers: "Follower" -followsYou: "Ti segue" +followsYou: "Segue" createList: "Aggiungi una nuova lista" manageLists: "Gestisci liste" error: "Errore" @@ -99,7 +106,7 @@ unfollow: "Non seguire" followRequestPending: "Richiesta in approvazione" enterEmoji: "Inserisci emoji" renote: "Rinota" -unrenote: "Annulla rinota" +unrenote: "Elimina la Rinota" renoted: "Rinotato!" cantRenote: "È impossibile rinotare questa nota." cantReRenote: "È impossibile rinotare una Rinota." @@ -127,15 +134,17 @@ renoteMute: "Silenzia i Rinota" renoteUnmute: "Non silenziare i Rinota" block: "Blocca" unblock: "Sblocca" -suspend: "Sospendi" +suspend: "Sospensione" unsuspend: "Revoca la sospensione" blockConfirm: "Vuoi davvero bloccare il profilo?" unblockConfirm: "Vuoi davvero sbloccare il profilo?" -suspendConfirm: "Vuoi sospendere questo profilo?" +suspendConfirm: "Vuoi davvero sospendere questo profilo?" unsuspendConfirm: "Vuoi revocare la sospensione si questo profilo?" selectList: "Seleziona una lista" +editList: "Modifica Lista" selectChannel: "Seleziona canale" selectAntenna: "Scegli un'antenna" +editAntenna: "Modifica Antenna" selectWidget: "Seleziona il riquadro" editWidgets: "Modifica i riquadri" editWidgetsExit: "Conferma le modifiche" @@ -148,13 +157,16 @@ addEmoji: "Aggiungi un emoji" settingGuide: "Configurazione suggerita" 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." +youCanCleanRemoteFilesCache: "Puoi svuotare tutta la cache cliccando il bottone 🗑️ nella gestione file" +cacheRemoteSensitiveFiles: "Memorizza nella cache i file sensibili remoti" +cacheRemoteSensitiveFilesDescription: "Disattivando questa opzione, i file sensibili verranno caricati direttamente dall'istanza remota senza essere salvati dal server." 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." flagAsCat: "Sono un gatto" flagAsCatDescription: "La modalità \"sono un gatto\" aggiunge le orecchie al tuo profilo" flagShowTimelineReplies: "Mostra le risposte alle note sulla timeline." -flagShowTimelineRepliesDescription: "Se è attiva, la timeline mostra le risposte alle altre note dell'utente oltre a quelle dell'utente stesso." -autoAcceptFollowed: "Accetta automaticamente le richieste di follow da utenti che già segui" +flagShowTimelineRepliesDescription: "Attivando, la timeline mostra le Note del profilo ed anche le risposte ad altre Note" +autoAcceptFollowed: "Accetta automaticamente le richieste di follow da profili che già segui" addAccount: "Aggiungi profilo" reloadAccountsList: "Ricarica l'elenco dei profili" loginFailed: "Accesso non riuscito" @@ -168,7 +180,7 @@ youHaveNoLists: "Non hai ancora creato nessuna lista" 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" +host: "Host" selectUser: "Seleziona profilo" recipient: "Destinatario" annotation: "Annotazione preventiva" @@ -205,7 +217,7 @@ blockedInstancesDescription: "Elenca le istanze che vuoi bloccare, una per riga. muteAndBlock: "Silenziati / Bloccati" mutedUsers: "Profili silenziati" blockedUsers: "Profili bloccati" -noUsers: "Nessun utente trovato" +noUsers: "Non ci sono profili" editProfile: "Modifica profilo" noteDeleteConfirm: "Vuoi davvero eliminare questa Nota?" pinLimitExceeded: "Non puoi fissare altre note " @@ -236,15 +248,15 @@ newPasswordRetype: "Conferma password" attachFile: "Allega file" more: "Di più!" featured: "Tendenze" -usernameOrUserId: "Nome utente o ID utente" -noSuchUser: "Nessun utente trovato" +usernameOrUserId: "Nome utente o ID" +noSuchUser: "Profilo non trovato" lookup: "Ricerca remota" announcements: "Annunci" imageUrl: "URL dell'immagine" remove: "Elimina" removed: "Eliminato con successo" removeAreYouSure: "Vuoi davvero eliminare \"{x}\"?" -deleteAreYouSure: "Eliminare \"{x}\"?" +deleteAreYouSure: "Vuoi davvero eliminare \"{x}\"?" resetAreYouSure: "Ripristinare?" saved: "Salvato" messaging: "Messaggi" @@ -263,19 +275,19 @@ noMoreHistory: "Non c'è più cronologia da visualizzare" startMessaging: "Nuovo messaggio" nUsersRead: "Letto da {n} persone" agreeTo: "Sono d'accordo con {0}" -agree: "D'accordo" +agree: "Accetto" agreeBelow: "Accetto quanto riportato sotto" basicNotesBeforeCreateAccount: "Note importanti" -termsOfService: "Informativa Privacy" +termsOfService: "Informativa ai sensi degli artt. 13 e 14 del Regolamento UE 2016/679 per la protezione dei dati personali (GDPR)" start: "Inizia!" home: "Home" -remoteUserCaution: "Può darsi che le informazioni siano incomplete perché questo è un utente remoto." +remoteUserCaution: "Le informazioni potrebbero essere incomplete poiché questo profilo remoto potrebbe non essere completamente federato." activity: "Attività" images: "Immagini" image: "Immagini" birthday: "Compleanno" yearsOld: "{age} anni" -registeredDate: "Iscrizione a.." +registeredDate: "Data iscrizione" location: "Posizione" theme: "Tema" themeForLightMode: "Tema da utilizzare per il modo chiaro" @@ -309,7 +321,7 @@ copyUrl: "Copia URL" rename: "Modifica nome" avatar: "Foto del profilo" banner: "Intestazione" -nsfw: "Contenuti sensibili" +displayOfSensitiveMedia: "Visibilità dei media sensibili" whenServerDisconnected: "Quando la connessione col server è persa" disconnectedFromServer: "Il server si è disconnesso" reload: "Ricarica" @@ -344,7 +356,6 @@ invite: "Invita" driveCapacityPerLocalAccount: "Capienza del Drive per profilo locale" driveCapacityPerRemoteAccount: "Capienza del Drive per profilo remoto" inMb: "in Megabytes" -iconUrl: "URL di icona (favicon, ecc.)" bannerUrl: "URL dell'immagine d'intestazione" backgroundImageUrl: "URL dello sfondo" basicInfo: "Informazioni fondamentali" @@ -384,9 +395,9 @@ connectedTo: "Connessione ai seguenti profili:" notesAndReplies: "Note e risposte" withFiles: "Con file in allegato" silence: "Silenzia" -silenceConfirm: "Vuoi davvero silenziare l'utente?" +silenceConfirm: "Vuoi davvero silenziare questo profilo?" unsilence: "Riattiva" -unsilenceConfirm: "Vuoi davvero riattivare l'utente?" +unsilenceConfirm: "Vuoi davvero riattivare questo profilo?" popularUsers: "Utenti popolari" recentlyUpdatedUsers: "Utenti attivi di recente" recentlyRegisteredUsers: "Utenti registrati di recente" @@ -400,10 +411,13 @@ aboutMisskey: "Informazioni di Misskey" administrator: "Amministratore" token: "Token" 2fa: "Autenticazione a due fattori" +setupOf2fa: "Impostare l'autenticazione a due fattori" totp: "App di autenticazione" totpDescription: "Inserisci un codice OTP tramite un'app di autenticazione" moderator: "Moderatore" moderation: "moderazione" +moderationNote: "Promemoria di moderazione" +addModerationNote: "Aggiungi promemoria di moderazione" nUsersMentioned: "{n} profili menzionati" securityKeyAndPasskey: "Chiave di sicurezza e accesso" securityKey: "Chiave di sicurezza" @@ -444,7 +458,7 @@ signinRequired: "Occorre avere un profilo registrato su questa istanza" invitations: "Invita" invitationCode: "Codice di invito" checking: "Confermando" -available: "Consigliati" +available: "Disponibile" unavailable: "Il nome utente è già in uso" usernameInvalidFormat: "Il nome utente può contenere solo lettere, numeri e '_'" tooShort: "Troppo breve" @@ -482,7 +496,7 @@ noFollowRequests: "Non hai alcuna richiesta di follow" openImageInNewTab: "Apri le immagini in un nuovo tab" dashboard: "Pannello di controllo" local: "Locale" -remote: "Remoto" +remote: "Remota" total: "Totale" weekOverWeekChanges: "Settimanale" dayOverDayChanges: "Giornaliero" @@ -516,9 +530,9 @@ serverLogs: "Log del server" deleteAll: "Cancella cronologia" showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline" showFixedPostFormInChannel: "Per i canali, mostra il modulo di pubblicazione in cima alla timeline" -newNoteRecived: "Vedi le nuove note" +newNoteRecived: "Nuove note da leggere" sounds: "Impostazioni suoni" -sound: "Impostazioni suoni" +sound: "Suono" listen: "Ascolta" none: "Nessuno" showInPage: "Visualizza in pagina" @@ -537,8 +551,8 @@ installedDate: "Data installazione" lastUsedDate: "Data di ultimo uso" state: "Stato" sort: "Ordina per" -ascendingOrder: "Ascendente" -descendingOrder: "Discendente" +ascendingOrder: "Aumenta" +descendingOrder: "Diminuisce" scratchpad: "ScratchPad" scratchpadDescription: "Lo Scratchpad offre un ambiente per esperimenti di AiScript. È possibile scrivere, eseguire e confermare i risultati dell'interazione del codice con Misskey." output: "Uscita" @@ -547,7 +561,7 @@ disablePagesScript: "Disabilita AiScript nelle pagine" updateRemoteUser: "Aggiornare le informazioni di utente remot@" deleteAllFiles: "Elimina tutti i file" deleteAllFilesConfirm: "Vuoi davvero eliminare tutti i file?" -removeAllFollowing: "Cancella tutti i follows" +removeAllFollowing: "Annulla tutti i follow" removeAllFollowingDescription: "Cancella tutti i follows del server {host}. Per favore, esegui se, ad esempio, l'istanza non esiste più." userSuspended: "L'utente è in sospensione" userSilenced: "L'utente è silenziat@." @@ -560,6 +574,7 @@ accountDeletedDescription: "Questo profilo è stato eliminato." menu: "Menù" divider: "Linea di separazione" addItem: "Aggiungi elemento" +rearrange: "Riordina" relays: "Ripetitori" addRelay: "Aggiungi ripetitore" inboxUrl: "Inbox URL" @@ -606,14 +621,14 @@ emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronic email: "Email" emailAddress: "Indirizzo di posta elettronica" smtpConfig: "Impostazioni del server SMTP" -smtpHost: "Server remoto" +smtpHost: "Host SMTP" smtpPort: "Porta" smtpUser: "Nome utente" 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" +emptyToDisableSmtpAuth: "Lasciare i campi vuoti se non c'è autenticazione SMTP" +smtpSecure: "Usare SSL/TLS implicito per le connessioni SMTP" smtpSecureInfo: "Disabilitare quando è attivo STARTTLS." -testEmail: "Testa la consegna di posta elettronica" +testEmail: "Verifica il funzionamento" wordMute: "Filtri parole" regexpError: "errore regex" regexpErrorDescription: "Si è verificato un errore nell'espressione regolare alla riga {line} della parola muta {tab}:" @@ -641,9 +656,10 @@ fileIdOrUrl: "ID o URL del file" behavior: "Comportamento" sample: "Esempio" abuseReports: "Segnalazioni" -reportAbuse: "Segnalazioni" +reportAbuse: "Segnala" +reportAbuseRenote: "Segnala la Rinota" reportAbuseOf: "Segnala {name}" -fillAbuseReportDescription: "Si prega di spiegare il motivo della segnalazione. Se riguarda una nota precisa, si prega di collegare anche l'URL della nota." +fillAbuseReportDescription: "Per favore, spiegaci il motivo della segnalazione. Se riguarda una Nota precisa, indica anche l'indirizzo URL." abuseReported: "La segnalazione è stata inviata. Grazie." reporter: "il corrispondente" reporteeOrigin: "Origine del segnalato" @@ -660,7 +676,7 @@ instanceTicker: "Informazioni sull'istanza da cui vengono le note" waitingFor: "Aspettando {x}" random: "Casuale" system: "Sistema" -switchUi: "Cambiare interfaccia" +switchUi: "Cambia interfaccia grafica" desktop: "Desktop" clip: "Clip" createNew: "Crea" @@ -669,6 +685,7 @@ createNewClip: "Crea una Clip" unclip: "Togli Nota dalla Clip" confirmToUnclipAlreadyClippedNote: "Questa nota è già inclusa in \"{name}\". Si desidera escludere la nota?" public: "Pubblica" +private: "Privato" i18nInfo: "Misskey è tradotto in diverse lingue da volontari. Anche tu puoi contribuire su {link}." manageAccessTokens: "Gestisci token di accesso" accountInfo: "Informazioni profilo" @@ -703,6 +720,8 @@ contact: "Contatti" useSystemFont: "Usa il carattere predefinito del sistema" clips: "Clip" experimentalFeatures: "Funzioni sperimentali" +experimental: "Sperimentale" +thisIsExperimentalFeature: "Questa è una funzionalità sperimentale. Potrebbe essere malfunzionante o cambiare in futuro." developer: "Sviluppatore" makeExplorable: "Profilo visibile pubblicamente nella pagina \"Esplora\"" makeExplorableDescription: "Disabilitando questa opzione, il tuo profilo non verrà elencato nella pagina \"Esplora\"." @@ -747,7 +766,7 @@ editCode: "Modifica codice" apply: "Applica" receiveAnnouncementFromInstance: "Ricevi i messaggi informativi dall'istanza" emailNotification: "Eventi per notifiche via mail" -publish: "Pubblico" +publish: "Pubblicare" inChannelSearch: "Cerca in canale" useReactionPickerForContextMenu: "Cliccare sul tasto destro per aprire il pannello di reazioni" typingUsers: "{users} sta(nno) scrivendo" @@ -766,10 +785,10 @@ info: "Informazioni" userInfo: "Informazioni utente" unknown: "Sconosciuto" onlineStatus: "Stato di connessione" -hideOnlineStatus: "Stato invisibile" -hideOnlineStatusDescription: "Abilitare l'opzione di stato invisibile può guastare la praticità di singole funzioni, come la ricerca." +hideOnlineStatus: "Modalità invisibile" +hideOnlineStatusDescription: "Attivando questa opzione potresti ridurre l'usabilità di alcune funzioni, come la ricerca." online: "Online" -active: "Attiv@" +active: "Attività" offline: "Offline" notRecommended: "Sconsigliato" botProtection: "Protezione contro i bot" @@ -779,7 +798,7 @@ switchAccount: "Cambia profilo" enabled: "Attivo" disabled: "Inattivo" quickAction: "Azioni rapide" -user: "Utente" +user: "Profilo" administration: "Gestione" accounts: "Profilo" switch: "Cambia" @@ -787,6 +806,7 @@ noMaintainerInformationWarning: "Le informazioni amministratore non sono imposta noBotProtectionWarning: "Nessuna protezione impostata contro i bot." configure: "Imposta" postToGallery: "Pubblicare nella galleria" +postToHashtag: "Pubblica a questo hashtag" gallery: "Galleria" recentPosts: "Le più recenti" popularPosts: "Le più visualizzate" @@ -805,30 +825,33 @@ previewNoteText: "Anteprima del testo" customCss: "CSS personalizzato" customCssWarn: "Questa impostazione deve essere eseguita da una persona esperta. Una configurazione errata può impedire al client di utilizzare correttamente il sistema." global: "Federata" -squareAvatars: "Mostra l'immagine del profilo come quadrato" -sent: "Inviare" +squareAvatars: "Foto profilo squadrate" +sent: "Inviato" received: "Ricevuto" searchResult: "Risultati della Ricerca" hashtags: "Hashtag" troubleshooting: "Risoluzione problemi" -useBlurEffect: "Utilizza effetto sfocatura nell'interfaccia" +useBlurEffect: "Utilizza effetto sfocatura" learnMore: "Più dettagli" misskeyUpdated: "Misskey è stato aggiornato!" whatIsNew: "Visualizza le informazioni sull'aggiornamento" -translate: "Traduzione" -translatedFrom: "Tradotto da {x}" +translate: "Traduci" +translatedFrom: "Traduzione da {x}" accountDeletionInProgress: "È in corso l'eliminazione del profilo" usernameInfo: "Un nome per identificare univocamente il tuo profilo sull'istanza. Puoi utilizzare caratteri alfanumerici maiuscoli, minuscoli e il trattino basso (_). Non potrai cambiare nome utente in seguito." aiChanMode: "Modalità Ai" +devMode: "Modalità sviluppatori" keepCw: "Mantieni il Content Warning" pubSub: "Publish/Subscribe del profilo" lastCommunication: "La comunicazione più recente" resolved: "Risolto" unresolved: "Non risolto" -breakFollow: "Non seguire" -breakFollowConfirm: "Vuoi davvero togliere follower?" +breakFollow: "Non farti più seguire" +breakFollowConfirm: "Vuoi davvero smettere di seguire questo profilo?" itsOn: "Abilitato" itsOff: "Disabilitato" +on: "Acceso" +off: "Spento" emailRequiredForSignup: "L'ndirizzo e-mail è obbligatorio per registrarsi" unread: "Non lette" filter: "Filtri" @@ -837,18 +860,18 @@ manageAccounts: "Gestisci i profili" makeReactionsPublic: "Pubblicare la lista delle reazioni." makeReactionsPublicDescription: "La lista delle reazioni che avete fatto è a disposizione di tutti." classic: "Classico" -muteThread: "Silenzia la conversazione" +muteThread: "Silenzia conversazione" unmuteThread: "Riattiva la conversazione" -ffVisibility: "Ambito pubblico del collegamento" -ffVisibilityDescription: "È possibile impostare la portata pubblica delle informazioni sui propri follower/seguaci." -continueThread: "Altri thread." +ffVisibility: "Visibilità delle connessioni" +ffVisibilityDescription: "Puoi scegliere a chi mostrare le tue relazioni con altri profili nel fediverso." +continueThread: "Altre conversazioni" deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?" incorrectPassword: "La password è errata." voteConfirm: "Votare per「{choice}」?" hide: "Nascondere" 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." +clickToFinishEmailVerification: "Premi il bottone \"{ok}\" per completare la verifica dell'indirizzo email." overridedDeviceKind: "Tipo di dispositivo" smartphone: "Smartphone" tablet: "Tablet" @@ -909,6 +932,7 @@ remoteOnly: "Solo remoto" failedToUpload: "errore di caricamento" cannotUploadBecauseInappropriate: "Non è possibile caricarlo perché è stato stabilito che potrebbe contenere contenuti inappropriati." cannotUploadBecauseNoFreeSpace: "Impossibile caricare a causa della mancanza di spazio libero sul drive." +cannotUploadBecauseExceedsFileSizeLimit: "Il file non può essere caricato perché eccede le dimensioni consentite." beta: "Versione beta" enableAutoSensitive: "Determinazione automatica del NSFW" enableAutoSensitiveDescription: "Se disponibile, il flag NSFW viene impostato automaticamente sui media utilizzando l'apprendimento automatico. Anche se questa funzione è disattivata, in alcuni casi può essere impostata automaticamente." @@ -942,6 +966,7 @@ didYouLikeMisskey: "Ti piace Misskey?" pleaseDonate: "Misskey è il software libero utilizzato su {host}. Offrendo una donazione è più facile continuare a svilupparlo!" roles: "Ruoli" role: "Ruolo" +noRole: "Ruolo non trovato" normalUser: "Profilo standard" undefined: "Indefinito" assign: "Assegna" @@ -981,10 +1006,13 @@ cannotBeChangedLater: "Non sarà più modificabile" reactionAcceptance: "Reazioni consentite" likeOnly: "Solo i Like" likeOnlyForRemote: "Solo Like remoti" +nonSensitiveOnly: "Solamente non sensibili" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "Solamente non sensibili (solo Mi piace remoti)" rolesAssignedToMe: "I miei ruoli" resetPasswordConfirm: "Vuoi davvero ripristinare la password?" sensitiveWords: "Parole sensibili" sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga." +sensitiveWordsDescription2: "Gli spazi creano la relazione \"E\" tra parole (questo E quello). Racchiudere una parola nelle slash \"/\" la trasforma in Espressione Regolare." notesSearchNotAvailable: "Non è possibile cercare tra le Note." license: "Licenza" unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?" @@ -996,16 +1024,20 @@ retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente." enableChartsForRemoteUser: "Abilita i grafici per i profili remoti" enableChartsForFederatedInstances: "Abilita i grafici per le istanze federate" showClipButtonInNoteFooter: "Aggiungi il bottone Clip tra le azioni delle Note" -largeNoteReactions: "Ingrandisci le reazioni" +reactionsDisplaySize: "Grandezza delle reazioni" noteIdOrUrl: "ID della Nota o URL" video: "Video" videos: "Video" dataSaver: "Risparmia dati" accountMigration: "Migrazione del profilo" accountMoved: "Questo profilo ha migrato altrove:" +accountMovedShort: "Questo profilo è stato migrato" +operationForbidden: "Operazione non consentita" forceShowAds: "Mostra sempre i banner" addMemo: "Aggiungi Memo" editMemo: "Modifica Memo" +reactionsList: "Chi ha reagito?" +renotesList: "Chi ha Rinotato?" notificationDisplay: "Stile delle notifiche" leftTop: "In alto a sinistra" rightTop: "In alto a destra" @@ -1016,21 +1048,121 @@ vertical: "Verticale" horizontal: "Laterale" position: "Posizione" serverRules: "Regolamento" -pleaseConfirmBelowBeforeSignup: "Ai sensi del regolamento EU 679/2016 GDPR, autorizzo il trattamento dati personali come descritto nella informativa Privacy." -pleaseAgreeAllToContinue: "Per continuare, occorre selezionare ed essere d'accordo su tutto." +pleaseConfirmBelowBeforeSignup: "Per iscriversi, occorre essere d'accordo con le seguenti condizioni." +pleaseAgreeAllToContinue: "Occorre accettare tutte le condizioni prima di continuare." continue: "Continua" +preservedUsernames: "Nomi utente riservati" +preservedUsernamesDescription: "Elenca, uno per linea, i nomi utente che non possono essere registrati durante la creazione del profilo. La restrizione non si applica agli amministratori. Inoltre, i profili già registrati sono esenti." +createNoteFromTheFile: "Crea Nota da questo file" +archive: "Archivio" +channelArchiveConfirmTitle: "Vuoi davvero archiviare {name}?" +channelArchiveConfirmDescription: "Un canale archiviato non compare nell'elenco canali, nemmeno nei risultati di ricerca. Non può ricevere nemmeno nuove Note." +thisChannelArchived: "Questo canale è stato archiviato." +displayOfNote: "Visualizzazione delle Note" +initialAccountSetting: "Impostazioni iniziali del profilo" youFollowing: "Seguiti" +preventAiLearning: "Impedisci l'apprendimento della IA" +preventAiLearningDescription: "Aggiungendo il campo \"noai\" alla risposta HTML, si indica ai Robot esterni di non usare testi e allegati per addestrare sistemi di Machine Learning (IA predittiva/generativa). Anche se è impossibile sapere se la richiesta venga onorata o semplicemente ignorata." options: "Opzioni del ruolo" +specifyUser: "Profilo specifico" +failedToPreviewUrl: "Anteprima non disponibile" +update: "Aggiorna" +rolesThatCanBeUsedThisEmojiAsReaction: "Ruoli che possono usare questa emoji come reazione" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Se non viene specificato alcun ruolo, chiunque può reagire con questa emoji." +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Questi ruoli devono essere pubblici" +cancelReactionConfirm: "Vuoi annullare la tua reazione?" +changeReactionConfirm: "Vuoi cambiare la tua reazione?" +later: "Non ora" +goToMisskey: "Vai a Misskey" +additionalEmojiDictionary: "Dizionario aggiuntivo emoji" +installed: "Installazione avvenuta" +branding: "Branding" +enableServerMachineStats: "Pubblicare le informazioni sul server" +enableIdenticonGeneration: "Generazione automatica delle Identicon" +turnOffToImprovePerformance: "Disattiva, per migliorare le prestazioni" +createInviteCode: "Genera codice di invito" +createWithOptions: "Genera con opzioni" +createCount: "Conteggio inviti" +inviteCodeCreated: "Inviti generati" +inviteLimitExceeded: "Hai raggiunto il numero massimo di codici invito generabili." +createLimitRemaining: "Inviti generabili: {limit} rimanenti" +inviteLimitResetCycle: "Alle {time}, il limite verrà ripristinato a {limit}" +expirationDate: "Scadenza" +noExpirationDate: "Senza scadenza" +inviteCodeUsedAt: "Codice di invito usato alle" +registeredUserUsingInviteCode: "Codice di invito usato da" +waitingForMailAuth: "In attesa della verifica email" +inviteCodeCreator: "Codice di invito creato da" +usedAt: "Usato alle" +unused: "Inutilizzato" +used: "Utilizzato" +expired: "Scaduto" +doYouAgree: "Accetti le condizioni?" +beSureToReadThisAsItIsImportant: "Si prega di leggere attentamente perché è importante." +iHaveReadXCarefullyAndAgree: "Dichiaro di aver letto attentamente \"{x}\" e accettarne le condizioni." +dialog: "Dialogo" +icon: "Ritratto" +forYou: "Per te" +currentAnnouncements: "Annunci attuali" +pastAnnouncements: "Annunci precedenti" +youHaveUnreadAnnouncements: "Ci sono Annunci non letti" +useSecurityKey: "Per utilizzare la chiave di sicurezza o la passkey, segui le indicazioni del dispositivo" +replies: "Rispondi" +renotes: "Rinota" +loadReplies: "Leggi le risposte" +loadConversation: "Leggi la conversazione" +pinnedList: "Elenco in primo piano" +keepScreenOn: "Mantieni lo schermo acceso" +verifiedLink: "Abbiamo confermato la validità di questo collegamento" +notifyNotes: "Notifica nuove Note" +unnotifyNotes: "Interrompi le notifiche di nuove Note" +_announcement: + forExistingUsers: "Solo ai profili attuali" + forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio." + needConfirmationToRead: "Richiede la conferma di lettura" + needConfirmationToReadDescription: "Sarà visualizzata una finestra di dialogo che richiede la conferma di lettura. Inoltre, non è soggetto a conferme di lettura massicce." + end: "Archivia l'annuncio" + tooManyActiveAnnouncementDescription: "L'esperienza delle persone può peggiorare se ci sono troppi annunci attivi. Considera anche l'archiviazione degli annunci conclusi." + readConfirmTitle: "Segnare come già letto?" + readConfirmText: "Hai già letto \"{title}˝?" +_initialAccountSetting: + accountCreated: "Il tuo profilo è stato creato!" + letsStartAccountSetup: "Per iniziare, impostiamo il tuo profilo." + letsFillYourProfile: "Innanzitutto, compila il tuo profilo." + profileSetting: "Impostazioni del profilo" + privacySetting: "Impostazioni sulla privacy" + theseSettingsCanEditLater: "In seguito, potrai cambiare la tua scelta." + youCanEditMoreSettingsInSettingsPageLater: "Nella pagina \"Impostazioni\", è possibile personalizzare di più il tuo profilo. Dacci un'occhiata dopo!" + followUsers: "Per comporre la tua Timeline Home (personale) segui i profili delle persone che ti interessano." + pushNotificationDescription: "Attivare le notifiche push ti permettera di ricevere informazioni sulla attività di {name} direttamente sul tuo dispositivo." + initialAccountSettingCompleted: "Hai completato la configurazione iniziale!" + haveFun: "Divertiti con {name}!" + ifYouNeedLearnMore: "Per saperne di più su come usare {name} (Misskey), visita la pagina {link}" + skipAreYouSure: "Vuoi davvero saltare la configurazione iniziale?" + laterAreYouSure: "Vuoi davvero rimandare la configurazione iniziale?" _serverRules: description: "In Europa è necessario mostrare l'informativa sul trattamento dei dati personali, prima della registrazione al servizio." +_serverSettings: + iconUrl: "URL dell'icona" + appIconDescription: "Indicare l'icona da usare quando {host} viene salvata come App." + appIconUsageExample: "Ad esempio quando si aggiunge il segnalibro alla PWA (Progressive Web App), oppure alla schermata iniziale del dispositivo mobile " + appIconStyleRecommendation: "Poiché l'icona potrebbe essere ritagliata in un quadrato o in un cerchio, si raccomanda che abbia un margine colorato." + appIconResolutionMustBe: "La risoluzione minima è {resolution}" + manifestJsonOverride: "Sostituire il file manifest.json" _accountMigration: moveFrom: "Migra un altro profilo dentro a questo" + moveFromSub: "Crea un alias verso un altro profilo remoto" moveFromLabel: "Profilo da cui migrare:" moveFromDescription: "Se desideri spostare i profili follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@istanza.it" moveTo: "Migrare questo profilo verso un un altro" moveToLabel: "Profilo verso cui migrare" + moveCannotBeUndone: "La migrazione è irreversibile, non può essere interrotta o annullata." moveAccountDescription: "Questa attività è irreversibile! Innanzitutto, assicurati di aver creato, nella istanza di destinazione, un alias con l'indirizzo di questo profilo. Successivamente, indica qui il profilo di destinazione in questo modo: @persona@istanza.it" + moveAccountHowTo: "Per migrare su un profilo remoto, crea prima un alias di questo profilo, sulla istanza di destinazione.\nDopo aver creato l'alias, inserisci l'indirizzo di destinazione, indicando ad esempio: @profilo@altra.istanza" + startMigration: "Avvia la migrazione" migrationConfirm: "Vuoi davvero migrare questo profilo su {account}? L'azione è irreversibile e non potrai più utilizzare questo profilo nel suo stato originale.\nInoltre, assicurati di aver già creato un alias sull'account a cui ti stai trasferendo." + movedAndCannotBeUndone: "Il tuo profilo è stato migrato.\nLa migrazione non può essere annullata." + postMigrationNote: "Questo profilo smetterà di seguire gli altri profili remoti a 24 ore dal termine della migrazione.\nSia i Follow che i Follower scenderanno a zero. I tuoi follower saranno comunque in grado di vedere le Note per soli follower, poiché non smetteranno di seguirti." movedTo: "Profilo verso cui migrare" _achievements: earnedAt: "Data di conseguimento" @@ -1271,6 +1403,9 @@ _achievements: title: "Brain Diver" description: "Pubblica un link a Brain Diver" flavor: "Sulle note di Brain Diver" + _smashTestNotificationButton: + title: "Prove eccessive" + description: "Hai provato le notifiche consecutivamente in un periodo di tempo molto breve" _role: new: "Nuovo ruolo" edit: "Modifica ruolo" @@ -1310,8 +1445,12 @@ _role: ltlAvailable: "Disponibilità della Timeline Locale" canPublicNote: "Può scrivere Note con Visibilità Pubblica" canInvite: "Genera codici di invito all'istanza" + inviteLimit: "Limite di codici invito" + inviteLimitCycle: "Intervallo di emissione del codice di invito" + inviteExpirationTime: "Scadenza del codice di invito" canManageCustomEmojis: "Gestire le emoji personalizzate" driveCapacity: "Capienza del Drive" + alwaysMarkNsfw: "Imposta sempre come NSFW" pinMax: "Quantità massima di Note in primo piano" antennaMax: "Quantità massima di Antenne" wordMuteMax: "Lunghezza massima del filtro parole" @@ -1327,10 +1466,10 @@ _role: _condition: isLocal: "Profilo locale" isRemote: "Profilo remoto" - createdLessThan: "Creato meno di" - createdMoreThan: "Creato più di" - followersLessThanOrEq: "Ha meno di N follower" - followersMoreThanOrEq: "Ha più di N follower" + createdLessThan: "Profilo creato da meno di N" + createdMoreThan: "Profilo creato da più di N" + followersLessThanOrEq: "Profilo con N follower o meno" + followersMoreThanOrEq: "Profilo con N follower o più" followingLessThanOrEq: "Segue N profili o meno" followingMoreThanOrEq: "Segue N profili o più" notesLessThanOrEq: "Conteggio Note inferiore o uguale a" @@ -1349,11 +1488,11 @@ _sensitiveMediaDetection: _emailUnavailable: used: "Email già in uso" format: "Formato email non valido" - disposable: "Email non riutilizzabile" + disposable: "Indirizzo email non utilizzabile" mx: "Server email non corretto" smtp: "Il server email non risponde" _ffVisibility: - public: "Pubblico" + public: "Pubblica" followers: "Mostra solo ai follower" private: "Invisibile" _signup: @@ -1371,6 +1510,7 @@ _ad: back: "Indietro" reduceFrequencyOfThisAd: "Visualizza questa pubblicità meno spesso" hide: "Nascondi" + timezoneinfo: "Il giorno della settimana è determinato in base al fuso orario del server." _forgotPassword: enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo." ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza." @@ -1390,11 +1530,11 @@ _plugin: installWarn: "Si prega di installare soltanto estensioni che provengono da fonti affidabili." manage: "Gestisci estensioni" _preferencesBackups: - list: "I backup creati." + list: "Elenco di impostazioni salvate in precedenza" saveNew: "Nuovo salvataggio" - loadFile: "Importa file" + loadFile: "Carica da file" apply: "Applicabile a questo dispositivo" - save: "Sovrascrivi il file di salvataggio" + save: "Sovrascrivi il backup" inputName: "Inserire il nome del backup." cannotSave: "Impossibile salvare." nameAlreadyExists: "Il nome del backup \"{name}\" esiste già. Si prega di specificare un nome diverso." @@ -1422,10 +1562,10 @@ _aboutMisskey: donate: "Sostieni Misskey" morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰" patrons: "Sostenitori" -_nsfw: - respect: "Nascondere i media segnati come sensibli" - ignore: "Visualizzare i media segnati come sensibili" - force: "Nascondere tutti i media" +_displayOfSensitiveMedia: + respect: "Nascondere i media sensibili" + ignore: "Non nascondere i media sensibili" + force: "Nascondi tutti i media" _instanceTicker: none: "Nascondi" remote: "Mostra solo per i profili remoti" @@ -1563,21 +1703,30 @@ _time: minute: "min" hour: "ore" day: "giorni" +_timelineTutorial: + title: "Come usare Misskey" + step1_1: "Questa è la \"Timeline\". tutte le \"Note\" pubblicate su {name} vengono elencate in ordine cronologico." + step1_2: "Le Timeline sono diverse, ad esempio, la \"Home\" elenca le Note dei profili che segui. Quella \"Locale\" elenca quelle di tutti i profili attivi su {name}." + step2_1: "Prova a pubblicare una Nota. Semplicemente premendo il bottone con l'icona di una matita." + step2_2: "Potresti scrivere la tua presentazione, oppure semplicemente \"Ciao da {name}!\"" + step3_1: "Hai pubblicato qualcosa?" + step3_2: "In tal caso, dovrebbe comparire subito nella tua \"Home\"" + step4_1: "Puoi reagire con un emoji alle Note." + step4_2: "To attach a reaction, press the \"+\" mark on a note and choose an emoji you'd like to react with.\nPer reagire con una emoji, premi il bottone \"+\" (più) visibile vicino ad ogni Nota e scegli dall'elenco la emoji che rappresenta la tua reazione." _2fa: alreadyRegistered: "La configurazione è stata già completata." 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: " + step2Uri: "Inserisci il seguente URL se desideri utilizzare una App per PC" step3Title: "Inserisci il codice di verifica" step3: "Inserite il token visualizzato nell'app e il gioco è fatto." + setupCompleted: "Impostazione completata" 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." @@ -1588,6 +1737,11 @@ _2fa: renewTOTPConfirm: "I codici di verifica nelle app di autenticazione esistenti smetteranno di funzionare" renewTOTPOk: "Ripristina" renewTOTPCancel: "No grazie" + checkBackupCodesBeforeCloseThisWizard: "Prima di chiudere questa procedura guidata, salva i tuoi codici usa-e-getta in un posto sicuro." + backupCodes: "Codici usa-e-getta" + backupCodesDescription: "Puoi usare questi codici usa-e-getta per ottenere l'accesso al tuo profilo in caso sia impossibile usare l'App col codice OTP. Salvali in un posto sicuro." + backupCodeUsedWarning: "È stato usato un codice usa-e-getta. Per favore, riconfigura l'autenticazione a due fattori il prima possibile, nel caso la configurazione precedente abbia smesso di funzionare." + backupCodesExhaustedWarning: "Hai esaurito i codici usa-e-getta. Se l'App che genera il codice OTP non è più disponibile, non potrai più accedere al tuo profilo. Ripeti la configurazione per l'autenticazione a due fattori." _permissions: "read:account": "Visualizza le informazioni sul profilo" "write:account": "Modifica le informazioni sul profilo" @@ -1621,6 +1775,10 @@ _permissions: "write:gallery": "Gestione della galleria" "read:gallery-likes": "Visualizza i contenuti della galleria." "write:gallery-likes": "Manipolazione dei \"Mi piace\" della galleria." + "read:flash": "Visualizza Play" + "write:flash": "Modifica Play" + "read:flash-likes": "Visualizza lista di Play piaciuti" + "write:flash-likes": "Modifica lista di Play piaciuti" _auth: shareAccessTitle: "Permessi dell'applicazione" shareAccess: "Vuoi autorizzare {name} ad accedere al tuo profilo?" @@ -1710,7 +1868,7 @@ _visibility: followersDescription: "Visibile solo ai tuoi follower" specified: "Nota diretta" specifiedDescription: "Visibile solo ai profili menzionati" - disableFederation: "Federazione disabilitata" + disableFederation: "Non federare" disableFederationDescription: "Non spedire attività alle altre istanze remote" _postForm: replyPlaceholder: "Rispondi a questa nota..." @@ -1735,6 +1893,7 @@ _profile: metadataContent: "Contenuto" changeAvatar: "Modifica immagine profilo" changeBanner: "Cambia intestazione" + verifiedLinkDescription: "Puoi verificare il tuo profilo mostrando una icona. Devi inserire la URL alla pagina che contiene un link al tuo profilo." _exportOrImport: allNotes: "Tutte le note" favoritedNotes: "Note preferite" @@ -1853,9 +2012,14 @@ _notification: youReceivedFollowRequest: "Hai ricevuto una richiesta di follow" yourFollowRequestAccepted: "La tua richiesta di follow è stata accettata" pollEnded: "Risultati del sondaggio." + newNote: "Nuove Note" unreadAntennaNote: "Antenna {name}" emptyPushNotificationMessage: "Le notifiche push sono state aggiornate." achievementEarned: "Obiettivo raggiunto" + testNotification: "Prova la notifica" + checkNotificationBehavior: "Prova il comportamento della notifica" + sendTestNotification: "Spedisci una notifica di prova" + notificationWillBeDisplayedLikeThis: "La notifica apparirà così" _types: all: "Tutto" follow: "Novità follower" @@ -1877,7 +2041,7 @@ _deck: alwaysShowMainColumn: "Mostra sempre la colonna principale" columnAlign: "Allineare colonne" addColumn: "Aggiungi colonna" - configureColumn: "Impostazioni della colonna." + configureColumn: "Impostazioni colonna" swapLeft: "Sposta a sinistra" swapRight: "Sposta a destra" swapUp: "Sposta in alto" @@ -1890,6 +2054,9 @@ _deck: introduction: "Combinate le colonne per creare la vostra interfaccia!" introduction2: "È possibile aggiungere colonne in qualsiasi momento premendo + sulla destra dello schermo." widgetsIntroduction: "Dal menu della colonna, selezionare \"Modifica i riquadri\" per aggiungere un un riquadro con funzionalità" + useSimpleUiForNonRootPages: "Visualizza sotto pagine con interfaccia web semplice" + usedAsMinWidthWhenFlexible: "Se \"larghezza flessibile\" è abilitato, questa diventa la larghezza minima" + flexible: "Larghezza flessibile" _columns: main: "Principale" widgets: "Riquadri" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index fcba3fb822..0869c0c455 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -15,7 +15,7 @@ gotIt: "わかった" cancel: "キャンセル" noThankYou: "やめておく" enterUsername: "ユーザー名を入力" -renotedBy: "{user}がRenote" +renotedBy: "{user}がリノート" noNotes: "ノートはありません" noNotifications: "通知はありません" instance: "サーバー" @@ -45,15 +45,20 @@ pin: "ピン留め" unpin: "ピン留め解除" copyContent: "内容をコピー" copyLink: "リンクをコピー" +copyLinkRenote: "リノートのリンクをコピー" delete: "削除" deleteAndEdit: "削除して編集" -deleteAndEditConfirm: "このノートを削除してもう一度編集しますか?このノートへのリアクション、Renote、返信も全て削除されます。" +deleteAndEditConfirm: "このノートを削除してもう一度編集しますか?このノートへのリアクション、リノート、返信も全て削除されます。" addToList: "リストに追加" +addToAntenna: "アンテナに追加" sendMessage: "メッセージを送信" copyRSS: "RSSをコピー" copyUsername: "ユーザー名をコピー" copyUserId: "ユーザーIDをコピー" copyNoteId: "ノートIDをコピー" +copyFileId: "ファイルIDをコピー" +copyFolderId: "フォルダーIDをコピー" +copyProfileUrl: "プロフィールURLをコピー" searchUser: "ユーザーを検索" reply: "返信" loadMore: "もっと見る" @@ -70,7 +75,7 @@ import: "インポート" export: "エクスポート" files: "ファイル" download: "ダウンロード" -driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを使用した全てのコンテンツからも削除されます。" +driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを使用した一部のコンテンツも削除されます。" unfollowConfirm: "{name}のフォローを解除しますか?" exportRequested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。" importRequested: "インポートをリクエストしました。これには時間がかかる場合があります。" @@ -100,19 +105,19 @@ followRequests: "フォロー申請" unfollow: "フォロー解除" followRequestPending: "フォロー許可待ち" enterEmoji: "絵文字を入力" -renote: "Renote" -unrenote: "Renote解除" -renoted: "Renoteしました。" -cantRenote: "この投稿はRenoteできません。" -cantReRenote: "RenoteをRenoteすることはできません。" +renote: "リノート" +unrenote: "リノート解除" +renoted: "リノートしました。" +cantRenote: "この投稿はリノートできません。" +cantReRenote: "リノートをリノートすることはできません。" quote: "引用" -inChannelRenote: "チャンネル内Renote" +inChannelRenote: "チャンネル内リノート" inChannelQuote: "チャンネル内引用" pinnedNote: "ピン留めされたノート" pinned: "ピン留め" you: "あなた" clickToShow: "クリックして表示" -sensitive: "閲覧注意" +sensitive: "センシティブ" add: "追加" reaction: "リアクション" reactions: "リアクション" @@ -120,8 +125,8 @@ reactionSetting: "ピッカーに表示するリアクション" reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。" rememberNoteVisibility: "公開範囲を記憶する" attachCancel: "添付取り消し" -markAsSensitive: "閲覧注意にする" -unmarkAsSensitive: "閲覧注意を解除する" +markAsSensitive: "センシティブとして設定" +unmarkAsSensitive: "センシティブを解除する" enterFileName: "ファイル名を入力" mute: "ミュート" unmute: "ミュート解除" @@ -136,8 +141,10 @@ unblockConfirm: "ブロック解除しますか?" suspendConfirm: "凍結しますか?" unsuspendConfirm: "解凍しますか?" selectList: "リストを選択" +editList: "リストを編集" selectChannel: "チャンネルを選択" selectAntenna: "アンテナを選択" +editAntenna: "アンテナを編集" selectWidget: "ウィジェットを選択" editWidgets: "ウィジェットを編集" editWidgetsExit: "編集を終了" @@ -149,7 +156,10 @@ emojiUrl: "絵文字画像URL" addEmoji: "絵文字を追加" settingGuide: "おすすめ設定" cacheRemoteFiles: "リモートのファイルをキャッシュする" -cacheRemoteFilesDescription: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。サーバーのストレージを節約できますが、サムネイルが生成されないので通信量が増加します。" +cacheRemoteFilesDescription: "この設定を有効にすると、リモートファイルをこのサーバーのストレージにキャッシュするようになります。画像の表示が高速になりますが、サーバーのストレージを多く消費します。リモートユーザーがどれほどキャッシュを保持するかは、ロールによるドライブ容量制限によって決定されます。この制限を超えた場合、古いファイルからキャッシュが削除されリンクになります。この設定が無効の場合、リモートのファイルを最初からリンクとして保持しますが、画像のサムネイル生成やユーザーのプライバシー保護のために、default.ymlでproxyRemoteFilesをtrueにすることをお勧めします。" +youCanCleanRemoteFilesCache: "ファイル管理の🗑️ボタンで全てのキャッシュを削除できます。" +cacheRemoteSensitiveFiles: "リモートのセンシティブなファイルをキャッシュする" +cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになります。" flagAsBot: "Botとして設定" flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。" flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!" @@ -311,7 +321,7 @@ copyUrl: "URLをコピー" rename: "名前を変更" avatar: "アイコン" banner: "バナー" -nsfw: "閲覧注意" +displayOfSensitiveMedia: "センシティブなメディアの表示" whenServerDisconnected: "サーバーとの接続が失われたとき" disconnectedFromServer: "サーバーから切断されました" reload: "リロード" @@ -321,7 +331,7 @@ watch: "ウォッチ" unwatch: "ウォッチ解除" accept: "許可" reject: "拒否" -normal: "正常" +normal: "通常" instanceName: "サーバー名" instanceDescription: "サーバーの紹介" maintainerName: "管理者の名前" @@ -346,7 +356,6 @@ invite: "招待" driveCapacityPerLocalAccount: "ローカルユーザーひとりあたりのドライブ容量" driveCapacityPerRemoteAccount: "リモートユーザーひとりあたりのドライブ容量" inMb: "メガバイト単位" -iconUrl: "アイコン画像のURL (faviconなど)" bannerUrl: "バナー画像のURL" backgroundImageUrl: "背景画像のURL" basicInfo: "基本情報" @@ -402,10 +411,13 @@ aboutMisskey: "Misskeyについて" administrator: "管理者" token: "確認コード" 2fa: "二要素認証" +setupOf2fa: "二要素認証のセットアップ" totp: "認証アプリ" totpDescription: "認証アプリを使ってワンタイムパスワードを入力" moderator: "モデレーター" moderation: "モデレーション" +moderationNote: "モデレーションノート" +addModerationNote: "モデレーションノートを追加する" nUsersMentioned: "{n}人が投稿" securityKeyAndPasskey: "セキュリティキー・パスキー" securityKey: "セキュリティキー" @@ -645,6 +657,7 @@ behavior: "動作" sample: "サンプル" abuseReports: "通報" reportAbuse: "通報" +reportAbuseRenote: "リノートを通報" reportAbuseOf: "{name}を通報する" fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。" abuseReported: "内容が送信されました。ご報告ありがとうございました。" @@ -672,14 +685,15 @@ createNewClip: "新しいクリップを作成" unclip: "クリップ解除" confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれています。ノートをこのクリップから除外しますか?" public: "パブリック" +private: "非公開" i18nInfo: "Misskeyは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。" manageAccessTokens: "アクセストークンの管理" accountInfo: "アカウント情報" notesCount: "ノートの数" repliesCount: "返信した数" -renotesCount: "Renoteした数" +renotesCount: "リノートした数" repliedCount: "返信された数" -renotedCount: "Renoteされた数" +renotedCount: "リノートされた数" followingCount: "フォロー数" followersCount: "フォロワー数" sentReactionsCount: "リアクションした数" @@ -693,9 +707,10 @@ driveUsage: "ドライブ使用量" noCrawle: "クローラーによるインデックスを拒否" noCrawleDescription: "外部の検索エンジンにあなたのユーザーページ、ノート、Pagesなどのコンテンツを登録(インデックス)しないよう要求します。" lockedAccountInfo: "フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。" -alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にする" +alwaysMarkSensitive: "デフォルトでメディアをセンシティブ設定にする" loadRawImages: "添付画像のサムネイルをオリジナル画質にする" disableShowingAnimatedImages: "アニメーション画像を再生しない" +highlightSensitiveMedia: "メディアがセンシティブであることを分かりやすく表示" verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。" notSet: "未設定" emailVerified: "メールアドレスが確認されました" @@ -920,8 +935,8 @@ cannotUploadBecauseInappropriate: "不適切な内容を含む可能性がある cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。" cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。" beta: "ベータ" -enableAutoSensitive: "自動NSFW判定" -enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにNSFWフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。" +enableAutoSensitive: "自動センシティブ判定" +enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにセンシティブフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。" activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。" navbar: "ナビゲーションバー" shuffle: "シャッフル" @@ -975,7 +990,7 @@ thisPostMayBeAnnoying: "この投稿は迷惑になる可能性があります thisPostMayBeAnnoyingHome: "ホームに投稿" thisPostMayBeAnnoyingCancel: "やめる" thisPostMayBeAnnoyingIgnore: "このまま投稿" -collapseRenotes: "見たことのあるRenoteを省略して表示" +collapseRenotes: "見たことのあるリノートを省略して表示" internalServerError: "サーバー内部エラー" internalServerErrorDescription: "サーバー内部で予期しないエラーが発生しました。" copyErrorInfo: "エラー情報をコピー" @@ -1010,7 +1025,7 @@ retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大するこ enableChartsForRemoteUser: "リモートユーザーのチャートを生成" enableChartsForFederatedInstances: "リモートサーバーのチャートを生成" showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" -largeNoteReactions: "ノートのリアクションを大きく表示" +reactionsDisplaySize: "リアクションの表示サイズ" noteIdOrUrl: "ノートIDまたはURL" video: "動画" videos: "動画" @@ -1023,7 +1038,7 @@ forceShowAds: "常に広告を表示する" addMemo: "メモを追加" editMemo: "メモを編集" reactionsList: "リアクション一覧" -renotesList: "Renote一覧" +renotesList: "リノート一覧" notificationDisplay: "通知の表示" leftTop: "左上" rightTop: "右上" @@ -1034,7 +1049,7 @@ vertical: "縦" horizontal: "横" position: "位置" serverRules: "サーバールール" -pleaseConfirmBelowBeforeSignup: "このサーバーに登録する前に、以下を確認してください。" +pleaseConfirmBelowBeforeSignup: "このサーバーに登録するには、以下の内容を確認し同意する必要があります。" pleaseAgreeAllToContinue: "続けるには、全ての「同意する」にチェックが入っている必要があります。" continue: "続ける" preservedUsernames: "予約ユーザー名" @@ -1062,6 +1077,58 @@ later: "あとで" goToMisskey: "Misskeyへ" additionalEmojiDictionary: "絵文字の追加辞書" installed: "インストール済み" +branding: "ブランディング" +enableServerMachineStats: "サーバーのマシン情報を公開する" +enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする" +turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。" +createInviteCode: "招待コードを作成" +createWithOptions: "オプションを指定して作成" +createCount: "作成数" +inviteCodeCreated: "招待コードを作成しました" +inviteLimitExceeded: "作成できる招待コードの数が上限に達しています。" +createLimitRemaining: "作成できる招待コード: 残り {limit} 個" +inviteLimitResetCycle: "{time}で最大 {limit} 個の招待コードを作成できます。" +expirationDate: "有効期限" +noExpirationDate: "有効期限を設けない" +inviteCodeUsedAt: "招待コードが使用された日時" +registeredUserUsingInviteCode: "招待コードを使用したユーザー" +waitingForMailAuth: "メール認証待ち" +inviteCodeCreator: "招待コードを作成したユーザー" +usedAt: "使用日時" +unused: "未使用" +used: "使用済み" +expired: "期限切れ" +doYouAgree: "同意しますか?" +beSureToReadThisAsItIsImportant: "重要ですので必ずお読みください。" +iHaveReadXCarefullyAndAgree: "「{x}」の内容をよく読み、同意します。" +dialog: "ダイアログ" +icon: "アイコン" +forYou: "あなたへ" +currentAnnouncements: "現在のお知らせ" +pastAnnouncements: "過去のお知らせ" +youHaveUnreadAnnouncements: "未読のお知らせがあります。" +useSecurityKey: "ブラウザまたはデバイスの指示に従って、セキュリティキーまたはパスキーを使用してください。" +replies: "返信" +renotes: "リノート" +loadReplies: "返信を見る" +loadConversation: "会話を見る" +pinnedList: "ピン留めされたリスト" +keepScreenOn: "デバイスの画面を常にオンにする" +verifiedLink: "このリンク先の所有者であることが確認されました" +notifyNotes: "投稿を通知" +unnotifyNotes: "投稿の通知を解除" +authentication: "認証" +authenticationRequiredToContinue: "続けるには認証を行ってください" + +_announcement: + forExistingUsers: "既存ユーザーのみ" + forExistingUsersDescription: "有効にすると、このお知らせ作成時点で存在するユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。" + needConfirmationToRead: "既読にするのに確認が必要" + needConfirmationToReadDescription: "有効にすると、このお知らせを既読にする際に確認ダイアログが表示されます。また、一括既読操作の対象になりません。" + end: "お知らせを終了" + tooManyActiveAnnouncementDescription: "アクティブなお知らせが多いため、UXが低下する可能性があります。終了したお知らせはアーカイブすることを検討してください。" + readConfirmTitle: "既読にしますか?" + readConfirmText: "「{title}」の内容を読み、既読にします。" _initialAccountSetting: accountCreated: "アカウントの作成が完了しました!" @@ -1082,6 +1149,16 @@ _initialAccountSetting: _serverRules: description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。" +_serverSettings: + iconUrl: "アイコン画像のURL" + appIconDescription: "{host}がアプリとして表示される際のアイコンを指定します。" + appIconUsageExample: "例: PWAや、スマートフォンのホーム画面にブックマークとして追加された時など" + appIconStyleRecommendation: "円形もしくは角丸にクロップされる場合があるため、塗り潰された余白のある背景を持つことが推奨されます。" + appIconResolutionMustBe: "解像度は必ず{resolution}である必要があります。" + manifestJsonOverride: "manifest.jsonのオーバーライド" + shortName: "略称" + shortNameDescription: "サーバーの正式名称が長い場合に、代わりに表示することのできる略称や通称。" + _accountMigration: moveFrom: "別のアカウントからこのアカウントに移行" moveFromSub: "別のアカウントへエイリアスを作成" @@ -1337,6 +1414,9 @@ _achievements: title: "Brain Diver" description: "Brain Diverへのリンクを投稿した" flavor: "Misskey-Misskey La-Tu-Ma" + _smashTestNotificationButton: + title: "テスト過剰" + description: "通知のテストをごく短時間のうちに連続して行った" _role: new: "ロールの作成" @@ -1351,8 +1431,8 @@ _role: conditional: "コンディショナル" condition: "条件" isConditionalRole: "これはコンディショナルロールです。" - isPublic: "ロールを公開" - descriptionOfIsPublic: "ロールにアサインされたユーザーを誰でも見ることができます。また、ユーザーのプロフィールでこのロールが表示されます。" + isPublic: "公開ロール" + descriptionOfIsPublic: "ユーザーのプロフィールでこのロールが表示されます。" options: "オプション" policies: "ポリシー" baseRole: "ベースロール" @@ -1361,8 +1441,8 @@ _role: iconUrl: "アイコン画像のURL" asBadge: "バッジとして表示" descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。" - isExplorable: "ロールタイムラインを公開" - descriptionOfIsExplorable: "オンにすると、ロールのタイムラインを公開します。ロールの公開がオフの場合、タイムラインの公開はされません。" + isExplorable: "ユーザーを見つけやすくする" + descriptionOfIsExplorable: "オンにすると、「みつける」でメンバー一覧が公開されるほか、ロールのタイムラインが利用可能になります。" displayOrder: "表示順" descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。" canEditMembersByModerator: "モデレーターのメンバー編集を許可" @@ -1377,6 +1457,9 @@ _role: ltlAvailable: "ローカルタイムラインの閲覧" canPublicNote: "パブリック投稿の許可" canInvite: "サーバー招待コードの発行" + inviteLimit: "招待コードの作成可能数" + inviteLimitCycle: "招待コードの発行間隔" + inviteExpirationTime: "招待コードの有効期限" canManageCustomEmojis: "カスタム絵文字の管理" driveCapacity: "ドライブ容量" alwaysMarkNsfw: "ファイルにNSFWを常に付与" @@ -1411,7 +1494,7 @@ _sensitiveMediaDetection: description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。" sensitivity: "検出感度" sensitivityDescription: "感度を低くすると、誤検知(偽陽性)が減ります。感度を高くすると、検知漏れ(偽陰性)が減ります。" - setSensitiveFlagAutomatically: "NSFWフラグを設定する" + setSensitiveFlagAutomatically: "センシティブフラグを設定する" setSensitiveFlagAutomaticallyDescription: "この設定をオフにしても内部的に判定結果は保持されます。" analyzeVideos: "動画の解析を有効化" analyzeVideosDescription: "静止画に加えて動画も解析するようにします。サーバーの負荷が少し増えます。" @@ -1445,6 +1528,7 @@ _ad: back: "戻る" reduceFrequencyOfThisAd: "この広告の表示頻度を下げる" hide: "表示しない" + timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されます。" _forgotPassword: enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。" @@ -1467,6 +1551,7 @@ _plugin: install: "プラグインのインストール" installWarn: "信頼できないプラグインはインストールしないでください。" manage: "プラグインの管理" + viewSource: "ソースを表示" _preferencesBackups: list: "作成したバックアップ" @@ -1504,9 +1589,9 @@ _aboutMisskey: morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰" patrons: "支援者" -_nsfw: - respect: "閲覧注意のメディアは隠す" - ignore: "閲覧注意のメディアを隠さない" +_displayOfSensitiveMedia: + respect: "センシティブ設定されたメディアを隠す" + ignore: "センシティブ設定されたメディアを隠さない" force: "常にメディアを隠す" _instanceTicker: @@ -1671,18 +1756,17 @@ _timelineTutorial: _2fa: alreadyRegistered: "既に設定は完了しています。" registerTOTP: "認証アプリの設定を開始" - passwordToTOTP: "パスワードを入力してください" step1: "まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。" step2: "次に、表示されているQRコードをアプリでスキャンします。" step2Click: "QRコードをクリックすると、お使いの端末にインストールされている認証アプリやキーリングに登録できます。" - step2Url: "デスクトップアプリでは次のURIを入力します:" + step2Uri: "デスクトップアプリを使用する場合は次のURIを入力します" step3Title: "確認コードを入力" - step3: "アプリに表示されている確認コード(トークン)を入力して完了です。" - step4: "これからログインするときも、同じように確認コードを入力します。" + step3: "アプリに表示されている確認コード(トークン)を入力します。" + setupCompleted: "設定が完了しました" + step4: "これからログインするときも、同じようにコードを入力します。" securityKeyNotSupported: "お使いのブラウザはセキュリティキーに対応していません。" registerTOTPBeforeKey: "セキュリティキー・パスキーを登録するには、まず認証アプリの設定を行なってください。" securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキー、端末の生体認証やPINロック、パスキーといった、WebAuthn由来の鍵を登録します。" - chromePasskeyNotSupported: "Chromeのパスキーは現在サポートしていません。" registerSecurityKey: "セキュリティキー・パスキーを登録する" securityKeyName: "キーの名前を入力" tapSecurityKey: "ブラウザの指示に従い、セキュリティキーやパスキーを登録してください" @@ -1690,9 +1774,14 @@ _2fa: removeKeyConfirm: "{name}を削除しますか?" whyTOTPOnlyRenew: "セキュリティキーが登録されている場合、認証アプリの設定は解除できません。" renewTOTP: "認証アプリを再設定" - renewTOTPConfirm: "今までの認証アプリの確認コードは使用できなくなります" + renewTOTPConfirm: "今までの認証アプリの確認コードおよびバックアップコードは使用できなくなります" renewTOTPOk: "再設定する" renewTOTPCancel: "やめておく" + checkBackupCodesBeforeCloseThisWizard: "このウィザードを閉じる前に、以下のバックアップコードを確認してください。" + backupCodes: "バックアップコード" + backupCodesDescription: "認証アプリが使用できなくなった場合、以下のバックアップコードを使ってアカウントにアクセスできます。これらのコードは必ず安全な場所に保管してください。各コードは一回だけ使用できます。" + backupCodeUsedWarning: "バックアップコードが使用されました。認証アプリが使えなくなっている場合、なるべく早く認証アプリを再設定してください。" + backupCodesExhaustedWarning: "バックアップコードが全て使用されました。認証アプリを利用できない場合、これ以上アカウントにアクセスできなくなります。認証アプリを再登録してください。" _permissions: "read:account": "アカウントの情報を見る" @@ -1727,6 +1816,10 @@ _permissions: "write:gallery": "ギャラリーを操作する" "read:gallery-likes": "ギャラリーのいいねを見る" "write:gallery-likes": "ギャラリーのいいねを操作する" + "read:flash": "Playを見る" + "write:flash": "Playを操作する" + "read:flash-likes": "Playのいいねを見る" + "write:flash-likes": "Playのいいねを操作する" _auth: shareAccessTitle: "アプリへのアクセス許可" @@ -1744,6 +1837,7 @@ _antennaSources: homeTimeline: "フォローしているユーザーのノート" users: "指定した一人または複数のユーザーのノート" userList: "指定したリストのユーザーのノート" + userBlacklist: "指定した一人または複数のユーザーを除いた全てのノート" _weekday: sunday: "日曜日" @@ -1850,6 +1944,7 @@ _profile: metadataContent: "内容" changeAvatar: "アイコン画像を変更" changeBanner: "バナー画像を変更" + verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。" _exportOrImport: allNotes: "全てのノート" @@ -1977,12 +2072,18 @@ _notification: youReceivedFollowRequest: "フォローリクエストが来ました" yourFollowRequestAccepted: "フォローリクエストが承認されました" pollEnded: "アンケートの結果が出ました" + newNote: "新しい投稿" unreadAntennaNote: "アンテナ {name}" emptyPushNotificationMessage: "プッシュ通知の更新をしました" achievementEarned: "実績を獲得" + testNotification: "通知テスト" + checkNotificationBehavior: "通知の表示を確かめる" + sendTestNotification: "テスト通知を送信する" + notificationWillBeDisplayedLikeThis: "通知はこのように表示されます" _types: all: "すべて" + note: "ユーザーの新規投稿" follow: "フォロー" mention: "メンション" reply: "リプライ" @@ -2017,6 +2118,9 @@ _deck: introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!" introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。" widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください" + useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示" + usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となります" + flexible: "幅を自動調整" _columns: main: "メイン" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 652814ca98..d5d414ea77 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -1,7 +1,7 @@ --- _lang_: "日本語 (関西弁)" headlineMisskey: "ノートでつながるネットワーク" -introMisskey: "ようお越し!Misskeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ノート」を作って、いま起こっとることを共有したり、あんたについて皆に発信しよう📡\n「ツッコミ」機能で、皆のノートに素早く反応を追加したりもできるで✌\nほな新しい世界を探検しよか🚀" +introMisskey: "ようお越し!Misskeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ノート」を作って、いま起こっとることを共有したり、あんたについて皆に発信しよう📡\n「ツッコミ」機能で、皆のノートに素早く反応を追加したりもできるで✌\nほな、新しい世界を探検しよか🚀" poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォームMisskeyのサーバーのひとつなんやで。" monthAndDay: "{month}月 {day}日" search: "探す" @@ -49,11 +49,15 @@ delete: "ほかす" deleteAndEdit: "ほかして直す" deleteAndEditConfirm: "このノートをほかしてもっかい直す?このノートへのツッコミ、Renote、返信も全部消えるんやけどそれでもええん?" addToList: "リストに入れたる" +addToAntenna: "アンテナに追加" sendMessage: "メッセージを送る" copyRSS: "RSSをコピー" copyUsername: "ユーザー名をコピー" copyUserId: "ユーザーIDをコピー" copyNoteId: "ノートIDをコピー" +copyFileId: "ファイルIDをコピー" +copyFolderId: "フォルダーIDをコピー" +copyProfileUrl: "プロフィールURLをコピー" searchUser: "ユーザーを検索" reply: "返事" loadMore: "まだまだあるで!" @@ -72,7 +76,7 @@ files: "ファイル" download: "ダウンロード" driveFileDeleteConfirm: "ファイル「{name}」をほかしてええか?このファイルを添付したノートも消えてまうで。" unfollowConfirm: "{name}のフォローを解除してもええんか?" -exportRequested: "エクスポートしてな、ってリクエストしたけど、これ多分めっちゃ時間かかるで。エクスポート終わったら「ドライブ」に突っ込んどくで。" +exportRequested: "エクスポートしてな、って言うたけど、これ多分めっちゃ時間かかるで。エクスポート終わったら「ドライブ」に突っ込んどくで。" importRequested: "インポートしてな、ってリクエストしたけど、これ多分めっちゃ時間かかるで。" lists: "リスト" noLists: "リストなんてあらへんで" @@ -136,8 +140,10 @@ unblockConfirm: "ブロックやめたるってほんまか?" suspendConfirm: "凍結してしもうてええか?" unsuspendConfirm: "解凍するけどええか?" selectList: "リストを選ぶ" +editList: "リスト直すで" selectChannel: "チャンネルを選ぶ" selectAntenna: "アンテナを選ぶ" +editAntenna: "アンテナを編集" selectWidget: "ウィジェットを選ぶ" editWidgets: "ウィジェットをいじる" editWidgetsExit: "編集終ったで" @@ -150,6 +156,9 @@ addEmoji: "絵文字を追加" settingGuide: "ええ感じの設定" cacheRemoteFiles: "リモートのファイルをキャッシュする" cacheRemoteFilesDescription: "この設定を切っとったら、リモートファイルをキャッシュせんと直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルを作らんなるから通信量が増えるで。" +youCanCleanRemoteFilesCache: "ファイル管理にある🗑️ボタンでキャッシュ全部ほかすで。" +cacheRemoteSensitiveFiles: "リモートのセンシティブなファイルをキャッシュする" +cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになるで。" flagAsBot: "Botにするで" flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖せんように開発者が使うたり、Misskeyのシステム上での扱いがBotに合ったもんになるからな。" flagAsCat: "Catやで" @@ -311,7 +320,7 @@ copyUrl: "URLをコピー" rename: "名前を変えるで" avatar: "アイコン" banner: "バナー" -nsfw: "見るんは気いつけてな" +displayOfSensitiveMedia: "センシティブなメディアの表示" whenServerDisconnected: "サーバーとの接続が失くなってしもうたとき" disconnectedFromServer: "サーバーが機嫌悪いねん" reload: "リロード" @@ -346,7 +355,6 @@ invite: "来てや" driveCapacityPerLocalAccount: "ローカルユーザーはんひとりあたりのドライブ容量" driveCapacityPerRemoteAccount: "リモートユーザーはんひとりあたりのドライブ容量" inMb: "メガバイト単位" -iconUrl: "アイコン画像のURL" bannerUrl: "バナー画像のURL" backgroundImageUrl: "背景画像のURL" basicInfo: "基本情報" @@ -406,6 +414,8 @@ totp: "認証アプリ" totpDescription: "認証アプリ使うてワンタイムパスワードを入れる" moderator: "モデレーター" moderation: "モデレーション" +moderationNote: "モデレーションノート" +addModerationNote: "モデレーションノートを追加するで" nUsersMentioned: "{n}人が投稿" securityKeyAndPasskey: "セキュリティキー・パスキー" securityKey: "セキュリティキー" @@ -672,6 +682,7 @@ createNewClip: "新しいクリップを作るで" unclip: "クリップ解除するで" confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれとるで。ノートをこのクリップから除外しよか?" public: "パブリック" +private: "非公開" i18nInfo: "Misskeyは有志によっていろんな言語に翻訳されとるで。{link}で翻訳に協力したってやー。" manageAccessTokens: "アクセストークンの管理" accountInfo: "アカウント情報" @@ -691,7 +702,7 @@ no: "あかん" driveFilesCount: "ドライブのファイル数" driveUsage: "ドライブ使用量やで" noCrawle: "クローラーによるインデックスを拒否するで" -noCrawleDescription: "検索エンジンにあんたのユーザーページ、ノート、Pagesとかのコンテンツを登録(インデックス)せぇへんように頼むで。" +noCrawleDescription: "検索エンジンにあんたのユーザーページ、ノート、Pagesとかのコンテンツを登録(インデックス)せんように頼むで。邪魔すんねんやったら帰って〜。" lockedAccountInfo: "フォローを承認制にしとっても、ノートの公開範囲を「フォロワー」にせぇへん限り、誰でもあんたのノートを見れるで。" alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にするで" loadRawImages: "添付画像のサムネイルをオリジナル画質にするで" @@ -737,7 +748,7 @@ value: "値" createdAt: "作成した日" updatedAt: "更新日時" saveConfirm: "保存するで?" -deleteConfirm: "ホンマに削除するで?" +deleteConfirm: "ホンマにほかすで?" invalidValue: "有効な値じゃないみたいやで。" registry: "レジストリ" closeAccount: "アカウントを閉鎖する" @@ -792,6 +803,7 @@ noMaintainerInformationWarning: "管理者情報が設定されてへんで" noBotProtectionWarning: "Botプロテクションが設定されてへんで。" configure: "設定する" postToGallery: "ギャラリーへ投稿" +postToHashtag: "このハッシュタグで投稿" gallery: "ギャラリー" recentPosts: "最近の投稿" popularPosts: "人気の投稿" @@ -825,6 +837,7 @@ translatedFrom: "{x}から翻訳するで" accountDeletionInProgress: "アカウント削除しとるで待っとってなー" usernameInfo: "サーバー上であんたのアカウントをあんたやと分かるようにするための名前やで。アルファベット(a~z, A~Z)、数字(0~9)、それとアンダーバー(_)が使って考えてな。この名前は後から変更することはできへんからちゃんと考えるんやで。" aiChanMode: "藍モードやで" +devMode: "開発者モード" keepCw: "CWを維持するで" pubSub: "Pub/Subのアカウント" lastCommunication: "直近の通信" @@ -834,6 +847,8 @@ breakFollow: "フォロワーを解除するで" breakFollowConfirm: "フォロワー解除してもええか?" itsOn: "オンになっとるよ" itsOff: "オフになってるで" +on: "オン" +off: "オフ" emailRequiredForSignup: "アカウント登録にメールアドレスを必須にするで" unread: "未読" filter: "フィルタ" @@ -988,6 +1003,8 @@ cannotBeChangedLater: "後からは変えられへんで。" reactionAcceptance: "ツッコミの受け入れ" likeOnly: "いいねだけ" likeOnlyForRemote: "リモートからはいいねだけな" +nonSensitiveOnly: "センシティブじゃないやつだけ" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "センシティブじゃないやつだけ (リモートはいいねだけ)" rolesAssignedToMe: "自分に割り当てられたロール" resetPasswordConfirm: "パスワード作り直すんでええな?" sensitiveWords: "けったいな単語" @@ -1004,7 +1021,6 @@ retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへん enableChartsForRemoteUser: "リモートユーザーのチャートを作る" enableChartsForFederatedInstances: "リモートサーバーのチャートを作る" showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" -largeNoteReactions: "ノートのツッコミを大きする" noteIdOrUrl: "ノートIDかURL" video: "動画" videos: "動画" @@ -1045,10 +1061,59 @@ preventAiLearning: "生成AIの学習に使わんといて" preventAiLearningDescription: "他の文章生成AIとか画像生成AIに、投稿したノートとか画像なんかを勝手に使わんように頼むで。具体的にはnoaiフラグをHTMLレスポンスに含めるんやけど、これ聞いてくれるんはAIの気分次第やから、使われる可能性もちょっとはあるな。" options: "オプション" specifyUser: "ユーザー指定" +failedToPreviewUrl: "プレビューできへん" +update: "更新" rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミとして使えるロール" rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールが一個も指定されてへんかったら、誰でもツッコミとして使えるで。" +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "ロールは公開ロールじゃないとアカンで。" cancelReactionConfirm: "ツッコむんをやっぱやめるか?" changeReactionConfirm: "ツッコミを別のに変えるか?" +later: "あとで" +goToMisskey: "Misskeyへ" +additionalEmojiDictionary: "絵文字の追加辞書" +installed: "インストール済み" +branding: "ブランディング" +enableServerMachineStats: "サーバーのマシン情報見せびらかすで" +enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする" +turnOffToImprovePerformance: "オフにしたらえらい軽うなるで。" +createInviteCode: "招待コードを作成" +createWithOptions: "オプションを指定して作成" +createCount: "作成数" +inviteCodeCreated: "招待コード作ったで" +inviteLimitExceeded: "招待コード作りすぎやで。" +createLimitRemaining: "作成できる招待コード: 残り {limit} 個やで" +inviteLimitResetCycle: "{time}で最大 {limit} 個の招待コードを作成できるで。" +expirationDate: "有効期限" +noExpirationDate: "有効期限を設けへん" +inviteCodeUsedAt: "招待コードが使用された日時" +registeredUserUsingInviteCode: "招待コードを使用したユーザー" +waitingForMailAuth: "メール認証待ち" +inviteCodeCreator: "招待コードを作成したユーザー" +usedAt: "使用日時" +unused: "つこてへん" +used: "もうつこてる" +expired: "期限切れ" +doYouAgree: "同意するんか?" +beSureToReadThisAsItIsImportant: "重要やから絶対読んでや。" +iHaveReadXCarefullyAndAgree: "「{x}」の内容をよう読んで、同意するで。" +dialog: "ダイアログ" +icon: "アイコン" +forYou: "あんたへ" +currentAnnouncements: "現在のお知らせやで" +pastAnnouncements: "過去のお知らせやで" +youHaveUnreadAnnouncements: "あんたまだこのお知らせ読んどらんやろ。" +useSecurityKey: "ブラウザまたはデバイスの言う通りに、セキュリティキーまたはパスキーを使ってや。" +replies: "返事" +renotes: "Renote" +_announcement: + forExistingUsers: "もうおるユーザーのみ" + forExistingUsersDescription: "有効にすると、このお知らせ作成時点でおるユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。" + needConfirmationToRead: "既読にするのに確認が必要やで" + needConfirmationToReadDescription: "有効にすると、このお知らせを既読にする際に確認ダイアログが表示されます。また、一括既読操作の対象にもなりません。" + end: "お知らせを終了" + tooManyActiveAnnouncementDescription: "アクティブなお知らせが多いため、UXが低下する可能性があります。終了したお知らせはアーカイブすることを検討した方がええよ。" + readConfirmTitle: "既読にしてええんやな?" + readConfirmText: "「{title}」の内容を読み、既読にします。" _initialAccountSetting: accountCreated: "アカウント作り終わったで。" letsStartAccountSetup: "アカウントの初期設定をしよか。" @@ -1063,8 +1128,11 @@ _initialAccountSetting: haveFun: "{name}、楽しんでな~" ifYouNeedLearnMore: "{name}(Misskey)の使い方とかをよー知りたいんやったら{link}をみてな。" skipAreYouSure: "初期設定飛ばすか?" + laterAreYouSure: "初期設定あとでやり直すん?" _serverRules: description: "新規登録前に見せる、サーバーの簡潔なルールを設定すんで。内容は使うための決め事の要約とすることを推奨するわ。" +_serverSettings: + iconUrl: "アイコン画像のURL" _accountMigration: moveFrom: "別のアカウントからこのアカウントに引っ越す" moveFromSub: "別のアカウントへエイリアスを作る" @@ -1358,6 +1426,9 @@ _role: ltlAvailable: "ローカルタイムラインの閲覧" canPublicNote: "パブリック投稿の許可" canInvite: "サーバー招待コードの発行" + inviteLimit: "招待コードの作成可能数" + inviteLimitCycle: "招待コードの発行間隔" + inviteExpirationTime: "招待コードの有効期限" canManageCustomEmojis: "カスタム絵文字の管理" driveCapacity: "ドライブ容量" alwaysMarkNsfw: "勝手にファイルにNSFWをくっつける" @@ -1420,6 +1491,7 @@ _ad: back: "戻る" reduceFrequencyOfThisAd: "この広告の表示頻度を下げるで" hide: "表示せん" + timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されるで。" _forgotPassword: enterEmail: "アカウントに登録したメールアドレスをここに入力してや。そのアドレス宛に、パスワードリセット用のリンクが送られるから待っててな~。" ifNoEmail: "メールアドレスを登録してへんのやったら、管理者まで教えてな~。" @@ -1471,9 +1543,9 @@ _aboutMisskey: donate: "Misskeyに寄付" morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰" patrons: "支援者" -_nsfw: - respect: "閲覧注意のメディアは隠すで" - ignore: "閲覧注意のメディアは隠さへんで" +_displayOfSensitiveMedia: + respect: "きわどいのは見とうない" + ignore: "きわどいのも見たい" force: "常にメディアを隠すで" _instanceTicker: none: "表示せん" @@ -1621,22 +1693,19 @@ _timelineTutorial: step3_1: "投稿できた?" step3_2: "あんたのノートがタイムラインに出てきたら成功や。" step4_1: "ノートには、「ツッコミ」を付けれるで。" - step4_2: "ツッコむんやったら、ノートの「+」マークを押して、好きな絵文字を選ぶで。" + step4_2: "ツッコむんやったら、ノートの「+」マークを押して、好きな絵文字を選ぶんやで。" _2fa: alreadyRegistered: "もう設定終わっとるわ。" registerTOTP: "認証アプリの設定はじめる" - passwordToTOTP: "パスワードを入れてーや" step1: "ほんなら、{a}や{b}とかの認証アプリを使っとるデバイスにインストールしてな。" step2: "次に、ここにあるQRコードをアプリでスキャンしてな~。" step2Click: "QRコードをクリックすると、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。" - step2Url: "デスクトップアプリやったら次のURLを入力してや:" step3Title: "確認コードを入れてーや" step3: "アプリに表示されているトークンを入力して終わりや。" step4: "これからログインするときも、同じようにトークンを入力するんやで" securityKeyNotSupported: "今使とるブラウザはセキュリティキーに対応してへんのやってさ。" registerTOTPBeforeKey: "セキュリティキー・パスキーを登録するんやったら、まず認証アプリを設定してーな。" securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキーか端末の指紋認証やPINを使ってログインするように設定できるで。" - chromePasskeyNotSupported: "Chromeのパスキーは今んとこ対応してないねん。" registerSecurityKey: "セキュリティキー・パスキーを登録するわ" securityKeyName: "キーの名前を入れてーや" tapSecurityKey: "ブラウザが言うこと聞いて、セキュリティキーとかパスキー登録しといでや" @@ -1644,7 +1713,7 @@ _2fa: removeKeyConfirm: "{name}を消すん?" whyTOTPOnlyRenew: "セキュリティキーが登録されとったら、認証アプリの設定は解除できへんで。" renewTOTP: "認証アプリをもっかい設定" - renewTOTPConfirm: "今までの人称アプリの確認コードは使えんくなるけどええか?" + renewTOTPConfirm: "今までの認証アプリの確認コードは使えんくなるけどええか?" renewTOTPOk: "もっかい設定する" renewTOTPCancel: "やめとく" _permissions: @@ -1680,6 +1749,10 @@ _permissions: "write:gallery": "ギャラリーを操作するで" "read:gallery-likes": "ギャラリーのいいねを見るで" "write:gallery-likes": "ギャラリーのいいねを操作するで" + "read:flash": "Playを見る" + "write:flash": "Playを操作する" + "read:flash-likes": "Playのええやん!を見る" + "write:flash-likes": "Playのええやん!を見る" _auth: shareAccessTitle: "アプリへのアクセス許してやったらどうや" shareAccess: "「{name}」がアカウントにアクセスすることを許可してええか?" @@ -1949,6 +2022,7 @@ _deck: introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょ!" introduction2: "画面の右にある + を押して、いつでもカラムを追加できるで。" widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選んでウィジェットを追加してなー" + useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示" _columns: main: "メイン" widgets: "ウィジェット" diff --git a/locales/jbo-EN.yml b/locales/jbo-EN.yml index ed97d539c0..d4fea291d7 100644 --- a/locales/jbo-EN.yml +++ b/locales/jbo-EN.yml @@ -1 +1,3 @@ --- +_lang_: "la .lojban." +headlineMisskey: "lo se tcana noi jorne fi loi notci" diff --git a/locales/kab-KAB.yml b/locales/kab-KAB.yml index 18fd8f5a58..22e24d3baa 100644 --- a/locales/kab-KAB.yml +++ b/locales/kab-KAB.yml @@ -56,6 +56,7 @@ accounts: "Imiḍan" searchByGoogle: "Nadi" file: "Ifuyla" account: "Imiḍan" +replies: "Err" _email: _follow: title: "Yeṭṭafaṛ-ik·em-id" diff --git a/locales/kn-IN.yml b/locales/kn-IN.yml index ef66f3fbd2..b3ad46f2b1 100644 --- a/locales/kn-IN.yml +++ b/locales/kn-IN.yml @@ -61,6 +61,7 @@ smtpPass: "ಗುಪ್ತಪದ" user: "ಬಳಕೆದಾರ" searchByGoogle: "ಹುಡುಕು" file: "ಕಡತಗಳು" +replies: "ಉತ್ತರಿಸು" _email: _follow: title: "ಹಿಂಬಾಲಿಸಿದರು" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index c36305137c..9e405396ba 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -2,20 +2,20 @@ _lang_: "한국어" headlineMisskey: "노트로 연결되는 네트워크" introMisskey: "환영합니다! Misskey는 오픈 소스 분산형 마이크로 블로그 서비스입니다.\n'노트'를 작성해서 지금 일어나고 있는 일을 공유하거나, 당신만의 이야기를 모두에게 발신하세요📡\n'리액션' 기능으로 친구의 노트에 총알같이 반응을 추가할 수도 있습니다👍\n새로운 세계를 탐험해 보세요🚀" -poweredByMisskeyDescription: "{name}은(는) 오픈소스 플랫폼 Misskey를 사용한 서버 가운데 하나입니다." +poweredByMisskeyDescription: "{name}은(는) 오픈소스 플랫폼Misskey를 사용한 서비스(Misskey 인스턴스라고 불립니다) 중 하나입니다." monthAndDay: "{month}월 {day}일" search: "검색" notifications: "알림" username: "유저명" password: "비밀번호" forgotPassword: "비밀번호 재설정" -fetchingAsApObject: "연합에서 조회 중" +fetchingAsApObject: "연합에 조회 중" ok: "확인" gotIt: "알겠어요" cancel: "취소" noThankYou: "나중에" enterUsername: "유저명 입력" -renotedBy: "{user}님의 리노트" +renotedBy: "{user}님이 리노트" noNotes: "노트가 없습니다" noNotifications: "표시할 알림이 없습니다" instance: "서버" @@ -40,20 +40,25 @@ favorites: "즐겨찾기" unfavorite: "즐겨찾기에서 제거" favorited: "즐겨찾기에 등록했습니다" alreadyFavorited: "이미 즐겨찾기에 등록되어 있습니다" -cantFavorite: "즐겨찾기에 등록하지 못했습니다." +cantFavorite: "즐겨찾기에 등록하지 못했습니다" pin: "프로필에 고정" unpin: "프로필에서 고정 해제" copyContent: "내용 복사" copyLink: "링크 복사" +copyLinkRenote: "Renote 링크 복사" delete: "삭제" deleteAndEdit: "삭제 후 편집" deleteAndEditConfirm: "이 노트를 삭제한 뒤 다시 편집하시겠습니까? 이 노트에 대한 리액션, 리노트, 답글 또한 모두 삭제됩니다." addToList: "리스트에 추가" +addToAntenna: "안테나에 추가" sendMessage: "메시지 보내기" copyRSS: "RSS 복사" copyUsername: "유저명 복사" copyUserId: "유저 ID 복사" copyNoteId: "노트 ID 복사" +copyFileId: "파일 ID 복사" +copyFolderId: "폴더 ID 복사" +copyProfileUrl: "프로필 URL 복사" searchUser: "사용자 검색" reply: "답글" loadMore: "더 보기" @@ -104,7 +109,7 @@ renote: "리노트" unrenote: "리노트 취소" renoted: "리노트했습니다" cantRenote: "이 게시물은 리노트 할 수 없습니다." -cantReRenote: "리노트를 리노트 할 수 없습니다." +cantReRenote: "리노트를 리노트할 수 없습니다." quote: "인용" inChannelRenote: "채널 내 리노트" inChannelQuote: "채널 내 인용" @@ -112,7 +117,7 @@ pinnedNote: "고정해놓은 노트" pinned: "프로필에 고정" you: "당신" clickToShow: "클릭하여 보기" -sensitive: "열람주의" +sensitive: "열람 주의" add: "추가" reaction: "리액션" reactions: "리액션" @@ -136,8 +141,10 @@ unblockConfirm: "이 계정의 차단을 해제하시겠습니까?" suspendConfirm: "이 계정을 정지하시겠습니까?" unsuspendConfirm: "이 계정의 정지를 해제하시겠습니까?" selectList: "리스트 선택" +editList: "리스트 편집" selectChannel: "채널 선택" selectAntenna: "안테나 선택" +editAntenna: "안테나 편집" selectWidget: "위젯 선택" editWidgets: "위젯 편집" editWidgetsExit: "편집 종료" @@ -149,11 +156,14 @@ emojiUrl: "이모지 URL" addEmoji: "이모지 추가" settingGuide: "추천 설정" cacheRemoteFiles: "리모트 파일을 캐시" -cacheRemoteFilesDescription: "이 설정을 해지하면 리모트 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다." +cacheRemoteFilesDescription: "이 설정을 활성화하면 리모트 파일을 이 서버의 스토리지에 캐시합니다. 미디어의 표시가 빨라지지만, 서버의 저장 용량을 크게 소모합니다. 리모트 유저의 미디어를 얼마나 보관할 지는 역할의 드라이브 용량 제한에 따라 결정되며, 정해진 용량을 넘길 경우 오래된 파일부터 차례대로 삭제한 뒤 링크로 전환합니다. \n비활성화하면 리모트 파일을 직접 링크하며, 이 경우 이미지 썸네일 생성 및 유저 프라이버시 보호를 위해 default.yml에서 proxyRemoteFiles를 true로 설정하는 것을 권장합니다." +youCanCleanRemoteFilesCache: "파일 관리 화면의 🗑️ 버튼을 눌러 모든 캐시를 삭제할 수 있습니다." +cacheRemoteSensitiveFiles: "리모트의 민감한 파일을 캐시" +cacheRemoteSensitiveFilesDescription: "이 설정을 비활성화하면 리모트의 민감한 파일은 캐시하지 않고 리모트에서 직접 가져오도록 합니다." flagAsBot: "나는 봇입니다" flagAsBotDescription: "이 계정을 자동화된 수단으로 운용할 경우에 활성화해 주세요. 이 플래그를 활성화하면, 다른 봇이 이를 참고하여 봇 끼리의 무한 연쇄 반응을 회피하거나, 이 계정의 시스템 상에서의 취급이 Bot 운영에 최적화되는 등의 변화가 생깁니다." flagAsCat: "나는 고양이다냥" -flagAsCatDescription: "이 계정이 고양이라면 활성화 해주세요." +flagAsCatDescription: "이 계정이 고양이라면 활성화해 주세요." flagShowTimelineReplies: "타임라인에 노트의 답글을 표시하기" flagShowTimelineRepliesDescription: "이 설정을 활성화하면 타임라인에 다른 유저 간의 답글을 표시합니다." autoAcceptFollowed: "팔로우 중인 유저로부터의 팔로우 요청을 자동 수락" @@ -199,7 +209,7 @@ instanceInfo: "서버 정보" statistics: "통계" clearQueue: "대기열 비우기" clearQueueConfirmTitle: "대기열을 비우시겠습니까?" -clearQueueConfirmText: "대기열에 남아 있는 노트는 더이상 연합되지 않습니다. 보통의 경우 이 작업은 필요하지 않습니다." +clearQueueConfirmText: "대기열에 남아 있는 노트는 더 이상 연합되지 않습니다. 보통의 경우 이 작업은 필요하지 않습니다." clearCachedFiles: "캐시 비우기" clearCachedFilesConfirm: "캐시된 리모트 파일을 모두 삭제하시겠습니까?" blockedInstances: "차단된 서버" @@ -311,7 +321,7 @@ copyUrl: "URL 복사" rename: "이름 변경" avatar: "아바타" banner: "배너" -nsfw: "열람주의" +displayOfSensitiveMedia: "민감한 미디어 표시" whenServerDisconnected: "서버와의 접속이 끊겼을 때" disconnectedFromServer: "서버와의 연결이 끊어졌습니다" reload: "새로고침" @@ -321,7 +331,7 @@ watch: "지켜보기" unwatch: "지켜보기 해제" accept: "허가" reject: "거부" -normal: "정상" +normal: "일반" instanceName: "서버 이름" instanceDescription: "서버 소개" maintainerName: "관리자 이름" @@ -346,7 +356,6 @@ invite: "초대" driveCapacityPerLocalAccount: "로컬 유저 한 명당 드라이브 용량" driveCapacityPerRemoteAccount: "리모트 유저 한 명당 드라이브 용량" inMb: "메가바이트 단위" -iconUrl: "아이콘 URL" bannerUrl: "배너 이미지 URL" backgroundImageUrl: "배경 이미지 URL" basicInfo: "기본 정보" @@ -402,6 +411,7 @@ aboutMisskey: "Misskey에 대하여" administrator: "관리자" token: "토큰" 2fa: "2단계 인증" +setupOf2fa: "2단계 인증 설정" totp: "인증 앱" totpDescription: "인증 앱을 사용하여 일회성 비밀번호 입력" moderator: "모더레이터" @@ -645,6 +655,7 @@ behavior: "동작" sample: "예시" abuseReports: "신고" reportAbuse: "신고" +reportAbuseRenote: "Renote를 신고" reportAbuseOf: "{name}을 신고하기" fillAbuseReportDescription: "신고하려는 이유를 자세히 알려주세요. 특정 게시물을 신고할 때에는 게시물의 URL도 포함해 주세요." abuseReported: "신고를 보냈습니다. 신고해 주셔서 감사합니다." @@ -672,6 +683,7 @@ createNewClip: "새 클립 만들기" unclip: "클립 해제" confirmToUnclipAlreadyClippedNote: "이 노트는 이미 \"{name}\" 클립에 포함되어 있습니다. 클립을 해제하시겠습니까?" public: "공개" +private: "비공개" i18nInfo: "Misskey는 자원봉사자들에 의해 다양한 언어로 번역되고 있습니다. {link}에서 번역에 참가할 수 있습니다." manageAccessTokens: "액세스 토큰 관리" accountInfo: "계정 정보" @@ -868,9 +880,9 @@ numberOfColumn: "한 줄에 보일 리액션의 수" searchByGoogle: "검색" instanceDefaultLightTheme: "서버 기본 라이트 테마" instanceDefaultDarkTheme: "서버 기본 다크 테마" -instanceDefaultThemeDescription: "객체 형식의 테마 코드를 입력해 주세요." +instanceDefaultThemeDescription: "객체 형식({}로 감싼 형태)의 테마 코드를 입력해 주세요." mutePeriod: "뮤트할 기간" -period: "투표 기한" +period: "기간" indefinitely: "무기한" tenMinutes: "10분" oneHour: "1시간" @@ -992,8 +1004,8 @@ cannotBeChangedLater: "나중에 변경할 수 없습니다." reactionAcceptance: "리액션 수신" likeOnly: "좋아요만 받기" likeOnlyForRemote: "리모트에서는 좋아요만 받기" -nonSensitiveOnly: "열람 주의로 설정되지 않았을 때만 받기" -nonSensitiveOnlyForLocalLikeOnlyForRemote: "열람 주의로 설정되지 않았을 때만 받기 (리모트에서는 좋아요만 받기)" +nonSensitiveOnly: "민감한 이모지를 제외하고 받기" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "민감한 이모지를 제외하고 받기 (리모트에서는 좋아요만 받기)" rolesAssignedToMe: "나에게 할당된 역할" resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?" sensitiveWords: "민감한 단어" @@ -1010,7 +1022,7 @@ retryAllQueuesConfirmText: "일시적으로 서버의 부하가 증가할 수 enableChartsForRemoteUser: "리모트 유저의 차트를 생성" enableChartsForFederatedInstances: "리모트 서버의 차트를 생성" showClipButtonInNoteFooter: "노트 동작에 클립을 추가" -largeNoteReactions: "노트의 리액션을 크게 표시" +reactionsDisplaySize: "리액션 표시 크기" noteIdOrUrl: "노트 ID 및 URL" video: "동영상" videos: "동영상" @@ -1062,6 +1074,48 @@ later: "나중에" goToMisskey: "Misskey로" additionalEmojiDictionary: "이모지 추가 사전" installed: "설치됨" +branding: "브랜딩" +enableServerMachineStats: "서버의 머신 사양을 공개하기" +enableIdenticonGeneration: "유저마다의 Identicon 생성 유효화" +turnOffToImprovePerformance: "이 기능을 끄면 성능이 향상될 수 있습니다." +createInviteCode: "초대 코드 생성" +createWithOptions: "옵션을 지정하여 생성" +createCount: "초대 수" +inviteCodeCreated: "초대 코드 생성됨" +inviteLimitExceeded: "초대 코드 생성 한도를 초과했습니다." +createLimitRemaining: "초대 한도: {limit}회 남음" +inviteLimitResetCycle: " {time}시간 이내에 최대 {limit}개의 초대 코드를 생성할 수 있습니다." +expirationDate: "만료 날짜" +noExpirationDate: "만료기간 없음" +inviteCodeUsedAt: "다음에 사용된 초대 코드" +registeredUserUsingInviteCode: "초대 코드 사용 대상" +waitingForMailAuth: "이메일 인증 보류 중" +inviteCodeCreator: "초대 코드 생성자" +usedAt: "사용 시각" +unused: "사용되지 않음" +used: "사용됨" +expired: "만료됨" +doYouAgree: "동의하십니까?" +beSureToReadThisAsItIsImportant: "중요하므로 반드시 읽어주십시오." +iHaveReadXCarefullyAndAgree: "\"{x}\"의 내용을 읽고 동의합니다." +dialog: "다이얼로그" +icon: "아바타" +forYou: "당신에게" +currentAnnouncements: "현재 공지사항" +pastAnnouncements: "과거 공지사항" +youHaveUnreadAnnouncements: "읽지 않은 공지사항이 있습니다." +useSecurityKey: "브라우저 또는 기기의 안내에 따라 보안 키 또는 패스키를 사용해 주십시오." +replies: "답글" +renotes: "리노트" +_announcement: + forExistingUsers: "기존 유저에게만 알림" + forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다." + needConfirmationToRead: "읽음으로 표시하기 전에 확인하기" + needConfirmationToReadDescription: "활성화하면 이 공지사항을 읽음으로 표시하기 전에 확인 알림창을 띄웁니다. '모두 읽음'의 대상에서도 제외됩니다." + end: "공지에서 내리기" + tooManyActiveAnnouncementDescription: "공지사항이 너무 많을 경우, 사용자 경험에 영향을 끼칠 가능성이 있습니다. 오래된 공지사항은 아카이브하시는 것을 권장드립니다." + readConfirmTitle: "읽음으로 표시합니까?" + readConfirmText: "\"{title}\"을(를) 읽음으로 표시합니다." _initialAccountSetting: accountCreated: "계정 생성이 완료되었습니다!" letsStartAccountSetup: "계정의 초기 설정을 진행합니다." @@ -1079,6 +1133,8 @@ _initialAccountSetting: laterAreYouSure: "초기 설정을 나중에 진행하시겠습니까?" _serverRules: description: "회원 가입 이전에 간단하게 표시할 서버 규칙입니다. 이용 약관의 요약으로 구성하는 것을 추천합니다." +_serverSettings: + iconUrl: "아이콘 URL" _accountMigration: moveFrom: "다른 계정에서 이 계정으로 이사" moveFromSub: "다른 계정에 대한 별칭을 생성" @@ -1372,6 +1428,9 @@ _role: ltlAvailable: "로컬 타임라인 보이기" canPublicNote: "공개 노트 허용" canInvite: "서버 초대 코드 발행" + inviteLimit: "초대 한도" + inviteLimitCycle: "초대 발급 간격" + inviteExpirationTime: "초대 만료 기간" canManageCustomEmojis: "커스텀 이모지 관리" driveCapacity: "드라이브 용량" alwaysMarkNsfw: "파일을 항상 NSFW로 지정" @@ -1434,6 +1493,7 @@ _ad: back: "뒤로" reduceFrequencyOfThisAd: "이 광고의 표시 빈도 낮추기" hide: "보이지 않음" + timezoneinfo: "요일은 서버의 표준 시간대에 따라 결정됩니다." _forgotPassword: enterEmail: "여기에 계정에 등록한 메일 주소를 입력해 주세요. 입력한 메일 주소로 비밀번호 재설정 링크를 발송합니다." ifNoEmail: "메일 주소를 등록하지 않은 경우, 관리자에 문의해 주십시오." @@ -1485,9 +1545,9 @@ _aboutMisskey: donate: "Misskey에 기부하기" morePatrons: "이 외에도 다른 많은 분들이 도움을 주시고 계십니다. 감사합니다🥰" patrons: "후원자" -_nsfw: - respect: "열람주의로 설정된 미디어 숨기기" - ignore: "열람 주의 미디어 항상 표시" +_displayOfSensitiveMedia: + respect: "민감한 콘텐츠로 표시된 미디어 숨기기" + ignore: "민감한 콘텐츠로 표시된 미디어 보이기" force: "미디어 항상 숨기기" _instanceTicker: none: "보이지 않음" @@ -1639,18 +1699,17 @@ _timelineTutorial: _2fa: alreadyRegistered: "이미 설정이 완료되었습니다." registerTOTP: "인증 앱 설정 시작" - passwordToTOTP: "비밀번호를 입력하세요." step1: "먼저, {a}나 {b}등의 인증 앱을 사용 중인 디바이스에 설치합니다." step2: "그 후, 표시되어 있는 QR코드를 앱으로 스캔합니다." step2Click: "QR 코드를 클릭하면 기기에 설치된 인증 앱에 등록할 수 있습니다." - step2Url: "데스크톱 앱에서는 다음 URL을 입력하세요:" + step2Uri: "데스크톱 앱을 사용하려면 다음 URI를 입력하십시오" step3Title: "인증 코드 입력" step3: "앱에 표시된 토큰을 입력하시면 완료됩니다." + setupCompleted: "설정 완료했습니다" step4: "다음 로그인부터는 토큰을 입력해야 합니다." securityKeyNotSupported: "이 브라우저는 보안 키를 지원하지 않습니다." registerTOTPBeforeKey: "보안 키 또는 패스키를 등록하려면 인증 앱을 등록하십시오." securityKeyInfo: "FIDO2를 지원하는 하드웨어 보안 키 혹은 디바이스의 지문인식이나 화면잠금 PIN을 이용해서 로그인하도록 설정할 수 있습니다." - chromePasskeyNotSupported: "현재 Chrome의 패스키는 지원되지 않습니다." registerSecurityKey: "보안 키 또는 패스키 등록" securityKeyName: "키 이름 입력" tapSecurityKey: "브라우저의 지시에 따라 보안 키 또는 패스키를 등록하여 주십시오" @@ -1661,6 +1720,11 @@ _2fa: renewTOTPConfirm: "기존에 등록되어 있던 인증 키는 사용하지 못하게 됩니다." renewTOTPOk: "재설정" renewTOTPCancel: "취소" + checkBackupCodesBeforeCloseThisWizard: "이 위자드를 닫기 전에 아래 백업 코드를 확인하십시오" + backupCodes: "백업 코드" + backupCodesDescription: "인증 앱을 사용할 수 없게 된 경우 아래 백업 코드를 사용하여 계정에 액세스 할 수 있습니다.이 코드들은 반드시 안전한 장소에 보관하십시오.각 코드는 한 번만 사용할 수 있습니다." + backupCodeUsedWarning: "백업 코드가 사용되었습니다.인증 앱을 사용할 수 없게 된 경우, 조속히 인증 앱을 다시 설정해 주십시오." + backupCodesExhaustedWarning: "백업 코드가 모두 사용되었습니다.인증 앱을 사용할 수 없는 경우 더 이상 계정에 액세스하는 것이 불가능합니다.인증 앱을 다시 등록해 주세요." _permissions: "read:account": "계정의 정보를 봅니다" "write:account": "계정의 정보를 변경합니다" @@ -1694,6 +1758,10 @@ _permissions: "write:gallery": "갤러리를 추가하거나 삭제합니다" "read:gallery-likes": "갤러리의 좋아요를 확인합니다" "write:gallery-likes": "갤러리에 좋아요를 추가하거나 취소합니다" + "read:flash": "Play를 봅니다" + "write:flash": "Play를 조작합니다" + "read:flash-likes": "Play의 좋아요를 봅니다" + "write:flash-likes": "Play의 좋아요를 조작합니다" _auth: shareAccessTitle: "어플리케이션의 접근 허가" shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용하시겠습니까?" @@ -1929,6 +1997,10 @@ _notification: unreadAntennaNote: "안테나 {name}" emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다" achievementEarned: "도전 과제를 달성했습니다" + testNotification: "알림 테스트" + checkNotificationBehavior: "알림 표시를 체크하기" + sendTestNotification: "테스트 알림 보내기" + notificationWillBeDisplayedLikeThis: "알림이 이렇게 표시됩니다" _types: all: "전부" follow: "팔로잉" @@ -1963,6 +2035,9 @@ _deck: introduction: "칼럼을 조합해서 나만의 인터페이스를 구성해 보아요!" introduction2: "나중에라도 화면 우측의 + 버튼을 눌러 새 칼럼을 추가할 수 있습니다." widgetsIntroduction: "칼럼 메뉴의 \"위젯 편집\"에서 위젯을 추가해 주세요" + useSimpleUiForNonRootPages: "루트 이외의 페이지로 접속한 경우 UI 간략화하기" + usedAsMinWidthWhenFlexible: "'폭 자동 조정'이 활성화된 경우 최소 폭으로 사용됩니다" + flexible: "폭 자동 조정" _columns: main: "메인" widgets: "위젯" diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index f686b04257..37251a95c8 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -20,6 +20,7 @@ noNotes: "ບໍ່ມີຫມາຍເຫດ" noNotifications: "ບໍ່ມີການແຈ້ງເຕືອນ" instance: "ອີນສະແຕນ" settings: "ກຳນົດຄ່າ" +notificationSettings: "ຕັ້ງຄ່າການແຈ້ງເຕືອນ" basicSettings: "ການຕັ້ງຄ່າພື້ນຖານ" otherSettings: "ການຕັ້ງຄ່າອື່ນໆ" openInWindow: "ເປີດຢູ່ໃນປ່ອງຢ້ຽມ" @@ -48,9 +49,15 @@ delete: "ລຶບ" deleteAndEdit: "ລົບ​ແລະ​ແກ້​ໄຂ​" deleteAndEditConfirm: "ເຈົ້າ​ແນ່​ໃຈ​ບໍ່? ທີ່ທ່ານຕ້ອງການທີ່ຈະລຶບບັນທຶກນີ້ແລະແກ້ໄຂມັນ ທ່ານອາດຈະສູນເສຍການໂຕ້ຕອບ, ບັນທຶກ, ແລະການຕອບກັບທັງໝົດ" addToList: "ເພີ່ມໃສ່ລາຍຊື່" +addToAntenna: "ເພີ່ມໃສ່ເສົາອາກາດ" sendMessage: "ສົ່ງຂໍ້ຄວາມ" copyRSS: "ສຳເນົາ RSS" copyUsername: "ສຳເນົາຊື່ຜູ້ໃຊ້" +copyUserId: "ສຳເນົາ ID ຜູ້ໃຊ້" +copyNoteId: "ສຳເນົາ ID ບັນທຶກ" +copyFileId: "ສຳເນົາ ID ໄຟລ໌" +copyFolderId: "ສຳເນົາ ID ໂຟນເດີ" +copyProfileUrl: "ສຳເນົາ URL ໂປຣໄຟລ໌" searchUser: "ຄົ້ນຫາຜູ້ໃຊ້" reply: "ຕອບ​ໄປ​ທີ" loadMore: "ໂຫຼດເພີ່ມເຕີມ" @@ -100,7 +107,11 @@ enterEmoji: "ປ້ອນອີໂມຈິ" renote: "Renote" unrenote: "ເລີກ Renote" renoted: "ເກັບບັນທຶກໄວ້" +cantRenote: "ໂພສນີ້ບໍ່ສາມາດຖືກບັນທຶກໄວ້ຄືນໃໝ່ໄດ້" +cantReRenote: "ບໍ່ສາມາດບັນທຶກຄືນໃໝ່ໄດ້" quote: "ລວມຂໍ້ຄວາມອ້າງອີງ" +inChannelRenote: "ຊ່ອງພຽງແຕ່ Renote" +inChannelQuote: "ຊ່ອງເທົ່ານັ້ນ Quote" pinnedNote: "ບັນທຶກທີ່ປັກໝຸດໄວ້" pinned: "ປັກໝຸດໄປຫາໂປຣໄຟລ໌" you: "ເຈົ້າ" @@ -109,6 +120,7 @@ sensitive: "NSFW" add: "ເພີ່ມ" reaction: "ປະຕິກິລິຍາ" reactions: "ປະຕິກິລິຍາ" +attachCancel: "ເອົາໄຟລ໌ແນບ" mute: "ປີດສຽງ" unmute: "ເປີດສຽງ" block: "ບ໋ອກ" @@ -116,6 +128,10 @@ unblock: "ຍົກເລີກກາຮົບລັອກ" suspend: "ລະງັບ" unsuspend: "ເຊົາ​ລະ​ງັບ" selectList: "ເລືອກບັນຊີລາຍການ" +editList: "ແກ້ໄຂລາຍຊື່" +selectChannel: "ເລືອກຊ່ອງ" +selectAntenna: "ເລືອກເສົາອາກາດ" +editAntenna: "ແກ້ໄຂເສົາອາກາດ" selectWidget: "ເລືອກວິກເຈັດ" editWidgets: "ແກ້ໄຂ Widget" editWidgetsExit: "ສຳເລັດແລ້ວ" @@ -125,6 +141,7 @@ emojis: "ອີໂມຈິ" emojiName: "ຊື່ Emoji" emojiUrl: "URL ອີໂມຈິ" addEmoji: "ຕື່ມອີໂມຈິ" +settingGuide: "ການຕັ້ງຄ່າທີ່ແນະນໍາ" flagAsBot: "ໝາຍບັນຊີນີ້ເປັນບັອດ" flagAsCat: "ໝາຍບັນຊີນີ້ເປັນແມວ" flagAsCatDescription: "ເປີດໃຊ້ຕົວເລືອກນີ້ເພື່ອໝາຍບັນຊີນີ້ເປັນແມວ" @@ -133,10 +150,13 @@ flagShowTimelineRepliesDescription: "ສະແດງການຕອບກັບ autoAcceptFollowed: "ອະນຸມັດອັດຕະໂນມັດຕາມຄຳຮ້ອງຂໍຈາກຜູ້ໃຊ້ທີ່ທ່ານກຳລັງຕິດຕາມຢູ່" addAccount: "ເພີ່ມບັນຊີ" loginFailed: "ການເຂົ້າສູ່ລະບົບບໍ່ສຳເລັດ" +showOnRemote: "ເບິ່ງຢູ່ໃນຕົວຢ່າງໄລຍະໄກ" general: "ທົ່ວໄປ" wallpaper: "ພາບພື້ນຫລັງ" setWallpaper: "ຕັ້ງເປັນພາບພື້ນຫຼັງ" +removeWallpaper: "ລຶບຮູບວໍເປເປີອອກ" searchWith: "ຊອກຫາ: {q}" +youHaveNoLists: "ທ່ານ​ບໍ່​ມີ​ລາຍ​ການ​ໃດໆ​" proxyAccount: "ບັນຊີພຣັອກຊີ" host: "ໂຮດສ" selectUser: "ເລືອກຜູ້ໃຊ້" @@ -155,7 +175,9 @@ operations: "ການດຳເນີນງານ" software: "ຊອບແວ" version: "ສະບັບ" metadata: "Metadata" +withNFiles: "{n} ໄຟລ໌(s)" monitor: "ຈໍພາບ" +jobQueue: "ຄິວວຽກ" cpuAndMemory: "CPU ແລະ ຫນ່ວຍຄວາມຈໍາ" network: "ເຄືອຂ່າຍ" disk: "ດິສກ໌" @@ -208,9 +230,12 @@ fromUrl: "ຈາກ URL" uploadFromUrl: "ອັບໂຫຼດຈາກ URL" uploadFromUrlDescription: "URL ຂອງໄຟລ໌ທີ່ທ່ານຕ້ອງການອັບໂຫລດ" uploadFromUrlRequested: "ຮ້ອງຂໍການອັບໂຫລດ" +explore: "ສຳຫຼວດ" messageRead: "ອ່ານແລ້ວ" startMessaging: "ເລີ່ມການສົນທະນາໃໝ່" nUsersRead: "ອ່ານໂດຍ {n}" +agree: "ຍອມຮັບ" +termsOfService: "ເງື່ອນໄຂການບໍລິການ" start: "ເລີ່ມຕົ້ນນຳໃຊ້ເລີຍ" home: "ໜ້າຫຼັກ" activity: "ກິດຈະກຳ" @@ -248,7 +273,7 @@ inputNewDescription: "ໃສ່ຄຳບັນຍາຍໃໝ່" inputNewFolderName: "ໃສ່ຊື່ໂຟນເດີໃໝ່" circularReferenceFolder: "ໂຟນເດີປາຍທາງແມ່ນໂຟນເດີຍ່ອຍຂອງໂຟນເດີທີ່ທ່ານຕ້ອງການຍ້າຍ" rename: "ປ່ຽນຊື່" -nsfw: "NSFW" +doNothing: "ບໍ່ສົນໃຈ" watch: "ເບິ່ງ" unwatch: "ຢຸດເບິ່ງ" accept: "ອະນຸຍາດ" @@ -277,7 +302,14 @@ enableRegistration: "ເປີດໃຊ້ການລົງທະບຽນຜ invite: "ເຊີນ" driveCapacityPerLocalAccount: "ຄວາມອາດສາມາດຂັບຕໍ່ຜູ້ໃຊ້ທ້ອງຖິ່ນ" driveCapacityPerRemoteAccount: "ໄດຣຟ໌ຄວາມອາດສາມາດຕໍ່ຜູ້ໃຊ້ທາງໄກ" +basicInfo: "ຂໍ້ມຸນເບື້ອງຕົ້ນ" pinnedNotes: "ບັນທຶກທີ່ປັກໝຸດໄວ້" +hcaptchaSiteKey: "ກະແຈໄຊທ໌" +hcaptchaSecretKey: "ກະແຈລັບ" +recaptcha: "reCAPTCHA" +enableRecaptcha: "ເປີດໃຊ້ງານລີແຄ໋ບຈາ" +recaptchaSiteKey: "ກະແຈໄຊທ໌" +recaptchaSecretKey: "ກະແຈລັບ" turnstileSiteKey: "ກະແຈໄຊທ໌" turnstileSecretKey: "ກະແຈລັບ" name: "ຊື່" @@ -285,24 +317,41 @@ userList: "ລາຍການ" about: "ກ່ຽວກັບ" aboutMisskey: "ກ່ຽວກັບ Misskey" administrator: "ຜູ້ບໍລິຫານ" +token: "ໂທເຄັນ" share: "ແບ່ງປັນ" notFound: "ບໍ່ພົບ" cacheClear: "ລຶບລ້າງແຄສ" +help: "ຊ່ວຍເຫຼືອ" +close: "ປິດ" invites: "ເຊີນ" +members: "ສະມາຊິກ" +transfer: "ໂອນຍ້າຍ" title: "ຫົວຂໍ້" text: "ຂໍ້ຄວາມ" enable: "ເປີດໃຊ້" next: "ຕໍ່ໄປ" +retype: "ເຂົ້າໄປອີກຄັ້ງ" +quoteAttached: "ວົງຢືມ" invitations: "ເຊີນ" +unavailable: "ບໍ່​ສາ​ມາດ​ໃຊ້​ໄດ້" language: "ພາສາ" +aboutX: "ກ່ຽວກັບ {x}" +emojiStyle: "ຮູບແບບອີໂມຈິ" native: "ພາ​ສາ​ແມ່" +noHistory: "​ບໍ່​ມີ​ລາຍ​ການ​ຢູ່​ບ່ອນ​ນີ້" +doing: "ກຳລັງປະມວນຜົນ..." category: "ຫມວດຫມູ່" tags: "ແທ໋ກ" createAccount: "ສ້າງບັນຊີ" existingAccount: "ທີ່ມີຢູ່" dashboard: "ໜ້າປັດ" local: "ທ້ອງຖິ່ນ" +numberOfDays: "ຈຳນວນມື້" +objectStorageBucket: "Bucket" +objectStoragePrefix: "Prefix" +objectStorageEndpoint: "Endpoint" objectStorageRegion: "ພາກ​ພື້ນ" +deleteAll: "ລຶບທັງໝົດ" sounds: "ສຽງ" sound: "ສຽງ" none: "ບໍ່ມີ" @@ -316,18 +365,42 @@ ascendingOrder: "ນ້ອຍໄປຫາໃຫຍ່" descendingOrder: "ໃຫຍ່ຫານ້ອຍ" output: "ຜົນຜະລິດ" script: "ບົດ​ຄວາມ" +menu: "ເມນູ" +rearrange: "ຈັດລຽງຄືນ" +poll: "ການພູນ" +description: "ລາຍລະອຽດ" +author: "ຜູ້ຂຽນ" +manage: "ການຈັດການ" +plugins: "ປລັ໋ກອີນ" +width: "ກວ້າງ" +height: "ຄວາມສູງ" +large: "ໃຫຍ່." +medium: "ປານກາງ" +small: "ເລັກ" +permission: "ການອະນຸຍາດ" +notificationType: "​ປະເພດການ​ແຈ້ງ​ເຕືອນ" +edit: "ແກ້ໄຂ" +email: "ອີເມວ" smtpHost: "ໂຮດສ" smtpUser: "ຊື່ຜູ້ໃຊ້" smtpPass: "ລະຫັດຜ່ານ" clearCache: "ລຶບລ້າງແຄສ" info: "ກ່ຽວກັບ" user: "ຜູ້ໃຊ້ຕ່າງໆ" +administration: "ການຈັດການ" +middle: "ປານກາງ" searchByGoogle: "ຄົ້ນຫາ" file: "ໄຟລ໌" +replies: "ຕອບ​ໄປ​ທີ" +renotes: "Renote" +_role: + _priority: + middle: "ປານກາງ" _email: _follow: title: "ໄດ້ຕິດຕາມທ່ານ" _theme: + description: "ລາຍລະອຽດ" keys: mention: "ໄດ້ກ່າວມາ" renote: "Renote" @@ -344,6 +417,7 @@ _widgets: timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​" activity: "ກິດຈະກຳ" federation: "ສະຫະພັນ" + jobQueue: "ຄິວວຽກ" _userList: chooseList: "ເລືອກບັນຊີລາຍການ" _cw: @@ -365,6 +439,7 @@ _timelines: home: "ໜ້າຫຼັກ" _play: script: "ບົດ​ຄວາມ" + summary: "ລາຍລະອຽດ" _pages: blocks: image: "ຮູບພາບ" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 6bca484f14..d75f807316 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -20,6 +20,7 @@ noNotes: "Geen notities" noNotifications: "Geen meldingen" instance: "Server" settings: "Instellingen" +notificationSettings: "Notificatie instellingen" basicSettings: "Basisinstellingen" otherSettings: "Overige instellingen" openInWindow: "In een venster openen" @@ -48,8 +49,15 @@ delete: "Verwijderen" deleteAndEdit: "Verwijderen en bewerken" deleteAndEditConfirm: "Weet je zeker dat je deze notitie wilt verwijderen en dan bewerken? Je verliest alle reacties, herdelingen en antwoorden erop." addToList: "Aan lijst toevoegen" +addToAntenna: "Voeg toe aan antenne" sendMessage: "Verstuur bericht" +copyRSS: "Kopieer RSS" copyUsername: "Kopiëren gebruikersnaam " +copyUserId: "Kopieer gebruiker ID" +copyNoteId: "Kopieer notitie ID" +copyFileId: "Kopieer veld ID" +copyFolderId: "Kopieer folder ID" +copyProfileUrl: "Kopieer profiel URL" searchUser: "Zoeken een gebruiker" reply: "Antwoord" loadMore: "Laad meer" @@ -296,7 +304,6 @@ copyUrl: "URL kopiëren" rename: "Hernoemen" avatar: "Avatar" banner: "Banner" -nsfw: "NSFW" whenServerDisconnected: "Wanneer de verbinding met de server wordt onderbroken" disconnectedFromServer: "Verbinding met de server onderbroken." reload: "Verversen" @@ -331,7 +338,6 @@ invite: "Uitnodigen" driveCapacityPerLocalAccount: "Opslagruimte per lokale gebruiker" driveCapacityPerRemoteAccount: "Opslagruimte per externe gebruiker" inMb: "in megabytes" -iconUrl: "Pictogram URL" bannerUrl: "Banner URL" backgroundImageUrl: "URL afbeelding" basicInfo: "Basisinformatie" @@ -419,6 +425,9 @@ pushNotificationAlreadySubscribed: "Pushberichtrn al ingeschakeld" windowMaximize: "Maximaliseren" windowRestore: "Herstellen" loggedInAsBot: "Momenteel als bot ingelogd" +icon: "Avatar" +replies: "Antwoord" +renotes: "Herdelen" _email: _follow: title: "volgde jou" diff --git a/locales/no-NO.yml b/locales/no-NO.yml index ec2900527b..35a3866b95 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -461,6 +461,9 @@ videos: "Videoer" continue: "Fortsett" youFollowing: "Følger" options: "Alternativ" +icon: "Avatar" +replies: "Svar" +renotes: "Renote" _initialAccountSetting: theseSettingsCanEditLater: "Du kan endre disse innstillingene senere." _achievements: diff --git a/locales/package.json b/locales/package.json new file mode 100644 index 0000000000..bedb411a91 --- /dev/null +++ b/locales/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 24eee60bae..065e228c0c 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -299,7 +299,6 @@ copyUrl: "Skopiuj adres URL" rename: "Zmień nazwę" avatar: "Awatar" banner: "Baner" -nsfw: "NSFW" whenServerDisconnected: "Po utracie połączenia z serwerem" disconnectedFromServer: "Utracono połączenie z serwerem." reload: "Odśwież" @@ -334,7 +333,6 @@ invite: "Zaproś" driveCapacityPerLocalAccount: "Powierzchnia dyskowa na lokalnego użytkownika" driveCapacityPerRemoteAccount: "Powierzchnia dyskowa na zdalnego użytkownika" inMb: "W megabajtach" -iconUrl: "Adres URL ikony" bannerUrl: "Adres URL banera" backgroundImageUrl: "Adres URL tła" basicInfo: "Podstawowe informacje" @@ -645,6 +643,7 @@ createNewClip: "Utwórz nowy klip" unclip: "Odczep" confirmToUnclipAlreadyClippedNote: "Ten wpis jest już częścią klipu \"{name}\". Czy chcesz ją usunąć z tego klipu?" public: "Publiczny" +private: "Prywatne" i18nInfo: "Misskey jest tłumaczone na wiele języków przez wolontariuszy. Możesz pomóc na {link}." manageAccessTokens: "Zarządzaj tokenami dostępu" accountInfo: "Informacje o koncie" @@ -871,6 +870,9 @@ like: "Polub" show: "Wyświetlanie" color: "Kolor" youFollowing: "Śledzeni" +icon: "Awatar" +replies: "Odpowiedz" +renotes: "Udostępnij" _role: priority: "Priorytet" _priority: @@ -955,10 +957,6 @@ _aboutMisskey: donate: "Przekaż darowiznę na Misskey" morePatrons: "Naprawdę doceniam wsparcie ze strony wielu niewymienionych tu osób. Dziękuję! 🥰" patrons: "Wspierający" -_nsfw: - respect: "Ukrywaj media NSFW" - ignore: "Nie ukrywaj mediów NSFW" - force: "Ukrywaj wszystkie media" _instanceTicker: none: "Nigdy nie pokazuj" remote: "Pokaż dla zdalnych użytkowników" @@ -1094,6 +1092,8 @@ _2fa: 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}?" + renewTOTPConfirm: "Spowoduje to, że kody weryfikacyjne z poprzedniej aplikacji przestaną działać" + renewTOTPOk: "Rekonfiguruj" renewTOTPCancel: "Nie teraz" _permissions: "read:account": "Wyświetl informacje o swoim koncie" @@ -1107,8 +1107,10 @@ _permissions: "read:following": "Wyświetlanie informacji o obserwowanych" "write:following": "Obserwowanie lub cofanie obserwacji innych kont" "read:messaging": "Zobacz swoje czaty" + "write:messaging": "Tworzenie lub usuwanie wiadomości czatu" "read:mutes": "Wyświetlanie listy osób, które wyciszyłeś(-aś)" "write:mutes": "Edycja listy osób, które wyciszyłeś(-aś)" + "write:notes": "Tworzenie lub usuwanie wpisów" "read:notifications": "Wyświetlanie powiadomień" "write:notifications": "Działanie na powiadomieniach" "read:reactions": "Wyświetlanie reakcji" @@ -1124,9 +1126,23 @@ _permissions: "write:channels": "Edytuj swoje kanały" "read:gallery": "Zobacz swoją galerię" "write:gallery": "Edytuj swoją galerię" + "read:gallery-likes": "Wyświetlanie listy polubionych postów w galerii" + "write:gallery-likes": "Edytowanie listy polubionych postów w galerii" _auth: + shareAccessTitle: "Przyznawanie uprawnień aplikacji" shareAccess: "Czy chcesz autoryzować „{name}” do dostępu do tego konta?" + shareAccessAsk: "Czy na pewno chcesz zezwolić tej aplikacji na dostęp do Twojego konta?" + permission: "{name} żąda następujących uprawnień" permissionAsk: "Ta aplikacja wymaga następujących uprawnień:" + pleaseGoBack: "Proszę, wróć do aplikacji" + callback: "Powracanie do aplikacji" + denied: "Odmowa dostępu" + pleaseLogin: "Zaloguj się, aby autoryzować aplikacje." +_antennaSources: + all: "Wszystkie wpisy" + homeTimeline: "Wpisy obserwowanych użytkowników" + users: "Wpisy określonych użytkowników" + userList: "Wpisy z określonej listy użytkowników" _weekday: sunday: "Niedziela" monday: "Poniedziałek" @@ -1159,8 +1175,10 @@ _widgets: serverMetric: "Metryka serwera" aiscript: "Konsola AiScript" aichan: "Ai" + userList: "Lista użytkowników" _userList: chooseList: "Wybierz listę" + clicker: "Clicker" _cw: hide: "Ukryj" show: "Załaduj więcej" @@ -1192,10 +1210,16 @@ _visibility: public: "Publiczny" publicDescription: "Twój wpis pojawi się w publicznych osiach czasu" home: "Strona główna" + homeDescription: "Publikuj tylko na głównej osi czasu" followers: "Obserwujący" + followersDescription: "Widoczne tylko dla obserwujących" specified: "Bezpośredni" specifiedDescription: "Napisz tylko określonym użytkownikom" + disableFederationDescription: "Nie przesyłaj do innych instancji" _postForm: + replyPlaceholder: "Odpowiedz na ten wpis..." + quotePlaceholder: "Zacytuj ten wpis…" + channelPlaceholder: "Publikuj na kanale..." _placeholders: a: "Co się dzieje?" b: "Co się wydarzyło?" @@ -1217,17 +1241,29 @@ _profile: changeBanner: "Zmień baner" _exportOrImport: allNotes: "Wszystkie wpisy" + favoritedNotes: "Ulubione wpisy" followingList: "Obserwowani" muteList: "Wycisz" blockingList: "Zablokuj" userLists: "Listy" + excludeMutingUsers: "Wyklucz wyciszonych użytkowników" + excludeInactiveUsers: "Wyklucz nieaktywnych użytkowników" _charts: federation: "Federacja" apRequest: "Żądania" + usersIncDec: "Różnica w liczbie użytkowników" usersTotal: "Łącznie # użytkowników" activeUsers: "Aktywni użytkownicy" + notesIncDec: "Różnica w liczbie wpisów" + notesTotal: "Całkowita liczba wpisów" + filesIncDec: "Różnica w liczbie plików" + filesTotal: "Całkowita liczba plików" + storageUsageIncDec: "Różnica w wykorzystaniu pamięci" + storageUsageTotal: "Całkowite wykorzystanie pamięci" _instanceCharts: requests: "Żądania" + users: "Różnica w liczbie użytkowników" + notes: "Różnica w liczbie wpisów" notesTotal: "Łącznie # wpisów" ff: "Różnica w # obserwujących" ffTotal: "Łączna liczba # obserwujących" @@ -1352,5 +1388,16 @@ _deck: mentions: "Wspomnienia" direct: "Bezpośredni" _webhookSettings: + createWebhook: "Stwórz Webhook" name: "Nazwa" + secret: "Sekret" + events: "Uruchomienie Webhooka" active: "Właczono" + _events: + follow: "Po zaobserwowaniu użytkownika" + followed: "Po zostaniu zaobserwowanym" + note: "Po opublikowaniu wpisu" + reply: "Po otrzymaniu odpowiedzi" + renote: "Po udostępnieniu wpisu" + reaction: "Po otrzymaniu reakcji" + mention: "Po zostaniu wspomnianym" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index e7b43f6380..737bab9adc 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -1,9 +1,10 @@ --- _lang_: "Português" headlineMisskey: "Uma rede ligada por notas" -introMisskey: "Bem-vindo! Misskey é um serviço de microblogue descentralizado de código aberto.\nCria \"notas\" e partilha o que te ocorre com todos à tua volta. 📡\nCom \"reações\" podes também expressar logo o que sentes às notas de todos. 👍\nExploremos um novo mundo! 🚀" +introMisskey: "Bem-vindo! O Misskey é um serviço de microblog descentralizado de código aberto.\nCrie \"notas\" para compartilhar o que está acontecendo agora ou para se expressar com todos à sua volta 📡\nVocê também pode adicionar rapidamente reações às notas de outras pessoas usando a função \"Reações\" 👍\nVamos explorar um novo mundo 🚀" +poweredByMisskeyDescription: "{name} é um dos servidores da plataforma de código aberto Misskey." monthAndDay: "{day}/{month}" -search: "Buscar" +search: "Pesquisar" notifications: "Notificações" username: "Nome de usuário" password: "Senha" @@ -12,122 +13,139 @@ fetchingAsApObject: "Buscando no Fediverso" ok: "OK" gotIt: "Entendi" cancel: "Cancelar" +noThankYou: "Não, obrigado" enterUsername: "Digite o nome de usuário" renotedBy: "Repostado por {user}" -noNotes: "Sem posts" +noNotes: "Sem notas" noNotifications: "Sem notificações" instance: "Instância" settings: "Configurações" +notificationSettings: "Configurações de notificação" basicSettings: "Configurações básicas" otherSettings: "Outras configurações" -openInWindow: "Abrir numa janela" +openInWindow: "Abrir em um janela" profile: "Perfil" -timeline: "Timeline" +timeline: "Linha do tempo" noAccountDescription: "Este usuário não tem uma descrição." login: "Iniciar sessão" loggingIn: "Iniciando sessão…" logout: "Sair" signup: "Registrar-se" uploading: "Enviando…" -save: "Guardar" +save: "Salvar" users: "Usuários" addUser: "Adicionar usuário" -favorite: "Favoritar" -favorites: "Favoritar" +favorite: "Adicionar aos favoritos" +favorites: "Favoritos" unfavorite: "Remover dos favoritos" favorited: "Adicionado aos favoritos." alreadyFavorited: "Já adicionado aos favoritos." cantFavorite: "Não foi possível adicionar aos favoritos." -pin: "Afixar no perfil" +pin: "Fixar no perfil" unpin: "Desafixar do perfil" copyContent: "Copiar conteúdos" -copyLink: "Copiar hiperligação" -delete: "Eliminar" -deleteAndEdit: "Eliminar e editar" -deleteAndEditConfirm: "Tens a certeza que pretendes eliminar esta nota e editá-la? Irás perder todas as suas reações, renotas e respostas." +copyLink: "Copiar link" +copyLinkRenote: "Copiar o link da repostagem" +delete: "Excluir" +deleteAndEdit: "Excluir e editar" +deleteAndEditConfirm: "Deseja excluir esta nota e editá-la novamente? Todas as reações, compartilhamentos e respostas a esta nota também serão excluídas." addToList: "Adicionar a lista" -sendMessage: "Enviar uma mensagem" +addToAntenna: "Adicionar à antena" +sendMessage: "Enviar mensagem" +copyRSS: "Copiar RSS" copyUsername: "Copiar nome de utilizador" -searchUser: "Pesquisar utilizador" +copyUserId: "Copiar ID do utilizador" +copyNoteId: "Copiar ID da publicação" +copyFileId: "Copiar o ID do arquivo" +copyFolderId: "Copiar o ID da pasta" +copyProfileUrl: "Copiar a URL do perfil" +searchUser: "Pesquisar usuário" reply: "Responder" loadMore: "Carregar mais" showMore: "Ver mais" showLess: "Fechar" youGotNewFollower: "Você tem um novo seguidor" -receiveFollowRequest: "Pedido de seguimento recebido" -followRequestAccepted: "Pedido de seguir aceito" +receiveFollowRequest: "Pedido de seguidor recebido" +followRequestAccepted: "Pedido de seguidor aceito" mention: "Menção" mentions: "Menções" directNotes: "Notas diretas" importAndExport: "Importar/Exportar" import: "Importar" export: "Exportar" -files: "Ficheiros" +files: "Arquivos" download: "Descarregar" -driveFileDeleteConfirm: "Tens a certeza que pretendes apagar o ficheiro \"{name}\"? As notas que tenham este ficheiro anexado serão também apagadas." -unfollowConfirm: "Tens a certeza que queres deixar de seguir {name}?" -exportRequested: "Pediste uma exportação. Este processo pode demorar algum tempo. Será adicionado à tua Drive após a conclusão do processo." -importRequested: "Pediste uma importação. Este processo pode demorar algum tempo." +driveFileDeleteConfirm: "Deseja excluir o arquivo '{name}'? Qualquer conteúdo que use este arquivo também será removido." +unfollowConfirm: "Gostaria de deixar de seguir {name}?" +exportRequested: "A sua solicitação de exportação foi enviada. Isso pode levar algum tempo. Assim que a exportação estiver concluída, ela será adicionada ao seu drive." +importRequested: "A sua solicitação de importação foi enviada. Isso pode levar algum tempo." lists: "Listas" -noLists: "Não tens nenhuma lista" +noLists: "Não possui nenhuma lista" note: "Post" notes: "Posts" following: "Seguindo" followers: "Seguidores" -followsYou: "Segue-te" +followsYou: "Te seguem" createList: "Criar lista" -manageLists: "Gerir listas" +manageLists: "Gerenciar listas" error: "Erro" somethingHappened: "Ocorreu um erro" -retry: "Tentar novamente" +retry: "Tente novamente" pageLoadError: "Ocorreu um erro ao carregar a página." -pageLoadErrorDescription: "Isto é normalmente causado por erros de rede ou pela cache do browser. Experimenta limpar a cache e tenta novamente após algum tempo." -serverIsDead: "O servidor não está respondendo. Por favor espere um pouco e tente novamente." -youShouldUpgradeClient: "Para visualizar essa página, por favor recarregue-a para atualizar seu cliente." +pageLoadErrorDescription: "Isso geralmente acontece devido ao cache do navegador ou da rede. Tente limpar o cache ou aguarde um pouco antes de tentar novamente." +serverIsDead: "Não há resposta do servidor. Aguarde um momento e tente novamente." +youShouldUpgradeClient: "Para visualizar esta página, recarregue-a e utilize a nova versão do cliente." enterListName: "Insira um nome para a lista" privacy: "Privacidade" -makeFollowManuallyApprove: "Pedidos de seguimento precisam ser aprovados" +makeFollowManuallyApprove: "Pedidos de seguidores precisam ser aprovados" defaultNoteVisibility: "Visibilidade padrão" follow: "Seguindo" -followRequest: "Mandar pedido de seguimento" -followRequests: "Pedidos de seguimento" +followRequest: "Enviar pedido de seguidor" +followRequests: "Pedidos de seguidor" unfollow: "Deixar de seguir" -followRequestPending: "Pedido de seguimento pendente" +followRequestPending: "Pedido de seguidor pendente" enterEmoji: "Inserir emoji" renote: "Repostar" -unrenote: "Desmarcar" +unrenote: "Remover repostagem" renoted: "Repostado" -cantRenote: "Não pode repostar" +cantRenote: "Não é possível repostar esta postagem" cantReRenote: "Não pode repostar este repost" quote: "Citar" -pinnedNote: "Post fixado" -pinned: "Afixar no perfil" +inChannelRenote: "Repostar no canal" +inChannelQuote: "Citar no canal" +pinnedNote: "Nota fixada" +pinned: "Fixar no perfil" you: "Você" clickToShow: "Clique para ver" sensitive: "Conteúdo sensível" add: "Adicionar" reaction: "Reações" reactions: "Reações" -reactionSetting: "Quais reações a mostrar no selecionador de reações" +reactionSetting: "Quais reações exibir no seletor de reações" reactionSettingDescription2: "Arraste para reordenar, clique para excluir, pressione + para adicionar." rememberNoteVisibility: "Lembrar das configurações de visibilidade de notas" attachCancel: "Remover anexo" markAsSensitive: "Marcar como sensível" unmarkAsSensitive: "Desmarcar como sensível" -enterFileName: "Digite o nome do ficheiro" -mute: "Silenciar" -unmute: "Dessilenciar" +enterFileName: "Digite o nome do arquivo" +mute: "Mutar" +unmute: "Desmutar" +renoteMute: "Mutar repostagens" +renoteUnmute: "Reativar repostagens" block: "Bloquear" unblock: "Desbloquear" suspend: "Suspender" unsuspend: "Cancelar suspensão" -blockConfirm: "Tem certeza que gostaria de bloquear essa conta?" -unblockConfirm: "Tem certeza que gostaria de desbloquear essa conta?" -suspendConfirm: "Tem certeza que gostaria de suspender essa conta?" -unsuspendConfirm: "Tem certeza que gostaria de cancelar a suspensão dessa conta?" -selectList: "Escolhe uma lista" -selectAntenna: "Escolhe uma antena" -selectWidget: "Escolhe um widget" +blockConfirm: "Tem certeza que gostaria de bloquear esta conta?" +unblockConfirm: "Tem certeza que gostaria de desbloquear esta conta?" +suspendConfirm: "Tem certeza que gostaria de suspender esta conta?" +unsuspendConfirm: "Tem certeza que gostaria de cancelar a suspensão desta conta?" +selectList: "Selecione uma lista" +editList: "Editar lista" +selectChannel: "Selecionar canal" +selectAntenna: "Selecione uma antena" +editAntenna: "Editar antena" +selectWidget: "Selecione um widget" editWidgets: "Editar widgets" editWidgetsExit: "Pronto" customEmojis: "Emoji personalizado" @@ -137,17 +155,21 @@ emojiName: "Nome do Emoji" emojiUrl: "URL do Emoji" addEmoji: "Adicionar um Emoji" settingGuide: "Guia de configuração" -cacheRemoteFiles: "Memória transitória de arquivos remotos" -cacheRemoteFilesDescription: "Se você desabilitar essa configuração, os arquivos remotos não serão armazenados em memória transitória e serão vinculados diretamente. Economiza o armazenamento do servidor, mas não gera miniaturas, o que aumenta o tráfego." +cacheRemoteFiles: "Cache de arquivos remotos" +cacheRemoteFilesDescription: "Ao desativar esta configuração, os arquivos remotos não serão mais armazenados em cache e serão vinculados diretamente. Isso economizará espaço de armazenamento no servidor, mas os thumbnails não serão gerados, o que pode aumentar o tráfego de dados." +youCanCleanRemoteFilesCache: "Pode excluir todos os caches com o botão 🗑️ de gestão de arquivos." +cacheRemoteSensitiveFiles: "Fazer cache de arquivos remotos sensíveis" +cacheRemoteSensitiveFilesDescription: "Desativar essa configuração faz com que arquivos remotos sensíveis sejam vinculados diretamente em vez de armazenados em cache." flagAsBot: "Marcar conta como robô" -flagAsBotDescription: "Se esta conta for operada por um programa, ative este sinalizador. Quando ativado, serve como um sinalizador para evitar o encadeamento de reações para outros programadores, e o manuseio do sistema do Misskey é adequado para ‘bots’." +flagAsBotDescription: "Se esta conta for operada por uma aplicação, ative esta opção. Ao ativá-la, ela servirá como um sinalizador para evitar reações em cadeia e ajudar outros desenvolvedores. Além disso, ajustará o tratamento da conta no sistema do Misskey para que se adeque a um Bot." flagAsCat: "Marcar conta como gato" -flagAsCatDescription: "Ative essa opção para marcar essa conta como gato." +flagAsCatDescription: "Ative esta opção para marcar essa conta como gato" flagShowTimelineReplies: "Mostrar respostas na linha de tempo" flagShowTimelineRepliesDescription: "Quando ativado, a linha do tempo mostra as respostas às outras notas do utilizador, além da nota do utilizador." autoAcceptFollowed: "Aprove automaticamente os seguidores dos seguintes utilizadores" addAccount: "Adicionar Conta" -loginFailed: "Não consegui logar" +reloadAccountsList: "Recarregar lista de contas" +loginFailed: "Falha ao logar" showOnRemote: "Exibir remotamente" general: "Geral" wallpaper: "Papel de parede" @@ -157,111 +179,116 @@ searchWith: "Buscar: {q}" youHaveNoLists: "Não tem nenhuma lista" followConfirm: "Tem certeza que quer deixar de seguir {name}?" proxyAccount: "Conta proxy" -proxyAccountDescription: "Uma conta proxy é uma conta que atua como seguidora remota para utilizadores sob determinadas condições. Por exemplo, quando um utilizador lista um utilizador remoto, a atividade não será entregue à instância, a menos que alguém esteja seguindo o utilizador listado, portanto, a conta proxy deve seguir." -host: "hospedeiro" -selectUser: "Selecionar utilizador" -recipient: "Morada" +proxyAccountDescription: "Uma conta de proxy é uma conta que assume o acompanhamento remoto de um usuário sob certas condições específicas. Por exemplo, quando um usuário inclui um usuário remoto em uma lista, mas ninguém na lista está seguindo o usuário remoto, a atividade não é entregue ao servidor. Nesse caso, a conta de proxy entra em ação para seguir o usuário remoto em vez disso." +host: "Host" +selectUser: "Selecionar usuário" +recipient: "Destinatário" annotation: "Anotação" -federation: "União" -instances: "Instância" +federation: "Federação" +instances: "Instâncias" registeredAt: "Registrado em" -latestRequestReceivedAt: "Recebeu a última solicitação" +latestRequestReceivedAt: "Última solicitação recebida" latestStatus: "Status mais recente" storageUsage: "Uso de armazenamento" -charts: "gráfico" -perHour: "por hora" -perDay: "por dia" +charts: "Gráfico" +perHour: "Por Hora" +perDay: "Por dia" stopActivityDelivery: "Parar a entrega de atividades" blockThisInstance: "Bloquear esta instância" -operations: "operar" -software: "Programas" -version: "versão" +operations: "Operações" +software: "Software" +version: "Versão" metadata: "Metadados" -withNFiles: "{n} Um arquivo" +withNFiles: "{n} arquivo(s)" monitor: "monitor" -jobQueue: "Fila de trabalhos" +jobQueue: "Fila de tarefas" cpuAndMemory: "CPU e memória" -network: "rede" -disk: "disco" +network: "Rede" +disk: "Disco" instanceInfo: "Informações da instância" -statistics: "Estatisticas" +statistics: "Estatísticas" clearQueue: "Limpar a fila" -clearQueueConfirmTitle: "Quer limpar a fila?" -clearQueueConfirmText: "Postagens não entregues não serão mais entregues. Normalmente você não precisa fazer isso." -clearCachedFiles: "Limpar memória transitória" -clearCachedFilesConfirm: "Tem certeza de que deseja excluir todos os arquivos remotos armazenados em memória transitória?" +clearQueueConfirmTitle: "Deseja limpar a fila?" +clearQueueConfirmText: "As postagens não entregues deixarão de ser enviadas. Geralmente, não é necessário realizar essa operação." +clearCachedFiles: "Limpar o cache" +clearCachedFilesConfirm: "Deseja excluir todos os arquivos remotos em cache?" blockedInstances: "Instância bloqueada" -blockedInstancesDescription: "Defina os anfitriões das instâncias que deseja bloquear, separados por quebras de linha. Uma instância bloqueada não poderá interagir com esta instância." +blockedInstancesDescription: "Configure os hosts dos servidores que deseja bloquear, separando-os por quebras de linha. Os servidores bloqueados não poderão interagir com este servidor, incluindo os subdomínios." muteAndBlock: "Silenciar e bloquear" -mutedUsers: "Silenciar utilizador" -blockedUsers: "Utilizadores bloqueados" +mutedUsers: "Usuários silenciados" +blockedUsers: "Usuários bloqueados" noUsers: "Sem usuários" editProfile: "Editar Perfil" noteDeleteConfirm: "Deseja excluir esta nota?" -pinLimitExceeded: "Não consigo mais fixar" +pinLimitExceeded: "Não é possível fixar novas notas" intro: "A instalação do Misskey está completa! Crie uma conta de administrador." done: "Concluído" processing: "Em Progresso" preview: "Pré-visualizar" -default: "Padrão" +default: "Predefinição" +defaultValueIs: "Predefinição: {value}" noCustomEmojis: "Não há emojis" -noJobs: "Sem trabalho" -federating: "federar" +noJobs: "Não há tarefas" +federating: "Federando" blocked: "Bloqueado" -suspended: "Cancelar subscrição" +suspended: "Suspenso" all: "Todos" -subscribing: "Subscrito" -publishing: "Executando" +subscribing: "Inscrito" +publishing: "Publicando" notResponding: "Sem resposta" instanceFollowing: "Seguir a instância" instanceFollowers: "Seguidores da instância" -instanceUsers: "Utilizador da instância" +instanceUsers: "Usuários da instância" changePassword: "Mudar senha" security: "Segurança" -retypedNotMatch: "As entradas não coincidem." -currentPassword: "Palavra-passe atual" -newPassword: "Nova palavra-passe" -newPasswordRetype: "Nova senha (redigite)" +retypedNotMatch: "As informações inseridas não coincidem." +currentPassword: "Senha atual" +newPassword: "Nova senha" +newPasswordRetype: "Nova senha (digite novamente)" attachFile: "Anexar arquivo" more: "Mais!" featured: "Destaques" -usernameOrUserId: "Nome de utilizador ou ID de utilizador" -noSuchUser: "Utilizador não encontrado" +usernameOrUserId: "Nome de usuário ou ID do usuário" +noSuchUser: "Usuário não encontrado" lookup: "Buscando" -announcements: "Notícia" +announcements: "Avisos" imageUrl: "URL da imagem" -remove: "Eliminar" -removed: "Foi deletado" +remove: "Remover" +removed: "Removido" removeAreYouSure: "Deseja excluir \"{x}\"?" deleteAreYouSure: "Deseja excluir \"{x}\"?" -resetAreYouSure: "Redefinir agora?" +resetAreYouSure: "Deseja reiniciar?" saved: "Salvo" messaging: "Chat" -upload: "Enviando" +upload: "Fazer upload" keepOriginalUploading: "Manter a imagem original" -keepOriginalUploadingDescription: "Mantenha a versão original ao carregar a imagem. Quando desligado, a imagem para publicação na web será gerada no navegador no momento do upload." -fromDrive: "\nDa unidade" +keepOriginalUploadingDescription: "Ao fazer o upload de uma imagem, ela será mantida em sua versão original. Caso desative esta opção, o navegador irá gerar uma versão da imagem otimizada para publicação na web durante o upload." +fromDrive: "Do drive" fromUrl: "Da URL" -uploadFromUrl: "Carregamento de URL" +uploadFromUrl: "Enviar por URL" uploadFromUrlDescription: "URL do arquivo que você deseja enviar" uploadFromUrlRequested: "Upload solicitado" uploadFromUrlMayTakeTime: "Pode levar algum tempo para que o upload seja concluído." explore: "Explorar" messageRead: "Lida" -noMoreHistory: "Sem mais história" +noMoreHistory: "Não existe histórico anterior" startMessaging: "Iniciar conversação" nUsersRead: "{n} Pessoas leem" agreeTo: "Eu concordo com {0}" +agree: "Concordar" +agreeBelow: "Eu concordo com o seguinte" +basicNotesBeforeCreateAccount: "Observações importantes" +termsOfService: "Termos de Uso" start: "começar" -home: "casa" +home: "Início" remoteUserCaution: "As informações estão incompletas porque é um utilizador remoto." activity: "atividade" images: "imagem" image: "imagem" -birthday: "aniversário" +birthday: "Aniversário" yearsOld: "{age} anos" registeredDate: "Data de registro" -location: "Lugar, colocar" +location: "Localização" theme: "tema" themeForLightMode: "Temas usados ​​no modo de luz" themeForDarkMode: "Temas usados ​​no modo escuro" @@ -270,7 +297,7 @@ dark: "Escuro" lightThemes: "Tema claro" darkThemes: "Tema escuro" syncDeviceDarkMode: "Sincronize com o modo escuro do dispositivo" -drive: "Unidades" +drive: "Drive" fileName: "Nome do Ficheiro" selectFile: "Selecione os arquivos" selectFiles: "Selecione os arquivos" @@ -280,11 +307,11 @@ renameFile: "Renomear ficheiro" folderName: "Nome da pasta" createFolder: "Criar pasta" renameFolder: "Renomear Pasta" -deleteFolder: "Eliminar Pasta" +deleteFolder: "Excluir pasta" addFile: "Adicionar arquivo" -emptyDrive: "A unidade está vazia" +emptyDrive: "O drive está vazio" emptyFolder: "A pasta está vazia" -unableToDelete: "Não é possível eliminar" +unableToDelete: "Não é possível excluir" inputNewFileName: "Por favor, digite um novo nome para a pasta!" inputNewDescription: "Insira uma nova legenda" inputNewFolderName: "Por favor, digite um novo nome para a pasta!" @@ -294,7 +321,7 @@ copyUrl: "Copiar URL" rename: "Renomear" avatar: "Avatar" banner: "Capa" -nsfw: "Conteúdo sensível" +displayOfSensitiveMedia: "Exibição de mídia sensível" whenServerDisconnected: "Quando a conexão com o servidor é perdida" disconnectedFromServer: "Desconectado do servidor" reload: "Recarregar" @@ -326,10 +353,9 @@ disablingTimelinesInfo: "Se você desabilitar essas linhas do tempo, administrad registration: "Registar" enableRegistration: "Permitir que qualquer pessoa se registre" invite: "Convidar" -driveCapacityPerLocalAccount: "Capacidade da unidade por utilizador local" -driveCapacityPerRemoteAccount: "Capacidade da unidade por utilizador remoto" +driveCapacityPerLocalAccount: "Capacidade do drive por usuário local" +driveCapacityPerRemoteAccount: "Capacidade do drive por usuário remoto" inMb: "Em ‘megabytes’" -iconUrl: "URL da imagem do ícone (favicon, etc.)" bannerUrl: "URL da imagem do ‘banner’" backgroundImageUrl: "URL da imagem de fundo" basicInfo: "Informações básicas" @@ -347,6 +373,8 @@ recaptcha: "reCAPTCHA" enableRecaptcha: "Habilitar reCAPTCHA" recaptchaSiteKey: "Chave do sítio ‘web’" recaptchaSecretKey: "Chave secreta" +turnstile: "Controle de acesso" +enableTurnstile: "Ativar controle de acesso" turnstileSiteKey: "Chave do sítio ‘web’" turnstileSecretKey: "Chave secreta" avoidMultiCaptchaConfirm: "O uso de vários captchas pode causar interferência. Deseja desativar outros captchas? Você também pode cancelar e deixar vários captchas ativados." @@ -382,19 +410,27 @@ about: "Informações" aboutMisskey: "Sobre Misskey" administrator: "Administrador" token: "Símbolo" +2fa: "Autenticação de dois fatores" +setupOf2fa: "Configuração de autenticação de dois fatores" +totp: "Aplicativo Autenticador" +totpDescription: "Digite a senha de uso único informado pelo aplicativo autenticador" moderator: "Moderador" +moderation: "Moderação" nUsersMentioned: "Postado por {n} pessoas" +securityKeyAndPasskey: "Chave de segurança / Chave de acesso" securityKey: "Chave de segurança" lastUsed: "Último uso" +lastUsedAt: "Última utilização: {t}" unregister: "Cancelar registro" passwordLessLogin: "Entrar sem senha" +passwordLessLoginDescription: "Faça login apenas com uma chave de segurança / chave de acesso sem utilização de senha" resetPassword: "Redefinir senha" newPasswordIs: "A nova senha é \"{password}\"" reduceUiAnimation: "Reduzir a animação da ‘interface’ do utilizador" share: "Compartilhar" notFound: "Não encontrado" notFoundDescription: "Não havia página correspondente ao URL especificado." -uploadFolder: "Destino de ‘upload’ padrão" +uploadFolder: "Destino de upload padrão" cacheClear: "Excluir memória transitória" markAsReadAllNotifications: "Marcar todas as notificações como lidas" markAsReadAllUnreadNotes: "Marcar todas as postagens como lidas" @@ -403,15 +439,59 @@ help: "Ajuda" inputMessageHere: "Escrever mensagem aqui" close: "Fechar" invites: "Convidar" +members: "Membros" +transfer: "Transferência" +title: "Título" +text: "Texto" +enable: "Habilitar" +next: "Seguinte" +retype: "Digite novamente" +noteOf: "Publicação de {user}" +quoteAttached: "Com citação" +quoteQuestion: "Anexar como citação?" +noMessagesYet: "Sem conversas até o momento" +newMessageExists: "Há uma nova mensagem" +onlyOneFileCanBeAttached: "Apenas um arquivo pode ser anexado a uma mensagem" +signinRequired: "É necessário se inscrever ou fazer login antes de continuar" invitations: "Convidar" +invitationCode: "Código de convite" +checking: "Verificando..." +available: "Disponível" +unavailable: "Não disponível" +usernameInvalidFormat: "Pode utilizar letras maiúsculas e minúsculas, números e sublinhado (_)" +tooShort: "Muito curto" +tooLong: "Muito longo" +weakPassword: "Senha fraca" +normalPassword: "Senha normal" +strongPassword: "Senha forte" +passwordMatched: "As senhas coincidem" +passwordNotMatched: "As senhas não coincidem" +signinWith: "Faça login com {x}" +signinFailed: "Não foi possível fazer login. Por favor, verifique o nome de usuário e a senha." +or: "Ou" +language: "Idioma" +uiLanguage: "Idioma de exibição da interface " +aboutX: "Sobre {x}" +emojiStyle: "Estilo de emojis" +native: "Nativo" +disableDrawer: "Não mostrar o menu em formato de gaveta" +showNoteActionsOnlyHover: "Exibir as ações da nota somente ao passar o cursor sobre ela" +noHistory: "Ainda não há histórico" +signinHistory: "Histórico de acesso" +enableAdvancedMfm: "Habilitar MFM avançado" +enableAnimatedMfm: "Habilitar MFM animado" +doing: "Processando..." +category: "Categoria" tags: "Etiquetas" docSource: "Fonte deste documento" createAccount: "Criar conta" existingAccount: "Contas existentes" regenerate: "Gerar novamente" fontSize: "Tamanho do texto" -noFollowRequests: "Não há aplicação de acompanhamento" -openImageInNewTab: "Abrir a imagem numa nova aba" +mediaListWithOneImageAppearance: "Altura da lista de mídias com apenas uma imagem" +limitTo: "Até {x}" +noFollowRequests: "Não há pedidos de seguidor pendentes" +openImageInNewTab: "Abrir a imagem em uma nova aba" dashboard: "Painel de controle" local: "Local" remote: "Remoto" @@ -434,8 +514,8 @@ objectStorageBucket: "Bucket" objectStorageBucketDesc: "Especifique o nome do bucket do serviço a ser usado." objectStoragePrefix: "Prefixo" objectStoragePrefixDesc: "Ele é armazenado neste diretório de prefixo." -objectStorageEndpoint: "Ponto final" -objectStorageEndpointDesc: "Especifique vazio para S3, caso contrário, especifique o ponto final para cada serviço. Especifique como''ou': '." +objectStorageEndpoint: "Endpoint" +objectStorageEndpointDesc: "No caso do S3, deixe em branco; para outros serviços, especifique o endpoint de cada serviço. Informe-o no formato '' ou ':'." objectStorageRegion: "Região" objectStorageRegionDesc: "Especifique uma região como 'xx-east-1'. Caso seu serviço não tenha o conceito de região, ele deve estar vazio ou 'us-east-1'." objectStorageUseSSL: "Usar SSL" @@ -443,9 +523,11 @@ objectStorageUseSSLDesc: "Desative-o se não quiser usar https para conexões de objectStorageUseProxy: "Usar proxy" objectStorageUseProxyDesc: "Se você não usa proxy para conexão de API, desative-o." objectStorageSetPublicRead: "Definir 'public-read' ao fazer o upload" +s3ForcePathStyleDesc: "Ao habilitar s3ForcePathStyle, o nome do bucket é especificado como parte do caminho em vez de ser o nome do host na URL. Isso pode ser necessário ao usar serviços auto-hospedados como o Minio." serverLogs: "Registro do servidor" -deleteAll: "Apagar Tudo" +deleteAll: "Excluir tudo" showFixedPostForm: "Exibir o formulário de postagem na parte superior da linha do tempo" +showFixedPostFormInChannel: "Exibir o campo de postagem na parte superior da linha do tempo (canais)" newNoteRecived: "Nova nota recebida" sounds: "Sons" sound: "Sons" @@ -456,45 +538,874 @@ popout: "Sair" volume: "Volume" masterVolume: "volume principal" details: "Detalhes" +chooseEmoji: "Selecione um emoji" +unableToProcess: "Não é possível concluir a operação" +recentUsed: "Usado recentemente" +install: "Instalar" +uninstall: "Desinstalar" +installedApps: "Aplicativos instalados" +nothing: "Não há nada aqui" +installedDate: "Data de instalação" +lastUsedDate: "Data de última utilização" +state: "Estado" +sort: "Ordenação" +ascendingOrder: "Ascendente" +descendingOrder: "Descendente" +scratchpad: "Bloco de rascunho" +scratchpadDescription: "O Bloco de rascunho fornece um ambiente experimental para AiScript. Permite escrever, executar e verificar os resultados do código para interagir com o Misskey." output: "Resultado" -smtpHost: "hospedeiro" +script: "Script" +disablePagesScript: "Desabilitar scripts nas páginas" +updateRemoteUser: "Atualizar informações do usuário remoto" +deleteAllFiles: "Excluir todos os arquivos" +deleteAllFilesConfirm: "Deseja excluir todos os arquivos?" +removeAllFollowing: "Deseja remover todos os seguidores?" +removeAllFollowingDescription: "Deixar de seguir todos de {host}. Faça isso se, por exemplo, o servidor não existir mais." +userSuspended: "Este usuário foi suspenso." +userSilenced: "Este usuário está silenciado." +yourAccountSuspendedTitle: "Esta conta está suspensa" +yourAccountSuspendedDescription: "Esta conta foi suspensa devido a violações dos termos de uso do servidor ou por outros motivos. Para mais detalhes, entre em contato com o administrador. Por favor, não crie uma nova conta." +tokenRevoked: "Token inválido" +tokenRevokedDescription: "Seu token de login expirou. Por favor, faça login novamente." +accountDeleted: "A conta foi removida" +accountDeletedDescription: "Esta conta foi removida." +menu: "Menu\n" +divider: "Separador" +addItem: "Adicionar item" +rearrange: "Reordernar" +relays: "Relays" +addRelay: "Adicionar relay" +inboxUrl: "Inbox URL" +addedRelays: "Relays adicionados" +serviceworkerInfo: "Deve estar habilitado para receber notificações por push." +deletedNote: "Postagem excluída" +invisibleNote: "Notas invisíveis" +enableInfiniteScroll: "Carregar automaticamente" +visibility: "Visibilidade" +poll: "Enquetes" +useCw: "Ocultar conteúdo" +enablePlayer: "Abrir o reprodutor de mídia" +disablePlayer: "Fechar o reprodutor de mídia" +expandTweet: "Expandir tweet" +themeEditor: "Editor de temas" +description: "Descrição" +describeFile: "Adicionar legenda" +enterFileDescription: "Insira uma legenda" +author: "Autor" +leaveConfirm: "Existem alterações não salvas. Deseja descartá-las?" +manage: "Administrar" +plugins: "Plugins" +preferencesBackups: "Definições de Backup" +deck: "Deck" +undeck: "Sair do deck" +useBlurEffectForModal: "Usar efeito de desfoque para modal" +useFullReactionPicker: "Usar o seletor de reações completo" +width: "Largura" +height: "Altura" +large: "Grande" +medium: "Médio" +small: "Pequeno" +generateAccessToken: "Gerar token de acesso" +permission: "Permissões" +enableAll: "Habilitar tudo" +disableAll: "Desabilitar tudo" +tokenRequested: "Autorização de acesso à conta" +pluginTokenRequestedDescription: "Este plugin poderá utilizar as permissões definidas aqui." +notificationType: "Tipos de notificação" +edit: "Editar" +emailServer: "Servidor de e-mail" +enableEmail: "Habilitar envio de e-mails" +emailConfigInfo: "Usado para confirmar o seu endereço de e-mail e redefinir sua senha" +email: "E-mail" +emailAddress: "Endereço de e-mail" +smtpConfig: "Configuração do servidor SMTP" +smtpHost: "Host" +smtpPort: "Porta" smtpUser: "Nome de usuário" smtpPass: "Senha" -clearCache: "Limpar memória transitória" +emptyToDisableSmtpAuth: "Desative a autenticação SMTP deixando o nome de usuário e a senha em branco." +smtpSecure: "Use SSL/TLS implícito para conexões SMTP" +smtpSecureInfo: "Desative esta opção ao utilizar STARTTLS." +testEmail: "Testar envio de e-mail" +wordMute: "Silenciar palavras" +regexpError: "Erro na expressão regular" +regexpErrorDescription: "Ocorreu um erro na expressão regular na linha {line} da palavra mutada {tab}:" +instanceMute: "Instâncias silenciadas" +userSaysSomething: "{name} disse algo" +makeActive: "Ativar" +display: "Visualizar" +copy: "Copiar" +metrics: "Métricas" +overview: "Visão geral" +logs: "Logs" +delayed: "atrasado" +database: "Banco de dados" +channel: "Canais" +create: "Criar" +notificationSetting: "Configurações de notificação" +notificationSettingDesc: "Selecione o tipo de notificação a ser exibido." +useGlobalSetting: "Utilizar a configuração global" +useGlobalSettingDesc: "Ao ativar, serão utilizadas as configurações de notificação da conta. Ao desativar, você poderá configurar individualmente." +other: "Outros" +regenerateLoginToken: "Gerar novo token de login" +regenerateLoginTokenDescription: "Gera novamente o token interno usado para o login. Normalmente, isso não é necessário. Ao regenerar, você será desconectado de todos os dispositivos." +setMultipleBySeparatingWithSpace: "Você pode configurar vários itens separando-os por espaço." +fileIdOrUrl: "ID do arquivo ou URL" +behavior: "Comportamento" +sample: "Exemplo" +abuseReports: "Denúncias" +reportAbuse: "Denúncias" +reportAbuseRenote: "Reportar repostagem" +reportAbuseOf: "Denunciar {name}" +fillAbuseReportDescription: "Por favor, forneça detalhes sobre o motivo da denúncia. Se houver uma nota específica envolvida, inclua também a URL dela." +abuseReported: "Denúncia enviada. Obrigado por sua ajuda." +reporter: "Denunciante" +reporteeOrigin: "Origem da denúncia" +reporterOrigin: "Origem do denunciante" +forwardReport: "Encaminhar a denúncia para o servidor remoto" +forwardReportIsAnonymous: "No servidor remoto, suas informações não serão visíveis, e você será apresentado como uma conta do sistema anônima." +send: "Enviar" +abuseMarkAsResolved: "Marcar denúncia como resolvida" +openInNewTab: "Abrir em nova aba" +openInSideView: "Abrir em visão lateral" +defaultNavigationBehaviour: "Navegação padrão" +editTheseSettingsMayBreakAccount: "Editar essas configurações pode resultar em danos à conta.\"" +instanceTicker: "Informações do servidor das notas" +waitingFor: "Aguardando por {x}" +random: "Aleatório" +system: "Sistema" +switchUi: "Alternar UI" +desktop: "Área de Trabalho" +clip: "Clipe" +createNew: "Criar novo" +optional: "Opcional" +createNewClip: "Criar novo clipe" +unclip: "Remover do clipe" +confirmToUnclipAlreadyClippedNote: "Esta nota já está incluída no clipe \"{name}\". Você deseja remover a nota deste clipe?" +public: "Público" +private: "Privado" +i18nInfo: "Misskey é traduzido para várias línguas por voluntários. Você pode ajudar com as traduções em {link}." +manageAccessTokens: "Gerenciar tokens de acesso" +accountInfo: "Informações da conta" +notesCount: "Número de notas" +repliesCount: "Número de respostas enviadas" +renotesCount: "Número de repostagens feitas" +repliedCount: "Número de respostas recebidas" +renotedCount: "Números de repostagens recebidas" +followingCount: "Número de contas seguidas" +followersCount: "Número de seguidores" +sentReactionsCount: "Número de reações enviadas" +receivedReactionsCount: "Número de reações recebidas" +pollVotesCount: "Número de votos feitos em enquetes" +pollVotedCount: "Número de votos recebidos em enquetes" +yes: "Sim" +no: "Não" +driveFilesCount: "Número de arquivos no drive" +driveUsage: "Capacidade do drive" +noCrawle: "Recusar indexação por crawler" +noCrawleDescription: "Solicitar que os mecanismos de pesquisa externos não indexem o conteúdo de suas páginas de usuário, notas, páginas etc." +lockedAccountInfo: "Mesmo que você defina a aprovação para seguir, a menos que você defina o alcance da nota para 'Apenas seguidores', qualquer pessoa poderá ver suas notas." +alwaysMarkSensitive: "Marcar como sensível por padrão" +loadRawImages: "Exibir as imagens originais ao invés de miniaturas" +disableShowingAnimatedImages: "Não reproduzir imagens animadas" +verificationEmailSent: "Um e-mail de confirmação foi enviado. Siga o link no e-mail para concluir a verificação." +notSet: "Não definido" +emailVerified: "O endereço de e-mail foi confirmado" +noteFavoritesCount: "Número de notas salvas nos favoritos" +pageLikesCount: "Número de páginas curtidas" +pageLikedCount: "Número de curtidas recebidas nas suas páginas" +contact: "Contato" +useSystemFont: "Utilizar a fonte padrão do sistema" +clips: "Clipe" +experimentalFeatures: "Funcionalidades Experimentais" +experimental: "Experimental" +thisIsExperimentalFeature: "Este é um recurso experimental. As funções podem mudar ou pode não funcionar corretamente." +developer: "Programador" +makeExplorable: "Deixe a sua conta mais fácil de encontrar." +makeExplorableDescription: "Se você desativá-lo, outros usuários não poderão encontrar a sua conta na aba Descoberta." +showGapBetweenNotesInTimeline: "Mostrar um espaço entre as notas na linha de tempo" +duplicate: "Duplicar" +left: "Esquerda" +center: "Centralizar" +wide: "Largo" +narrow: "Estreito" +reloadToApplySetting: "As configurações serão refletidas após recarregar a página. Deseja recarregar agora?" +needReloadToApply: "É necessário recarregar a página para refletir as alterações." +showTitlebar: "Exibir barra de título" +clearCache: "Limpar o cache" +onlineUsersCount: "Pessoas Online" +nUsers: "Usuários" +nNotes: "Notas" +sendErrorReports: "Enviar relatórios de erro" +sendErrorReportsDescription: "Ao ativar essa opção, informações detalhadas de erro serão compartilhadas com o Misskey em caso de problemas, o que pode ajudar a melhorar a qualidade do software. As informações de erro podem incluir a versão do sistema operacional, o tipo de navegador e o sua atividade no Misskey." +myTheme: "Meu tema" +backgroundColor: "Cor de fundo" +accentColor: "Cor de destaque" +textColor: "Cor do texto" +saveAs: "Salvar como" +advanced: "Avançado" +advancedSettings: "Configurações avançadas" +value: "Valor" +createdAt: "Data de criação" +updatedAt: "Última atualização" +saveConfirm: "Deseja salvá-lo?" +deleteConfirm: "Confirma a exclusão?" +invalidValue: "Valor inválido" +registry: "Registo" +closeAccount: "Encerrar conta" +currentVersion: "Versão Atual" +latestVersion: "Última versão" +youAreRunningUpToDateClient: "Você está usando a última versão do cliente" +newVersionOfClientAvailable: "Nova versão do cliente disponível" +usageAmount: "Quantidade utilizada" +capacity: "Capacidade" +inUse: "Em uso" +editCode: "Editar código" +apply: "Aplicar" +receiveAnnouncementFromInstance: "Receba as notificações da instância" +emailNotification: "Notificações por e-mail" +publish: "Publicar" +inChannelSearch: "Pesquisar no canal" +useReactionPickerForContextMenu: "Clique com o botão direito do mouse para abrir o seletor de reações." +typingUsers: "digitando" +jumpToSpecifiedDate: "Pular para uma data específica" +showingPastTimeline: "Visualizar linha de tempo anterior" +clear: "Limpar" +markAllAsRead: "Marcar todas como lidas" +goBack: "Voltar" +unlikeConfirm: "Deseja realmente deixar de curtir?" +fullView: "Visão completa" +quitFullView: "Sair da visualização completa" +addDescription: "Adicionar descrição" +userPagePinTip: "Notas podem ser mostradas aqui ao clicar em \"Fixar no Perfil\" no menu de notas individuais." +notSpecifiedMentionWarning: "Esta nota menciona usuários que não foram incluídos como recipientes." info: "Informações" +userInfo: "Informações do Usuário" +unknown: "Desconhecido" +onlineStatus: "On-line" +hideOnlineStatus: "Ocultar o status on-line." +hideOnlineStatusDescription: "Esconder que está Ativo reduzirá a utilidade de certas funções (como, por exemplo, a Pesquisa)." +online: "Online" +active: "Ativo" +offline: "Inativo" +notRecommended: "Não recomendado" +botProtection: "Proteção contra Bot" +instanceBlocking: "Instâncias bloqueadas" +selectAccount: "Selecionar conta" +switchAccount: "Trocar conta" +enabled: "Ativado" +disabled: "Desativado" +quickAction: "Ações rápidas" user: "Usuários" -searchByGoogle: "Buscar" +administration: "Administrar" +accounts: "Contas" +switch: "Trocar" +noMaintainerInformationWarning: "A informação de administrador não foi configurada." +noBotProtectionWarning: "A proteção contra bots não foi configurada." +configure: "Configurar" +postToGallery: "Criar publicação em galeria" +postToHashtag: "Publicar nesta Hashtag" +gallery: "Galeria" +recentPosts: "Notas recentes" +popularPosts: "Notas populares" +shareWithNote: "Compartilhar em Notas" +ads: "Anúncios" +expiration: "Data limite" +startingperiod: "Data de início" +memo: "Nota" +priority: "Prioridade" +high: "Alto" +middle: "Meio" +low: "Baixo" +emailNotConfiguredWarning: "Endereço de e-mail não configurado. " +ratio: "Ratio" +previewNoteText: "Visualizar Nota" +customCss: "CSS Personalizado" +customCssWarn: "Esta configuração só deve ser usada se souber o que está fazendo. Valores impróprios podem causar erros no funcionamento do cliente." +global: "Global" +squareAvatars: "Exibir ícones quadrados" +sent: "Enviar" +received: "Recebido" +searchResult: "Pesquisar" +hashtags: "Hashtags" +troubleshooting: "Resolução de problemas" +useBlurEffect: "Usar efeito de desfoque na UI" +learnMore: "Saiba mais" +misskeyUpdated: "Misskey foi atualizado!" +whatIsNew: "Ver atualizações" +translate: "Traduzir" +translatedFrom: "Traduzido de" +accountDeletionInProgress: "Encerramento de conta em andamento" +usernameInfo: "O nome para identificar exclusivamente a sua conta no servidor. Pode conter letras (az, AZ), números (0~9) e sublinhados (_). O nome de usuário não pode ser alterado posteriormente." +aiChanMode: "Modo AI-chan" +devMode: "Modo de Desenvolvedor" +keepCw: "Manter aviso de conteúdo" +pubSub: "Publicar/Inscrever no perfil" +lastCommunication: "Ultima atualização" +resolved: "Resolvido" +unresolved: "Não resolvido" +breakFollow: "Remover seguidor" +breakFollowConfirm: "Deseja realmente deixar de seguir?" +itsOn: "Ativado" +itsOff: "Desativado" +on: "Ligado" +off: "Desligado" +emailRequiredForSignup: "Tornar o endereço de e-mail obrigatório durante o cadastro" +unread: "Não lido" +filter: "Filtrar" +controlPanel: "Painel de controle" +manageAccounts: "Gerenciar contas" +makeReactionsPublic: "Deixar o histórico de reações em Público" +makeReactionsPublicDescription: "Isto vai deixar o histórico de todas as suas reações visíveis para qualquer um ver." +classic: "Clássico" +muteThread: "Silenciar esta conversa" +unmuteThread: "Desativar silêncio desta conversa" +ffVisibility: "Visibilidade de Seguidos/Seguidores" +ffVisibilityDescription: "Permite configurar quem pode ver quem lhe segue e quem você está seguindo." +continueThread: "Ver mais desta conversa" +deleteAccountConfirm: "Deseja realmente excluir a conta?" +incorrectPassword: "Senha inválida." +voteConfirm: "Deseja confirmar o seu voto em \"{choice}\"?" +hide: "Ocultar" +useDrawerReactionPickerForMobile: "Mostrar em formato de gaveta" +welcomeBackWithName: "Bem-vindo de volta, {name}" +clickToFinishEmailVerification: "Clique em [{ok}] para completar a validação do endereço de e-mail." +overridedDeviceKind: "Sobrepor dispositivo" +smartphone: "Celular" +tablet: "Tablet" +auto: "Automático" +themeColor: "Cor do tema" +size: "Tamanho" +numberOfColumn: "Número da coluna" +searchByGoogle: "Pesquisar" +instanceDefaultLightTheme: "Tema diurno padrão para toda a instância" +instanceDefaultDarkTheme: "Tema noturno para toda a instância" +instanceDefaultThemeDescription: "Insira o código do tema em formato de objeto." +mutePeriod: "Duração de silenciamento" +period: "Data limite" +indefinitely: "Indefinitivamente" +tenMinutes: "10 minutos" +oneHour: "1 hora" +oneDay: "1 dia" +oneWeek: "1 semana" +oneMonth: "1 mês" +reflectMayTakeTime: "As mudanças podem demorar a aparecer." +failedToFetchAccountInformation: "Não foi possível obter informações da conta" +rateLimitExceeded: "Taxa limite excedido" +cropImage: "Recortar imagem" +cropImageAsk: "Deseja recortar esta imagem?" +cropYes: "Recortar" +cropNo: "Manter deste jeito" file: "Ficheiros" +recentNHours: "Últimas {n} horas" +recentNDays: "Últimos {n} dias" +noEmailServerWarning: "Servidor de e-mail não configurado." +thereIsUnresolvedAbuseReportWarning: "Existem denúncias não resolvidas." +recommended: "Recomendado" +check: "Verificar" +driveCapOverrideLabel: "Altere a capacidade do drive para este usuário" +driveCapOverrideCaption: "Altere a capacidade para o valor padrão informando o valor 0 ou inferior." +requireAdminForView: "Para visualizar, é necessário acessar com uma conta de administrador." +isSystemAccount: "É uma conta criada e gerenciada automaticamente pelo sistema." +typeToConfirm: "Para realizar essa operação, digite {x}." +deleteAccount: "Excluir conta" +document: "Documentação" +numberOfPageCache: "Número de cache de página" +numberOfPageCacheDescription: "Aumentar isso melhora a conveniência, mas também resulta em maior carga e uso de memória." +logoutConfirm: "Gostaria de encerrar a sessão?" +lastActiveDate: "Última data de uso" +statusbar: "Barra de status" +pleaseSelect: "Por favor, selecione." +reverse: "Inversão" +colored: "Colorido" +refreshInterval: "Intervalo de atualização" +label: "Etiqueta" +type: "Tipo" +speed: "Velocidade" +slow: "Lento" +fast: "Rápido" +sensitiveMediaDetection: "Detecção de conteúdo sensível" +localOnly: "Apenas local" +remoteOnly: "Apenas remoto" +cannotUploadBecauseExceedsFileSizeLimit: "Não é possível realizar o upload deste arquivo porque ele excede o tamanho máximo permitido." +beta: "Beta" +enableAutoSensitive: "Marcar automaticamente como conteúdo sensível" +enableAutoSensitiveDescription: "Quando disponível, a marcação de mídia sensível será automaticamente atribuído ao conteúdo de mídia usando aprendizado de máquina. Mesmo que você desative essa função, em alguns servidores, isso pode ser configurado automaticamente." +activeEmailValidationDescription: "A validação do endereço de e-mail do usuário será realizada de forma mais rigorosa, considerando se é um endereço descartável ou se é possível realizar comunicação efetiva. Se desativado, apenas a validade do formato do endereço será verificada como uma sequência de caracteres." +shuffle: "Aleatório" +account: "Contas" +move: "Mover" +pushNotification: "Notificações Push" +subscribePushNotification: "Ativar notificações push" +unsubscribePushNotification: "Desativar notificações push" +windowMinimize: "Minimizar" +windowRestore: "Restaurar" +caption: "legenda" +tools: "Ferramentas" +like: "Curtir" +unlike: "Remover curtida" +numberOfLikes: "Número de curtidas" +show: "Visualizar" +neverShow: "Não exibir novamente" +remindMeLater: "Lembrar mais tarde" +didYouLikeMisskey: "Você gostou do Misskey?" +pleaseDonate: "O Misskey é um software gratuito utilizado por {host}. Para que possamos continuar o desenvolvimento, pedimos que considerem fazer doações. A sua contribuição é muito importante!" +roles: "Cargos" +role: "Cargo" +noRole: "Nenhum cargo" +normalUser: "Usuários padrão" +undefined: "Indefinido" +assign: "Atribuir" +unassign: "Remover" +color: "Cor" +manageCustomEmojis: "Gerenciar Emojis customizados" +youCannotCreateAnymore: "Você atingiu o limite de criação." +cannotPerformTemporary: "Ação temporariamente indisponível" +cannotPerformTemporaryDescription: "Esta ação não pôde ser concluída devido ao excesso de pedidos em sucessão. Tente novamente em alguns momentos." +invalidParamError: "Parâmetros inválidos" +invalidParamErrorDescription: "Parâmetros requisitados inválidos. Isto normalmente acontece devido a um erro, mas também pode ocorrer devido à entrada de valores além do limite ou algo semelhante." +permissionDeniedError: "Operação recusada" +permissionDeniedErrorDescription: "Esta conta não tem permissão para executar esta ação." +preset: "Predefinições" +selectFromPresets: "Escolher de predefinições" +achievements: "Conquistas" +gotInvalidResponseError: "Resposta do servidor inválida" +gotInvalidResponseErrorDescription: "Servidor fora do ar ou em manutenção. Favor tentar mais tarde." +thisPostMayBeAnnoying: "Esta nota pode incomodar outras pessoas." +thisPostMayBeAnnoyingHome: "Postar na linha do tempo inicial" +thisPostMayBeAnnoyingCancel: "Cancelar" +thisPostMayBeAnnoyingIgnore: "Postar mesmo assim" +collapseRenotes: "Ocultar repostagens já visualizadas" +internalServerError: "Erro interno de servidor" +emailNotSupported: "O envio de e-mails não é suportado nesta instância" +likeOnly: "Apenas curtidas" +likeOnlyForRemote: "Tudo (somente curtidas remotas)" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "Apenas não sensíveis (somente curtidas remotas)" +rolesAssignedToMe: "Cargos atribuídos a mim" +unfavoriteConfirm: "Deseja realmente remover dos favoritos?" +drivecleaner: "Limpeza do drive" +retryAllQueuesConfirmTitle: "Gostaria de tentar novamente agora?" +reactionsDisplaySize: "Tamanho de exibição das reações" +reactionsList: "Reações" +renotesList: "Repostagens" +leftTop: "Superior esquerdo" +rightTop: "Superior direito" +leftBottom: "Inferior esquerdo" +rightBottom: "Inferior direito" +vertical: "Vertical" +horizontal: "Exibir painel lateral inteiro" +position: "Posição" +serverRules: "Regras do servidor" +continue: "Continuar" +preservedUsernamesDescription: "Liste os nomes de usuário que deseja reservar, separando-os por quebras de linha. Os nomes de usuário especificados aqui não poderão ser utilizados durante a criação de contas. No entanto, esta restrição não se aplica quando a conta é criada por um administrador. Além disso, as contas que já existem não serão afetadas." +archive: "Arquivo" +channelArchiveConfirmTitle: "Deseja realmente arquivar {name}?" +youFollowing: "Seguindo" +preventAiLearningDescription: "Solicita-se que o conteúdo de notas e imagens enviadas não seja usado como objeto de aprendizado por sistemas externos de geração de texto ou imagens. Isso é alcançado incluindo a flag 'noai' na resposta HTML. No entanto, o cumprimento dessa solicitação depende do próprio sistema de IA, portanto, não é garantia total de prevenção de aprendizado." +options: "Opções" +rolesThatCanBeUsedThisEmojiAsReaction: "Cargos que podem utilizar este emoji como reação" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Se nenhum cargo for especificado, qualquer pessoa pode usar este emoji como reação." +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Estes cargos devem ser públicos." +waitingForMailAuth: "Verificação de e-mail pendente " +icon: "Avatar" +replies: "Responder" +renotes: "Repostar" +keepScreenOn: "Manter a tela do dispositivo sempre ligada" +_initialAccountSetting: + followUsers: "Siga usuários que lhe interessam para criar a sua linha do tempo." +_serverSettings: + iconUrl: "URL do ícone" +_accountMigration: + moveFromDescription: "Se você deseja migrar de outra conta para esta, é necessário criar um alias aqui. Por favor, insira a conta de origem da migração no seguinte formato: @username@server.example.com. Para excluir o alias, deixe o campo em branco e clique em salvar (não recomendado)." + moveAccountDescription: "Você está migrando para uma nova conta.\n ・Seus seguidores irão automaticamente seguir a nova conta.\n ・Todas as suas conexões de seguidores nesta conta serão removidas.\n ・Você não poderá mais criar novas notas nesta conta.\n\nA migração dos seguidores é automática, mas a migração das pessoas que você segue deve ser feita manualmente. Antes de migrar, exporte quem você está seguindo nesta conta e, assim que migrar, importe essa lista na nova conta.\nO mesmo se aplica para listas, silenciamentos e bloqueios, que também devem ser migrados manualmente.\n\n(Esta descrição se refere ao comportamento do servidor Misskey v13.12.0 ou posterior. Outros softwares ActivityPub, como Mastodon, podem ter comportamentos diferentes.)" + moveAccountHowTo: "Para realizar a migração da conta, primeiro crie um alias para esta conta no destino da migração. Após criar o alias, insira a conta de destino da migração no seguinte formato: @username@server.example.com." + migrationConfirm: "Tem certeza de que deseja migrar esta conta para '{account}'? Uma vez migrada, não poderá ser desfeita e não será possível usar esta conta novamente em seu estado original." + postMigrationNote: "A remoção dos seguidores desta conta será realizada 24 horas após a operação de migração. O número de seguidores e seguidos desta conta se tornará zero. Os seguidores não serão removidos, portanto, eles continuarão a ver as postagens destinadas aos seguidores desta conta." +_achievements: + earnedAt: "Data de aquisição" + _types: + _notes1: + title: "Configurando o meu misskey" + description: "Postou uma nota pela primeira vez" + flavor: "Divirta-se com o Misskey!" + _notes10: + title: "Algumas notas" + description: "Postou 10 notas" + _notes100: + title: "Um monte de notas" + description: "Postou 100 notas" + _notes500: + title: "Coberto por notas" + description: "Postou 500 notas" + _notes1000: + title: "Uma montanha de notas" + description: "Postou 1000 notas" + _notes5000: + title: "Enxurrada de notas" + description: "Postou 5000 notas" + _notes10000: + title: "Super nota" + description: "Postou 10000 notas" + _notes20000: + title: "Preciso... de mais... notas..." + description: "Postou 20000 notas" + _notes30000: + title: "Notas, Notas, NOTAS!" + description: "Postou 30000 notas" + _notes40000: + title: "Fábrica de notas" + description: "Postou 40000 notas" + _notes50000: + title: "Planeta de notas" + description: "Postou 50000 notas" + _notes60000: + title: "Quasar de notas" + description: "Postou 60000 notas" + _notes70000: + title: "Buraco negro de notas" + description: "Postou 70000 notas" + _notes80000: + title: "Galáxia de notas" + description: "Postou 80000 notas" + _notes90000: + title: "Universo de notas" + description: "Postou 90000 notas" + _notes100000: + title: "ALL YOUR NOTE ARE BELONG TO US" + description: "Postou 100000 notas" + flavor: "Você realmente tem muita coisa para escrever" + _login3: + title: "Iniciante I" + description: "Fez login por um total de 3 dias" + flavor: "De hoje em diante, me chame apenas de Misskist" + _login7: + title: "Iniciante II" + description: "Fez login por um total de 7 dias" + flavor: "Pegando o jeito da coisa?" + _login15: + title: "Iniciante III" + description: "Fez login por um total de 15 dias" + _login30: + title: "Misskist I" + description: "Fez login por um total de 30 dias" + _login60: + title: "Misskist II" + description: "Fez login por um total de 60 dias" + _login100: + title: "Misskist III" + description: "Fez login por um total de 100 dias" + flavor: "Misskist violento" + _login200: + title: "Freguês I" + description: "Fez login por um total de 200 dias" + _login300: + title: "Freguês II" + description: "Fez login por um total de 300 dias" + _login400: + title: "Freguês III" + description: "Fez login por um total de 400 dias" + _login500: + title: "Veterano I" + description: "Fez login por um total de 500 dias" + flavor: "Cavalheiros, tudo o que peço são notas" + _login600: + title: "Veterano II" + description: "Fez login por um total de 600 dias" + _login700: + title: "Veterano III" + description: "Fez login por um total de 700 dias" + _login800: + title: "Mestre das notas I" + description: "Fez login por um total de 800 dias" + _login900: + title: "Mestre das notas II" + description: "Fez login por um total de 900 dias" + _login1000: + title: "Mestre das notas III" + description: "Fez login por um total de 1000 dias" + flavor: "Obrigado por utilizar o Misskey!" + _noteClipped1: + title: "Não posso deixar de adicionar ao clipe" + description: "Adicionou a um clipe a sua primeira nota" + _noteFavorited1: + title: "Astrônomo amador" + description: "Adicionou uma nota aos favoritos pela primeira vez" + _myNoteFavorited1: + title: "Cabeça nas estrelas" + description: "Teve uma das suas notas adicionada aos favoritos de alguém" + _profileFilled: + title: "Tudo pronto" + description: "Configurou o seu perfil" + _markedAsCat: + title: "Eu Sou Um Gato" + description: "Marcou a sua conta como um gato" + flavor: "Ainda não tenho um nome." + _following1: + title: "Primeira vez seguindo alguém" + description: "Seguiu um usuário pela primeira vez" + _following10: + title: "Circulando, circulando" + description: "Seguiu 10 usuários" + _following50: + title: "Muitos amigos" + description: "Seguiu 50 usuários" + _following100: + title: "100 amigos" + description: "Seguiu 100 usuários" + _following300: + title: "Sobrecarga de amigos" + description: "Seguiu 300 usuários" + _followers1: + title: "Primeiro seguidor" + description: "Ganhou o seu primeiro seguidor" + _followers10: + title: "Sigam-me os bons!" + description: "Ganhou 10 seguidores" + _followers50: + title: "Aos montes" + description: "Ganhou 50 seguidores" + _followers100: + title: "Popular" + description: "Ganhou 100 seguidores" + _followers300: + title: "Em fila única, por favor" + description: "Ganhou 300 seguidores" + _followers500: + title: "Torre de celular" + description: "Ganhou 500 seguidores" + _followers1000: + title: "Influencer" + description: "Ganhou 1000 seguidores" + _noteDeletedWithin1min: + title: "Deixa pra lá" + description: "Excluí a postagem dentro de 1 minuto após ter publicado" + _driveFolderCircularReference: + title: "Referência circular" +_role: + new: "Novo cargo" + edit: "Editar cargo" + name: "Nome do Cargo" + description: "Descrição do cargo" + permission: "Permissões do cargo" + descriptionOfPermission: "Moderador permite que você execute operações básicas relacionadas à moderação.\nAdministradores podem alterar todas as configurações do servidor." + assignTarget: "Atribuir" + descriptionOfAssignTarget: "Manual para gerenciar manualmente quem está incluído neste cargo.\nCondicional define uma condição e os usuários que corresponderem a ela serão incluídos automaticamente." + manual: "Documentação" + conditional: "Condicional" + condition: "Condição" + isConditionalRole: "Este é um cargo condicional." + isPublic: "Cargo público" + descriptionOfIsPublic: "Este cargo será exibido no perfil do usuário." + options: "Opções" + policies: "Políticas" + baseRole: "Cargo padrão" + useBaseValue: "Usar o valor do cargo padrão" + chooseRoleToAssign: "Selecionar o cargo a ser atribuído" + iconUrl: "URL do ícone" + asBadge: "Exibir como insígnia" + descriptionOfAsBadge: "Quando ativado, o ícone do cargo será exibido ao lado do nome de usuário" + isExplorable: "Fazer o cargo explorável" + descriptionOfIsExplorable: "Ao ativar, a lista de membros será pública na seção 'Explorar' e a linha do tempo do cargo ficará disponível." + displayOrder: "Ordenação" + descriptionOfDisplayOrder: "Quanto maior o número, maior a posição de destaque na interface do usuário." + canEditMembersByModerator: "Permitir a edição de membros deste cargo por moderadores" + descriptionOfCanEditMembersByModerator: "Quando ativado, os moderadores também poderão atribuir/remover usuários deste papel, além dos administradores. Quando desativado, apenas os administradores poderão fazê-lo." + priority: "Prioridade" + _priority: + low: "Baixa" + middle: "Médio" + high: "Alta" + _options: + gtlAvailable: "Visualizar Linha do Tempo Global" + ltlAvailable: "Visualizar Linha do Tempo Local" + canPublicNote: "Permitir postagem pública" + canInvite: "Permitir a criação de códigos de convites para a instância" + inviteLimit: "Limite de códigos de convite" + inviteLimitCycle: "Intervalo de emissão do código de convite" + inviteExpirationTime: "Prazo de validade do código de convite" + canManageCustomEmojis: "Permitir gerenciar emojis personalizados" + driveCapacity: "Capacidade do drive" + alwaysMarkNsfw: "Sempre marcar arquivos como NSFW" + pinMax: "Número máximo de notas fixadas" + antennaMax: "Número máximo de antenas" + wordMuteMax: "Número máximo de caracteres nas palavras silenciadas" + webhookMax: "Número máximo de webhooks" + clipMax: "Número máximo de clipes" + noteEachClipsMax: "Número máximo de notas em um clipe" + userListMax: "Número máximo de listas de usuários" + userEachUserListsMax: "Número máximo de usuários em uma lista" + rateLimitFactor: "Taxa de limitação" + descriptionOfRateLimitFactor: "Valores menores são menos restritivos, valores maiores são mais restritivos." + canHideAds: "Permitir ocultar anúncios" + canSearchNotes: "Permitir a busca de notas" + _condition: + isLocal: "Usuário local" + isRemote: "Usuário remoto" + createdLessThan: "Menos de X passados desde a criação da conta" + createdMoreThan: "Mais de X passados desde a criação da conta" + followersLessThanOrEq: "Possui X ou menos seguidores" + followersMoreThanOrEq: "Possui X ou mais seguidores" + followingLessThanOrEq: "Segue X ou menos contas" + followingMoreThanOrEq: "Segue X ou mais contas" + notesLessThanOrEq: "A quantidade de postagens é menor ou igual a" + notesMoreThanOrEq: "A quantidade de postagens é maior ou igual a" + and: "~ E ~ (Condicional)" + or: "~ OU ~ (Condicional)" + not: "Não ~ (Condicional)" +_sensitiveMediaDetection: + description: "Use o aprendizado de máquina para detectar automaticamente mídias sensíveis para moderação. Isso pode aumentar ligeiramente a carga no servidor." + sensitivityDescription: "Ao reduzir a sensibilidade, as detecções incorretas (falsos positivos) diminuem. Ao aumentar a sensibilidade, as falhas de detecção (falsos negativos) diminuem." +_emailUnavailable: + used: "O endereço de e-mail informado já está sendo utilizado" + format: "Formado de e-mail inválido" + disposable: "Endereços de e-mail descartáveis não devem ser utilizados" + mx: "O servidor de informado é inválido" + smtp: "O servidor de e-mail não está respondendo" +_ffVisibility: + public: "Público" + followers: "Visível apenas para seguidores" + private: "Privado" +_signup: + almostThere: "Quase pronto" + emailAddressInfo: "Por favor, insira o seu endereço de e-mail. Ele não será divulgado." + emailSent: "Um e-mail de confirmação foi enviado para o endereço de e-mail fornecido ({email}). Acesse o link fornecido no e-mail para concluir a criação de sua conta." +_accountDelete: + accountDelete: "Remover Conta" + mayTakeTime: "A exclusão de uma conta é um processo que requer muito recurso, portanto, se você tiver muito conteúdo criados ou arquivos enviados, pode levar algum tempo até ser concluída." + sendEmail: "Quando a exclusão da conta estiver concluída, enviaremos uma notificação para o endereço de e-mail registrado." + requestAccountDelete: "Solicitar exclusão de conta" + started: "O processo de exclusão foi iniciado." + inProgress: "A exclusão está em andamento" +_ad: + back: "Voltar" + reduceFrequencyOfThisAd: "Diminuir frequência deste anúncio" + hide: "Não exibir anúncios" +_forgotPassword: + enterEmail: "Por favor, insira o endereço de e-mail usado no cadastro de sua conta. Um link para redefinição de senha será enviado para esse endereço." + ifNoEmail: "Caso você não tenha registrado um endereço de e-mail, por favor, entre em contato com o administrador." +_gallery: + liked: "Postagens curtidas" + like: "Curtir" + unlike: "Remover curtida" _email: _follow: title: "Você tem um novo seguidor" + _receiveFollowRequest: + title: "Você recebeu um pedido de seguidor" +_preferencesBackups: + cannotSave: "Não foi possível salvar" + applyConfirm: "Deseja aplicar o backup '{name}' ao dispositivo atual? As configurações atuais do dispositivo serão perdidas." + deleteConfirm: "Deseja excluir {name}?" + cannotLoad: "Não foi possível carregar" +_channel: + featured: "Destaques" + following: "Seguindo" + usersCount: "usuários ativos" + notesCount: "notas" + nameAndDescription: "Nome e descrição" +_menuDisplay: + sideFull: "Exibir painel lateral inteiro" + top: "Exibir barra superior" + hide: "Ocultar" +_instanceMute: + instanceMuteDescription: "Todas as notas e repostagens do servidor configurado serão silenciados, incluindo respostas aos usuários do servidor mutado." _theme: + description: "Descrição" + alpha: "Opacidade" + deleteConstantConfirm: "Confirma a exclusão da constante {const}?" keys: mention: "Menção" renote: "Repostar" + divider: "Separador" _sfx: note: "Posts" notification: "Notificações" chat: "Chat" +_ago: + invalid: "Não há nada aqui" +_timelineTutorial: + step1_2: "Existem vários tipos de linhas do tempo, por exemplo, na 'Linha do Tempo Principal', você verá as notas das pessoas que está seguindo, e na 'Linha do Tempo Local', verá todas as notas de {name}." +_2fa: + securityKeyInfo: "Além da autenticação por impressão digital ou PIN, você também pode configurar a autenticação por chaves de segurança de hardware compatível com FIDO2 para proteger ainda mais a sua conta." + removeKeyConfirm: "Deseja excluir {name}?" + renewTOTPCancel: "Não, obrigado" +_permissions: + "read:account": "Visualizar informações da conta" + "write:account": "Editar informações da conta" + "read:blocks": "Visualizar a sua lista de usuários bloqueados" + "write:blocks": "Editar a sua lista de usuários bloqueados" + "read:drive": "Visualizar os seus arquivos e pastas do drive" + "write:drive": "Editar ou excluir os seus arquivos e pastas do drive" + "read:favorites": "Visualizar a sua lista de favoritos" + "write:favorites": "Editar a sua lista de favoritos" + "read:following": "Visualizar informações de quem você segue" + "write:following": "Seguir ou deixar de seguir outras contas" + "read:messaging": "Visualizar os seus chats" + "write:messaging": "Compor ou editar mensagens de chat" + "read:mutes": "Visualizar a sua lista de usuários silenciados" + "write:mutes": "Editar a sua lista de usuários silenciados" + "write:notes": "Compor ou excluir notas" + "read:notifications": "Visualizar as suas notificações" + "write:notifications": "Gerenciar as suas notificações" + "read:reactions": "Visualizar as suas reações" + "write:reactions": "Editar as suas reações" + "write:votes": "Votar em enquetes" + "read:pages": "Visualizar as suas páginas" + "write:pages": "Editar ou excluir as suas páginas" + "read:page-likes": "Visualizar as suas curtidas em páginas" + "write:page-likes": "Editar as suas curtidas em páginas" + "read:user-groups": "Visualizar os seus grupos de usuários" + "write:user-groups": "Editar ou excluir os seus grupos de usuários" + "read:channels": "Visualizar os seus canais" + "write:channels": "Editar os seus canais" + "read:gallery": "Visualizar a sua galeria" + "write:gallery": "Editar sua galeria" + "read:gallery-likes": "Visualizar a sua lista de curtidas da galeria" + "write:gallery-likes": "Editar a sua lista de curtidas da galeria" _widgets: profile: "Perfil" instanceInfo: "Informações da instância" + memo: "Notas adesivas" notifications: "Notificações" - timeline: "Timeline" - activity: "atividade" - federation: "União" - jobQueue: "Fila de trabalhos" + timeline: "Linha do tempo" + calendar: "Calendário" + trends: "Destaques" + clock: "Relógio" + rss: "Leitor de RSS" + rssTicker: "Ticker RSS" + activity: "Atividades" + photos: "Fotos" + digitalClock: "Relógio digital" + unixClock: "Hora UNIX" + federation: "Federação" + instanceCloud: "Nuvem de instâncias" + postForm: "Campo de postagem" + slideshow: "Apresentação de slides" + button: "Botão" + onlineUsers: "Usuários Online" + jobQueue: "Fila de tarefas" + serverMetric: "Métricas do servidor" + aiscript: "Console AiScript" + aiscriptApp: "AiScript App" + aichan: "Ai" + userList: "Lista de usuários" _userList: - chooseList: "Escolhe uma lista" + chooseList: "Selecione uma lista" + clicker: "Clicker" _cw: show: "Carregar mais" +_poll: + canMultipleVote: "Permitir múltipla seleção" + vote: "Votar em enquetes" _visibility: - home: "casa" + home: "Início" followers: "Seguidores" + followersDescription: "Tornar visível apenas para os meus seguidores" _profile: name: "Nome" username: "Nome de usuário" _exportOrImport: + favoritedNotes: "Notas nos favoritos" followingList: "Seguindo" muteList: "Silenciar" blockingList: "Bloquear" @@ -502,8 +1413,25 @@ _exportOrImport: _charts: federation: "União" _timelines: - home: "casa" + home: "Início" +_play: + new: "Criar Play" + edit: "Editar Play" + created: "Play criado" + updated: "Play editado" + deleted: "Play foi excluído" + pageSetting: "Configurações do Play" + editThisPage: "Editar este Play" + my: "Meus Plays" + liked: "Plays curtidos" + script: "Script" + summary: "Descrição" _pages: + deleted: "Página excluída com sucesso" + viewPage: "Visualizar as suas páginas" + like: "Curtir" + unlike: "Remover curtida" + liked: "Páginas curtidas" blocks: image: "imagem" _relayStatus: @@ -515,13 +1443,14 @@ _notification: youGotMention: "{name} te mencionou" youGotReply: "{name} te respondeu" youGotQuote: "{name} te citou" + youRenoted: "Repostagens de {name}" youWereFollowed: "Você tem um novo seguidor" - youReceivedFollowRequest: "Você recebeu um pedido de seguimento" - yourFollowRequestAccepted: "Seu pedido de seguimento foi aceito" + youReceivedFollowRequest: "Você recebeu um pedido de seguidor" + yourFollowRequestAccepted: "Seu pedido de seguidor foi aceito" pollEnded: "Os resultados da enquete agora estão disponíveis" emptyPushNotificationMessage: "As notificações de alerta foram atualizadas" _types: - all: "Todos" + all: "Todas" follow: "Seguindo" mention: "Menção" reply: "Respostas" @@ -529,8 +1458,8 @@ _notification: quote: "Citar" reaction: "Reações" pollEnded: "Enquetes terminando" - receiveFollowRequest: "Recebeu pedidos de seguimento" - followRequestAccepted: "Aceitou pedidos de seguimento" + receiveFollowRequest: "Recebeu pedidos de seguidor" + followRequestAccepted: "Aceitou pedidos de seguidor" app: "Notificações de aplicativos conectados" _actions: followBack: "te seguiu de volta" @@ -546,6 +1475,7 @@ _deck: swapDown: "Trocar de posição com a coluna abaixo" popRight: "Acoplar coluna à direita" profile: "Perfil" + deleteProfile: "Remover perfil" _columns: main: "Principal" widgets: "Widgets" @@ -553,7 +1483,17 @@ _deck: tl: "Timeline" antenna: "Antenas" list: "Listas" + channel: "Canais" mentions: "Menções" direct: "Notas diretas" + roleTimeline: "Linha do tempo do cargo" +_drivecleaner: + orderBySizeDesc: "Tamanho descendente" + orderByCreatedAtAsc: "Data ascendente" _webhookSettings: name: "Nome" + active: "Ativado" + _events: + follow: "Quando seguindo um usuário" + followed: "Quando sendo seguido" + renote: "Quando repostado" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 6ad45d0636..605fcc82f7 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -294,7 +294,6 @@ copyUrl: "Copiază URL" rename: "Redenumește" avatar: "Avatar" banner: "Banner" -nsfw: "NSFW" whenServerDisconnected: "Când pierzi conexiunea cu serverul" disconnectedFromServer: "Conecțiunea cu serverul a fost pierdută" reload: "Reîncarcă" @@ -329,7 +328,6 @@ invite: "Invită" driveCapacityPerLocalAccount: "Capacitatea Drive-ului per utilizator local" driveCapacityPerRemoteAccount: "Capacitatea Drive-ului per utilizator extern" inMb: "În megabytes" -iconUrl: "URL-ul iconiței" bannerUrl: "URL-ul imaginii de banner" backgroundImageUrl: "URL-ul imaginii de fundal" basicInfo: "Informații de bază" @@ -631,6 +629,9 @@ sent: "Trimite" searchByGoogle: "Caută" file: "Fișiere" show: "Arată" +icon: "Avatar" +replies: "Răspunde" +renotes: "Re-notează" _role: _priority: middle: "Mediu" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 8c79a4502d..edf531dfcc 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -2,7 +2,7 @@ _lang_: "Русский" headlineMisskey: "Сеть, сплетённая из заметок" introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀" -poweredByMisskeyDescription: "{name} – один из инстансов (также называемый экземпляром Misskey), использующий платформу с открытым исходным кодом Misskey." +poweredByMisskeyDescription: "{name} – сервис на платформе с открытым исходным кодом Misskey, называемый инстансом Misskey." monthAndDay: "{day}.{month}" search: "Поиск" notifications: "Уведомления" @@ -49,9 +49,15 @@ delete: "Удалить" deleteAndEdit: "Удалить и отредактировать" deleteAndEditConfirm: "Удалить эту заметку и создать отредактированную? Все реакции, ссылки и ответы на существующую будут будут потеряны." addToList: "Добавить в список" +addToAntenna: "Добавить к антенне" sendMessage: "Отправить сообщение" copyRSS: "Скопировать RSS" copyUsername: "Скопировать имя пользователя" +copyUserId: "Скопировать ID пользователя" +copyNoteId: "Скопировать ID заметки" +copyFileId: "Скопировать ID файла" +copyFolderId: "Скопировать ID папки" +copyProfileUrl: "Скопировать URL профиля " searchUser: "Поиск людей" reply: "Ответить" loadMore: "Показать еще" @@ -134,8 +140,10 @@ unblockConfirm: "Разблокировать этот аккаунт?" suspendConfirm: "Заморозить этот аккаунт?" unsuspendConfirm: "Разморозить этот аккаунт?" selectList: "Выберите список" +editList: "Редактировать список" selectChannel: "Выберите канал" selectAntenna: "Выберите антенну" +editAntenna: "Редактировать антенну" selectWidget: "Выберите виджет" editWidgets: "Редактировать виджеты" editWidgetsExit: "Готово" @@ -148,6 +156,8 @@ addEmoji: "Добавить эмодзи" settingGuide: "Рекомендуемые настройки" cacheRemoteFiles: "Кешировать внешние файлы" cacheRemoteFilesDescription: "Когда эта настройка отключена, файлы с других сайтов будут загружаться прямо оттуда. Это сэкономит место на сервере, но увеличит трафик, так как не будут создаваться эскизы." +cacheRemoteSensitiveFiles: "Кешировать внешние файлы" +cacheRemoteSensitiveFilesDescription: "Описание удаленных внешних файлов в кэше" flagAsBot: "Аккаунт бота" flagAsBotDescription: "Включите, если этот аккаунт управляется программой. Это позволит системе Misskey учитывать это, а также поможет разработчикам других ботов предотвратить бесконечные циклы взаимодействия." flagAsCat: "Аккаунт кота" @@ -309,7 +319,7 @@ copyUrl: "Копировать ссылку" rename: "Переименовать" avatar: "Аватар" banner: "Шапка" -nsfw: "Содержимое не для всех" +displayOfSensitiveMedia: "Определение деликатного контента" whenServerDisconnected: "Когда соединение с сервером потеряно" disconnectedFromServer: "Разорвано соединение с сервером" reload: "Перезагрузить" @@ -344,7 +354,6 @@ invite: "Пригласить" driveCapacityPerLocalAccount: "Объём диска на одного локального пользователя" driveCapacityPerRemoteAccount: "Объём диска на одного пользователя с другого сайта" inMb: "В мегабайтах" -iconUrl: "Ссылка на аватар" bannerUrl: "Ссылка на изображение в шапке" backgroundImageUrl: "Ссылка на фоновое изображение" basicInfo: "Общая информация" @@ -649,8 +658,8 @@ abuseReported: "Жалоба отправлена. Большое спасибо reporter: "Сообщивший" reporteeOrigin: "О ком сообщено" reporterOrigin: "Кто сообщил" -forwardReport: "Перенаправление отчета на инстант." -forwardReportIsAnonymous: "Удаленный инстант не сможет увидеть вашу информацию и будет отображаться как анонимная системная учетная запись." +forwardReport: "Отправить жалобу на инстанс автора." +forwardReportIsAnonymous: "Жалоба на удалённый инстанс будет отправлена анонимно. Вместо ваших данных у получателя будет отображена системная учётная запись." send: "Отправить" abuseMarkAsResolved: "Отметить жалобу как решённую" openInNewTab: "Открыть в новой вкладке" @@ -670,6 +679,7 @@ createNewClip: "Новая подборка" unclip: "Убрать из подборки" confirmToUnclipAlreadyClippedNote: "Эта заметка уже есть в подборке «{name}». Удалить из этой подборки?" public: "Общедоступно" +private: "Показываются только вам" i18nInfo: "Misskey переводят на разные языки добровольцы со всего света. Ваша помощь тоже пригодится здесь: {link}." manageAccessTokens: "Управление токенами доступа" accountInfo: "Сведения об учётной записи" @@ -790,6 +800,7 @@ noMaintainerInformationWarning: "Не заполнены сведения об noBotProtectionWarning: "Ботозащита не настроена" configure: "Настроить" postToGallery: "Опубликовать в галерею" +postToHashtag: "Опубликовать пост с этим хештегом" gallery: "Галерея" recentPosts: "Недавние публикации" popularPosts: "Популярные публикации" @@ -823,6 +834,7 @@ translatedFrom: "Перевод. Язык оригинала — {x}" accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи" usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже." aiChanMode: "Режим Ай" +devMode: "Режим разработчика" keepCw: "Сохраняйте Предупреждения о содержимом" pubSub: "Учётные записи Pub/Sub" lastCommunication: "Последнее сообщение" @@ -832,6 +844,8 @@ breakFollow: "Отписка" breakFollowConfirm: "Удалить из подписок пользователя ?" itsOn: "Включено" itsOff: "Выключено" +on: "Вкл" +off: "Выкл" emailRequiredForSignup: "Для регистрации учётной записи нужен адрес электронной почты" unread: "Непрочитанное" filter: "Фильтры" @@ -914,8 +928,8 @@ cannotUploadBecauseInappropriate: "Файл не может быть загру cannotUploadBecauseNoFreeSpace: "Файл не может быть загружен, так как не осталось места на диске" cannotUploadBecauseExceedsFileSizeLimit: "Файл не может быть загружен, так как он превышает лимит размера файла." beta: "Бета" -enableAutoSensitive: "Автоматическое определение NSFW" -enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена ​​автоматически в зависимости от инстанта." +enableAutoSensitive: "Автоматическое определение содержимого не для всех" +enableAutoSensitiveDescription: "Позволяет определять наличие содержимого не для всех при помощи искусственного интеллекта там, где это возможно. Даже если эту опцию отключить, она всё равно может быть включена на весь инстанс." activeEmailValidationDescription: "Если включено, будет проводиться более строгая проверка адреса электронной почты, в том числе на то, что он действительный и не временный. Если же отключено, то проверяется только корректность написания адреса." navbar: "Панель навигации" shuffle: "Перемешать" @@ -986,6 +1000,7 @@ cannotBeChangedLater: "Это нельзя изменить позже" reactionAcceptance: "Принятие реакций" likeOnly: "Только лайки" likeOnlyForRemote: "Только лайки с удалённых серверов" +nonSensitiveOnly: "Безопасный серфинг" rolesAssignedToMe: "Мои роли" resetPasswordConfirm: "Сбросить пароль?" sensitiveWords: "Чувствительные слова" @@ -1001,14 +1016,66 @@ retryAllQueuesConfirmTitle: "Хотите попробовать ещё раз?" retryAllQueuesConfirmText: "Нагрузка на сервер может увеличиться" enableChartsForRemoteUser: "Создание диаграмм для удалённых пользователей" enableChartsForFederatedInstances: "Создание диаграмм для удалённых серверов" -largeNoteReactions: "Показывать большие реакции на заметки" noteIdOrUrl: "ID или ссылка на заметку" video: "Видео" videos: "Видео" dataSaver: "Экономия трафика" +accountMigration: "Перенести учётную запись" +accountMoved: "Учетная запись перенесена" +accountMovedShort: "Эта учётная запись перемещена" +operationForbidden: "Эта операция невозможна." +forceShowAds: "Всегда отображать рекламу" +addMemo: "Добавить заметку" +editMemo: "Редактировать заметку" +reactionsList: "Реакции" +renotesList: "Репосты" +notificationDisplay: "Отображение уведомления" +leftTop: "Верхний левый угол" +rightTop: "Сверху справа" +leftBottom: "Снизу слева" +rightBottom: "Снизу справа" +vertical: "Вертикальная" horizontal: "Сбоку" +position: "Позиция" +serverRules: "Правила сервера" +pleaseConfirmBelowBeforeSignup: "Для регистрации на данном сервере, необходимо согласится с нижеследующими положениями." +pleaseAgreeAllToContinue: "Чтобы продолжить, необходимо поставить отметки во всех полях \"согласен\"." +continue: "Продолжить" +preservedUsernames: "Зарезервированные имена пользователей" +preservedUsernamesDescription: "Перечислите зарезервированные имена пользователей, отделяя их строками. Они станут недоступны при создании учётной записи. Это ограничение не применяется при создании учётной записи администраторами. Также, уже существующие учётные записи останутся без изменений." +createNoteFromTheFile: "Создать заметку из этого файла" +archive: "Архив" +channelArchiveConfirmTitle: "Переместить {name} в архив?" +channelArchiveConfirmDescription: "Архивированные каналы перестанут отображаться в списке каналов или результатах поиска. В них также нельзя будет добавлять новые записи." +displayOfNote: "Отображение заметок" +initialAccountSetting: "Настройка профиля" youFollowing: "Подписки" +preventAiLearning: "Отказаться от использования в машинном обучении (Генеративный ИИ)" options: "Настройки ролей" +specifyUser: "Указанный пользователь" +failedToPreviewUrl: "Предварительный просмотр недоступен" +update: "Обновить" +later: "Позже" +goToMisskey: "К Misskey" +additionalEmojiDictionary: "Дополнительные словари эмодзи" +installed: "Установлено" +branding: "Бренд" +expirationDate: "Дата истечения" +unused: "Неиспользуемый" +expired: "Срок действия приглашения истёк" +doYouAgree: "Согласны?" +icon: "Аватар" +replies: "Ответить" +renotes: "Репост" +_initialAccountSetting: + accountCreated: "Аккаунт успешно создан!" + letsStartAccountSetup: "Давайте настроим вашу учётную запись." + profileSetting: "Настройки профиля" + privacySetting: "Настройки конфиденциальности" + initialAccountSettingCompleted: "Первоначальная настройка успешно завершена!" + skipAreYouSure: "Пропустить настройку?" +_serverSettings: + iconUrl: "Адрес на иконку роли" _achievements: earnedAt: "Разблокировано в" _types: @@ -1180,6 +1247,9 @@ _achievements: _client30min: title: "Перерыв на обед" description: "Прошло 30 минут с момента запуска клиента" + _client60min: + title: "Не наглядеться на Misskey" + description: "Misskey был открыт 60 минут подряд" _noteDeletedWithin1min: title: "Ой, нет!" description: "Заметка удалена через минуту после публикации" @@ -1282,6 +1352,7 @@ _role: canInvite: "Может создавать пригласительные коды" canManageCustomEmojis: "Управлять пользовательскими эмодзи" driveCapacity: "Доступное пространство на «диске»" + alwaysMarkNsfw: "Всегда отмечать файлы как «не для всех»" pinMax: "Доступное количество закреплённых заметок" antennaMax: "Доступное количество антенн" wordMuteMax: "Доступное количество знаков в списке скрытия слов" @@ -1309,7 +1380,7 @@ _sensitiveMediaDetection: description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно." sensitivity: "Чувствительность обнаружения" sensitivityDescription: "Более низкая чувствительность уменьшает количество ложных срабатываний (false positives). Повышение чувствительности уменьшает утечку при обнаружении (ложноотрицательные результаты)." - setSensitiveFlagAutomatically: "Установить флаг NSFW" + setSensitiveFlagAutomatically: "Обозначить как не для всех" setSensitiveFlagAutomaticallyDescription: "Даже если этот параметр отключен, результат оценки сохраняется внутри системы." analyzeVideos: "Анализировать видео?" analyzeVideosDescription: "Анализируйте видео в дополнение к неподвижным изображениям. Нагрузка на сервер немного увеличивается." @@ -1389,10 +1460,6 @@ _aboutMisskey: donate: "Пожертвование на Misskey" morePatrons: "Большое спасибо и многим другим, кто принял участие в этом проекте! 🥰" patrons: "Материальная поддержка" -_nsfw: - respect: "Скрывать содержимое не для всех" - ignore: "Показывать содержимое не для всех" - force: "Скрывать вообще все файлы" _instanceTicker: none: "Не показывать" remote: "Только для других сайтов" @@ -1528,21 +1595,28 @@ _time: minute: "мин" hour: "ч" day: "сут" +_timelineTutorial: + title: "Как пользоваться Misskey" + step1_1: "Это лицо Misskey, так называемая лента. Ваш инстанс, {name}, покажет тут все опубликованные на нём заметки в хронологическом порядке." + step1_2: "Здесь есть несколько лент. К примеру «персональная» лента отображает заметки тех, на кого вы подписаны. А «местная» — заметки тех, кого приютил {name}." + step2_1: "Что ж, теперь самое время опубликовать заметку. Если нажать вверху страницы на изображение карандаша, появится форма для текста." + step2_2: "Почему бы не написать немного о себе? Ну, или хотя бы «Привет, {name}»?" + step3_1: "Справились с первой заметкой?" + step3_2: "Отлично, теперь она должна появиться в вашей ленте." + step4_1: "А ещё здесь можно делиться своими реакциями на заметки." + step4_2: "Отмечайте реакции, нажимая на символ «+» под заметкой и выбирая значок по душе." _2fa: alreadyRegistered: "Двухфакторная аутентификация уже настроена." 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: "Пожалуйста, следуйте инструкциям в вашем браузере, чтобы зарегистрировать свой ключ безопасности или пароль" @@ -1865,9 +1939,14 @@ _deck: channel: "Каналы" mentions: "Упоминания" direct: "Личное" + roleTimeline: "История Ролей" _dialog: charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}" charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}" +_disabledTimeline: + title: "Лента отключена" + description: "Ваша текущая роль не позволяет пользоваться этой лентой." _webhookSettings: + createWebhook: "Создать вебхук" name: "Название" active: "Вкл." diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 3e96119389..a5639ce59b 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -303,7 +303,6 @@ copyUrl: "Kopírovať URL" rename: "Premenovať" avatar: "Avatar" banner: "BAnner" -nsfw: "NSFW" whenServerDisconnected: "Keď sa stratí spojenie so serverom" disconnectedFromServer: "Spojenie so serverom bolo prerušené" reload: "Obnoviť" @@ -338,7 +337,6 @@ invite: "Pozvať" driveCapacityPerLocalAccount: "Kapacita disku pre používateľa" driveCapacityPerRemoteAccount: "Kapacita disku pre vzdialeného používateľa" inMb: "V megabajtoch" -iconUrl: "Favicon URL" bannerUrl: "URL obrázku bannera" backgroundImageUrl: "URL obrázku pozadia" basicInfo: "Základné informácie" @@ -655,6 +653,7 @@ createNewClip: "Vytvoriť nový klip" unclip: "Odopnúť" confirmToUnclipAlreadyClippedNote: "Táto poznámka je už pripnutá ako \"{name}\". Naozaj ju chcete odopnúť?" public: "Verejné" +private: "Súkromné" i18nInfo: "Misskey je prekladaný do rôznych jazykov dobrovoľníkmi. Pomôcť môžete na {link}." manageAccessTokens: "Spravovať prístupové tokeny" accountInfo: "Informácie o účte" @@ -919,6 +918,9 @@ pleaseDonate: "Misskey je bezplatný softvér, ktorý používa {host}. Prosím, color: "Farba" horizontal: "Strana" youFollowing: "Sledované" +icon: "Avatar" +replies: "Odpovedať" +renotes: "Preposlať" _role: priority: "Priorita" _priority: @@ -1009,10 +1011,6 @@ _aboutMisskey: donate: "Podporiť Misskey" morePatrons: "Takisto oceňujeme podporu mnoých ďalších, ktorí tu nie sú uvedení. Ďakujeme! 🥰" patrons: "Prispievatelia" -_nsfw: - respect: "Skryť NSFW médiá" - ignore: "Neskrývať NSFW médiá" - force: "Skryť všetky médiá" _instanceTicker: none: "Nikdy nezobrazovať" remote: "Zobraziť pre vzdialených používateľov" @@ -1152,7 +1150,6 @@ _2fa: alreadyRegistered: "Už ste zaregistrovali 2-faktorové autentifikačné zariadenie." 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." diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index 7a00e3fcea..507492d52c 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -20,6 +20,7 @@ noNotes: "Inga noteringar" noNotifications: "Inga notifikationer" instance: "Instanser" settings: "Inställningar" +notificationSettings: "Notifieringsinställningar" basicSettings: "Basinställningar" otherSettings: "Andra inställningar" openInWindow: "Öppna i ett fönster" @@ -51,6 +52,10 @@ addToList: "Lägg till i lista" sendMessage: "Skicka ett meddelande" copyRSS: "Kopiera RSS" copyUsername: "Kopiera användarnamn" +copyUserId: "Kopiera användar-ID" +copyNoteId: "Kopiera noter-ID" +copyFileId: "Kopiera Fil-ID" +copyFolderId: "Kopiera mapp-ID" searchUser: "Sök användare" reply: "Svara" loadMore: "Ladda mer" @@ -103,6 +108,8 @@ renoted: "Omnoterad." cantRenote: "Inlägget kunde inte bli omnoterat." cantReRenote: "En omnotering kan inte bli omnoterad." quote: "Citat" +inChannelRenote: "Omnotera inom kanalen" +inChannelQuote: "I kanal citat" pinnedNote: "Fästad not" pinned: "Fäst till profil" you: "Du" @@ -129,7 +136,10 @@ unblockConfirm: "Är du säkert att du vill avblockera kontot?" suspendConfirm: "Är du säker att du vill suspendera detta konto?" unsuspendConfirm: "Är du säker att du vill avsuspendera detta konto?" selectList: "Välj lista" +editList: "Redigera lista" +selectChannel: "Välj en kanal" selectAntenna: "Välj en antenn" +editAntenna: "Redigera en antenn" selectWidget: "Välj en widget" editWidgets: "Redigera widgets" editWidgetsExit: "Avsluta redigering" @@ -256,6 +266,9 @@ noMoreHistory: "Det finns ingen mer historik" startMessaging: "Starta en chatt" nUsersRead: "läst av {n}" agreeTo: "Jag accepterar {0}" +agree: "Överens" +termsOfService: "Användarvillkor" +start: "Kom igång" home: "Hem" remoteUserCaution: "Då denna användaren kommer från en fjärrinstans, kan informationen visad vara ofullständig." activity: "Aktivitet" @@ -297,10 +310,10 @@ copyUrl: "Kopiera URL" rename: "Byt namn" avatar: "Profilbild" banner: "Banner" -nsfw: "Känsligt innehåll" reload: "Ladda om" doNothing: "Ignorera" reloadConfirm: "Vill du ladda om tidslinjen?" +watch: "Titta" accept: "Tillåt" reject: "Neka" normal: "Normal" @@ -320,16 +333,30 @@ connectService: "Anslut" disconnectService: "Koppla från" enableLocalTimeline: "Aktivera lokal tidslinje" enableGlobalTimeline: "Aktivera global tidslinje" +registration: "Registrera" enableRegistration: "Aktivera registrering av nya användare" +invite: "Inbjudan" inMb: "I megabyte" -iconUrl: "URL till profilbilden" bannerUrl: "URL till banner-bilden" +basicInfo: "Grundläggande info" +pinnedUsers: "Fästa användare" +pinnedPages: "Fästa sidor" pinnedNotes: "Fästad not" +hcaptcha: "hCaptcha" enableHcaptcha: "Aktivera hCaptcha" +hcaptchaSiteKey: "Webbplatsnyckel" +hcaptchaSecretKey: "Hemlig nyckel" +recaptcha: "reCAPTCHA" enableRecaptcha: "Aktivera reCAPTCHA" +recaptchaSiteKey: "Webbplatsnyckel" +recaptchaSecretKey: "Hemlig nyckel" +turnstile: "Turnstile" enableTurnstile: "Aktivera Turnstile" +turnstileSiteKey: "Webbplatsnyckel" +turnstileSecretKey: "Hemlig nyckel" antennas: "Antenner" manageAntennas: "Hantera Antenner" +name: "Namn" antennaSource: "Antennkälla" antennaKeywords: "Nyckelord att lyssna efter" antennaExcludeKeywords: "Nyckelord att exkludera" @@ -338,39 +365,112 @@ notifyAntenna: "Notifiera om nya noter" withFileAntenna: "Endast noter med filer" enableServiceworker: "Aktivera pushnotiser i denna webbläsaren" antennaUsersDescription: "Ange ett användarnamn per linje" +withReplies: "Med svar" +notesAndReplies: "Inlägg och svar" +silence: "Tystnad" recentlyUpdatedUsers: "Nyligen aktiva användare" recentlyRegisteredUsers: "Nyligen registrerade användare" +exploreFediverse: "Utforska Fediverse" +popularTags: "Populära taggar" userList: "Listor" +about: "Om" aboutMisskey: "Om Misskey" administrator: "Administratör" +2fa: "Tvåfaktorsautentisering" +totp: "Autentiseringsapp" +moderator: "Moderator" passwordLessLogin: "Lösenordsfri inloggning" passwordLessLoginDescription: "Tillåter lösenordsfri inloggning med endast en säkerhetsnyckel eller en passkey." resetPassword: "Återställ Lösenord" newPasswordIs: "Det nya lösenordet är \"{password}\"" share: "Dela" +help: "Hjälp" +close: "Stäng" +invites: "Inbjudan" +members: "Medlemmar" +transfer: "Överför" +text: "Text" enable: "Aktivera" +next: "Nästa" +invitations: "Inbjudan" +invitationCode: "Inbjudningskod" +available: "Tillgängligt" weakPassword: "Svagt Lösenord" normalPassword: "Medel Lösenord" strongPassword: "Starkt Lösenord" signinFailed: "Kan inte logga in. Det angivna användarnamnet eller lösenordet är felaktigt." +or: "eller" +language: "Språk" +aboutX: "Om {x}" +category: "Kategori" +tags: "Taggar" +createAccount: "Skapa ett konto" +existingAccount: "Existerande konto" +regenerate: "Regenerera" +fontSize: "Textstorlek" +openImageInNewTab: "Öppna bild i ny flik" +clientSettings: "Klientinställningar" +accountSettings: "Kontoinställningar" +numberOfDays: "Antal dagar" +deleteAll: "Radera alla" +sounds: "Ljud" +sound: "Ljud" +listen: "Lyssna" +none: "Ingen" +volume: "Volym" +chooseEmoji: "Välj en emoji" +recentUsed: "Senast använd" +install: "Installera" +uninstall: "Avinstallera" +menu: "Meny" serviceworkerInfo: "Måste vara aktiverad för pushnotiser." enableInfiniteScroll: "Ladda mer automatiskt" enablePlayer: "Öppna videospelare" +permission: "Behörigheter" enableAll: "Aktivera alla" +edit: "Ändra" enableEmail: "Aktivera epost-utskick" +email: "E-post" smtpHost: "Värd" smtpUser: "Användarnamn" smtpPass: "Lösenord" emptyToDisableSmtpAuth: "Lämna användarnamn och lösenord tomt för att avaktivera SMTP verifiering" +logs: "Logg" +channel: "kanal" +create: "Skapa" +other: "Mer" +send: "Skicka" +openInNewTab: "Öppna i ny flik" +createNew: "Skapa ny" +i18nInfo: "Misskey översätts till många olika språk av volontärer. Du kan hjälpa till med översättningen på {link}." +accountInfo: "Kontoinformation" +clips: "Klipp" +duplicate: "Duplicera" +reloadToApplySetting: "Inställningen tillämpas efter sidan laddas om. Vill du göra det nu?" clearCache: "Rensa cache" onlineUsersCount: "{n} användare är online" +nNotes: "{n} Noter" +backgroundColor: "Bakgrundsbild" +textColor: "Text" +youAreRunningUpToDateClient: "Klienten du använder är uppdaterat." +newVersionOfClientAvailable: "Ny version av klienten är tillgänglig." +publish: "Publicera" +typingUsers: "{users} skriver" +info: "Om" enabled: "Aktiverad" user: "Användare" +customCssWarn: "Den här inställningen borde bara ändrats av en som har rätta kunskaper. Om du ställer in det här fel så kan klienten sluta fungera rätt." global: "Global" squareAvatars: "Visa fyrkantiga profilbilder" +sent: "Skicka" +misskeyUpdated: "Misskey har uppdaterats!" incorrectPassword: "Fel lösenord." +welcomeBackWithName: "Välkommen tillbaka, {name}" +clickToFinishEmailVerification: "Tryck på [{ok}] för att slutföra bekräftelsen på e-postadressen." searchByGoogle: "Sök" file: "Filer" +cannotUploadBecauseNoFreeSpace: "Kan inte ladda upp filen för att det finns inget lagringsutrymme kvar." +cannotUploadBecauseExceedsFileSizeLimit: "Kan inte ladda upp filen för att den är större än filstorleksgränsen." enableAutoSensitive: "Automatisk NSFW markering" enableAutoSensitiveDescription: "Tillåter automatiskt detektering och marketing av NSFW media genom Maskininlärning när möjligt. Även om denna inställningen är avaktiverad, kan det vara aktiverat på hela instansen." pushNotification: "Pushnotiser" @@ -381,12 +481,19 @@ pushNotificationNotSupported: "Din webbläsare eller instans har inte stöd för windowMaximize: "Maximera" windowMinimize: "Minimera" windowRestore: "Återställ" +pleaseDonate: "Misskey är en gratis programvara som används på {host}. Donera gärna för att göra utvecklingen ständigt, tack!" resetPasswordConfirm: "Återställ verkligen ditt lösenord?" +dataSaver: "Databesparing" +icon: "Profilbild" +replies: "Svara" +renotes: "Omnotera" _achievements: _types: _open3windows: title: "Flera Fönster" description: "Ha minst 3 fönster öppna samtidigt" +_ffVisibility: + public: "Publicera" _email: _follow: title: "följde dig" @@ -403,7 +510,6 @@ _sfx: chat: "Chatt" antenna: "Antenner" _2fa: - passwordToTOTP: "Skriv in ditt lösenord" renewTOTPCancel: "Nej tack" _antennaSources: all: "Alla noter" @@ -426,6 +532,7 @@ _visibility: home: "Hem" followers: "Följare" _profile: + name: "Namn" username: "Användarnamn" changeAvatar: "Ändra profilbild" changeBanner: "Ändra banner" @@ -461,6 +568,8 @@ _deck: tl: "Tidslinje" antenna: "Antenner" list: "Listor" + channel: "kanal" mentions: "Omnämningar" _webhookSettings: + name: "Namn" active: "Aktiverad" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index d8e68202d7..f9262fea7e 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -1,9 +1,9 @@ --- _lang_: "ภาษาไทย" -headlineMisskey: "เชื่อมต่อเครือข่ายโดยโน้ต" -introMisskey: "ยินดีต้อนรับจ้าาา! Misskey เป็นบริการไมโครบล็อกโอเพ่นซอร์ส แบบการกระจายอำนาจ\nสร้าง \"โน้ต\" เพื่อแบ่งปันความคิดของคุณกับทุกคนรอบตัวคุณกันเถอะ 📡\nด้วยการ \"รีแอคชั่นผู้คน\" คุณยังสามารถแสดงความรู้สึกของคุณเกี่ยวกับบันทึกของทุกคนได้อย่างรวดเร็ว 👍\n\nแล้วมาท่องสำรวจโลกใบใหม่กันเถอะ! 🚀" +headlineMisskey: "เชื่อมต่อระบบ Network ด้วย Note" +introMisskey: "ยินดีต้อนรับทุกคนจ้า! Misskey คือ บริการไมโครบล็อกกิ้ง (MicroBlogging) แบบกระจายศูนย์อำนาจ (Decentralized) \n\nเขียน \"โน้ต (Note)\" เพื่อส่งต่อเรื่องราวของคุณให้ทั้งโลกได้รับรู้📡\nและอย่าลืมที่จะ \"React\" กับเรื่องราวของคนอื่น ๆ ด้วย! 👍\n\nมุ่งสู่โลกใบใหม่กันเถอะ🚀" poweredByMisskeyDescription: "{name} เป็นส่วนหนึ่งในบริการที่ถูกขับเคลื่อนโดยแพลตฟอร์มโอเพ่นซอร์ส Misskey (เรียกว่า \"อินสแตนซ์ Misskey\")" -monthAndDay: "{เดือน}/{วัน}" +monthAndDay: "{month}/{day}" search: "ค้นหา" notifications: "การเเจ้งเตือน" username: "ชื่อผู้ใช้" @@ -15,7 +15,7 @@ gotIt: "เข้าใจแล้ว !" cancel: "ยกเลิก" noThankYou: "ไม่เป็นไร" enterUsername: "ใส่ชื่อผู้ใช้" -renotedBy: "รีโน้ตโดย {ผู้ใช้}" +renotedBy: "รีโน้ตโดย {user}" noNotes: "ไม่มีโน้ต" noNotifications: "ไม่มีการแจ้งเตือน" instance: "อินสแตนซ์" @@ -45,13 +45,20 @@ pin: "ปักหมุดไปยังโปรไฟล์" unpin: "เลิกปักหมุดจากโปรไฟล์" copyContent: "คัดลอกเนื้อหา" copyLink: "คัดลอกลิงก์" +copyLinkRenote: "คัดลอกลิงก์รีโน้ต" delete: "ลบ" deleteAndEdit: "ลบและแก้ไข" deleteAndEditConfirm: "นายแน่ใจแล้วเหรอ? ว่าต้องการลบโน้ตนี้และแก้ไข คุณอาจจะสูญเสียการโต้ตอบ, โน้ต, และการตอบกลับทั้งหมดได้นะ" addToList: "เพิ่มในลิสต์" +addToAntenna: "เพิ่มไปยังเสาอากาศ" sendMessage: "ส่งข้อความ" copyRSS: "คัดลอก RSS" copyUsername: "คัดลอกชื่อผู้ใช้" +copyUserId: "คัดลอก ID ผู้ใช้" +copyNoteId: "คัดลอก ID โน้ต " +copyFileId: "คัดลอกไฟล์ ID" +copyFolderId: "คัดลอกโฟลเดอร์ ID" +copyProfileUrl: "คัดลอกโปรไฟล์ URL" searchUser: "ค้นหาผู้ใช้งาน" reply: "ตอบกลับ" loadMore: "โหลดเพิ่มเติม" @@ -68,13 +75,13 @@ import: "นำเข้า" export: "นำออก" files: "ไฟล์" download: "ดาวน์โหลด" -driveFileDeleteConfirm: "นายแน่ใจแล้วหรอ? ว่าต้องการลบไฟล์ \"{name}\" โน้ตย่อที่แนบมากับไฟล์นี้ก็จะถูกลบด้วยนะ" +driveFileDeleteConfirm: "คุณต้องการลบไฟล์ \"{name}\" ใช่หรือไม่? โน้ตย่อที่แนบมากับไฟล์นี้ก็จะถูกลบไปด้วย" unfollowConfirm: "นายแน่ใจแล้วหรอว่าต้องการเลิกติดตาม {name}?" exportRequested: "เมื่อคุณได้ร้องขอการส่งออก อาจจะต้องใช้เวลาสักครู่ และจะถูกเพิ่มในไดรฟ์ของคุณเมื่อเสร็จสิ้นแล้ว" importRequested: "เมื่อคุณได้ร้องขอการนำเข้า อาจจะต้องใช้เวลาสักครู่นะ" lists: "รายการ" -noLists: "คุณไม่มีลิสต์ใดๆนะ" -note: "ตัวโน้ต" +noLists: "คุณไม่มีลิสต์ใด ๆ" +note: " โน้ต" notes: "ตัวโน้ต" following: "กำลังติดตาม" followers: "ผู้ติดตาม" @@ -86,13 +93,13 @@ somethingHappened: "อุ๊ย ! มีอะไรบางอย่างผ retry: "ลองใหม่อีกครั้ง" pageLoadError: "เกิดข้อผิดพลาดในการโหลดหน้านี้" pageLoadErrorDescription: "โดยปกติแล้วมักจะเกิดจากข้อผิดพลาดของเครือข่ายหรือแคชของเบราว์เซอร์ ลองล้างแคชแล้วลองใหม่อีกครั้งหลังจากรอสักครู่ " -serverIsDead: "เซิร์ฟเวอร์นี้ไม่มีการตอบสนอง ได้โปรดกรุณารอสักครู่แล้วลองใหม่อีกครั้งนะ" -youShouldUpgradeClient: "หากต้องการดูหน้านี้ได้โปรดกรุณา รีเซ็ตเพื่ออัปเดตไคลเอ็นต์ของคุณนะ" +serverIsDead: "เซิร์ฟเวอร์นี้ไม่มีการตอบสนอง โปรดกรุณารอสักครู่แล้วลองใหม่อีกครั้ง" +youShouldUpgradeClient: "หากต้องการดูหน้านี้ กรุณาโหลดหน้าใหม่เพื่ออัปเดตไคลเอ็นต์ของคุณ" enterListName: "ใส่ชื่อสำหรับรายการลิสต์" privacy: "ความเป็นส่วนตัว" makeFollowManuallyApprove: "ติดตามคำขอที่ต้องได้รับการอนุมัติ" defaultNoteVisibility: "การมองเห็นที่เป็นค่าเริ่มต้น" -follow: "กำลังติดตาม" +follow: "ติดตาม" followRequest: "ส่งคำขอติดตาม" followRequests: "ส่งคำขอติดตาม" unfollow: "เลิกติดตาม" @@ -100,15 +107,15 @@ followRequestPending: "กำลังรอดำเนินการร้อ enterEmoji: "ใส่อีโมจิ" renote: "รีโน้ต" unrenote: "เลิกรีโน้ต" -renoted: "รีโน้ตแล้วนะ" +renoted: "รีโน้ตแล้ว" cantRenote: "โพสต์นี้ไม่สามารถรีโน้ตไว้ใหม่ได้นะ" cantReRenote: "ไม่สามารถรีโน้ตเอาไว้ใหม่ได้นะ" -quote: "อ้างคำพูด" +quote: "อ้างอิง" inChannelRenote: "รีโน้ตช่องแชลแนลเท่านั้น" inChannelQuote: "อ้างช่องเท่านั้น" pinnedNote: "โน้ตที่ปักหมุดเอาไว้" pinned: "ปักหมุดไปยังโปรไฟล์" -you: "ตัวเอง" +you: "คุณ" clickToShow: "คลิกเพื่อแสดง" sensitive: "เนื้อหาที่ละเอียดอ่อน NSFW" add: "เพิ่ม" @@ -131,11 +138,13 @@ suspend: "ถูกระงับ" unsuspend: "ยกเลิกระงับ" blockConfirm: "คุณแน่ใจแล้วเหรอ? ว่าต้องการบล็อกบัญชีนี้" unblockConfirm: "คุณแน่ใจแล้วเหรอ? ว่าต้องการปลดบล็อคบัญชีนี้" -suspendConfirm: "นายแน่ใจแล้วเหรอว่าต้องการระงับบัญชีนี้อ่ะ?" +suspendConfirm: "แน่ใจว่าคุณต้องการระงับบัญชีนี้?" unsuspendConfirm: "นายแน่ใจแล้วหรอ? ว่าต้องการยกเลิกการระงับบัญชีนี้" selectList: "เลือกรายการ" +editList: "แก้ไขรายการ" selectChannel: "เลือกแชนแนล" selectAntenna: "เลือกเสาอากาศ" +editAntenna: "แก้ไขเสาอากาศ" selectWidget: "เลือกวิดเจ็ต" editWidgets: "แก้ไขวิดเจ็ต" editWidgetsExit: "เรียบร้อย" @@ -148,6 +157,9 @@ addEmoji: "แทรกอีโมจิ" settingGuide: "การตั้งค่าที่แนะนำ" cacheRemoteFiles: "แคชไฟล์ระยะไกล" cacheRemoteFilesDescription: "เมื่อปิดใช้งานการตั้งค่านี้ ไฟล์ระยะไกลนั้นจะถูกโหลดโดยตรงจากอินสแตนซ์ระยะไกล แต่กรณีการปิดใช้งานนี้จะช่วยลดปริมาณการใช้พื้นที่จัดเก็บข้อมูล แต่เพิ่มปริมาณการใช้งาน เพราะเนื่องจากจะไม่มีการสร้างภาพขนาดย่อ" +youCanCleanRemoteFilesCache: "คุณสามารถล้างแคชได้โดยคลิกที่ปุ่ม 🗑️ ในมุมมองการจัดการไฟล์" +cacheRemoteSensitiveFiles: "ไฟล์ระยะไกลที่มีความละเอียดอ่อนแคช" +cacheRemoteSensitiveFilesDescription: "เมื่อปิดการใช้งานแล้วการตั้งค่านี้ ไฟล์รีโมตที่มีความละเอียดอ่อนนั้นจะถูกโหลดโดยตรงจากอินสแตนซ์ระยะไกลโดยที่ไม่มีการแคช" flagAsBot: "ทำเครื่องหมายบอกว่าบัญชีนี้เป็นบอท" flagAsBotDescription: "การเปิดใช้งานตัวเลือกนี้หากบัญชีนี้ถูกควบคุมโดยนักเขียนโปรแกรม หรือ ถ้าหากเปิดใช้งาน มันจะทำหน้าที่เป็นแฟล็กสำหรับนักพัฒนารายอื่นๆ และเพื่อป้องกันการโต้ตอบแบบไม่มีที่สิ้นสุดกับบอทตัวอื่นๆ และยังสามารถปรับเปลี่ยนระบบภายในของ Misskey เพื่อปฏิบัติต่อบัญชีนี้เป็นบอท" flagAsCat: "ทำเครื่องหมายบอกว่าบัญชีนี้เป็นแมว" @@ -164,7 +176,7 @@ wallpaper: "วอลล์เปเปอร์" setWallpaper: "ตั้งวอลเปเปอร์" removeWallpaper: "นำวอลเปเปอร์ออก" searchWith: "ค้นหา: {q}" -youHaveNoLists: "รายการนี้ว่างเปล่า" +youHaveNoLists: "คุณไม่มีลิสต์ใด ๆ " followConfirm: "คุณแน่ใจแล้วหรอว่าต้องการที่จะติดตาม {name}?" proxyAccount: "บัญชี พร็อกซี่" proxyAccountDescription: "บัญชีพร็อกซี่ คือ บัญชีที่จะทำหน้าที่เป็นผู้ติดตามระยะไกลสำหรับผู้ใช้งานที่อยู่ภายใต้ด้วยเงื่อนไขบางอย่าง ยกตัวอย่าง เช่น เมื่อมีผู้ใช้งานนั้นได้เพิ่มผู้ใช้งานจากระยะไกลลงในรายการ แต่กิจกรรมของผู้ใช้ในระยะไกลนั้นจะไม่ถูกส่งไปยังอินสแตนซ์หากไม่มีผู้ใช้งานในพื้นที่ติดตามผู้ใช้รายนั้น ดังนั้นบัญชีพร็อกซีนี้จะติดตามแทน" @@ -173,7 +185,7 @@ selectUser: "เลือกผู้ใช้งาน" recipient: "ผู้รับ" annotation: "ความคิดเห็น" federation: "เฟดิเวิร์ส" -instances: "ตัวอย่าง" +instances: "Server" registeredAt: "จดทะเบียนที่" latestRequestReceivedAt: "ได้รับคำขอล่าสุดไปแล้ว" latestStatus: "สถานะล่าสุด" @@ -186,7 +198,7 @@ blockThisInstance: "บล็อกอินสแตนซ์นี้" operations: "ดำเนินการ" software: "ซอฟต์แวร์" version: "เวอร์ชั่น" -metadata: "ข้อมูลเมตา" +metadata: "Metadata" withNFiles: "{n} ไฟล์(s)" monitor: "มอนิเตอร์" jobQueue: "คิวงาน" @@ -213,7 +225,7 @@ intro: "การติดตั้ง Misskey เสร็จสิ้นแล done: "เสร็จสิ้น" processing: "กำลังประมวลผล..." preview: "แสดงตัวอย่าง" -default: "ค่าตั้งต้น" +default: "ค่าเริ่มต้น" defaultValueIs: "ค่าเริ่มต้น: {value}" noCustomEmojis: "ไม่มีอีโมจิ" noJobs: "ไม่มีชิ้นงาน" @@ -234,17 +246,17 @@ currentPassword: "รหัสผ่านปัจจุบัน" newPassword: "รหัสผ่านใหม่" newPasswordRetype: "ใส่รหัสผ่านใหม่อีกครั้ง" attachFile: "แนบไฟล์" -more: "เพิ่มเติม!" +more: "เพิ่มเติม" featured: "ไฮไลท์" usernameOrUserId: "ชื่อผู้ใช้หรือรหัสผู้ใช้งาน" -noSuchUser: "ไม่มีผู้ใช้นี้อยู่ในระบบ" +noSuchUser: "ไม่พบผู้ใช้" lookup: "การค้นหา" announcements: "ประกาศ" imageUrl: "url รูปภาพ" remove: "ลบ" removed: "ถูกลบไปแล้ว" removeAreYouSure: "นายแน่ใจจริงหรอว่าต้องการที่จะลบออก \"{x}\"" -deleteAreYouSure: "นายแน่ใจจริงหรอว่าต้องการที่จะลบออก \"{x}\"" +deleteAreYouSure: "ต้องการลบ {x} หรือไม่คะ?" resetAreYouSure: "รีเซ็ตเลยไหม" saved: "บันทึกแล้ว" messaging: "แชท" @@ -282,7 +294,7 @@ themeForLightMode: "ธีมที่จะใช้ในโหมดแสง themeForDarkMode: "ธีมที่จะใช้ในโหมดมืด" light: "สว่าง" dark: "มืด" -lightThemes: "ธีมสีสว่าง" +lightThemes: "ธีมสว่าง" darkThemes: "ธีมมืด" syncDeviceDarkMode: "ซิงค์โหมดมืดด้วยการตั้งค่ากับอุปกรณ์" drive: "ไดรฟ์" @@ -298,8 +310,8 @@ renameFolder: "เปลี่ยนชื่อโฟลเดอร์" deleteFolder: "ลบโฟลเดอร์" addFile: "เพิ่มไฟล์" emptyDrive: "ไดรฟ์ของคุณว่างเปล่านะ" -emptyFolder: "โฟลเดอร์นี้น่าจะว่างเปล่านะ" -unableToDelete: "ไม่สามารถลบออกได้นะ" +emptyFolder: "โฟลเดอร์นี้ว่างเปล่า" +unableToDelete: "ไม่สามารถลบออกได้" inputNewFileName: "ป้อนชื่อไฟล์ใหม่นะ" inputNewDescription: "กรุณาใส่แคปชั่นใหม่" inputNewFolderName: "กรุณาใส่ชื่อโฟลเดอร์ใหม่นะ\n" @@ -309,7 +321,7 @@ copyUrl: "คัดลอก URL" rename: "เปลี่ยนชื่อ" avatar: "ไอคอน" banner: "แบนเนอร์" -nsfw: "เนื้อหาที่ละเอียดอ่อน NSFW" +displayOfSensitiveMedia: "แสดงผลสื่อละเอียดอ่อน" whenServerDisconnected: "สูญเสียการเชื่อมต่อกับเซิร์ฟเวอร์" disconnectedFromServer: "ถูกตัดการเชื่อมต่อออกจากเซิร์ฟเวอร์" reload: "รีโหลด" @@ -319,7 +331,7 @@ watch: "ดู" unwatch: "หยุดดู" accept: "ยอมรับ" reject: "ปฏิเสธ" -normal: "โหมดปกติ" +normal: "ปกติ" instanceName: "ชื่อ อินสแตนซ์" instanceDescription: "คำอธิบายอินสแตนซ์" maintainerName: "ผู้ดูแล" @@ -328,9 +340,9 @@ tosUrl: "เงื่อนไขการให้บริการ URL" thisYear: "ปีนี้" thisMonth: "เดือนนี้" today: "วันนี้" -dayX: "{วัน}" -monthX: "{เดือน}" -yearX: "{ปี}" +dayX: "{day}" +monthX: "เดือน {month}" +yearX: "{year}" pages: "หน้า" integration: "รวบรวม" connectService: "เชื่อมต่อ" @@ -344,7 +356,6 @@ invite: "เชิญชวน" driveCapacityPerLocalAccount: "ความจุของไดรฟ์ต่อผู้ใช้ภายในเครื่อง" driveCapacityPerRemoteAccount: "ความจุของไดรฟ์ต่อผู้ใช้ระยะไกล" inMb: "เป็นเมกะไบต์" -iconUrl: "ไอคอน URL" bannerUrl: "URL รูปภาพแบนเนอร์" backgroundImageUrl: "URL ภาพพื้นหลัง" basicInfo: "ข้อมูลเบื้องต้น" @@ -353,7 +364,7 @@ pinnedUsersDescription: "ลิสต์ชื่อผู้ใช้โดย pinnedPages: "หน้าที่ปักหมุด" pinnedPagesDescription: "ป้อนเส้นทางของหน้าที่คุณต้องการตรึงไว้ที่หน้าแรกของอินสแตนซ์นี้ โดยคั่นด้วยตัวแบ่งบรรทัด" pinnedClipId: "ID ของคลิปที่จะปักหมุด" -pinnedNotes: "โน้ตที่ปักหมุดเอาไว้" +pinnedNotes: "โน้ตที่ปักหมุดไว้" hcaptcha: "hCaptcha" enableHcaptcha: "เปิดใช้ hCaptcha" hcaptchaSiteKey: "คีย์ไซต์" @@ -394,12 +405,13 @@ recentlyDiscoveredUsers: "ผู้ใช้ที่เพิ่งค้นพ exploreUsersCount: "มีผู้ใช้ {จำนวน} ราย" exploreFediverse: "สำรวจเฟดดิเวิร์ส" popularTags: "แท็กยอดนิยม" -userList: "รายการ" +userList: "ลิสต์" about: "เกี่ยวกับ" aboutMisskey: "เกี่ยวกับ Misskey" administrator: "ผู้ดูแลระบบ" token: "โทเค็น" 2fa: "การยืนยันตัวตนแบบสองชั้น" +setupOf2fa: "ตั้งค่าการยืนยันตัวตนแบบสองชั้น" totp: "แอป Authenticator" totpDescription: "ใช้แอปยืนยันตัวตนเพื่อป้อนรหัสผ่านแบบใช้ครั้งเดียว" moderator: "ผู้ควบคุม" @@ -434,7 +446,7 @@ text: "ข้อความ" enable: "เปิดใช้งาน" next: "ถัด​ไป" retype: "พิมพ์รหัสอีกครั้ง" -noteOf: "โน้ต โดย {ผู้ใช้งาน}" +noteOf: "โน้ต โดย {user}" quoteAttached: "อ้างอิง" quoteQuestion: "นายต้องการที่จะอ้างอิงหรอ?" noMessagesYet: "ยังไม่มีข้อความนะ" @@ -643,6 +655,7 @@ behavior: "พฤติกรรม" sample: "ตัวอย่าง" abuseReports: "รายงาน" reportAbuse: "รายงาน" +reportAbuseRenote: "รายงานรีโน้ต" reportAbuseOf: "รายงาน {ชื่อ}" fillAbuseReportDescription: "กรุณากรอกรายละเอียดเกี่ยวกับรายงานนี้ หากเป็นเรื่องเกี่ยวกับโน้ตโดยเฉพาะ ได้โปรดระบุ URL" abuseReported: "เราได้ส่งรายงานของคุณไปแล้ว ขอบคุณมากๆนะ" @@ -670,6 +683,7 @@ createNewClip: "สร้างคลิปใหม่" unclip: "ลบคลิป" confirmToUnclipAlreadyClippedNote: "โน้ตนี้เป็นส่วนหนึ่งของคลิป \"{name}\" แล้ว คุณต้องการลบออกจากคลิปนี้แทนอย่างงั้นหรอ?" public: "สาธารณะ" +private: "ส่วนตัว" i18nInfo: "Misskey กำลังได้รับการแปลเป็นภาษาต่างๆ โดยอาสาสมัคร คุณสามารถช่วยเหลือได้ที่ {link}" manageAccessTokens: "การจัดการโทเค็นการเข้าถึง" accountInfo: "ข้อมูลบัญชี" @@ -790,6 +804,7 @@ noMaintainerInformationWarning: "ข้อมูลผู้ดูแลไม noBotProtectionWarning: "ไม่ได้กำหนดค่าการป้องกันบอทนะ" configure: "กำหนดค่า" postToGallery: "สร้างโพสต์แกลเลอรี่ใหม่" +postToHashtag: "โพสต์ไปที่แฮชแท็กนี้" gallery: "แกลเลอรี่" recentPosts: "โพสต์ล่าสุด" popularPosts: "โพสต์ติดอันดับ" @@ -823,6 +838,7 @@ translatedFrom: "แปลมาจาก {x}" accountDeletionInProgress: "กำลังดำเนินการลบบัญชีอยู่" usernameInfo: "ชื่อที่ระบุบัญชีของคุณจากผู้อื่นในเซิร์ฟเวอร์นี้ คุณสามารถใช้ตัวอักษร (a~z, A~Z), ตัวเลข (0~9) หรือขีดล่าง (_) ชื่อผู้ใช้ไม่สามารถเปลี่ยนแปลงได้ในภายหลัง" aiChanMode: "โหมด Ai " +devMode: "โหมดนักพัฒนา" keepCw: "เก็บคำเตือนเนื้อหา" pubSub: "บัญชีผับ/ย่อย" lastCommunication: "การสื่อสารครั้งสุดท้ายล่าสุด" @@ -832,6 +848,8 @@ breakFollow: "ลบผู้ติดตาม" breakFollowConfirm: "ลบผู้ติดตามนี้ออกจริงหรอ?" itsOn: "เปิดใช้งาน" itsOff: "ปิดใช้งาน" +on: "เปิด" +off: "ปิด" emailRequiredForSignup: "จำเป็นต้องการใช้ที่อยู่อีเมลสำหรับการสมัคร" unread: "ไม่ได้อ่าน" filter: "กรอง" @@ -850,7 +868,7 @@ incorrectPassword: "รหัสผ่านไม่ถูกต้อง" voteConfirm: "ยืนยันการโหวต \"{choice}\" มั้ย?" hide: "ซ่อน" useDrawerReactionPickerForMobile: "แสดงผล ตัวเลือกปฏิกิริยาเป็นลิ้นชักบนมือถือ" -welcomeBackWithName: "ยินดีต้อนรับการกลับมานะค่ะ, {name}" +welcomeBackWithName: "ยินดีต้อนรับการกลับมานะคะ, {name}" clickToFinishEmailVerification: "กรุณาคลิก [{ok}] เพื่อดำเนินการยืนยันอีเมลให้เสร็จสมบูรณ์นะ" overridedDeviceKind: "ประเภทอุปกรณ์" smartphone: "สมาร์ทโฟน" @@ -943,7 +961,7 @@ show: "แสดงผล" neverShow: "ไม่ต้องแสดงข้อความนี้อีก" remindMeLater: "ไว้ครั้งหน้าแล้วกัน" didYouLikeMisskey: "คุณเคยชอบ Misskey ไหม?" -pleaseDonate: "{host} ใช้ซอฟต์แวร์ฟรี Misskey เราขอขอบคุณการบริจาคของคุณอย่างสูงเพื่อให้การพัฒนา Misskey สามารถดำเนินต่อไปได้นะ!" +pleaseDonate: "Misskey เป็นซอฟต์แวร์ฟรีที่ใช้งานโดย {host} เราขอขอบคุณการสนับสนุนของคุณอย่างสูงเพื่อให้การพัฒนา Misskey สามารถดำเนินต่อไปได้!" roles: "บทบาท" role: "บทบาท" noRole: "ไม่พบบทบาท" @@ -955,7 +973,7 @@ color: "สี" manageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" youCannotCreateAnymore: "คุณถึงขีดจํากัดการสร้างแล้วนะ" cannotPerformTemporary: "ไม่สามารถใช้การได้ชั่วคราว" -cannotPerformTemporaryDescription: "การดําเนินการนี้ไม่สามารถดําเนินการได้ชั่วคราว เนื่องจากเกินขีดจํากัดการดําเนินการ กรุณารอสักครู่แล้วลองใหม่อีกครั้งนะค่ะ" +cannotPerformTemporaryDescription: "ไม่สามารถดําเนินการได้ชั่วคราว เนื่องจากเกินขีดจํากัดการดําเนินการ กรุณารอสักครู่แล้วลองใหม่อีกครั้ง" invalidParamError: "ข้อผิดพลาดพารามิเตอร์" invalidParamErrorDescription: "คำขอพารามิเตอร์ไม่ถูกต้อง สิ่งนี้มักจะเกิดจากข้อผิดพลาด แต่อาจเกิดจากอินพุตเกินขีดจำกัดของขนาดหรือที่คล้ายกัน" permissionDeniedError: "การดำเนินถูกปฏิเสธ" @@ -977,20 +995,23 @@ joinThisServer: "ลงชื่อสมัครใช้ในอินสแ exploreOtherServers: "มองหาอินสแตนซ์อื่น" letsLookAtTimeline: "ลองดูที่ไทม์ไลน์" disableFederationConfirm: "ปิดใช้งานสหพันธ์จริงๆหรอแน่ใจแล้วนะ?" -disableFederationConfirmWarn: "แม้ว่าจะถูกยกเลิกเอาไว้โพสต์ดังกล่าวนั้นจะยังคงเป็นสาธารณะต่อไป เว้นแต่ว่า...จะตั้งค่าเป็นอย่างอื่น โดยปกติคุณไม่จำเป็นต้องทำตรงนี้หรอกนะค่ะ" +disableFederationConfirmWarn: "โพสต์จะยังคงเป็นสาธารณะต่อไป เว้นแต่จะตั้งค่าเป็นอย่างอื่น" disableFederationOk: "ปิดการใช้งาน" -invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญที่ถูกต้องถึงจะลงทะเบียนได้นะค่ะ" -emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ" +invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญ เพื่องลงทะเบียนเข้าใช้งาน" +emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมล" postToTheChannel: "โพสต์ลงช่อง" cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ" reactionAcceptance: "การยอมรับรีแอคชั่น" likeOnly: "ที่ชอบเท่านั้น" likeOnlyForRemote: "ไลค์สำหรับอินสแตนซ์ระยะไกลเท่านั้น" +nonSensitiveOnly: "ไม่มีความอ่อนไหวเท่านั้น" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "ไม่มีความอ่อนไหวเท่านั้น (เฉพาะไลค์จากระยะไกลเท่านั้น)" rolesAssignedToMe: "บทบาทที่ได้รับมอบหมายให้ฉัน" resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?" sensitiveWords: "คำที่ละเอียดอ่อน" sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ" -notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งานนะค่ะ" +sensitiveWordsDescription2: "การใช้ช่องว่างนั้นอาจจะสร้างนิพจน์ AND และคำหลักที่มีเครื่องหมายทับล้อมรอบจะเปลี่ยนเป็นนิพจน์ทั่วไปนะ" +notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งาน" license: "ใบอนุญาต" unfavoriteConfirm: "ลบออกจากรายการโปรดแน่ใจหรอ?" myClips: "คลิปของฉัน" @@ -1001,7 +1022,6 @@ retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการ enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล" enableChartsForFederatedInstances: "สร้างแผนภูมิข้อมูลอินสแตนซ์ระยะไกล" showClipButtonInNoteFooter: "เพิ่ม \"คลิป\" เพื่อบันทึกเมนูการทำงาน" -largeNoteReactions: "ขยายรีแอคชั่นการแสดงผล" noteIdOrUrl: "โน้ต ID หรือ URL" video: "วีดีโอ" videos: "วีดีโอ" @@ -1025,29 +1045,103 @@ vertical: "แนวตั้ง" horizontal: "ด้านข้าง" position: "ตำแหน่ง" serverRules: "กฎของเซิฟเวอร์" -pleaseConfirmBelowBeforeSignup: "โปรดยืนยันด้านล่างก่อนกำลังลงชื่อสมัครนะค่ะ" +pleaseConfirmBelowBeforeSignup: "โปรดยืนยันที่ด้านล่างก่อนสมัครใช้งาน" pleaseAgreeAllToContinue: "คุณต้องยอมรับทุกช่องตรงด้านบนเพื่อดำเนินการต่อค่ะ" continue: "ดำเนินการต่อ" preservedUsernames: "ชื่อผู้ใช้ที่สงวนไว้" preservedUsernamesDescription: "ลิสต์ชื่อผู้ใช้ที่จะสำรองโดยคั่นด้วยการแบ่งบรรทัดนั้น เพราะสิ่งเหล่านี้จะไม่สามารถทำได้ในระหว่างการสร้างบัญชีตามปกติ บัญชีที่มีอยู่แล้วนั้นโดยใช้ชื่อผู้ใช้เหล่านี้จะไม่ได้รับผลกระทบอะไร" createNoteFromTheFile: "เรียบเรียงโน้ตจากไฟล์นี้" archive: "เก็บถาวร" +channelArchiveConfirmTitle: "เก็บถาวรจริงๆ {name} มั้ย?" +channelArchiveConfirmDescription: "ช่องที่ถูกเก็บถาวรแล้วนั้นจะไม่ปรากฏในรายการช่องหรือผลการค้นหานั้นอีกต่อไปไม่สามารถเพิ่มโพสต์ใหม่ได้อีกต่อไปนะ" +thisChannelArchived: "ช่องนี้ถูกเก็บถาวรแล้วนะ" +displayOfNote: "การแสดงโน้ต" +initialAccountSetting: "ตั้งค่าโปรไฟล์" youFollowing: "ติดตามแล้ว" +preventAiLearning: "ปฏิเสธการใช้งาน ในการเรียนรู้ของเครื่อง (Generative AI)" +preventAiLearningDescription: "การส่งคำร้องขอโปรแกรมรวบรวมข้อมูลไม่ให้ใช้ข้อความที่โพสต์หรือรูปภาพ ฯลฯ ในชุดข้อมูลแมชชีนเลิร์นนิง (Predictive / Generative AI) สิ่งนี้นั้นทำได้โดยการเพิ่มแฟล็กการตอบสนอง \"noai\" HTML ให้กับเนื้อหาที่เกี่ยวข้อง แต่อย่างไรก็ตามแล้ว การป้องกันโดยสมบูรณ์นั้นไม่สามารถทำได้ผ่านแฟล็กนี้เนื่องจากอาจจะทำให้ถูกเพิกเฉยได้" options: "ตัวเลือกบทบาท" +specifyUser: "ผู้ใช้เฉพาะ" +failedToPreviewUrl: "ไม่สามารถดูตัวอย่างได้" +update: "อัปเดต" +rolesThatCanBeUsedThisEmojiAsReaction: "บทบาทที่สามารถใช้อิโมจินี้เป็นรีแอคชั่นได้" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ถ้าหากไม่ได้ระบุบทบาท ทุกคนนั้นก็สามารถใช้อิโมจินี้เป็นการแสดงความรู้สึกได้นะ" +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "บทบาทเหล่านี้ต้องเป็นสาธารณะ" +cancelReactionConfirm: "ต้องการลบรีแอคชั่นของคุณจริงๆหรอ?" +changeReactionConfirm: "ต้องการเปลี่ยนรีแอคชั่นของคุณจริงๆหรอ?" +later: "ไว้ทีหลัง" +goToMisskey: "ถึง Misskey" +additionalEmojiDictionary: "พจนานุกรมอีโมจิเพิ่มเติม" +installed: "ติดตั้งแล้ว" +branding: "แบรนดิ้ง" +enableServerMachineStats: "เผยแพร่สถานะฮาร์ดแวร์ของเซิร์ฟเวอร์" +enableIdenticonGeneration: "เปิดใช้งานผู้ใช้สร้างตัวระบุ" +turnOffToImprovePerformance: "การปิดส่วนนี้สามารถเพิ่มประสิทธิภาพได้" +createInviteCode: "สร้างคำเชิญ" +createWithOptions: "สร้างด้วยตัวเลือก" +createCount: "จำนวนการเชิญ" +inviteCodeCreated: "สร้างคำเชิญแล้ว" +inviteLimitExceeded: "คุณสร้างคำเชิญเกินถึงขีดจำกัดแล้วนะ" +createLimitRemaining: "ขีดจำกัดการเชิญ: {limit} ที่เหลืออยู่" +inviteLimitResetCycle: "ขีดจำกัดนี้จะถูกรีเซ็ตเป็น {limit} ที่ {time}." +expirationDate: "วันที่หมดอายุ" +noExpirationDate: "ไม่มีหมดอายุ" +inviteCodeUsedAt: "รหัสคำเชิญใช้แล้วที่" +registeredUserUsingInviteCode: "ใช้คำเชิญแล้วโดย" +waitingForMailAuth: "กำลังรอการยืนยันอีเมล" +inviteCodeCreator: "สร้างการเชิญแล้วโดย" +usedAt: "ใช้แล้วที่" +unused: "ไม่ใช้แล้ว" +used: "ใช้แล้ว" +expired: "หมดอายุแล้ว" +doYouAgree: "ยอมรับมั้ย?" +beSureToReadThisAsItIsImportant: "กรุณาอ่านข้อมูลที่สำคัญอันนี้" +iHaveReadXCarefullyAndAgree: "ฉันได้อ่านข้อความ \"{x}\" และยินยอม" +dialog: "ไดอะล็อก" +icon: "ไอคอน" +forYou: "สำหรับคุณ" +replies: "ตอบกลับ" +renotes: "รีโน้ต" +loadReplies: "แสดงการตอบกลับ" +loadConversation: "แสดงบทสนทนา" +_announcement: + forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน" + needConfirmationToReadDescription: "ข้อความแจ้งแยก ถ้าหากต้องการเพื่อยืนยันว่ากำลังทำเครื่องหมายประกาศนี้ว่าอ่านแล้วจะแสดงขึ้นถ้าหากเปิดใช้งาน การประกาศนั้นจะไม่รวมอยู่ในฟังก์ชั่นว่า \"ทำเครื่องหมายทั้งหมดว่าอ่านแล้ว\"" + end: "ประกาศเก็บถาวร" + tooManyActiveAnnouncementDescription: "การมีประกาศที่ใช้งานมากเกินไปนั้นอาจจะทำให้ประสบการณ์ของผู้ใช้งานนั้นดูแย่ลง โปรดกรุณาพิจารณาการเก็บประกาศที่ล้าสมัยด้วยนะค่ะ" + readConfirmTitle: "ทำเครื่องหมายบอกว่าอ่านแล้วเลยมั้ย?" + readConfirmText: "การดำเนินการนี้จะทำเครื่องหมายเนื้อหาของ \"{title}\" บอกว่าอ่านแล้วนะ" +_initialAccountSetting: + accountCreated: "คุณได้สร้างบัญชีของคุณสำเร็จเรียบร้อยแล้ว!" + letsStartAccountSetup: "สำหรับผู้เริ่มต้นมาตั้งค่าโปรไฟล์ของคุณกันเถอะ" + letsFillYourProfile: "ก่อนอื่นมาตั้งค่าโปรไฟล์ของคุณ" + profileSetting: "ตั้งค่าโปรไฟล์" + privacySetting: "ตั้งค่าความเป็นส่วนตัว" + theseSettingsCanEditLater: "คุณสามารถเปลี่ยนการตั้งค่าเหล่านี้ได้ในภายหลังได้ตลอดเวลานะ" + youCanEditMoreSettingsInSettingsPageLater: "ยังมีการตั้งค่าอื่นๆ อีกมากมายที่คุณนั้นสามารถกำหนดค่าได้จาก \"การตั้งค่า\" เพื่อให้แน่ใจว่าได้เยี่ยมชมมันได้ภายหลังนะ" + followUsers: "ลองติดตามผู้ใช้บางคนที่คุณอาจจะสนใจเพื่อสร้างไทม์ไลน์ของคุณสิ !" + pushNotificationDescription: "กำลังเปิดใช้งานการแจ้งเตือนแบบพุชจะช่วยให้คุณได้รับการแจ้งเตือนจาก {name} โดยตรงบนอุปกรณ์ของคุณนะ" + initialAccountSettingCompleted: "ตั้งค่าโปรไฟล์เสร็จสมบูรณ์แล้ว!" + haveFun: "ขอให้สนุก {name}!" + ifYouNeedLearnMore: "ถ้าหากคุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับวิธีใช้ {ชื่อ} (Misskey) กรุณาไปที่ {link}" + skipAreYouSure: "ต้องการข้ามการตั้งค่าโปรไฟล์จริงๆแบบนั้นหรอ?" + laterAreYouSure: "ต้องการตั้งค่าโปรไฟล์ในภายหลังจริงๆอย่างงั้นหรอ?" _serverRules: description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ" +_serverSettings: + iconUrl: "ไอคอน URL" _accountMigration: moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง" moveFromSub: "สร้างนามแฝงไปยังบัญชีอื่น" moveFromLabel: "บัญชีที่จะย้ายจาก:" - moveFromDescription: "สร้างนามแฝงสำหรับบัญชีที่จะย้ายจากบัญชีนี้ ถ้าหากคุณต้องการโอนผู้ติดตาม สิ่งนี้ต้องทำก่อนโอนก่อนนะค่ะ! หลังจากนั้น ป้อนบัญชีที่จะย้ายไปในรูปแบบต่อไปนี้: @person@instance.com" + moveFromDescription: "ถ้าหากคุณต้องการโอนข้อมูล คุณจำเป็นต้องสร้างบัญชีสำรองสำหรับการย้ายบัญชี หลังจากนั้นป้อนบัญชีที่จะย้ายไปในรูปแบบต่อไปนี้: @person@instance.com" moveTo: "ย้ายข้อมูลบัญชีนี้ไปยังบัญชีอีกหนึ่ง" moveToLabel: "บัญชีที่จะย้ายไปที่:" moveCannotBeUndone: "ไม่สามารถยกเลิกการโอนย้ายบัญชีได้" moveAccountDescription: "การกระทำนี้ไม่สามารถย้อนกลับได้นะ ขั้นตอนแรก ต้องสร้างนามแฝงสำหรับบัญชีนี้ในบัญชีที่คุณต้องการย้ายไป หลังจากนั้นแล้ว ป้อนบัญชีที่จะย้ายไปในรูปแบบดังต่อไปนี้: @person@instance.com" moveAccountHowTo: "หากต้องการย้ายข้อมูลก่อนอื่นให้สร้างชื่อแทนสำหรับบัญชีนี้ ในบัญชีที่จะต้องการย้ายไป\nหลังจากที่คุณสร้างนามแฝงนั้นแล้ว ให้ป้อนบัญชีที่ต้องการจะย้ายไปในรูปแบบดังต่อไปนี้: @username@server.example.com" startMigration: "โอนย้าย" - migrationConfirm: "ย้ายข้อมูลบัญชีนี้ไปที่ {account} จริงๆนะ เมื่อมีการเริ่มต้นแล้ว กระบวนการนี้จะไม่สามารถหยุดหรือนำกลับคืนมาได้ และคุณจะไม่สามารถใช้บัญชีนี้ในสถานะดั้งเดิมได้อีกต่อไป\n\nนอกจากนี้ เพื่อให้แน่ใจยืนยันว่าคุณได้สร้างนามแฝงในบัญชีที่จะย้ายข้อมูลนะค่ะ" + migrationConfirm: "ยืนยันการย้ายข้อมูลบัญชีนี้ไปที่ {account} เมื่อเริ่มแล้วจะไม่สามารถหยุดหรือนำกลับคืนมาได้ และคุณจะไม่สามารถใช้บัญชีนี้ในสถานะดั้งเดิมได้อีกต่อไป\n\nนอกจากนี้ คุณจำเป็นต้องสร้างบัญชีสำรองสำหรับการย้ายบัญชี" movedAndCannotBeUndone: "\nบัญชีนี้ถูกโอนย้ายไปแล้ว\nไม่สามารถย้อนกลับโอนย้ายข้อมูลได้" postMigrationNote: "บัญชีนี้จะถูกเลิกติดตามบัญชีทั้งหมดที่กำลังติดตามภายใน 24 ชั่วโมงหลังจากการย้ายข้อมูลนั้นเสร็จสิ้น ทั้งจำนวนผู้ติดตามและผู้ติดตามนั้นจะกลายเป็นศูนย์ เพื่อหลีกเลี่ยงป้องกันไม่ให้ผู้ติดตามของคุณนั้นไม่สามารถเห็นโพสต์เฉพาะผู้ติดตามของบัญชีนี้ได้ แต่อย่างไรก็ตามแล้วพวกเขาจะยังคงติดตามบัญชีนี้ต่อไป" movedTo: "บัญชีที่จะย้ายไปที่:" @@ -1055,8 +1149,8 @@ _achievements: earnedAt: "ได้รับเมื่อ" _types: _notes1: - title: "เพียงแค่ตั้งค่า msky ของฉัน" - description: "โพสต์โน้ตครั้งแรกของคุณ" + title: "just setting up my msky" + description: "โพสต์โน้ตแรกของคุณ" flavor: "ขอให้มีช่วงเวลาที่ดีกับ Misskey นะคะ!" _notes10: title: "โน้ตบางอย่าง" @@ -1215,7 +1309,7 @@ _achievements: _iLoveMisskey: title: "ฉันรัก Misskey" description: "โพสต์ \"I ❤ #Misskey\"" - flavor: "ทีมผู้พัฒนา Misskey ได้ขอบคุณสำหรับการสนับสนุนของคุณ!" + flavor: "ขอบคุณที่ใช้ Misskey! by ทีมผู้พัฒนา" _foundTreasure: title: "ล่าสมบัติ" description: "คุณพบสมบัติที่ซ่อนอยู่" @@ -1223,7 +1317,7 @@ _achievements: title: "พักผ่อนสักหน่อย" description: "ใช้เวลา 30 นาทีบน Misskey" _client60min: - title: "ไม่มี \"Miss\" ใน Misskey นะค่ะ !" + title: "ไม่มี \"Miss\" ใน Misskey " description: "เปิด Misskey ค้างไว้แล้วอย่างน้อย 60 นาที" _noteDeletedWithin1min: title: "ไม่เป็นไร" @@ -1290,6 +1384,8 @@ _achievements: title: "Brain Diver" description: "โพสต์ลิงก์ไปยัง Brain Diver" flavor: "Misskey-Misskey La-Tu-Ma" + _smashTestNotificationButton: + title: "ทดสอบโอเวอร์โฟลว์" _role: new: "บทบาทใหม่" edit: "แก้ไขบทบาท" @@ -1329,6 +1425,9 @@ _role: ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น" canPublicNote: "สามารถส่งโน้ตสาธารณะ" canInvite: "สร้างรหัสเชิญอินสแตนซ์" + inviteLimit: "จำกัดการเชิญ" + inviteLimitCycle: "จำกัดการเชิญไว้คูลดาวน์" + inviteExpirationTime: "วันหมดอายุของรหัสการเชิญ" canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" driveCapacity: "ความจุของไดรฟ์" alwaysMarkNsfw: "ทำเครื่องหมายไฟล์ว่าเป็น NSFW เสมอ" @@ -1369,7 +1468,7 @@ _sensitiveMediaDetection: _emailUnavailable: used: "ที่อยู่อีเมลนี้ได้ถูกใช้ไปแล้ว" format: "รูปแบบของที่อยู่อีเมลนี้ไม่ถูกต้อง" - disposable: "ที่อยู่อีเมลที่ใช้แล้วทิ้งนั้นไม่สามารถใช้ได้" + disposable: "ไม่สามารถใช้อีเมลชั่วคราวได้" mx: "เซิร์ฟเวอร์อีเมลนี้ไม่ถูกต้อง" smtp: "เซิร์ฟเวอร์อีเมลนี้ไม่มีการตอบสนอง" _ffVisibility: @@ -1391,6 +1490,7 @@ _ad: back: "ย้อนกลับ" reduceFrequencyOfThisAd: "แสดงโฆษณานี้ให้น้อยลง" hide: "ไม่ต้องแสดง" + timezoneinfo: "วันในสัปดาห์นี้จะถูกกำหนดจากโซนเวลาของเซิร์ฟเวอร์" _forgotPassword: enterEmail: "ป้อนที่อยู่อีเมลที่คุณเคยใช้ในการลงทะเบียนไว้ ลิงก์ที่คุณสามารถรีเซ็ตรหัสผ่านได้นั้นจะถูกส่งไปนะ" ifNoEmail: "ถ้าหากคุณไม่ได้ใช้อีเมลระหว่างการลงทะเบียน กรุณาติดต่อผู้ดูแลระบบอินสแตนซ์แทนนะ" @@ -1438,13 +1538,13 @@ _aboutMisskey: contributors: "ผู้สนับสนุนหลัก" allContributors: "ผู้มีส่วนร่วมทั้งหมด" source: "ซอร์สโค้ด" - translation: "รับแปลภาษา Misskey" + translation: "แปลภาษา Misskey" donate: "บริจาคให้กับ Misskey" - morePatrons: "เราขอขอบคุณสำหรับความช่วยเหลือจากผู้ช่วยอื่นๆ ที่ไม่ได้ระบุไว้ที่นี่นะ ขอขอบคุณ! 🥰" + morePatrons: " ขอบคุณทุกท่านที่ร่วมกันช่วยเหลือตลอดมานะคะ 🥰" patrons: "สมาชิกพันธมิตร" -_nsfw: - respect: "ซ่อนสื่อ NSFW" - ignore: "อย่าซ่อนสื่อ NSFW" +_displayOfSensitiveMedia: + respect: "ซ่อนสื่อทำเครื่องหมายบอกว่าละเอียดอ่อน" + ignore: "แสดงผลสื่อทำเครื่องหมายบอกว่าละเอียดอ่อน" force: "ซ่อนสื่อทั้งหมด" _instanceTicker: none: "ไม่ต้องแสดง" @@ -1585,25 +1685,27 @@ _time: day: "วัน" _timelineTutorial: title: "วิธีใช้งาน Misskey" + step1_1: "นี่คือ \"ไทม์ไลน์\" \"โน้ต\" ทั้งหมดที่ส่งใน {name} จะแสดงรายการตามลำดับเวลาที่นี่นะ" + step1_2: "อาจจะมีไทม์ไลน์ที่แตกต่างกันเล็กน้อยยกตัวอย่างเช่น \"ไทม์ไลน์หน้าแรก\" จะมีโน้ตของผู้ใช้ที่คุณติดตามและ \"ไทม์ไลน์ท้องถิ่น\" จะมีโน้ตจากผู้ใช้ทั้งหมดของ {name}" + step2_1: "มาลองโพสต์โน้ตต่อไปกัน คุณสามารถทำได้โดยการกดปุ่มที่มีไอคอนดินสอ" + step2_2: "ยังไงไหนลองเขียนแนะนำตัวเองหรือแค่ \"สวัสดี {name}!\" ถ้าคุณไม่รู้สึกเหมือนมัน?" step3_1: "เสร็จสิ้นการโพสต์โน้ตย่อแรกของคุณแล้วอย่างงั้นหรอ?" step3_2: "ไชโย! ตอนนี้โน้ตย่อแรกของคุณได้ปรากฏบนไทม์ไลน์ของคุณแล้วนะ" - step4_1: "คุณยังสามารถแนบ \"ปฏิกิริยา\" ไปกับโน้ตได้อีกด้วยนะค่ะ" + step4_1: "คุณสามารถเพิ่ม \"การตอบสนอง\" ในโน้ตได้" step4_2: "หากต้องการแนบการแสดงความรู้สึก ให้กดเครื่องหมาย \"+\" บนโน้ตแล้วเลือกอิโมจิที่คุณต้องการแสดงความรู้สึกที่ตนเองชอบได้เลย" _2fa: alreadyRegistered: "คุณได้ลงทะเบียนอุปกรณ์ยืนยันตัวตนแบบ 2 ชั้นแล้ว" registerTOTP: "ลงทะเบียนแอพตัวตรวจสอบสิทธิ์" - passwordToTOTP: "กรอกรหัสผ่าน" step1: "ขั้นตอนแรก ติดตั้งแอปยืนยันตัวตน (เช่น {a} หรือ {b}) บนอุปกรณ์ของคุณ" step2: "จากนั้นสแกนรหัส QR ที่แสดงบนหน้าจอนี้" step2Click: "การคลิกที่รหัส QR นี้จะช่วยให้คุณนั้นสามารถลงทะเบียน 2FA กับคีย์ความปลอดภัยหรือแอปตรวจสอบความถูกต้องของโทรศัพท์ได้" - step2Url: "คุณยังสามารถป้อนบน URL นี้หากคุณใช้โปรแกรมเดสก์ท็อป:" step3Title: "ป้อนรหัสยืนยัน" step3: "ป้อนโทเค็นที่แอปของคุณให้มาเพื่อเสร็จสิ้นการตั้งค่า" + setupCompleted: "ตั้งค่าสำเร็จแล้ว" step4: "นับจากนี้เป็นต้นไปการพยายามเข้าสู่ระบบในอนาคตนั้น อาจจะต้องขอโทเค็นในการเข้าสู่ระบบดังกล่าว" securityKeyNotSupported: "เบราว์เซอร์ของคุณไม่รองรับคีย์ความปลอดภัยนะ" registerTOTPBeforeKey: "กรุณาตั้งค่าแอปยืนยันตัวตนเพื่อลงทะเบียนรหัสความปลอดภัยหรือรหัสผ่าน" securityKeyInfo: "นอกจากนี้การตรวจสอบความถูกต้องด้วยลายนิ้วมือหรือ PIN แล้ว คุณยังสามารถตั้งค่าการตรวจสอบสิทธิ์ผ่านคีย์ความปลอดภัยของฮาร์ดแวร์ที่รองรับ FIDO2 เพื่อเพิ่มความปลอดภัยให้กับบัญชีของคุณ" - chromePasskeyNotSupported: "ขณะนี้ยังไม่รองรับรหัสผ่านของ Chrome" registerSecurityKey: "ลงทะเบียนรหัสความปลอดภัยหรือรหัสผ่าน" securityKeyName: "ป้อนชื่อคีย์" tapSecurityKey: "กรุณาทำตามเบราว์เซอร์ของคุณเพื่อลงทะเบียนรหัสความปลอดภัยหรือรหัสผ่าน" @@ -1614,6 +1716,7 @@ _2fa: renewTOTPConfirm: "วิธีการแบบนี้จะทําให้รหัสยืนยันจากแอพก่อนหน้าของคุณหยุดทํางานเลยนะ" renewTOTPOk: "ตั้งค่าคอนฟิกใหม่" renewTOTPCancel: "ไม่เป็นไร" + backupCodes: "รหัสสำรองข้อมูล" _permissions: "read:account": "ดูข้อมูลบัญชีของคุณ" "write:account": "แก้ไขข้อมูลบัญชีของคุณ" @@ -1647,6 +1750,10 @@ _permissions: "write:gallery": "แก้ไขแกลเลอรี่ของคุณ" "read:gallery-likes": "ดูรายการโพสต์ในแกลเลอรีที่ชอบของคุณ" "write:gallery-likes": "แก้ไขรายการโพสต์ในแกลเลอรีที่ชอบของคุณ" + "read:flash": "วิว เพลย์" + "write:flash": "แก้ไขเพลย์" + "read:flash-likes": "ดูรายชื่อของไลค์ เพลย์" + "write:flash-likes": "แก้ไขรายชื่อของไลค์ เพลย์" _auth: shareAccessTitle: "การให้สิทธิ์แอปพลิเคชัน" shareAccess: "คุณต้องการอนุญาตให้ \"{name}\" เข้าถึงบัญชีนี้เลยมั้ย?" @@ -1685,7 +1792,7 @@ _widgets: photos: "รูปภาพ" digitalClock: "นาฬิกาดิจิตอล" unixClock: "นาฬิกา UNIX" - federation: "สหพันธ์" + federation: "Fediration" instanceCloud: "อินสแตนซ์คลาวด์" postForm: "แบบฟอร์มการโพสต์" slideshow: "แสดงภาพนิ่ง" @@ -1695,7 +1802,7 @@ _widgets: serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์" aiscript: "AiScript คอนโซล" aiscriptApp: "AiScript แอพ" - aichan: "เอไอ" + aichan: "ไอ" userList: "รายชื่อผู้ใช้" _userList: chooseList: "เลือกรายการ" @@ -1882,6 +1989,10 @@ _notification: unreadAntennaNote: "เสาอากาศ {name}" emptyPushNotificationMessage: "การแจ้งเตือนแบบพุชได้รับการอัพเดทแล้ว" achievementEarned: "รับความสำเร็จ" + testNotification: "ทดสอบการแจ้งเตือน" + checkNotificationBehavior: "ตรวจสอบลักษณะที่ปรากฏการแจ้งเตือน" + sendTestNotification: "ส่งทดสอบการแจ้งเตือน" + notificationWillBeDisplayedLikeThis: "การแจ้งเตือนมีลักษณะแบบนี้" _types: all: "ทั้งหมด" follow: "กำลังติดตาม" @@ -1916,6 +2027,9 @@ _deck: introduction: "สร้างอินเทอร์เฟซที่สมบูรณ์แบบสำหรับคุณโดยจัดเรียงคอลัมน์ได้อย่างอิสระ!" introduction2: "คลิกที่เครื่องหมาย + ทางขวาของหน้าจอเพื่อเพิ่มคอลัมน์ใหม่ทุกครั้งที่คุณต้องการ" widgetsIntroduction: "กรุณาเลือก \"แก้ไขวิดเจ็ต\" ในเมนูคอลัมน์และเพิ่มวิดเจ็ต" + useSimpleUiForNonRootPages: "แสดง UI ของ Root Page อย่างง่าย " + usedAsMinWidthWhenFlexible: "ความกว้างขั้นต่ำนั้นจะถูกใช้งานสำหรับสิ่งนี้เมื่อเปิดใช้งานตัวเลือก \"ปรับความกว้างอัตโนมัติ\" หากเลือกเปิดใช้งานแล้ว" + flexible: "ปรับความกว้างอัตโนมัติ" _columns: main: "หลัก" widgets: "วิดเจ็ต" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index 7bd8188a48..f8fb275eb9 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -1,6 +1,8 @@ --- _lang_: "Türkçe" +headlineMisskey: "Notlarla bağlanmış bir ağ" introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Misskey'e hoş geldiniz.\nMisskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀." +poweredByMisskeyDescription: "name}Açık kaynak bir platform\nMisskeyDünya'nın en sunucularında biri。" monthAndDay: "{month}Ay {day}Gün" search: "Arama" notifications: "Bildirim" @@ -10,10 +12,14 @@ forgotPassword: "şifremi unuttum" ok: "TAMAM" gotIt: "Anladım" cancel: "İptal" +noThankYou: "Hayır, teşekkürler" enterUsername: "Kullanıcı adınızı giriniz" +renotedBy: "{user} tarafından Renotelandı" noNotes: "Notlar mevcut değil." noNotifications: "Bildirim bulunmuyor" +instance: "Sunucu" settings: "Ayarlar" +notificationSettings: "Bildirim Ayarları" basicSettings: "Temel Ayarlar" otherSettings: "Diğer Ayarlar" openInWindow: "Bir pencere ile aç" @@ -21,9 +27,11 @@ profile: "Profil" timeline: "Zaman çizelgesi" noAccountDescription: "Bu kullanıcı henüz biyografisini yazmadı" login: "Giriş Yap " +loggingIn: "Oturum aç" logout: "Çıkış Yap" signup: "Kayıt Ol" uploading: "Yükleniyor" +save: "Kaydet" users: "Kullanıcı" addUser: "Kullanıcı Ekle" favorite: "Favoriler" @@ -31,6 +39,7 @@ favorites: "Favoriler" unfavorite: "Favorilerden Kaldır" favorited: "Favorilerime eklendi." alreadyFavorited: "Zaten favorilerinizde kayıtlı." +cantFavorite: "Favorilere kayıt yapılamadı" pin: "Sabitlenmiş" unpin: "Sabitlemeyi kaldır" copyContent: "İçeriği kopyala" @@ -39,24 +48,403 @@ delete: "Sil" deleteAndEdit: "Sil ve yeniden düzenle" deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir." addToList: "Listeye ekle" +addToAntenna: "Antene ekle" sendMessage: "Mesaj Gönder" +copyRSS: "RSSKopyala" copyUsername: "Kullanıcı Adını Kopyala" +copyUserId: "KullanıcıyıKopyala" +copyNoteId: "Kimlik notunu kopyala" +copyFileId: "Dosya ID'sini kopyala" +copyFolderId: "Klasör ID'sini kopyala" +copyProfileUrl: "Profil URL'sini kopyala" searchUser: "Kullanıcıları ara" +reply: "yanıt" +loadMore: "Devamını yükle" +showMore: "Devamını yükle" +showLess: "Kapat" +youGotNewFollower: "seni takip etti" +receiveFollowRequest: "Takip isteği alındı" +followRequestAccepted: "Takip isteği kabul edildi" +mention: "Bahset" +mentions: "Bahsetmeler" +directNotes: "Kişisel mesajlar" +importAndExport: "İçeri/Dışarı aktar" +import: "İçeri aktar" +export: "Dışa aktar" +files: "Dosyalar" +download: "İndir" +driveFileDeleteConfirm: "\"{name}\" dosyası silinsin mi? Dosya kullanıldığı tüm notlardan kaybolacaktır." +unfollowConfirm: "{name} takipten çıkarılsın mı?" +exportRequested: "Dışa aktarım talep ettiniz. Bu biraz zaman alabilir. İşlem bitince Sürücünüze eklenecektir." +importRequested: "Dışa aktarım talep ettiniz. Bu işlem biraz zaman alabilir." +lists: "Listeler" +noLists: "Liste yok" +note: "not" +notes: "notlar" +following: "takipçi" +followers: "takipçi" +followsYou: "seni takip ediyor" +createList: "Liste oluştur" +manageLists: "Yönetici Listeleri" +error: "hata" +somethingHappened: "Bir hata oluştu" +retry: "Tekrar dene" +pageLoadError: "Sayfa yüklenemedi." +pageLoadErrorDescription: "Bu genelde ağ veya tarayıcı ön belleği hatalarından olur. Lütfen ön belleği temizlemeyi veya birkaç dakika beklemeyi ve sayfayı yenilemeyi deneyin." +serverIsDead: "Sunucu yanıt vermiyor. Birkaç dakika sonra tekrar deneyin." +youShouldUpgradeClient: "Sayfayı görüntülemek için yenileyin." +enterListName: "Liste ismi" +privacy: "Gizlilik" +makeFollowManuallyApprove: "Takip istekleri elle onaylansın" +defaultNoteVisibility: "Varsayılan görünürlük" +follow: "takipçi" +followRequest: "Takip isteği" +followRequests: "Takip istekleri" +unfollow: "takip etmeyi bırak" +followRequestPending: "Bekleyen Takip Etme Talebi" +enterEmoji: "Emoji Giriniz" +renote: "vazgeçme" +unrenote: "not alma" +renoted: "yeniden adlandırılmış" +cantRenote: "Ayrılamama" +cantReRenote: "not alabilirmiyim" +quote: "alıntı" +inChannelRenote: "Kanal içi Renote" +inChannelQuote: "Kanal içi Alıntı" +pinnedNote: "Sabitlenen" pinned: "Sabitlenmiş" +you: "sen" +clickToShow: "Görüntülemek için tıkla" +sensitive: "Hassas içerik" +add: "Ekle" +reaction: "Tepkiler" +reactions: "Tepkiler" +reactionSetting: "Palette görünecek tepkiler" +reactionSettingDescription2: "Sıralamak için sürükleyin, silmek için tıklayın, eklemek için \"+\" tuşuna tıklayın." +rememberNoteVisibility: "Görünürlük ayarlarını hatırla" +attachCancel: "Eki sil" +markAsSensitive: "Hassas içerik olarak işaretle" +unmarkAsSensitive: "Hassas içerik işaretini kaldır" +enterFileName: "Dosya ismini gir" +mute: "Gizle" +unmute: "sesi aç" +renoteMute: "sesi kapat" +renoteUnmute: "sesi açmayı iptal et" +block: "engelle" +unblock: "engellemeyi kaldır" +suspend: "askıya al" +unsuspend: "askıya alma" +blockConfirm: "Onayı engelle" +unblockConfirm: "engellemeyi kaldır onayla" +suspendConfirm: "Hesap askıya alınsın mı?" +unsuspendConfirm: "Hesap askıdan kaldırılsın mı" +selectList: "Bir liste seç" +editList: "Listeyi düzenle" +selectChannel: "Kanal seç" +selectAntenna: "Bir anten seç" +editAntenna: "Anteni düzenle" +selectWidget: "Araç seç" +editWidgets: "Araçları düzenle" +editWidgetsExit: "Tamam" +customEmojis: "Özel Emoji" +emoji: "Emoji" +emojis: "Emoji" +emojiName: "Emoji adı" +emojiUrl: "Emoji URL'si" +addEmoji: "Emoji ekle" +settingGuide: "Önerilen ayarlar" +cacheRemoteFiles: "Uzak dosyalar ön belleğe alınsın" +cacheRemoteFilesDescription: "Bu ayar açık olduğunda diğer sitelerin dosyaları doğrudan uzak sunucudan yüklenecektir. Bu ayarı kapatmak depolama kullanımını azaltacak ama küçük resimler oluşturulmadığından trafiği arttıracaktır." +cacheRemoteSensitiveFiles: "Hassas uzak dosyalar ön belleğe alınsın" +cacheRemoteSensitiveFilesDescription: "Bu ayar kapalı olduğunda hassas uzak dosyalar ön belleğe alınmadan doğrudan uzak sunucudan yüklenecektir." +flagAsBot: "Bot olarak işaretle" +flagAsBotDescription: "Bu seçeneği hesap bir program tarafından kontrol ediliyorsa işaretleyin. Bu, diğer geliştiricilerin sonsuz etkileşim zincirleri oluşturmasını engellemeye yardımcı olur ve Misskey'in iç sisteminin hesaba bir bot gibi davranmasını sağlar." +flagAsCat: "Kedi hesabı" +flagAsCatDescription: "Kedi hesabı" +flagShowTimelineReplies: "Zaman akışında notlara gelen cevapları göster" +flagShowTimelineRepliesDescription: "Açık olduğu durumda, zaman akışında kullanıcıların başkalarına verdiği cevaplar gözükür." +autoAcceptFollowed: "Takip edilen hesapların takip isteklerini kabul et" +addAccount: "Hesap ekle" +reloadAccountsList: "Hesap listesini güncelle" +loginFailed: "Giriş başarısız oldu" +showOnRemote: "Uzak sunucuda görüntüle" +general: "Genel" +wallpaper: "Duvar kağıdı" +setWallpaper: "Duvar kağıdını ayarla" +removeWallpaper: "Duvar kağıdını sil" +searchWith: "Arama: {q}" +youHaveNoLists: "Hiç listeniz yok" +followConfirm: "{name} takip edilsin mi?" +proxyAccount: "Vekil hesabı" +proxyAccountDescription: "Proxy hesabı, belirli koşullar altında kullanıcılar için uzaktan takipçi işlevi gören bir hesaptır. Örneğin, bir kullanıcı listeye bir uzak kullanıcı eklediğinde, o kullanıcıyı takip eden yerel bir kullanıcı yoksa uzak kullanıcının etkinliği örneğe teslim edilmeyecektir, dolayısıyla bunun yerine proxy hesabı takip edilecektir." +host: "Sağlayıcı" +selectUser: "Kullanıcı seç" +recipient: "Kime" +annotation: "Açıklamalar" +federation: "Federasyon" +instances: "Sunucu" +registeredAt: "Katılma tarihi" +latestRequestReceivedAt: "Alınan son talep" +latestStatus: "En son durum" +storageUsage: "Depolama kullanımı" +charts: "Çizelgeler" +perHour: "Saatlik" +perDay: "Günlük" +stopActivityDelivery: "Durum güncellemelerini gönderme" +blockThisInstance: "Bu sunucuyu engelle" +operations: "İşlemler" +software: "Yazılımlar" +version: "Sürüm" +metadata: "Meta Verileri" +withNFiles: "{n} tane dosya" +monitor: "Monitör" +jobQueue: "İşlem sırası" +cpuAndMemory: "İşlemci ve Hafıza" +network: "Ağ" +disk: "Disk" +instanceInfo: "Sunucu Bilgisi" +statistics: "İstatistikler" +clearQueue: "Sırayı temizle" +clearQueueConfirmTitle: "Sıra silinsin mi?" +clearQueueConfirmText: "Sırada kalan hiçbir şey iletilmeyecek. Genelde bu işlem gerekli değildir." +clearCachedFiles: "Ön belleği temizle" +clearCachedFilesConfirm: "Ön belleğe alınmış tüm uzak sunucu dosyaları silinsin mi?" +blockedInstances: "Engellenen sunucular" +blockedInstancesDescription: "Engellemek istediğiniz sunucuların alan adlarını satır sonlarıyla ayırarak yazın. Yazılan sunucular bu sunucuyla iletişime geçemeyecek." +muteAndBlock: "Susturma ve Engelleme" +mutedUsers: "Susturulan kullanıcılar" +blockedUsers: "Engellenen kullanıcılar" +noUsers: "Kullanıcı yok" +editProfile: "Profili düzenle" +noteDeleteConfirm: "Bu notu silmek istediğinizden emin misiniz?" +pinLimitExceeded: "Daha fazla not sabitlenemez" +intro: "Misskey yüklemesi tamamlandı! Lütfen yönetici hesabını oluşturun." +done: "Tamamlandı" +preview: "Önizleme" +default: "Varsayılan" +defaultValueIs: "Varsayılan: {value}" +noCustomEmojis: "Emoji bulunamadı" +noJobs: "Hiç işlem yok" +federating: "Federe ediliyor" +blocked: "Engellenmiş" +suspended: "Askıya alınmış" +all: "Tümü" +subscribing: "Abonelik" +publishing: "Paylaşım" +notResponding: "Cevap yok" +instanceFollowing: "Sunucuda takip edenler" +instanceFollowers: "Sunucu takipçileri" +instanceUsers: "Sunucu kullanıcıları" +changePassword: "Şifreyi değiştir" +security: "Güvenlik" +retypedNotMatch: "Girişler uyuşmuyor." +currentPassword: "Geçerli şifre" +newPassword: "Yeni şifre" +newPasswordRetype: "Yeni şifre (tekrar)" +attachFile: "Dosya ekle" +more: "Daha!" +featured: "Öne Çıkan" +usernameOrUserId: "Kullanıcı adı veya ID'si" +noSuchUser: "Kullanıcı bulunamadı" +lookup: "Sorgu" +announcements: "Duyurular" +imageUrl: "Görsel URL'si" remove: "Sil" +removed: "Silindi" +removeAreYouSure: "\"{x}\" silmek istediğinizden emin misiniz?" +deleteAreYouSure: "\"{x}\" silmek istediğinizden emin misiniz?" +resetAreYouSure: "Sıfırlansın mı?" +saved: "Kaydedildi" +messaging: "Mesajlar" +upload: "Yükle" +keepOriginalUploading: "Orijinal görseli koru" +keepOriginalUploadingDescription: "Orijinal olarak yüklenen görüntüyü olduğu gibi kaydeder. Kapatılırsa, yükleme sırasında web'de görüntülenecek bir sürüm oluşturulur." +fromDrive: "Drive Dosyasından" +fromUrl: "Bağlantıdan" +uploadFromUrl: "Bağlantıdan yükle" +uploadFromUrlDescription: "Yüklemek istediğiniz dosyanın bağlantısı" +uploadFromUrlRequested: "Yükleme talep edildi" +uploadFromUrlMayTakeTime: "Yüklemenin tamamlanması biraz süre alabilir." +explore: "Keşfet" +messageRead: "Okundu" +noMoreHistory: "Bundan öncesi yok" +startMessaging: "Yeni bir sohbet başlat" +nUsersRead: "{n} kişi okudu" +agreeTo: "Kabul Ediyorum: {0}" +agree: "Kabul Et" +agreeBelow: "Aşağıdakileri kabul ederim" +basicNotesBeforeCreateAccount: "Önemli notlar" +termsOfService: "Şartlar ve Koşullar" +start: "Başla" +home: "Ana sayfa" +remoteUserCaution: "Bu kullanıcı bir uzak sunucudan olduğu için alınan bilgiler tam olmayabilir." +activity: "Etkinlik" +images: "Görseller" +image: "Görseller" +birthday: "Doğum günü" +yearsOld: "{age} yaşında" +registeredDate: "Kayıt tarihi" +location: "Konum" +theme: "Temalar" +themeForLightMode: "Aydınlık Tema" +themeForDarkMode: "Karanlık Tema" +light: "Aydınlık" +dark: "Karanlık" +lightThemes: "Aydınlık Temalar" +darkThemes: "Karanlık Temalar" +syncDeviceDarkMode: "Sistem Koyu Modu ile senkronize et" +drive: "Sürücü" +fileName: "Dosya adı" +selectFile: "Dosya seç" +selectFiles: "Dosya seç" +selectFolder: "Klasör seç" +selectFolders: "Klasör seç" +renameFile: "Dosyayı yeniden adlandır" +folderName: "Klasör adı" +createFolder: "Klasör oluştur" +renameFolder: "Klasörü Yeniden Adlandır" +deleteFolder: "Klasörü sil" +addFile: "Dosya ekle" +emptyDrive: "Sürücü boş" +emptyFolder: "Bu klasör boş" +unableToDelete: "Silme mümkün değil" +inputNewFileName: "Yeni dosya ismini girin" +inputNewDescription: "Yeni bir başlık gir" +inputNewFolderName: "Yeni klasör ismini girin" +circularReferenceFolder: "Hedef klasör taşınan klasörün bir alt klasörü." +hasChildFilesOrFolders: "Klasör boş olmadığından silinemiyor" +copyUrl: "URL'yi kopyala" +rename: "Yeniden adlandır" +avatar: "Avatar" +banner: "Banner" +displayOfSensitiveMedia: "Hassas içerik gösterimi" +whenServerDisconnected: "Sunucu bağlantısı kesildiğinde" +disconnectedFromServer: "Sunucu bağlantısı koptu" +reload: "Yenile" +doNothing: "Bir şey yapma" +reloadConfirm: "Zaman akışı yenilensin mi?" +watch: "İzle" +unwatch: "İzlemeyi bırak" +accept: "Kabul et" +reject: "Reddet" +normal: "Normal" +instanceName: "Sunucu ismi" +instanceDescription: "Sunucu açıklaması" +maintainerName: "Yönetici ismi" +maintainerEmail: "Yöneticinin e-postası" +tosUrl: "Hizmet Koşulları Bağlantısı" +thisYear: "Bu yıl" +thisMonth: "Bu ay" +today: "Bugün" +monthX: "{month} ay" +pages: "Sayfalar" +integration: "Entegrasyon" +enableRegistration: "Kayıtlara izin ver" +basicInfo: "Temel bilgiler" +pinnedUsers: "Sabitlenmiş kullanıcılar" +pinnedNotes: "Sabitlenen" +manageAntennas: "Anten ayarları" +userList: "Listeler" +resetPassword: "Şifre sıfırlama" +noMessagesYet: "Şimdilik mesaj yok" +details: "Detaylar" +deck: "Güverte" +smtpHost: "Sağlayıcı" smtpUser: "Kullanıcı Adı" smtpPass: "Şifre" +notificationSetting: "Bildirim ayarları" +instanceTicker: "Notların sunucu bilgileri" +noCrawleDescription: "Arama motorlarından profilinde, notlarında, sayfalarında vb. dolaşılmamasını ve dizine eklememesini talep et." +clearCache: "Ön belleği temizle" +onlineUsersCount: "{n} kullanıcı çevrim içi" user: "Kullanıcı" +global: "Küresel" +squareAvatars: "Kare avatarlar" searchByGoogle: "Arama" +file: "Dosyalar" +pushNotification: "Push bildirimleri" +subscribePushNotification: "Push bildirimlerini etkinleştir" +unsubscribePushNotification: "Push bildirimlerini kapat" +pushNotificationAlreadySubscribed: "Push bildirimleri zaten açık" +pushNotificationNotSupported: "Push bildirimleri sunucu veya tarayıcı tarafından desteklenmiyor" +noRole: "Rol bulunamadı" +color: "Renk" +addMemo: "Kısa not ekle" +icon: "Avatar" +replies: "yanıt" +renotes: "vazgeçme" +_accountDelete: + started: "Silme işlemi başlatıldı" +_email: + _follow: + title: "seni takip etti" +_theme: + color: "Renk" + keys: + mention: "Bahset" + renote: "vazgeçme" _sfx: + note: "notlar" notification: "Bildirim" + chat: "Mesajlar" +_2fa: + renewTOTPCancel: "Hayır, teşekkürler" +_permissions: + "read:blocks": "Engellenen hesapları gör" + "write:blocks": "Engellenen hesap listesini düzenle" _widgets: profile: "Profil" + instanceInfo: "Sunucu Bilgisi" notifications: "Bildirim" timeline: "Zaman çizelgesi" + calendar: "Takvim" + clock: "Saat" + activity: "Etkinlik" + federation: "Federasyon" + jobQueue: "İşlem sırası" + _userList: + chooseList: "Bir liste seç" +_cw: + show: "Devamını yükle" +_poll: + vote: "Oy kullan" +_visibility: + publicDescription: "Herkese açık" + home: "Ana sayfa" + followers: "takipçi" _profile: username: "Kullanıcı Adı" +_exportOrImport: + followingList: "takipçi" + muteList: "Gizle" + blockingList: "engelle" + userLists: "Listeler" +_charts: + federation: "Federasyon" +_timelines: + home: "Ana sayfa" + global: "Küresel" +_pages: + blocks: + image: "Görseller" +_notification: + youWereFollowed: "seni takip etti" + unreadAntennaNote: "{name} anteni" + _types: + follow: "takipçi" + mention: "Bahset" + renote: "vazgeçme" + quote: "alıntı" + reaction: "Tepkiler" + receiveFollowRequest: "Takip isteği alındı" + followRequestAccepted: "Takip isteği kabul edildi" + _actions: + reply: "yanıt" + renote: "vazgeçme" _deck: + configureColumn: "Sütun seçenekleri" _columns: notifications: "Bildirim" tl: "Zaman çizelgesi" + list: "Listeler" + mentions: "Bahsetmeler" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 4f215bc980..777933bf53 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -20,6 +20,7 @@ noNotes: "Немає нотаток" noNotifications: "Немає сповіщень" instance: "Інстанс" settings: "Налаштування" +notificationSettings: "Параметри сповіщень" basicSettings: "Основні налаштування" otherSettings: "Інші налаштування" openInWindow: "Відкрити у вікні" @@ -48,9 +49,12 @@ delete: "Видалити" deleteAndEdit: "Видалити й редагувати" deleteAndEditConfirm: "Ви впевнені, що хочете видалити цю нотатку та відредагувати її? Ви втратите всі реакції, поширення та відповіді на неї." addToList: "Додати до списку" +addToAntenna: "Додати в антени" sendMessage: "Надіслати повідомлення" copyRSS: "Скопіювати RSS" copyUsername: "Скопіювати ім’я користувача" +copyUserId: "Копіювати ID користувача" +copyNoteId: "блокнот ID користувача" searchUser: "Пошук користувачів" reply: "Відповісти" loadMore: "Показати більше" @@ -300,7 +304,6 @@ copyUrl: "Копіювати URL" rename: "Перейменувати" avatar: "Аватар" banner: "Банер" -nsfw: "NSFW" whenServerDisconnected: "Коли зв’язок із сервером втрачено" disconnectedFromServer: "Зв’язок із сервером було перервано" reload: "Оновити" @@ -335,7 +338,6 @@ invite: "Запросити" driveCapacityPerLocalAccount: "Об'єм диска на одного локального користувача" driveCapacityPerRemoteAccount: "Об'єм диска на одного віддаленого користувача" inMb: "В мегабайтах" -iconUrl: "URL аватара" bannerUrl: "URL банера" backgroundImageUrl: "URL-адреса фонового зображення" basicInfo: "Основна інформація" @@ -645,6 +647,7 @@ createNewClip: "Створити нотатку" unclip: "Незакріплений" confirmToUnclipAlreadyClippedNote: "Ця нотатка вже включена до кліпу \"{name}\". Ви хочете виключити нотатку з цього кліпу?" public: "Публічний" +private: "Приватне" i18nInfo: "Misskey перекладається на різні мови волонтерами. Ви можете допомогти: {link}" manageAccessTokens: "Керування токенами доступу" accountInfo: "Інформація про акаунт" @@ -901,6 +904,9 @@ exploreOtherServers: "Знайти інший сервер" letsLookAtTimeline: "Перегляд історії" horizontal: "Збоку" youFollowing: "Підписки" +icon: "Аватар" +replies: "Відповісти" +renotes: "Поширити" _achievements: earnedAt: "Відкрито" _types: @@ -1200,10 +1206,6 @@ _aboutMisskey: donate: "Пожертвувати Misskey" morePatrons: "Ми дуже цінуємо підтримку багатьох інших помічників, не перелічених тут. Дякуємо! 🥰" patrons: "Підтримали" -_nsfw: - respect: "Приховувати NSFW медіа" - ignore: "Не приховувати NSFW медіа" - force: "Приховувати всі медіа файли" _instanceTicker: none: "Не відображати" remote: "Відображати для віддалених користувачів" @@ -1336,7 +1338,6 @@ _2fa: alreadyRegistered: "Двофакторна автентифікація вже налаштована." step1: "Спершу встановіть на свій пристрій програму автентифікації (наприклад {a} або {b})." step2: "Потім відскануйте QR-код, який відображається на цьому екрані." - step2Url: "Ви також можете ввести цю URL-адресу, якщо використовуєте програму для ПК:" step3: "Щоб завершити налаштування, введіть токен, наданий вашою програмою." step4: "Відтепер будь-які майбутні спроби входу вимагатимуть такого токена." renewTOTPCancel: "Не зараз" diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml new file mode 100644 index 0000000000..8dbcbd9d09 --- /dev/null +++ b/locales/uz-UZ.yml @@ -0,0 +1,1086 @@ +--- +_lang_: "O'zbek tili" +headlineMisskey: "Qaydlar tarmog'i" +introMisskey: "Xush kelibsiz! Misskey ochiq kodli, markazlashmagan mikroblogging xizmati.\nO'zingizni fikrlaringizni atrofingizdagilar bilan ulashish uchun \"Qaydlar\" yarating. 📡\nUstiga-ustak, \"Reaktsiyalar\" yordamida siz boshqalarning xatlari haqidagi o'zingizni xissiyotlaringizni bildiring. 👍\nQani, yangi dunyoni kashf qilaylik! 🚀" +poweredByMisskeyDescription: "{name} ochiq manbali Misskey(\"Misskey instance\" deb ataladi) platformasi tomonidan qurilgan servislardan biri. " +monthAndDay: "{day}/{month}" +search: "Izlash" +notifications: "Xabarnomalar" +username: "Foydalanuvchi nomi" +password: "Parol" +forgotPassword: "Parolni unutib qo'ydim" +fetchingAsApObject: "Fediversedan olib kelinmoqda..." +ok: "Ho'p" +gotIt: "Tushunarli!" +cancel: "Bekor qilish" +noThankYou: "Hozir emas" +enterUsername: "Foydalanuvchini nomini kiriting" +renotedBy: "{user} tomonidan qayta qayd etildi" +noNotes: "Qaydlar mavjud emas" +noNotifications: "Xabarlar mavjud emas" +instance: "Server" +settings: "Sozlamalar" +notificationSettings: "Xabarnoma sozlamalari" +basicSettings: "Asosiy sozlamalar" +otherSettings: "Qo‘shimcha sozlamalar" +openInWindow: "Yangi oynada ochish" +profile: "Profil" +timeline: "Xronologiya" +noAccountDescription: "Ushbu foydalanuvchi hali o'zi haqida ma'lumot yozmagan." +login: "Kirish" +loggingIn: "Kirilmoqda" +logout: "Chiqish" +signup: "Ro'yxatdan o'tish" +uploading: "Yuklanmoqda..." +save: "Saqlash" +users: "Foydalanuvchilar" +addUser: "Foydalanuvchi qo'shish" +favorite: "Sevimli" +favorites: "Sevimlilar" +unfavorite: "Sevimlidan chiqarish" +favorited: "sevimli" +alreadyFavorited: "allaqachon sevimlilar orasida" +cantFavorite: "sevimlilarga qo'shib bo'lmadi" +pin: "Profilga qadab qo'yish" +unpin: "Profildan olib tashlash" +copyContent: "Tarkibini nusxalash" +copyLink: "Havolani nusxalash" +delete: "O'chirib tashlash" +deleteAndEdit: "O'chirish va tahrirlash" +deleteAndEditConfirm: "O'chirib, tahrirlamoqchiligingizga ishonchingiz komilmi? Siz bu qaydga tegishli barcha reaktsiyalar va javoblarni yo'qotasiz." +addToList: "Ro‘yxatga qo‘shish" +addToAntenna: "Antennaga qo'shish" +sendMessage: "Xabar yuborish" +copyRSS: "RSS'ni nusxalash" +copyUsername: "Foydalanuvchi nomini nusxalash" +copyUserId: "Foydalanuvchi IDsini nusxalash" +copyNoteId: "Qayd IDsini ko'chirish" +copyFileId: "Fayl ID raqamini nusxalash" +copyFolderId: "Jild ID raqamini nusxalash" +copyProfileUrl: "Profil manzilini nusxalash" +searchUser: "Foydalanuvchini izlash" +reply: "Javob berish" +loadMore: "Ko‘proq ko‘rish" +showMore: "Ko‘proq ko‘rish" +showLess: "Yopish" +youGotNewFollower: "sizga obuna bo'ldi" +receiveFollowRequest: "Obuna bo'lishga ruxsat qabul qilindi" +followRequestAccepted: "Obuna bo'lishga ruxsat berildi" +mention: "Murojat" +mentions: "Eslatib o'tish" +directNotes: "Bevosita qaydlar" +importAndExport: "Import/eksport" +import: "Import" +export: "Eksport" +files: "Fayllar" +download: "Yuklab olish" +driveFileDeleteConfirm: "\"{name}\" o'chirib tashlamoqchimisiz? Ushbu fayldan foydalanadigan har qanday kontent ham oʻchiriladi." +unfollowConfirm: "{name}ga obunani bekor qilmoqchimisiz?" +exportRequested: "Eksport so'raldi. Bu ozgina vaqt olishi mumkin. Tugatilgandan so'ng sizning Diskingizga qo'shiladi" +importRequested: "Import so'raldi. Bu ozgina vaqt olishi mumkin." +lists: "Ro'yxatlar" +noLists: "Hech qanday ro'yxatlar mavjud emas" +note: "Qayd" +notes: "Qaydlar" +following: "Obuna bo‘lish" +followers: "Obunachilar" +followsYou: "Sizning obunachingiz." +createList: "Ro'yxat yaratish" +manageLists: "Ro'yxatlarni boshqarish." +error: "Xato" +somethingHappened: "Xatolik yuz berdi" +retry: "Qayta urinib ko'rish" +pageLoadError: "Sahifani yuklayotganda xatolik yuz berdi" +pageLoadErrorDescription: "Buni odatda tarmoq muammolarni yoki browser keshi keltirib chiqaradi. Keshni tozalab, keyinroq urinib ko'ring" +serverIsDead: "Server javob bermayabdi. Iltimos kuting va keyinroq urinib ko'ring" +youShouldUpgradeClient: "Iltimos, ushbu sahifani ko'rish uchun sahifani yangilang." +enterListName: "Ro'yxatga nom kiriting" +privacy: "Maxfiylik" +makeFollowManuallyApprove: "Yopiq akkaunt" +defaultNoteVisibility: "Standart ko'rinish" +follow: "Obuna bo‘lish" +followRequest: "Obuna bo'lish uchun ruxsat olish" +followRequests: "Obuna bo'lmoqchilar" +unfollow: "obunani bekor qilish" +followRequestPending: "obuna bo'lishga ruxsat kutilmoqda" +enterEmoji: "Emojini kiriting" +renote: "Qayta qayd etish" +unrenote: "Qayta qayd etishni bekor qilish" +renoted: "Qayta qayd etildi" +cantRenote: "Qayta qayd etish mumkin emas" +cantReRenote: "Repostni qayta joylashtirish mumkin emas." +quote: "Iqtibos keltirish" +inChannelRenote: "Faqat kanalga qayta qayd etish" +inChannelQuote: "Kanaldagi eslatmalar" +pinnedNote: "Qadalgan qayd" +pinned: "Profilga qadab qo'yish" +you: "Siz" +clickToShow: "Ko'rsatish uchun bosing" +sensitive: "Sezuvchan" +add: "Qo'shish" +reaction: "Reaktsiyalar" +reactions: "Reaktsiyalar" +reactionSetting: "Reaksiyalar ro'yxati" +reactionSettingDescription2: "Qayta tartiblash uchun ushlab turib siljiting, oʻchirish uchun bosing, qoʻshish uchun “+” tugmasini bosing." +rememberNoteVisibility: "Qaydning ko'rinish sozlamarini eslab qolish" +attachCancel: "Qo'shimchani olib tashlash" +markAsSensitive: "\"Hamma ko'rishi mumkin emas\" deb belgilash" +unmarkAsSensitive: "\"Hamma ko'rishi mumkin\" deb belgilash" +enterFileName: "Fayl nomini kiriting" +mute: "Ovozni o‘chirish" +unmute: "Ovozni yoqish" +renoteMute: "Qayta qaydlarni ovozini o'chirish" +renoteUnmute: "Qayta qaydlarni ovozini yoqish" +block: "Bloklash" +unblock: "Blokdan chiqarish" +suspend: "To'xtatish" +unsuspend: "Blokdan chiqarish" +blockConfirm: "Haqiqatdan ham quyidagi hisobni bloklashni xohlaysizmi? " +unblockConfirm: "Haqiqatdan ham quyidagi hisobni blokdan chiqarishni xohlaysizmi? " +suspendConfirm: "Bu hisobni to‘xtatib qo‘ymoqchi ekanligingizga ishonchingiz komilmi?" +unsuspendConfirm: "Tasdiqlashni to'xtatib turish" +selectList: "Ro'yxat tanlash" +editList: "Roʻyxatni tahrirlash" +selectChannel: "Kanalni tanlang" +selectAntenna: "Antennani tanlang" +editAntenna: "Antennani tahrirlang" +selectWidget: "Vidjet tanlash" +editWidgets: "Vidjetni tahrirlash" +editWidgetsExit: "Tugadi" +customEmojis: "Maxsus emoji" +emoji: "Emoji" +emojis: "Emoji" +emojiName: "Emoji nomi" +emojiUrl: "Emoji URL'i" +addEmoji: "Emoji qo'shish" +settingGuide: "Tavsiya qilingan sozlamalar" +cacheRemoteFiles: "Tashqi fayllarni keshlash" +cacheRemoteFilesDescription: "Ushbu sozlama o'chirilgan bo'lsa tashqi fayllar bevosita tashqi serverdan yuklanadi. Buni o'chirish ombor ishlatilishini kamaytiradi, lekin traffikni ko'paytiradi, chunki eskizlar generatsiya qilinmaydi." +youCanCleanRemoteFilesCache: "Fayl menejeridagi 🗑️ tugmasi yordamida barcha keshlarni oʻchirib tashlashingiz mumkin." +cacheRemoteSensitiveFiles: "Tashqi fayllarni keshlash" +cacheRemoteSensitiveFilesDescription: "Bu sozlama oʻchiq boʻlsa, \"barcha ko'rishi mumkin bo'lmagan\" fayllar keshlashsiz toʻgʻridan-toʻgʻri masofaviy serverdan yuklanadi." +flagAsBot: "Ushbu akkauntni bot sifatida belgilash" +flagAsBotDescription: "Agar bu akkaunt bot tomonidan boshqaralayotgan bo'lsa, bu sozlamani yoqing. Sozlama yoqilganda, boshqa foydalanuvchilar uchun belgi sifatida ishlaydi, va Misskey ichki tizimlari bu akkauntni bot ekanini biladi." +flagAsCat: "Bu akkauntni mushuk sifatida belgilash" +flagAsCatDescription: "Ushbu akkauntni mushuk sifatida belgilash uchun ushbu sozlamani yoqing." +flagShowTimelineReplies: "Javoblarni xronogoliya bo'yicha ko'rsatish" +flagShowTimelineRepliesDescription: "Bu parametr yoqilganda, lentada foydalanuvchi xabarlariga javob berilgan xabarlar ham ko'rinadi" +autoAcceptFollowed: "Obunachilarni avtomatik ravishda qabul qilish" +addAccount: "Akkaunt qo'shish" +reloadAccountsList: "Hisoblar ro'yxatini yangilash" +loginFailed: "Tizimga kirishda xatolik yuz berdi" +showOnRemote: "Masofaviy boshqaruvni ko'rish" +general: "Asosiy" +wallpaper: "Fon rasmi" +setWallpaper: "Fon rasmini o'rnatish" +removeWallpaper: "Fon rasmini olib tashlash" +searchWith: "Izlash: {q}" +youHaveNoLists: "Sizda hech qanday ro'yxatlar mavjud emas" +followConfirm: "{name} ga obuna bo'lmoqchimisiz?" +proxyAccount: "Proksi hisob" +proxyAccountDescription: "Proksi-hisob qaydnomasi - bu ma'lum shartlar ostida foydalanuvchi uchun masofaviy kuzatuvchi sifatida ishlaydigan hisob. Misol uchun, foydalanuvchi uzoq foydalanuvchini roʻyxatga qoʻyganda, roʻyxatdagi foydalanuvchini hech kim kuzatib turmasa, faoliyat serverga yetkazilmaydi, shuning uchun biz proksi hisobi ularning oʻrniga ularni kuzatishini xohlaymiz." +host: "Host" +selectUser: "Foydalanuvchini tanlang" +recipient: "Qabul qiluvchi" +annotation: "Izohlar" +federation: "Federatsiya" +instances: "Serverlar" +registeredAt: "Ro'yhatdan o'tgan" +latestRequestReceivedAt: "Oxirgi qabul qilingan so'rov" +latestStatus: "So'nggi holat" +storageUsage: "Ishlatilgan xotira" +charts: "Diagrammalar" +perHour: "Soatbay" +perDay: "Kunbay" +stopActivityDelivery: "Faollikni jo'natishi to'xtatish" +blockThisInstance: "Ko;rsatilgan serverni bloklash" +operations: "Amallar" +software: "Dastur" +version: "Versiya" +metadata: "Meta ma'lumot" +withNFiles: "{n} ta fayl(lar)" +monitor: "Kuzatish" +jobQueue: "Vazifalar navbati" +cpuAndMemory: "CPU va Xotira" +network: "Tarmoq" +disk: "Disk" +instanceInfo: "Instans haqida ma'lumot" +statistics: "Statistika" +clearQueue: "Navbatni tozalash" +clearQueueConfirmTitle: "Navbatni tozalamoqchimisiz?" +clearQueueConfirmText: "Yetkazib berilmagan xabarlar yetkazilmaydi. Odatda buni qilish shart emas." +clearCachedFiles: "Keshni tozalash" +clearCachedFilesConfirm: "Barcha keshlangan masofaviy fayllar oʻchirilsinmi?" +blockedInstances: "Bloklangan serverlar" +blockedInstancesDescription: "Bloklanmoqchi bo'lgan serverlaringiz hostlarini yangi qatorlar bilan ajrating. Bloklangan server bu server bilan o‘zaro aloqada bo‘lmaydi. Subdomenlar ham bloklangan." +muteAndBlock: "Ovozsiz va Bloklangan" +mutedUsers: "Ovozsiz foydalanuvchilar" +blockedUsers: "Bloklangan foydalanuvchilar" +noUsers: "Foydalanuvchilar yo‘q" +editProfile: "Profilni o'zgartirish" +noteDeleteConfirm: "Haqiqatan ham bu qaydni oʻchirib tashlamoqchimisiz?" +pinLimitExceeded: "Siz boshqa qaydlarni mahkamlay olmaysiz" +intro: "Misskeyni o'rnatish tugallandi! Iltimos, administrator foydalanuvchi yarating." +done: "Bajarildi" +processing: "Amaliyotda" +preview: "Ko'rish" +default: "Odatiy" +defaultValueIs: "Sukut bo'yicha: {value}" +noCustomEmojis: "Emojilar mavjud emas" +noJobs: "Vazifalar yo'q" +federating: "Ittifoqdosh" +blocked: "Bloklangan" +suspended: "To'xtatilgan" +all: "Barcha" +subscribing: "Obuna bo'lish" +publishing: "Yuborilmoqda" +notResponding: "Javob bermayapti" +instanceFollowing: "server obuna bo'ladi" +instanceFollowers: "server obunachisi" +instanceUsers: "server foydalanuvchisi" +changePassword: "Parolni o‘zgartirish" +security: "Xavfsizlik" +retypedNotMatch: "Maydonlar mos kelmayapti" +currentPassword: "Joriy parol" +newPassword: "Yangi parol" +newPasswordRetype: "Yangi parolni boshqatdan tering" +attachFile: "Fayl biriktirish" +more: "Ko'proq!" +featured: "ta'kidlash" +usernameOrUserId: "Foydalanuvchi nomi yoki identifikatori" +noSuchUser: "Foydalanuvchi topilmadi" +lookup: "So'rov" +announcements: "Bildirishnomalar" +imageUrl: "Rasm URL" +remove: "O'chirib tashlash" +removed: "Muvaffaqiyatli o'chirildi" +removeAreYouSure: "“{x}”ni olib tashlamoqchi ekanligingizga ishonchingiz komilmi?" +deleteAreYouSure: "“{x}”ni chindan ham yo'q qilmoqchimisiz?" +resetAreYouSure: "Haqiqatan ham qayta tiklansinmi?" +saved: "Saqlandi" +messaging: "Suhbat" +upload: "Yuklash" +keepOriginalUploading: "Asl rasmni saqlang" +keepOriginalUploadingDescription: "Rasmlarni yuklashda asl nusxasini saqlaydi. Agar o'chirilgan bo'lsa, brauzer yuklangandan keyin nashr qilish uchun rasm yaratadi." +fromDrive: "Drive orqali" +fromUrl: "URL dan" +uploadFromUrl: "URL orqali yuklash" +uploadFromUrlDescription: "Yuklamoqchi bo'lgan faylingizga havola" +uploadFromUrlRequested: "yuklab olish so'ralgan" +uploadFromUrlMayTakeTime: "Yuklash tugallanishi uchun biroz vaqt ketishi mumkin." +explore: "Ko'rib chiqish" +messageRead: "O‘qildi" +noMoreHistory: "Buning ortida hech qanday hikoya yo'q" +startMessaging: "Yangi suhbatni boshlash" +nUsersRead: "{n} tomonidan o'qildi" +agreeTo: "Men {0} ga roziman" +agree: "Rozi bo'lish" +agreeBelow: "Men quyidagilarga roziman" +basicNotesBeforeCreateAccount: "Muhim qaydlar" +termsOfService: "Foydalanish shartlari" +start: "Boshlash" +home: "Bosh sahifa" +remoteUserCaution: "Bu foydalanuvchi uzoqda bo'lganligi sababli, ko'rsatilgan ma'lumotlar to'liq bo'lmasligi mumkin." +activity: "Faollik" +images: "Rasmlar" +image: "Rasm" +birthday: "Tug'ilgan kun" +yearsOld: "{age} yashar" +registeredDate: "Ro'yxatdan o'tgan sanasi" +location: "Manzil" +theme: "Rang sxemasi" +themeForLightMode: "Yorug' rejim uchun rang sxemasi" +themeForDarkMode: "Qorong'i rejim uchun rang sxemasi" +light: "Yorug'" +dark: "Qorongʻi" +lightThemes: "Yorug‘ rang sxemasi" +darkThemes: "Qorong'i rang sxemasi" +syncDeviceDarkMode: "Qurilmangizning qorong‘i rejimi bilan sinxronlashtiring" +drive: "Disk" +fileName: "Fayl nomi" +selectFile: "Faylni tanlang" +selectFiles: "Fayllarni tanlang" +selectFolder: "Jildni tanlang" +selectFolders: "Jildlarni tanlang" +renameFile: "Faylni nomini tahrirlash" +folderName: "Jild nomi" +createFolder: "Papka qo'shish" +renameFolder: "Papka nomini o‘zgartirish" +deleteFolder: "Papkani o‘chirish" +addFile: "Fayl qo‘shish" +emptyDrive: "Diskingiz bo'sh" +emptyFolder: "Ushbu papka bo'sh" +unableToDelete: "O'chirilmadi" +inputNewFileName: "Yangi fayl nomini kiriting" +inputNewDescription: "Iltimos, yangi sarlavha kiriting." +inputNewFolderName: "Yangi papka nomini kiriting" +circularReferenceFolder: "Belgilangan papka siz ko'chirmoqchi bo'lgan jildning pastki jildidir." +hasChildFilesOrFolders: "Bu papka boʻsh emas va uni oʻchirib boʻlmaydi." +copyUrl: "Bog'lamadan nusxa olish" +rename: "Qayta nomlash" +avatar: "Avatar" +banner: "Banner" +displayOfSensitiveMedia: "Nozik kontentni ko'rish" +whenServerDisconnected: "server bilan aloqa uzilganda" +disconnectedFromServer: "Server bilan ulanish uzulib qoldi" +reload: "Yangilash" +doNothing: "E'tiborsiz qoldirish" +reloadConfirm: "Timeline'ni yangilashni xohlaysizmi?" +watch: "Kuzatmoq" +unwatch: "Kuzatishni to'xtatish" +accept: "Ruxsat" +reject: "Rad etish" +normal: "Yaxshi" +instanceName: "Server nomi" +instanceDescription: "Server tavsifi" +maintainerName: "Qo'llab-quvvatlovchi" +maintainerEmail: "Administratorning elektron pochtasi" +tosUrl: "Foydalanish shartlariga havola" +thisYear: "Joriy yil" +thisMonth: "Shu oy" +today: "Bugun" +dayX: "{day}" +monthX: "{month}" +yearX: "{year}" +pages: "Sahifalar" +integration: "Integratsiya" +connectService: "Ulash" +disconnectService: "Uzish" +enableLocalTimeline: "Mahalliy vaqt mintaqasini yoqing" +enableGlobalTimeline: "Global vaqt mintaqasini yoqing" +disablingTimelinesInfo: "Administratorlar va Moderatorlar har doim barcha vaqt jadvallariga kirish huquqiga ega bo'ladilar, hatto ular yoqilmagan bo'lsa ham." +registration: "Ro'yxatdan o'tish" +enableRegistration: "Ro'yxatdan o'tishni yoqing" +invite: "Taklif qilish" +driveCapacityPerLocalAccount: "Har bir mahalliy foydalanuvchi uchun disk maydoni" +driveCapacityPerRemoteAccount: "Har bir masofaviy foydalanuvchi uchun disk maydoni" +inMb: "Megabaytlarda" +bannerUrl: "Banner URLi" +backgroundImageUrl: "Fon rasmi URL manzili" +basicInfo: "Asosiy ma'lumot" +pinnedUsers: "Qadalgan foydalanuvchilar" +pinnedUsersDescription: "Har bir qatorga bitta foydalanuvchi nomini kiriting. Bu yerda sanab oʻtilgan foydalanuvchilar “Oʻrganish” yorligʻiga bogʻlanadi." +pinnedPages: "Qadalgan Sahifalar" +pinnedClipId: "Qadalgan xabar IDsi" +pinnedNotes: "Qadalgan qayd" +hcaptcha: "hCaptcha" +enableHcaptcha: "hCaptchani yoqish" +hcaptchaSiteKey: "Sayt kaliti" +hcaptchaSecretKey: "Mahfiy kalit" +recaptcha: "reCAPTCHA" +enableRecaptcha: "reCAPTCHA ni yoqish" +recaptchaSiteKey: "Sayt kaliti" +recaptchaSecretKey: "Maxfiy kalit" +turnstile: "Turniket" +enableTurnstile: "Turniketni yoqish" +turnstileSiteKey: "Sayt kaliti" +turnstileSecretKey: "Maxfiy kalit" +avoidMultiCaptchaConfirm: "\nBir nechta Captcha tizimlaridan foydalanish ular o'rtasida noqulaylik olib kelishi mumkin. Hozirda faol bo'lgan boshqa Captcha tizimlarini o'chirib qo'ymoqchimisiz? Agar siz ularning faol bo'lishini istasangiz, bekor qilish tugmasini bosing." +antennas: "Antennalar" +manageAntennas: "Antennalarni boshqarish" +name: "Ism" +antennaSource: "Antenna manbai" +antennaKeywords: "Kalit so'zni qabul qilish" +antennaExcludeKeywords: "Istisno qilingan kalit so'zlar" +antennaKeywordsDescription: "VA sharti uchun bo'shliqlar bilan yoki YOKI sharti uchun qator uzilishlari bilan ajrating." +notifyAntenna: "Yangi qaydlar haqida menga xabar bering" +withFileAntenna: "Faqatgina fayli bor qaydlar" +enableServiceworker: "Bildirish nomalarni olish" +antennaUsersDescription: "Har bir foydalunvchi nomini alohida qatorga yozing" +caseSensitive: "Katta-kichik harfni farqlash" +withReplies: "Javob yo'llash" +connectedTo: "Quyidagi akkountlarga ulangan" +notesAndReplies: "Qaydlar va javoblar" +withFiles: "Fayllar" +silence: "Jim qilish" +silenceConfirm: "Rostdan ham ushbu foydalanuvchini jim qilmoqchimisiz?" +unsilence: "Jim qilishni bekor qilish" +unsilenceConfirm: "Rostdan ham ushbu foydalanuvchini ovozsiz \nqilmoqchimisiz?" +popularUsers: "Mashhur foydalanuvchilar." +recentlyUpdatedUsers: "Yaqinda ro'yxatdan o'tgan foydalanuvchilar" +recentlyRegisteredUsers: "Yaqinda ro'yxatdan o'tgan foydalanuvchilar" +recentlyDiscoveredUsers: "Yangi foydalanuvchilar" +exploreUsersCount: "{count} ta foydalanuvchi bor" +exploreFediverse: "Fediversni ko'rib chiqing" +popularTags: "Ommabop teglar" +userList: "Ro'yxatlar" +about: "Haqida" +aboutMisskey: "Misskey haqida" +administrator: "Administrator" +token: "Tasdiqlash" +2fa: "Ikki faktorli autentifikatsiya" +totp: "Autentifikatsiya ilovasi" +totpDescription: "Bir martalik parollarni kiritish uchun autentifikatsiya ilovasidan foydalaning" +moderator: "Moderator" +moderation: "Moderatsiya" +nUsersMentioned: "{n} tomonidan chop etilgan" +securityKeyAndPasskey: "Xavfsizlik kaliti va maxfiy so'z" +securityKey: "Xavfsizlik kaliti" +lastUsed: "Oxirgi marta foydalanilgan" +lastUsedAt: "Oxirgi marta {t} da foydalanilgan" +unregister: "ro'yxatdan chiqarish" +passwordLessLogin: "Parolsiz kirshni sozlash" +passwordLessLoginDescription: "Parolsiz kirish" +resetPassword: "Parolni tiklash" +newPasswordIs: "Yangi parolingiz {password}" +reduceUiAnimation: "Interfeysdagi animatsiyani kamaytirish" +share: "Yuborish" +notFound: "Topilmadi" +notFoundDescription: "Ushbu sahifa topilmadi" +uploadFolder: "Jildni yuklash" +cacheClear: "Keshni tozalash" +markAsReadAllNotifications: "Bildirishnomalarni o'qilgan deb belgilash" +markAsReadAllUnreadNotes: "Barch xabarlarni oq'ilgan deb belgilash" +markAsReadAllTalkMessages: "Barcha suhbatlarni o'qilgan deb belgilang" +help: "Yordam" +inputMessageHere: "Xabar kiriting" +close: "Yopish" +invites: "Taklif qilish" +members: "A'zolar" +transfer: "topshiriq" +title: "Sarlavha" +text: "Matn" +enable: "Yoqish" +next: "Keyingisi" +retype: "Qayta kiriting" +noteOf: "{user} tomonidan joylandi\n" +quoteAttached: "Iqtibos" +quoteQuestion: "Iqtibos sifatida qo'shilsinmi?" +noMessagesYet: "Bu yerda xabarlar yo'q" +newMessageExists: "Yangi xabarlar bor" +onlyOneFileCanBeAttached: "Faqat bitta faylni biriktirish mumkin" +signinRequired: "Davom etishdan oldin ro'yhatdan o'tishingiz yoki tizimga kirishingiz kerak" +invitations: "Taklif qilish" +invitationCode: "taklif qilish kodi" +checking: "Tekshirilmoqda" +available: "Mavjud" +unavailable: "Mavjud emas" +usernameInvalidFormat: "Siz a~z, A~Z, 0~9, _ dan foydalanishingiz mumkin" +tooShort: "Juda qisqa" +tooLong: "juda uzun" +weakPassword: "Zaif parol" +normalPassword: "Oddiy parol" +strongPassword: "Kuchli parol" +passwordMatched: "Mos keldi" +passwordNotMatched: "mos kelmadi" +signinWith: "{x} bilan tizimga kirish" +signinFailed: "Tizimga kirishda xatolik yuz berdi. Iltimos, foydalanuvchi nomingiz va parolingizni tekshiring." +or: "yoki" +language: "til" +uiLanguage: "Interfeys tili" +aboutX: "{x} haqida" +emojiStyle: "Emoji ko'rinishi" +native: "Mahalliy" +disableDrawer: "Slayd menyusidan foydalanmang" +showNoteActionsOnlyHover: "Eslatma amallarini faqat sichqonchani olib borganda ko‘rsatish" +noHistory: "Tarix yo'q" +signinHistory: "kirish tarixi" +enableAdvancedMfm: "MFMni faollashtirish" +doing: "Bajarilmoqda..." +category: "kategoriya" +tags: "teg" +docSource: "Ushbu hujjatning manbasi" +createAccount: "Akkaunt yaratish" +existingAccount: "mavjud akkaunt" +regenerate: "regeneratsiya" +fontSize: "shrift hajmi" +limitTo: "{x} gacha" +noFollowRequests: "obuna uchun so'rov yo'q" +openImageInNewTab: "Rasmni boshqa oynada ochish" +dashboard: "Boshqaruv paneli" +local: "Mahalliy" +remote: "masofaviy" +total: "Jami" +weekOverWeekChanges: "Oxirgi haftadagi o'zgarishlar" +dayOverDayChanges: "Kecha bo'lgan o'zgarishlar" +appearance: "Tasgqi ko'rinish" +clientSettings: "Klient sozlamalari" +accountSettings: "Profil sozlamalari" +promotion: "rag'batlantirish" +promote: "targ'ib qilish" +numberOfDays: "kunlar soni" +hideThisNote: "bu eslatmani yashiring" +showFeaturedNotesInTimeline: "Tanlangan qaydlarni Timelineda ko'rsatish" +objectStorage: "ob'ektni saqlash" +useObjectStorage: "Ob'ektni saqlashdan foydalaning" +objectStorageBaseUrl: "Asosiy URL" +objectStorageBaseUrlDesc: "Malumot va foydalanish uchun URL. Agar siz CDN yoki proksi-serverdan foydalanayotgan bo'lsangiz, URL manzili, S3: 'https://.s3.amazonaws.com', GCS va boshqalar: 'https://storage.googleapis.com/'." +objectStorageBucket: "Bucket" +objectStorageBucketDesc: "Iltimos, foydalaniladigan xizmatning bucket nomini belgilang." +objectStoragePrefix: "Prefix" +objectStorageEndpoint: "Endpoint" +objectStorageRegion: "Mintaqa" +objectStorageRegionDesc: "'xx-east-1' kabi mintaqani belgilang. Agar xizmatingizda mintaqa tushunchasi bo'lmasa, `us-east-1` dan foydalaning. AWS konfiguratsiya fayllari yoki muhit oʻzgaruvchilariga havola qilishda boʻsh qoldiring." +objectStorageUseSSL: "SSL dan foydalaning" +objectStorageUseSSLDesc: "API ulanishlari uchun https dan foydalanmasangiz, belgini olib tashlang" +objectStorageUseProxy: "Proksi-serverdan foydalaning" +objectStorageUseProxyDesc: "Proksi-serverdan foydalanishni xohlamasangiz, uni o'chiring" +objectStorageSetPublicRead: "Yuklashda \"public-read\" ni o'rnating" +serverLogs: "Server protokoli" +deleteAll: "Hammasini o'chirib tashlash" +showFixedPostForm: "Taqdim etish shaklini vaqt jadvalining yuqori qismida ko'rsating" +newNoteRecived: "Yangi qaydlar mavjud emas" +sounds: "Tovushlar" +sound: "ovoz" +listen: "Eshitish" +none: "Hechnima" +showInPage: "Sahifada ko'rsatish" +popout: "Oching" +volume: "Ovoz balandligi" +details: "Batafsil" +chooseEmoji: "Emojini tanlang" +unableToProcess: "Opertsiya bajarilmadi" +recentUsed: "Oxirgi ishlatilganlar" +install: "O‘rnatish" +uninstall: "O‘chirib tashlash" +installedApps: "O'rnatilgan ilovalar" +nothing: "Hech narsa yo'q" +installedDate: "O'rnatish sanasi" +lastUsedDate: "Oxirgi marta ishlatilgan sana" +state: "Holat" +sort: "saralamoq" +ascendingOrder: "O'sish bo'yicha" +descendingOrder: "Kamayish bo'yicha" +scratchpad: "Qoralama" +output: "Chiqish" +script: "Skript" +disablePagesScript: "AiScriptni sahifalardan o'chirish" +updateRemoteUser: "Masofaviy foydalanuvchi ma'lumotlarini yangilash" +deleteAllFiles: "barcha fayllarni o'chirish" +deleteAllFilesConfirm: "Barcha fayllar oʻchirilsinmi?" +removeAllFollowing: "Barcha obunalarni o'chirish" +userSuspended: "Bu foydalanuvchi muzlatilgan." +userSilenced: "Ushbu foydalanuvchi jim qilingan" +yourAccountSuspendedTitle: "akkaunt muzlatilgan" +yourAccountSuspendedDescription: "Ushbu akkaunt serverning xizmat ko'rsatish shartlarini buzish kabi sabablarga ko'ra to'xtatilgan. Tafsilotlar uchun administratoringizga murojaat qiling. Iltimos, yangi akkaunt yaratmang." +tokenRevoked: "token yaroqsiz" +tokenRevokedDescription: "Kirish tokeningizni muddati tugagan. Iltimos, qaytadan kiring." +accountDeleted: "akkaunt o'chirildi" +accountDeletedDescription: "Bu akkaunt oʻchirildi." +menu: "Menyu" +divider: "Ajratrmoq" +addItem: "Element qo'shish" +rearrange: "Qayta saralash" +inboxUrl: "Qabul qilingan xabarlar URL manzili" +serviceworkerInfo: "bildirishnomalar uchun yoqilgan bo'lishi kerak." +deletedNote: "Oʻchirilgan post" +visibility: "Ko'rinishi" +poll: "So'ro'vnoma" +useCw: "Kontentni yashirish" +enablePlayer: "Video pleyerni ochish" +disablePlayer: "Video pleyerni yopish" +expandTweet: "Xabarni kengaytirish" +themeEditor: "Rang sxemasi muharriri" +description: "tavsif" +describeFile: "sarlavha qo'shing" +enterFileDescription: "sarlavha kiriting" +author: "muallif" +leaveConfirm: "Sizda saqlanmagan oʻzgarishlar bor. Bekor qilinsinmi?" +manage: "Administratsiya" +plugins: "Kengaytmalar, plaginlar" +preferencesBackups: "Sozlamalarni zahiralash" +useBlurEffectForModal: "Modal uchun xiralashtirish effektidan foydalaning" +useFullReactionPicker: "Katta oynada reaksiya tanlash" +width: "kengligi" +height: "balandligi" +large: "Katta" +medium: "O'rta" +small: "kichik" +generateAccessToken: "Kirish tokenini yaratish" +permission: "Ruxsatlar" +enableAll: "Yoqish" +disableAll: "hammasini o'chirib qo'ying" +tokenRequested: "Hisobga kirish" +pluginTokenRequestedDescription: "Bu plagin shu yerda belgilanganlarga qodir bo'ladi" +notificationType: "Bildirishnoma turi" +edit: "Tahrirlash" +emailServer: "Email server" +email: "Email" +emailAddress: "E-pochtangiz:" +smtpConfig: "SMTP server sozlamalari" +smtpHost: "Host" +smtpPort: "Port" +smtpUser: "Foydalanuvchi nomi" +smtpPass: "Parol" +testEmail: "Email jo'natmani testlash" +userSaysSomething: "{name} nimadir dedi" +makeActive: "Faol" +display: "Displey" +copy: "Nusxa olish" +metrics: "Metrikalar" +overview: "Umumiy ma'lumot" +logs: "Jurnallar" +delayed: "Kechiktirildi" +database: "Ma'lumotlar bazasi" +channel: "Kanallar" +create: "Yaratish" +notificationSetting: "Bildirishnoma sozlamalari" +notificationSettingDesc: "Ko'rsatish uchun bildirishnoma turlarini tanlang." +useGlobalSetting: "Global sozlamalardan foydalanish" +other: "Qo‘shimcha" +regenerateLoginToken: "Kirish tokenini qayta yaratish" +setMultipleBySeparatingWithSpace: "Bo'sh joy qoldirib, bir necha ma'lumot kiritish mumkin" +fileIdOrUrl: "Fayl ID'si yoki URL havolasi" +behavior: "Hatti-harakatlar" +sample: "Namuna" +abuseReports: "Shikoyatlar" +reportAbuse: "Shikoyat qilish" +reportAbuseOf: "{name} ustidan shikoyat qilish" +abuseReported: "Shikoyatingiz yetkazildi. Ma'lumot uchun rahmat." +reporter: "Shikoyat qiluvchi" +reporteeOrigin: "Xabarning kelib chiqishi" +reporterOrigin: "Xabarchining joylashuvi" +forwardReport: "Xabarni masofadagi serverga yuborish" +forwardReportIsAnonymous: "Sizning yuborayotgan xabaringiz o'z akkountingiz emas balki anonim tarzda qoladi" +send: "Yuborish" +abuseMarkAsResolved: "Yuborilgan xabarni hal qilingan deb belgilash" +openInNewTab: "Yangi tab da ochish" +openInSideView: "Yon panelda ochish" +defaultNavigationBehaviour: "Standart navigatsiya harakati" +editTheseSettingsMayBreakAccount: "Bu sozlamalarni o'zgartirish hisobingizga zarar yetkazishi mumkin." +waitingFor: "{x}ni kutayapman" +random: "Tasodifiy" +system: "Tizim" +switchUi: "Interfeysni almashtirish" +desktop: "Brauzer rejimi" +clip: "Klip" +createNew: "Yangi yaratish" +optional: "Ixtiyoriy" +createNewClip: "Yangi klip yaratish" +unclip: "qirqish\n" +confirmToUnclipAlreadyClippedNote: "Ushbu xat allaqachon \"{name}\" klipga tegishli. Uni ushbu klipdan olib tashlashni xohlaysizmi?" +public: "Ommaviy" +i18nInfo: "Misskey bir qancha volontyorlar yordamida bir qancha tillarga tarjima qilingan. Ushbu {link} orqali ularga yordam berishingiz mumkin." +manageAccessTokens: "Kirish tokenlarini boshqarish" +accountInfo: "Akkount haqida ma'lumot" +notesCount: "Xatlar soni" +repliesCount: "Yuborilgan javoblar soni" +renotesCount: "Qayta yuborilgan xatlar soni" +repliedCount: "Qabul qilingan javoblar soni" +renotedCount: "Qayta yuborilgan xatlar soni" +followingCount: "Obuna bo'lingan akkountlar soni" +followersCount: "Obunachilar soni" +sentReactionsCount: "Yuborilgan reaksiyalar soni" +receivedReactionsCount: "Qabul qilingan reaksiyalar soni" +pollVotesCount: "Berilgan ovozlar soni" +pollVotedCount: "Qabul qilingan ovozlar soni" +yes: "Ha" +no: "Yo'q" +driveFilesCount: "Diskdagi fayllar soni" +driveUsage: "Ishlatilgan disk joyi" +noCrawleDescription: "Qidiruv tizimlari sizning profilingiz, sahifalaringiz, xatlaringiz va hokazolarni belgilamasligi uchun so'rov yuborish" +lockedAccountInfo: "Xatlaringizni faqatgina obunachilaringizga ko'rsatishni xohlasangiz unda \"Faqat Obunachilar uchun\" xususiyatini yoqishingiz lozim. Bo'lmasa sizning yozgan xatlaringiz hammaga ko'rinadi." +alwaysMarkSensitive: "Avvaldan ta'sirchan kontent deb belgilash" +loadRawImages: "Thumbnaillarsiz Original rasmni yuklash" +disableShowingAnimatedImages: "Animatsiyali rasmlarni ko'rsatmaslik" +verificationEmailSent: "Emailingizga tasdiqlash xabari yuborildi. Iltimos linkda ko'rsatilgan amallarga rioya qiling" +notSet: "Sozlanmagan" +emailVerified: "Elektron pochta manzili tasdiqlandi" +pageLikesCount: "Sahifadagi like'lar soni" +contact: "aloqa uchun manzil" +useSystemFont: "Tizimdagi standart shriftidan foydalaning" +clips: "Klip" +experimentalFeatures: "eksperimental xususiyatlar" +experimental: "eksperimental" +developer: "Dasturchi" +makeExplorable: "Akkauntingizni topishni osonlashtiring" +duplicate: "Dublikat" +left: "Chap(dagi)" +center: "Markaz" +wide: "Keng" +narrow: "Tor" +reloadToApplySetting: "Bu sozlamalar sahifa yangilangandagina kuchga kiradi. Hozir yangilashni istaysizmi?" +needReloadToApply: "Sahifani yangilash talab etiladi." +clearCache: "Keshni tozalash" +onlineUsersCount: "Faol userlar" +nUsers: "{n} ta foydalanuvchi" +myTheme: "Mening rang sxemam" +backgroundColor: "Fon" +accentColor: "Urg'u" +textColor: "Matn" +saveAs: "Boshqacha saqlash" +advanced: "Murakkab" +advancedSettings: "Qo'shimcha sozlashlar" +value: "Qiymati" +createdAt: "Yaratilish vaqti" +updatedAt: "yangilangan sana" +saveConfirm: "O'zgartirishni saqlash?" +deleteConfirm: "o'chirishni tasdiqlash" +invalidValue: "noto'g'ri qiymat" +registry: "ro'yhatga olish" +closeAccount: "hisobni yopish / profilni yopish" +currentVersion: "joriy versiya" +latestVersion: "so'ngi versiya" +youAreRunningUpToDateClient: "siz so'ngi versiyali ilovani ishlatyapsiz" +newVersionOfClientAvailable: "Mijozning yangi versiyasi mavjud." +usageAmount: "foydalanish miqdori" +capacity: "sig'im" +inUse: "allaqachon band" +editCode: "kodni tahrirlash" +apply: "Ilova" +receiveAnnouncementFromInstance: "Serverdan bildirishnomalarni oling" +emailNotification: "E-mail xabarlari" +publish: "Chiqarish" +inChannelSearch: "Kanal qidirish" +useReactionPickerForContextMenu: "kontekst menyusi uchun reaktsiya tanlash vositasidan foydalaning" +typingUsers: "{users} yozmoqda" +jumpToSpecifiedDate: "Muayyan sanaga o'tish" +showingPastTimeline: "O'tgan vaqt jadvallarini ko'rsatish" +clear: "aniq" +markAllAsRead: "hammasini o'qilgan deb belgilang" +goBack: "qaytish" +unlikeConfirm: "Un like qilishni xohlaysizmi?" +fullView: "to'liq ko'rish" +quitFullView: "Toʻliq koʻrishdan chiqish" +addDescription: "Tavsif qo'shing" +info: "Haqida" +userInfo: "Foydalanuvchi ma'lumotlari" +unknown: "aniq emas" +onlineStatus: "onlayn holat" +hideOnlineStatus: "onlayn holatni yashirish" +hideOnlineStatusDescription: "Onlayn statusingizni yashirish, qidiruv kabi baʼzi funksiyalardan foydalanish imkoniyatini kamaytirishi mumkin." +online: "onlayn" +active: "Aktiv" +offline: "oflayn" +notRecommended: "tavsiya etilmaydi" +selectAccount: "Akkauntni tanlang" +switchAccount: "akkauntni almashtirish" +enabled: "yaroqli" +disabled: "yaroqsiz" +quickAction: "tezkor harakat" +user: "Foydalanuvchilar" +administration: "Administratsiya" +accounts: "akkaunt" +switch: "almashtirish" +noBotProtectionWarning: "Bot himoyasi sozlanmagan." +configure: "sozlamoq" +postToGallery: "Yangi galleriya posti" +gallery: "Galereya" +recentPosts: "So'nggi postlar" +popularPosts: "Mashhur postlar" +shareWithNote: "Eslatmani ulashish" +ads: "Reklama" +startingperiod: "Boshlanish davri" +memo: "Eslatma" +priority: "Ustuvorlik" +high: "Yuqori" +middle: "O'rta" +low: "Quyi" +ratio: "nisbat" +previewNoteText: "Razm solish" +customCss: "Maxsus CSS" +global: "Global" +squareAvatars: "Kvadrat avatarkalar" +sent: "Yuborish" +received: "Qabul qilingan" +searchResult: "Qidiruv natijalari" +hashtags: "Hashteglar" +troubleshooting: "Muammolarni bartaraf qilish" +useBlurEffect: "Interfeysda xiralashtiruvchi effektlardan foydalanish" +learnMore: "Batafsilroq" +misskeyUpdated: "Misskey yangilandi!" +whatIsNew: "O'zgarishlarni ko'rish" +translate: "Tarjima qilish" +translatedFrom: "{x} tilidan tarjima qilindi" +devMode: "Dasturchi rejimi" +lastCommunication: "Oxirgi muloqot" +resolved: "Hal qilingan" +unresolved: "Hal qilinmagan" +breakFollow: "Obunachini o'chirish" +breakFollowConfirm: "Obunachini o'chirmoqchimisiz?" +itsOn: "Yoqilgan" +itsOff: "O'chirilgan" +on: "Yoqish" +off: "O'chirish" +emailRequiredForSignup: "Ro'yxatdan o'tish uchun email talab qilish" +unread: "Oʻqilmagan xabarlar" +filter: "Filter" +controlPanel: "Boshqaruv paneli" +manageAccounts: "Hisobni boshqarish" +classic: "Klassik" +hide: "Yashirish" +searchByGoogle: "Izlash" +indefinitely: "Hech qachon" +file: "Fayllar" +recommended: "Tavsiya qilingan" +check: "Tekshirish" +requireAdminForView: "Ko'rish uchun adminstrator hisobi bilan tizimga kirgan bo'lishingiz kerak." +isSystemAccount: "Ushbu hisob tizim tomonidan avtomatik tarzda yaratilgan va boshqariladi." +typeToConfirm: "Ushbu amalni bajarish uchun {x}ni kiriting" +deleteAccount: "Hisobni o'chirish" +document: "hujjat" +numberOfPageCache: "Sahifa keshlar soni" +logoutConfirm: "Chiqishni xohlaysizmi?" +lastActiveDate: "oxirgi foydalanish sanasi" +statusbar: "Holat paneli" +pleaseSelect: "Iltimos tanlang" +reverse: "Teskari" +colored: "rangli" +refreshInterval: "yangilash oralig'i" +label: "Yorliq" +type: "turi" +speed: "tezlik" +slow: "Sekin" +fast: "Tez" +localOnly: "Faqat mahalliy" +remoteOnly: "faqat masofadan turib" +failedToUpload: "yuklanmadi" +cannotUploadBecauseInappropriate: "Faylni yuklab bo'lmaydi, chunki unda nomaqbul kontent borligi aniqlangan." +cannotUploadBecauseNoFreeSpace: "Yuklab bo'lmadi, chunki diskda bo'sh joy yo'q." +cannotUploadBecauseExceedsFileSizeLimit: "Faylni yuklash mumkin emas, chunki u fayl hajmi chegarasidan oshib ketgan." +beta: "beta" +account: "akkaunt" +show: "Displey" +color: "Rang" +disableFederationConfirm: "Federatsiyani o'chirmoqchimisiz?" +disableFederationOk: "O'chirish" +emailNotSupported: "Bu server E-pochtalar yuborishni qo'llab-quvvatlamaydi" +postToTheChannel: "Kanalga joylash" +cannotBeChangedLater: "Buni keyinchalik o'zgartirishni iloji yo'q" +likeOnly: "Faqat like'lar" +nonSensitiveOnly: "Xavfsiz rejim" +rolesAssignedToMe: "Mening rollarim" +resetPasswordConfirm: "Qayta parol o'rnatmoqchimisiz?" +sensitiveWords: "Ta'sirchan so'zlar" +icon: "Avatar" +replies: "Javob berish" +renotes: "Qayta qayd etish" +_achievements: + _types: + _viewInstanceChart: + title: "Tahlilchi" +_role: + priority: "Ustuvorlik" + _priority: + low: "Quyi" + middle: "O'rta" + high: "Yuqori" +_ffVisibility: + public: "Chiqarish" +_ad: + back: "qaytish" + hide: "Boshqa ko'rsatilmasin" +_email: + _follow: + title: "sizga obuna bo'ldi" +_registry: + key: "Kalit" + keys: "Kalit" +_instanceTicker: + none: "Boshqa ko'rsatilmasin" + always: "Doimo ko'rsatilsin" +_theme: + install: "Rang sxemasini o'rnatish" + manage: "Rang sxemalarini boshqarish" + code: "Rang sxemasining kodi" + description: "Tavsif" + installed: "{name} o'rnatildi" + installedThemes: "O'rnatilgan rang sxemalari" + alreadyInstalled: "Ushbu rang sxemasi allaqachon o'rnatilgan" + invalid: "Ushbu rang sxemasining formati yaroqsiz" + make: "Rang sxemasini yasash" + base: "Asos" + addConstant: "O'zgarmas qo'shish" + constant: "O'zgarmas" + color: "Rang" + key: "Kalit" + func: "Funksiyalar" + funcKind: "Funksiya turi" + argument: "Argument" + darken: "Qoraytirish" + lighten: "Yoritish" + inputConstantName: "Ushbu o'zgarmas uchun nom kiriting" + deleteConstantConfirm: "Siz rostdan ham {const} o'zgarmasni o'chirmoqchimisiz?" + keys: + accent: "Urg'u" + bg: "Fon" + fg: "Matn" + focus: "Fokus" + panel: "Panel" + shadow: "Soya" + header: "Sarlavha" + navBg: "Yon panel foni" + navFg: "Yon panel matni" + mention: "Murojat" + renote: "Qayta qayd etish" + divider: "Ajratrmoq" + accentDarken: "Urg'u (Qoraytirilgan)" + accentLighten: "Urg'u (Yoritilgan)" + fgHighlighted: "Belgilangan matn" +_sfx: + note: "Qaydlar" + notification: "Xabarnomalar" + chat: "Suhbat" +_ago: + minutesAgo: "{n} daqiqa oldin" + hoursAgo: "{n} soat oldin" + daysAgo: "{n} kun oldin" + invalid: "Hech narsa yo'q" +_2fa: + renewTOTPCancel: "Hozir emas" +_permissions: + "read:blocks": "Bloklangan foydalanuvchilar roʻyxatini koʻring" + "write:blocks": "Bloklangan foydalanuvchilar roʻyxatini tahrirlang" +_weekday: + saturday: "Shanba" +_widgets: + profile: "Profil" + instanceInfo: "Instans haqida ma'lumot" + notifications: "Xabarnomalar" + timeline: "Xronologiya" + clock: "Soat" + activity: "Faollik" + photos: "Rasmlar" + digitalClock: "Raqamli soat" + unixClock: "UNIX soat" + federation: "Federatsiya" + button: "Tugma" + jobQueue: "Vazifalar navbati" + _userList: + chooseList: "Ro'yxat tanlash" +_cw: + show: "Ko‘proq ko‘rish" + chars: "{count} ta belgi(lar)" + files: "{count} ta fayl(lar)" +_poll: + noOnlyOneChoice: "Kamida ikkita tanvol kerak" + infinite: "Hech qachon" + at: "...da tugatish" + after: "...dan keyin tugatish" + deadlineTime: "Vaqt" + duration: "Davomiylik" + votesCount: "{n} ovozlar" + totalVotes: "Umuman {n} ovozlar" + vote: "Ovoz berish" + showResult: "Natijalarni ko'rish" + voted: "Ovoz berildi" + closed: "Yakunladi" + remainingDays: "{d} kun {h} soat qoldi" + remainingHours: "{h} soat {m} daqiqa qoldi" + remainingMinutes: "{m} daqiqa {s} sekund qoldi" + remainingSeconds: "{s} sekund qoldi" +_visibility: + public: "Ommaviy" + publicDescription: "Sizning ovozingiz barcha foydalanuvchilarga ko'rinadi" + home: "Bosh sahifa" + followers: "Obunachilar" + specified: "Bevosita" +_profile: + name: "Ism" + username: "Foydalanuvchi nomi" + description: "Biografiya" + metadata: "Qo'shimcha ma'lumot" + metadataLabel: "Yorliq" + metadataContent: "Tarkib" + changeBanner: "Bannerni o'zgartirish" +_exportOrImport: + allNotes: "Barcha qaydlar" + followingList: "Obuna bo‘lish" + muteList: "Ovozni o‘chirish" + blockingList: "Bloklangan foydalanuvchilar" + userLists: "Ro'yxatlar" +_charts: + federation: "Federatsiya" + apRequest: "So'rovlar" + usersTotal: "Foydalanuvchilarning umumiy soni" + activeUsers: "Faol foydalanuvchilar" + notesTotal: "Qaydlarning umumiy soni" + filesTotal: "Fayllarning umumiy soni" +_instanceCharts: + requests: "So'rovlar" + notes: "Qaydlar sonidagi farq" + cacheSize: "Kesh hajmidagi farq" + files: "Fayllar sonidagi farq" +_timelines: + home: "Bosh sahifa" + local: "Mahalliy" + social: "Ijtimoiy" + global: "Global" +_play: + featured: "Mashhur" + title: "Sarlavha" + script: "Skript" + summary: "Tavsif" +_pages: + newPage: "Yangi Sahifa yaratish" + editPage: "Ushbu Sahifani tahrirlash" + created: "Sahifa muvaffaqiyatli yaratildi" + updated: "Sahifa muvaffaqiyatli tahrirlandi" + deleted: "Sahifa muvaffaqiyatli o'chirildi" + pageSetting: "Sahifa sozlamalari" + nameAlreadyExists: "Ko'rsatilgan Sahifa URL'i allaqachon mavjud" + invalidNameTitle: "Ko'rsatilgan Sahifa URL'i yaroqsiz" + editThisPage: "Ushbu Sahifani tahrirlash" + viewPage: "Sizning Sahifalaringizni ko'rish" + my: "Mening Sahifalarim" + featured: "Mashhur" + contents: "Tarkib" + title: "Sarlavha" + url: "Sahifa URL'i" + summary: "Sahifa bayoni" + font: "Shrift" + fontSerif: "Serif" + fontSansSerif: "Sans Serif" + selectType: "Turni tanlang" + contentBlocks: "Tarkib" + blocks: + text: "Matn" + textarea: "Matn maydoni" + section: "Bo'lim" + image: "Rasmlar" + button: "Tugma" + note: "Biriktirilgan qayd" + _note: + id: "Qayd ID" + detailed: "Batafsil ko'rinishi" +_relayStatus: + requesting: "Kutilmoqda" + accepted: "Tasdiqlandi" + rejected: "Rad etildi" +_notification: + fileUploaded: "Fayl muvaffaqiyatli yuklandi" + youGotMention: "{name} sizni eslab o'tdi" + youGotReply: "{name} sizga javob berdi" + youGotQuote: "{name} sizdan iqtibos keltirdi" + youRenoted: "{name} dan qayta qayd qilish" + youWereFollowed: "sizga obuna bo'ldi" + unreadAntennaNote: "Antenna {name}" + _types: + all: "Barchasi" + follow: "Obuna bo‘lish" + mention: "Murojat" + renote: "Qayta qayd etish" + quote: "Iqtibos keltirish" + reaction: "Reaktsiyalar" + receiveFollowRequest: "Qabul qilingan kuzatuv so'rovlari" + _actions: + reply: "Javob berish" + renote: "Qayta qayd qilish" +_deck: + alwaysShowMainColumn: "Har doim asosiy ustunni ko'rsatish" + columnAlign: "Ustunlarni tekislash" + addColumn: "Ustun qo'shish" + configureColumn: "Ustun sozlamalari" + swapLeft: "Chapdagi ustun bilan joyni almashtirish" + swapRight: "O'ngdagi ustun bilan joyni almashtirish" + swapUp: "Yuqoridagi ustun bilan joyni almashtirish" + swapDown: "Quyidagi ustun bilan joyni almashtirish" + profile: "Profil" + newProfile: "Yangi profil" + deleteProfile: "Profilni o‘chirib tashlash" + _columns: + main: "Asosiy" + notifications: "Xabarnomalar" + tl: "Xronologiya" + antenna: "Antennalar" + list: "Ro‘yxat" + channel: "Kanal" + mentions: "Eslatib o'tish" + direct: "Bevosita qaydlar" + roleTimeline: "Rol xronologiyasi" +_webhookSettings: + name: "Ism" + active: "Yoqilgan" + _events: + renote: "Qayta qayd qilinganda" + mention: "Eslanganda" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index d620d99d80..dec9e7f888 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1,5 +1,5 @@ --- -_lang_: "Tiếng Việt" +_lang_: "Tiếng Nhậ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ở" @@ -20,6 +20,7 @@ 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" +notificationSettings: "Cài đặt thông báo" basicSettings: "Thiết lập chung" otherSettings: "Thiết lập khác" openInWindow: "Mở trong cửa sổ mới" @@ -44,13 +45,20 @@ pin: "Ghim" unpin: "Bỏ ghim" copyContent: "Chép nội dung" copyLink: "Chép liên kết" +copyLinkRenote: "Sao chép liên kết ghi chú" delete: "Xóa" 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" +addToAntenna: "Thêm vào Ăngten" sendMessage: "Gửi tin nhắn" copyRSS: "Sao chép RSS" copyUsername: "Chép tên người dùng" +copyUserId: "Sao chép ID người dùng" +copyNoteId: "Sao chép ID ghi chú" +copyFileId: "Sao chép ID tập tin" +copyFolderId: "Sao chép ID thư mục" +copyProfileUrl: "Sao chép URL hồ sơ" searchUser: "Tìm kiếm người dùng" reply: "Trả lời" loadMore: "Tải thêm" @@ -122,6 +130,8 @@ unmarkAsSensitive: "Bỏ đánh dấu nhạy cảm" enterFileName: "Nhập tên tập tin" mute: "Ẩn" unmute: "Bỏ ẩn" +renoteMute: "Mute Renotes" +renoteUnmute: "Unmute Renotes" block: "Chặn" unblock: "Bỏ chặn" suspend: "Vô hiệu hóa" @@ -131,8 +141,10 @@ 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" +editList: "Chỉnh sửa danh sách" selectChannel: "Lựa chọn kênh" selectAntenna: "Chọn một antenna" +editAntenna: "Chỉnh sửa Ăngten" selectWidget: "Chọn tiện ích" editWidgets: "Sửa tiện ích" editWidgetsExit: "Xong" @@ -145,6 +157,9 @@ addEmoji: "Thêm emoji" settingGuide: "Cài đặt đề xuất" 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." +youCanCleanRemoteFilesCache: "Bạn có thể xoá bộ nhớ đệm bằng cách nhấn vào nút🗑️ở trong phần quản lý tệp." +cacheRemoteSensitiveFiles: "Lưu các tập tin nhạy cảm vào bộ nhớ tạm từ xa" +cacheRemoteSensitiveFilesDescription: "Khi bạn tắt tính năng này, các tệp nhạy cảm sẽ được tải trực tiếp từ máy chủ và không được lưu vào bộ nhớ tạm" 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: "Chế độ Mèeeeeeeeeeo!!" @@ -153,6 +168,7 @@ 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" addAccount: "Thêm tài khoản" +reloadAccountsList: "Cập nhật danh sách tài khoản" loginFailed: "Đăng nhập không thành công" showOnRemote: "Truy cập trang của người này" general: "Tổng quan" @@ -259,8 +275,10 @@ noMoreHistory: "Không còn gì để đọc" startMessaging: "Bắt đầu trò chuyện" nUsersRead: "đọc bởi {n}" agreeTo: "Tôi đồng ý {0}" +agree: "Đồng ý" agreeBelow: "Đồng ý với nội dung dưới đây" basicNotesBeforeCreateAccount: "Những điều cơ bản cần chú ý " +termsOfService: "Điều khoản và Điều kiện" start: "Bắt đầu" home: "Trang chính" remoteUserCaution: "Vì người dùng này ở máy chủ khác, thông tin hiển thị có thể không đầy đủ." @@ -303,7 +321,7 @@ copyUrl: "Sao chép URL" rename: "Đổi tên" avatar: "Ảnh đại diện" banner: "Ảnh bìa" -nsfw: "Nhạy cảm" +displayOfSensitiveMedia: "Hiển thị nội dung nhạy cảm (NSFW)" whenServerDisconnected: "Khi mất kết nối tới máy chủ" disconnectedFromServer: "Mất kết nối tới máy chủ" reload: "Tải lại" @@ -338,7 +356,6 @@ invite: "Mời" driveCapacityPerLocalAccount: "Dung lượng ổ đĩa tối đa cho mỗi người dùng" driveCapacityPerRemoteAccount: "Dung lượng ổ đĩa tối đa cho mỗi người dùng từ xa" inMb: "Tính bằng MB" -iconUrl: "URL Icon" bannerUrl: "URL Ảnh bìa" backgroundImageUrl: "URL Ảnh nền" basicInfo: "Thông tin cơ bản" @@ -394,10 +411,13 @@ aboutMisskey: "Về Misskey" administrator: "Quản trị viên" token: "Token" 2fa: "Xác thực 2 yếu tố" +setupOf2fa: "Thiết lập 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" +moderationNote: "Ghi chú kiểm duyệt" +addModerationNote: "Thêm ghi chú 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" @@ -457,6 +477,7 @@ 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" +showNoteActionsOnlyHover: "Chỉ hiển thị các hành động ghi chú khi di chuột" noHistory: "Không có dữ liệu" signinHistory: "Lịch sử đăng nhập" enableAdvancedMfm: "Xem bài MFM chất lượng cao." @@ -469,6 +490,7 @@ createAccount: "Tạo tài khoản" existingAccount: "Tài khoản hiện có" regenerate: "Tạo lại" fontSize: "Cỡ chữ" +limitTo: "Giới hạn tỷ lệ {x}" noFollowRequests: "Bạn không có yêu cầu theo dõi nào" openImageInNewTab: "Mở ảnh trong tab mới" dashboard: "Trang chính" @@ -505,6 +527,7 @@ objectStorageSetPublicRead: "Đặt \"public-read\" khi tải lên" serverLogs: "Nhật ký máy chủ" deleteAll: "Xóa tất cả" showFixedPostForm: "Hiện khung soạn tút ở phía trên bảng tin" +showFixedPostFormInChannel: "Hiển thị mẫu bài đăng ở phía trên bản tin" newNoteRecived: "Đã nhận tút mới" sounds: "Âm thanh" sound: "Âm thanh" @@ -542,9 +565,14 @@ userSuspended: "Người này đã bị vô hiệu hóa." userSilenced: "Người này đã bị ẩn" yourAccountSuspendedTitle: "Tài khoản bị vô hiệu hóa" yourAccountSuspendedDescription: "Tài khoản này đã bị vô hiệu hóa do vi phạm quy tắc máy chủ hoặc điều tương tự. Liên hệ với quản trị viên nếu bạn muốn biết lý do chi tiết hơn. Vui lòng không tạo tài khoản mới." +tokenRevoked: "Token đã bị từ chối" +tokenRevokedDescription: "Phiên đăng nhập đã hết hạn. Vui lòng đăng nhập lại." +accountDeleted: "Tài khoản đã bị xóa" +accountDeletedDescription: "Tài khoản này đã bị xóa." menu: "Menu" divider: "Phân chia" addItem: "Thêm mục" +rearrange: "Sắp xếp lại" relays: "Chuyển tiếp" addRelay: "Thêm chuyển tiếp" inboxUrl: "URL Hộp thư đến" @@ -654,6 +682,7 @@ createNewClip: "Tạo một ghim mới" unclip: "Bỏ ghim" confirmToUnclipAlreadyClippedNote: "Bài đăng này là một phần của \"{name}\" ghim. Bạn có muốn bỏ khỏi ghim?" public: "Công khai" +private: "Riêng tư" i18nInfo: "Misskey đang được các tình nguyện viên dịch sang nhiều thứ tiếng khác nhau. Bạn có thể hỗ trợ tại {link}." manageAccessTokens: "Tạo mã truy cập" accountInfo: "Thông tin tài khoản" @@ -688,6 +717,8 @@ contact: "Liên hệ" useSystemFont: "Dùng phông chữ mặc định của hệ thống" clips: "Lưu bài viết" experimentalFeatures: "Tính năng thử nghiệm" +experimental: "Thử nghiệm" +thisIsExperimentalFeature: "Tính năng này đang trong quá trình thử nghiệm. Tính năng có thể không hoạt động, hoặc đặc tính kỹ thuật có thể bị thay đổi sau này." developer: "Nhà phát triển" makeExplorable: "Không hiện tôi trong \"Khám phá\"" makeExplorableDescription: "Nếu bạn tắt, tài khoản của bạn sẽ không hiện trong mục \"Khám phá\"." @@ -772,6 +803,7 @@ noMaintainerInformationWarning: "Chưa thiết lập thông tin vận hành." noBotProtectionWarning: "Bảo vệ Bot chưa thiết lập." configure: "Thiết lập" postToGallery: "Tạo tút có ảnh" +postToHashtag: "Đăng bài với hashtag này" gallery: "Thư viện ảnh" recentPosts: "Tút gần đây" popularPosts: "Tút được xem nhiều nhất" @@ -805,6 +837,7 @@ translatedFrom: "Dịch từ {x}" accountDeletionInProgress: "Đang xử lý việc xóa tài khoản" usernameInfo: "Bạn có thể sử dụng chữ cái (a ~ z, A ~ Z), chữ số (0 ~ 9) hoặc dấu gạch dưới (_). Tên người dùng không thể thay đổi sau này." aiChanMode: "Chế độ Ai" +devMode: "Chế độ dành cho nhà phát triển" keepCw: "Giữ cảnh báo nội dung" pubSub: "Tài khoản Chính/Phụ" lastCommunication: "Lần giao tiếp cuối" @@ -814,6 +847,8 @@ 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" +on: "Bật" +off: "Tắt" emailRequiredForSignup: "Yêu cầu địa chỉ email khi đăng ký" unread: "Chưa đọc" filter: "Bộ lọc" @@ -858,6 +893,7 @@ 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?" +cropYes: "Cắt" cropNo: "Để nguyên" file: "Tập tin" recentNHours: "{n}h trước" @@ -893,6 +929,7 @@ remoteOnly: "Chỉ máy chủ từ xa" failedToUpload: "Tải lên thất bại" cannotUploadBecauseInappropriate: "Không thể tải lên tập tin này vì các phần của tập tin đã được phát hiện có khả năng là NSFW." cannotUploadBecauseNoFreeSpace: "Tải lên không thành công do thiếu dung lượng Drive." +cannotUploadBecauseExceedsFileSizeLimit: "Không thể tải lên tập tin vì kích thước quá lớn." beta: "Beta" enableAutoSensitive: "Tự động đánh dấu NSFW" enableAutoSensitiveDescription: "Cho phép tự động phát hiện và đánh dấu media NSFW thông qua học máy, nếu có thể. Ngay cả khi tùy chọn này bị tắt, nó vẫn có thể được bật trên toàn máy chủ." @@ -905,9 +942,11 @@ 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" +pushNotificationNotSupported: "Trình duyệt của bạn không hỗ trợ 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" +windowMinimize: "Thu nhỏ tối đa" windowRestore: "Khôi phục" caption: "Mô tả" loggedInAsBot: "Đang đăng nhập bằng tài khoản Bot" @@ -924,12 +963,22 @@ 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ò" +noRole: "Bạn chưa được cấp quyền." normalUser: "Người dùng bình thường" undefined: "Chưa xác định" +assign: "Phân công" +unassign: "Hủy phân công" color: "Màu sắc" manageCustomEmojis: "Quản lý CustomEmoji" +youCannotCreateAnymore: "Bạn đã tới giới hạn tạo." 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." +invalidParamError: "Lỗi tham số" +invalidParamErrorDescription: "Có vấn đề với các tham số được request. Thông thường, đây là do bug, nhưng cũng có thể do bạn đã nhập vào quá nhiều ký tự." +permissionDeniedError: "Thao tác bị từ chối" +permissionDeniedErrorDescription: "Tài khoản này không có đủ quyền hạn để thực hiện thao tác này." +preset: "Mẫu thiết lập" +selectFromPresets: "Chọn từ mẫu" 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. " @@ -944,8 +993,95 @@ 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" +emailNotSupported: "Máy chủ này không hỗ trợ gửi email" +postToTheChannel: "Đăng lên kênh" +cannotBeChangedLater: "Không thể thay đổi sau này." +rolesAssignedToMe: "Vai trò được giao cho tôi" +resetPasswordConfirm: "Bạn thực sự muốn đặt lại mật khẩu?" +sensitiveWords: "Các từ nhạy cảm" +license: "Giấy phép" +unfavoriteConfirm: "Bạn thực sự muốn xoá khỏi mục yêu thích?" +retryAllQueuesConfirmText: "Điều này sẽ tạm thời làm tăng mức độ tải của máy chủ." +enableChartsForRemoteUser: "Tạo biểu đồ người dùng từ xa" +video: "Video" +videos: "Các video" +dataSaver: "Tiết kiệm dung lượng" +accountMigration: "Chuyển tài khoản" +accountMoved: "Người dùng này đã chuyển sang một tài khoản mới:" +accountMovedShort: "Tài khoản này đã được chuyển" +operationForbidden: "Thao tác này không thể thực hiện" +forceShowAds: "Luôn hiện quảng cáo" +notificationDisplay: "Thông báo" +leftTop: "Phía trên bên tráí" +rightTop: "Phía trên bên phải" +leftBottom: "Phía dưới bên trái" +rightBottom: "Phía dưới bên phải" +stackAxis: "Hướng chồng" +vertical: "Dọc" horizontal: "Thanh bên" +position: "Vị trí" +serverRules: "Luật của máy chủ" youFollowing: "Đang theo dõi" +later: "Để sau" +goToMisskey: "Tới Misskey" +installed: "Đã tải xuống" +branding: "Thương hiệu" +turnOffToImprovePerformance: "Tắt mục này có thể cải thiện hiệu năng." +expirationDate: "Ngày hết hạn" +noExpirationDate: "Vô thời hạn" +waitingForMailAuth: "Đang chờ xác nhận email" +unused: "Chưa được sử dụng" +used: "Đã được sử dụng" +expired: "Đã hết hạn" +doYouAgree: "Đồng ý?" +iHaveReadXCarefullyAndAgree: "Tôi đã đọc và đồng ý với \"x\"." +dialog: "Hộp thoại" +icon: "Ảnh đại diện" +forYou: "Dành cho bạn" +currentAnnouncements: "Thông báo hiện tại" +pastAnnouncements: "Thông báo trước đó" +youHaveUnreadAnnouncements: "Có thông báo chưa đọc." +replies: "Trả lời" +renotes: "Đăng lại" +loadReplies: "Hiển thị các trả lời" +pinnedList: "Các mục đã được ghim" +keepScreenOn: "Giữ màn hình luôn bật" +verifiedLink: "Chúng tôi đã xác nhận bạn là chủ sở hữu của đường dẫn này" +_announcement: + forExistingUsers: "Chỉ những người dùng đã tồn tại" + forExistingUsersDescription: "Nếu được bật, thông báo này sẽ chỉ hiển thị với những người dùng đã tồn tại vào lúc thông báo được tạo. Nếu tắt đi, những tài khoản mới đăng ký sau khi thông báo được đăng lên cũng sẽ thấy nó." + end: "Lưu trữ thông báo" + tooManyActiveAnnouncementDescription: "Có quá nhiều thông báo sẽ làm trải nghiệm của người dùng tệ đi. Vui lòng lưu trữ những thông báo đã hết hiệu lực." + readConfirmTitle: "Đánh dấu là đã đọc?" + readConfirmText: "Điều này sẽ đánh dấu nội dung của \"{title}\" là đã đọc." +_initialAccountSetting: + accountCreated: "Tài khoản của bạn đã được tạo thành công!" + letsStartAccountSetup: "Để bắt đầu, hãy cùng thiết lập tài khoản nhé." + letsFillYourProfile: "Đầu tiên, hãy thiết lập hồ sơ của bạn." + profileSetting: "Thiết lập hồ sơ" + privacySetting: "Cài đặt quyền riêng tư" + theseSettingsCanEditLater: "Bạn vẫn có thể thay đổi những cài đặt này." + youCanEditMoreSettingsInSettingsPageLater: "Còn rất nhiều những cài đặt khác bạn có thể thay đổi ở trang \"Cài đặt\". Hãy nhớ ghé thăm trong lần sau nhé." + followUsers: "Thử theo dõi một vài người mà bạn có thể thích để xây dựng dòng thời gian của mình." + pushNotificationDescription: "Bật thông báo đẩy sẽ cho phép bạn nhận thông báo từ {name} trực tiếp từ thiết bị của bạn." + initialAccountSettingCompleted: "Thiết lập tài khoản thành công!" + haveFun: "Hãy tận hưởng {name} nhé!" + ifYouNeedLearnMore: "Nếu bạn muốn tìm hiểu thêm về cách sử dụng {name} (Misskey), hãy vào {link}." + skipAreYouSure: "Bạn thực sự muốn bỏ qua mục thiết lập tài khoản?" + laterAreYouSure: "Bạn thực sự muốn thiết lập tài khoản vào lúc khác?" +_serverSettings: + iconUrl: "Biểu tượng URL" + appIconResolutionMustBe: "Độ phân giải tối thiểu là {resolution}." + manifestJsonOverride: "Ghi đè manifest.json" +_accountMigration: + moveFrom: "Chuyển một tài khoản khác vào tài khoản này" + moveFromLabel: "Tài khoản gốc #{n}" + moveTo: "Chuyển tài khoản này vào một tài khoản khác" + moveCannotBeUndone: "Việc chuyển tài khoản không thể huỷ." + moveAccountDescription: "Điều này sẽ chuyển tài khoản này sang một tài khoản khác.\n ・Những người theo dõi sẽ tự động được chuyển sang tài khoản mới\n ・Tài khoản này sẽ tự bỏ theo dõi những người mà bạn đã theo dõi trước đây\n ・Bạn sẽ không thể đăng tút mới, v.v trên tài khoản này\n\nDù việc chuyển người theo dõi được diễn ra tự động, bạn vẫn phải tự chuẩn bị một vài bước để chuyển danh sách những người dùng bạn đang theo dõi. Để làm vậy, vui lòng thực hiện việc xuất dữ liệu những người dùng đã theo dõi mà sau này bạn sẽ dùng để nhập vào tài khoản mới ở menu Cài đặt. Hành động tương tự áp dụng với danh sách những người dùng bị chặn hoặc tắt tiếng.\n\n(Điều này áp dụng cho phiên bản Misskey v13.12.0 và sau này. Các phần mềm ActivityPub khác , ví dụ như Mastodon, sẽ có thể hoạt động khác đi.)" + startMigration: "Chuyển" + movedAndCannotBeUndone: "\nTài khoản này đã được chuyển đi.\nViệc di chuyển tài khoản không thể bị huỷ bỏ." + movedTo: "Tài khoản mới:" _achievements: earnedAt: "Ngày thu nhận" _types: @@ -984,6 +1120,8 @@ _achievements: title: "Hàng tinh đăng bài" description: "Đã đăng bài 50,000 lần rồi" _notes100000: + title: "ALL YOUR NOTE ARE BELONG TO US" + description: "Đăng 100,000 tút" flavor: "Liệu viết bài gì tầm này vậy? " _login3: title: "Sơ cấp I" @@ -1015,6 +1153,15 @@ _achievements: _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" + _login1000: + flavor: "Cảm ơn bạn đã sử dụng Misskey!" + _noteFavorited1: + title: "Nhà thiên văn học" + _myNoteFavorited1: + title: "Đi tìm những ngôi sao" + _profileFilled: + title: "Luôn sẵn sàng" + description: "Thiết lập tài khoản của bạn" _markedAsCat: title: "Tôi là một con mèo" description: "Bật chế độ mèo" @@ -1040,8 +1187,18 @@ _achievements: _followers10: title: "FOLLOW ME!!" description: "Người theo dõi bạn vượt lên 10 người" + _followers50: + title: "Từng chút một" + description: "Đạt được 50 lượt theo dõi" + _followers100: + title: "Người nổi tiếng" + description: "Đạt được 100 lượt theo dõi" + _followers300: + title: "Vui lòng xếp thành hàng nào" + description: "Đạt được 300 lượt theo dõi" _followers500: title: "Trạm phát sóng" + description: "Đạt được 500 lượt theo dõi" _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" @@ -1060,11 +1217,15 @@ _achievements: description: "Tìm thấy được những kho báu cất giấu" _client30min: title: "Giải lao xỉu" + description: "Giữ Misskey mở trong ít nhất 30 phút" + _client60min: + description: "Giữ Misskey mở trong ít nhất 60 phút" _noteDeletedWithin1min: title: "Xem như không có gì đâu nha" _postedAtLateNight: title: "Loài ăn đêm" description: "Đăng bài trong đêm khuya " + flavor: "Đến giờ đi ngủ rồi." _postedAt0min0sec: title: "Tín hiệu báo giờ" description: "Đăng bài vào 0 phút 0 giây" @@ -1095,6 +1256,8 @@ _achievements: _setNameToSyuilo: title: "Ngưỡng mộ với vị thần" description: "Đạt tên là syuilo" + _passedSinceAccountCreated1: + title: "Kỷ niệm một năm" _loggedInOnBirthday: title: "Sinh nhật vủi vẻ" description: "Đăng nhập vào ngày sinh" @@ -1105,6 +1268,7 @@ _achievements: _cookieClicked: flavor: "Bạn nhầm phầm mềm chứ?" _role: + assignTarget: "Phân công" priority: "Ưu tiên" _priority: low: "Thấp" @@ -1212,10 +1376,6 @@ _aboutMisskey: donate: "Ủng hộ Misskey" morePatrons: "Chúng tôi cũng trân trọng sự hỗ trợ của nhiều người đóng góp khác không được liệt kê ở đây. Cảm ơn! 🥰" patrons: "Người ủng hộ" -_nsfw: - respect: "Ẩn nội dung NSFW" - ignore: "Hiện nội dung NSFW" - force: "Ẩn mọi media" _instanceTicker: none: "Không hiển thị" remote: "Hiện cho người dùng từ máy chủ khác" @@ -1351,15 +1511,24 @@ _time: minute: "phút" hour: "giờ" day: "ngày" +_timelineTutorial: + step4_1: "Bạn có thể thêm \"Reaction\" vào ghi chú" + step4_2: "Khi thêm biểu cảm hãy nhấn dấu \"+\"" _2fa: alreadyRegistered: "Bạn đã đăng ký thiết bị xác minh 2 bước." - passwordToTOTP: "Nhắn mật mã" + registerTOTP: "Đăng ký ứng dụng xác thực" 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:" + step2Click: "Quét mã QR trên ứng dụng xác thực (Authy, Google authenticator, v.v.)" + step3Title: "Nhập mã xác thực" 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 đó." + securityKeyNotSupported: "Trình duyệt của bạn không hỗ trợ khóa bảo mật" + registerTOTPBeforeKey: "Vui lòng thiết lập một ứng dụng xác thực để đăng ký khóa bảo mật hoặc mật khẩu." 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." + registerSecurityKey: "Tạo khóa bảo mật hoặc mã bảo mật" + securityKeyName: "Nhập tên khóa bảo mật" + tapSecurityKey: "Vui lòng làm theo hướng dẫn của trình duyệt để đăng ký mã bảo mật hoặc mã khóa" 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" @@ -1682,5 +1851,11 @@ _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}" _webhookSettings: + createWebhook: "Tạo Webhook" name: "Tên" + secret: "Mã bí mật" + events: "Sự kiện Webhook" active: "Đã bật" + _events: + reaction: "Khi nhận được sự kiện" + mention: "Khi có người nhắc tới bạn" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 9c278ea751..3026682890 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -15,7 +15,7 @@ gotIt: "我明白了" cancel: "取消" noThankYou: "不用,谢谢" enterUsername: "输入用户名" -renotedBy: "由 {user} 转贴" +renotedBy: "{user} 转发了" noNotes: "没有帖文" noNotifications: "无通知" instance: "服务器" @@ -45,15 +45,20 @@ pin: "置顶" unpin: "取消置顶" copyContent: "复制内容" copyLink: "复制链接" +copyLinkRenote: "复制转帖链接" delete: "删除" deleteAndEdit: "删除并编辑" deleteAndEditConfirm: "要删除此帖并再次编辑吗?对此帖的所有回应、转发和回复也将被删除。" addToList: "添加至列表" +addToAntenna: "添加到天线" sendMessage: "发送" copyRSS: "复制RSS" copyUsername: "复制用户名" -copyUserId: "复制用户ID" -copyNoteId: "复制帖子ID" +copyUserId: "复制用户 ID" +copyNoteId: "复制帖子 ID" +copyFileId: "复制文件ID" +copyFolderId: "复制文件夹ID" +copyProfileUrl: "复制配置文件URL" searchUser: "搜索用户" reply: "回复" loadMore: "查看更多" @@ -71,7 +76,7 @@ export: "导出" files: "文件" download: "下载" driveFileDeleteConfirm: "要删除「{name}」文件吗?附加此文件的帖子也会被删除。" -unfollowConfirm: "要取消对{name}的关注吗?" +unfollowConfirm: "要取消对 {name} 的关注吗?" exportRequested: "导出请求已提交,这可能需要花一些时间,导出的文件将保存到网盘中。" importRequested: "导入请求已提交,这可能需要花一点时间。" lists: "列表" @@ -136,8 +141,10 @@ unblockConfirm: "确定要解除拉黑吗?" suspendConfirm: "要冻结吗?" unsuspendConfirm: "要解除冻结吗?" selectList: "选择列表" +editList: "编辑列表" selectChannel: "选择频道" selectAntenna: "选择天线" +editAntenna: "编辑天线" selectWidget: "选择小工具" editWidgets: "编辑部件" editWidgetsExit: "完成编辑" @@ -149,11 +156,14 @@ emojiUrl: "emoji 地址" addEmoji: "添加表情符号" settingGuide: "推荐配置" cacheRemoteFiles: "缓存远程文件" -cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。" +cacheRemoteFilesDescription: "启用此设定时,将在此服务器上缓存远程文件。虽然可以加快图片显示的速度,但是相对的会消耗大量的服务器存储空间。用户角色内的网盘容量决定了这个远程用户能在服务器上保留保留多少缓存。当超出了这个限制时,旧的文件将从缓存中被删除,成为链接。当禁用此设定时,则是从一开始就将远程文件保留为链接。此时推荐将 default.yml 的 proxyRemoteFiles 设置为 true 以优化缩略图生成及保护用户隐私。" +youCanCleanRemoteFilesCache: "可以使用文件管理的🗑️按钮来删除所有的缓存。" +cacheRemoteSensitiveFiles: "缓存远程敏感媒体文件" +cacheRemoteSensitiveFilesDescription: "如果禁用这项设定,远程服务器的敏感媒体将不会被缓存,而是直接链接。" flagAsBot: "这是一个机器人账号" -flagAsBotDescription: "如果此账户由程序控制,请启用此项。启用后,此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为,并让Misskey的内部系统将此账户识别为机器人。" -flagAsCat: "将这个账户设定为一只猫" -flagAsCatDescription: "如果您想表明此帐户是一只猫,请打开此标志。\n开启后,会在您的头像上出现猫耳朵,并将你的帖子中的「na」替换为「nya」,日文同理。" +flagAsBotDescription: "如果此账户由程序控制,请启用此项。启用后,此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为,并让 Misskey 的内部系统将此账户识别为机器人。" +flagAsCat: "喵!!!!!!!!!!!!" +flagAsCatDescription: "喵喵喵??" flagShowTimelineReplies: "在时间线上显示帖子的回复" flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。" autoAcceptFollowed: "自动允许来自我关注的用户对我的关注请求" @@ -167,9 +177,9 @@ setWallpaper: "设置壁纸" removeWallpaper: "移除壁纸" searchWith: "搜索:{q}" youHaveNoLists: "列表为空" -followConfirm: "你确定要关注{name}吗?" +followConfirm: "你确定要关注 {name} 吗?" proxyAccount: "代理账户" -proxyAccountDescription: "代理账户是在某些情况下充当用户的远程关注者的账户。 例如,当一个用户列出一个远程用户时,如果没有人跟随该列出的用户,则该活动将不会传递到该服务器,因此将代之以代理账户。" +proxyAccountDescription: "代理账户是在某些情况下替代用户进行远程关注用的账户。 例如说,当用户将一位远程用户放入一个列表中时,如果本地服务器上没有任何人关注这位远程用户,则这位远程用户的账户活动将不会被送到本地服务器上。作为替代,此时将使用代理账户进行关注。" host: "主机名" selectUser: "选择用户" recipient: "收件人" @@ -189,7 +199,7 @@ operations: "操作" software: "软件" version: "版本" metadata: "元数据" -withNFiles: "{n}个文件" +withNFiles: "{n} 个文件" monitor: "服务器状态" jobQueue: "作业队列" cpuAndMemory: "CPU和内存" @@ -199,11 +209,11 @@ instanceInfo: "服务器信息" statistics: "统计" clearQueue: "清除队列" clearQueueConfirmTitle: "确定清除队列?" -clearQueueConfirmText: "未送达的帖子将不会送达。 通常,您不需要这样做。" +clearQueueConfirmText: "未送达的帖子将不会投递。 通常,您不需要这样做。" clearCachedFiles: "清除缓存" clearCachedFilesConfirm: "确定要清除缓存文件?" -blockedInstances: "被阻拦的服务器" -blockedInstancesDescription: "设定要阻拦的服务器,以换行来进行分割。被阻拦的服务器将无法与本服务器进行交换通讯。" +blockedInstances: "被封锁的服务器" +blockedInstancesDescription: "设定要封锁的服务器,以换行来进行分割。被封锁的服务器将无法与本服务器进行交换通讯。子域名也同样会被封锁。" muteAndBlock: "屏蔽/拉黑" mutedUsers: "已屏蔽用户" blockedUsers: "已拉黑的用户" @@ -211,7 +221,7 @@ noUsers: "无用户" editProfile: "编辑资料" noteDeleteConfirm: "要删除该帖子吗?" pinLimitExceeded: "无法置顶更多了" -intro: "Misskey的部署结束啦!填写管理员账号吧!" +intro: "Misskey 的部署结束啦!创建管理员账号吧!" done: "完成" processing: "正在处理" preview: "预览" @@ -238,11 +248,11 @@ newPasswordRetype: "重新输入密码:" attachFile: "插入附件" more: "更多!" featured: "热门" -usernameOrUserId: "用户名或用户ID" +usernameOrUserId: "用户名或用户 ID" noSuchUser: "用户不存在" lookup: "查询" announcements: "公告" -imageUrl: "图片URL" +imageUrl: "图片 URL" remove: "删除" removed: "已删除" removeAreYouSure: "要删掉「{x}」吗?" @@ -256,15 +266,15 @@ keepOriginalUploadingDescription: "上传图片时保留原始图片。关闭时 fromDrive: "从网盘中" fromUrl: "从 URL" uploadFromUrl: "从网址上传" -uploadFromUrlDescription: "输入文件的URL" +uploadFromUrlDescription: "输入文件的 URL" uploadFromUrlRequested: "请求上传" uploadFromUrlMayTakeTime: "上传可能需要一些时间完成。" explore: "发现" messageRead: "已读" noMoreHistory: "没有更多的历史记录" startMessaging: "添加聊天" -nUsersRead: "{n}人已读" -agreeTo: "勾选则表示已阅读并同意{0}" +nUsersRead: "{n} 人已读" +agreeTo: "勾选则表示已阅读并同意 {0}" agree: "同意" agreeBelow: "同意以下内容" basicNotesBeforeCreateAccount: "基本注意事项" @@ -305,13 +315,13 @@ unableToDelete: "无法删除" inputNewFileName: "请输入新文件名" inputNewDescription: "请输入新标题" inputNewFolderName: "请输入新文件夹名" -circularReferenceFolder: "目标文件夹是您要移动的文件夹的子文件夹。" +circularReferenceFolder: "目标文件夹是要移动的文件夹的子文件夹。" hasChildFilesOrFolders: "此文件夹中有文件,无法删除。" copyUrl: "复制链接" rename: "重命名" avatar: "头像" banner: "横幅" -nsfw: "敏感内容" +displayOfSensitiveMedia: "显示敏感媒体" whenServerDisconnected: "与服务器连接中断时" disconnectedFromServer: "已和服务器断开连接" reload: "重新加载" @@ -326,7 +336,7 @@ instanceName: "服务器名称" instanceDescription: "服务器简介" maintainerName: "管理员名称" maintainerEmail: "管理员电子邮箱" -tosUrl: "服务条款URL" +tosUrl: "服务条款 URL" thisYear: "今年" thisMonth: "本月" today: "今天" @@ -337,49 +347,48 @@ pages: "页面" integration: "关联" connectService: "连接" disconnectService: "断开连接" -enableLocalTimeline: "启用本地时间线功能" +enableLocalTimeline: "启用本地时间线" enableGlobalTimeline: "启用全局时间线" -disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和数据图表也可以继续使用。" +disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和协作者也可以继续使用。" registration: "注册" enableRegistration: "允许任何人注册" invite: "邀请" -driveCapacityPerLocalAccount: "每个用户的网盘空间" +driveCapacityPerLocalAccount: "每个用户的网盘容量" driveCapacityPerRemoteAccount: "每个远程用户的网盘容量" inMb: "以兆字节(MegaByte)为单位" -iconUrl: "图标URL" -bannerUrl: "横幅URL" -backgroundImageUrl: "背景图URL" +bannerUrl: "横幅 URL" +backgroundImageUrl: "背景图 URL" basicInfo: "基本信息" pinnedUsers: "置顶用户" -pinnedUsersDescription: "在「发现」页面中使用换行标记想要置顶的用户。" +pinnedUsersDescription: "输入您想要固定到“发现”页面的用户,一行一个。" pinnedPages: "固定页面" -pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,以换行符分隔。" -pinnedClipId: "置顶的便签ID" +pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,一行一个。" +pinnedClipId: "置顶的便签 ID" pinnedNotes: "已置顶的帖子" hcaptcha: "hCaptcha" enableHcaptcha: "启用 hCaptcha" hcaptchaSiteKey: "网站密钥" -hcaptchaSecretKey: "密钥" +hcaptchaSecretKey: "hCaptcha 密钥(SecretKey)" recaptcha: "reCAPTCHA" enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)" recaptchaSiteKey: "网站密钥" recaptchaSecretKey: "reCAPTCHA 密钥(SecretKey)" turnstile: "Turnstile" -enableTurnstile: "启用Turnstile" +enableTurnstile: "启用 Turnstile" turnstileSiteKey: "网站密钥" turnstileSecretKey: "Turnstile 密钥(SecretKey)" -avoidMultiCaptchaConfirm: "使用多种验证方式可能会造成干扰,您要禁用其他验证方式吗?您可以按“取消”按钮,仍然保持启用多种验证方式。" +avoidMultiCaptchaConfirm: "使用多种验证方式可能会造成干扰,您要禁用其他验证方式吗?您可以按“取消”按钮,继续保持启用多种验证方式。" antennas: "天线" manageAntennas: "天线管理" name: "名称" antennaSource: "接收来源" antennaKeywords: "包含关键字" antennaExcludeKeywords: "排除关键字" -antennaKeywordsDescription: "使用空格分隔会产生AND规范,并且使用换行符分隔会产生OR规范" +antennaKeywordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。" notifyAntenna: "开启通知" withFileAntenna: "仅带有附件的帖子" -enableServiceworker: "启用ServiceWorker" -antennaUsersDescription: "指定用户名,用换行符分隔" +enableServiceworker: "启用 ServiceWorker" +antennaUsersDescription: "指定用户名,一行一个" caseSensitive: "区分大小写" withReplies: "包括回复" connectedTo: "您的账号已连到接以下第三方账号" @@ -393,7 +402,7 @@ popularUsers: "热门用户" recentlyUpdatedUsers: "最近投稿的用户" recentlyRegisteredUsers: "最近登录的用户" recentlyDiscoveredUsers: "最近发现的用户" -exploreUsersCount: "有{count}个用户" +exploreUsersCount: "有 {count} 个用户" exploreFediverse: "探索联邦宇宙" popularTags: "热门标签" userList: "列表" @@ -402,10 +411,13 @@ aboutMisskey: "关于 Misskey" administrator: "管理员" token: "Token (令牌)" 2fa: "双因素认证" +setupOf2fa: "设置双因素认证" totp: "身份验证应用" totpDescription: "使用认证应用输入一次性密码。" moderator: "监察员" moderation: "管理" +moderationNote: "管理笔记" +addModerationNote: "添加管理笔记" nUsersMentioned: "{n} 被提到" securityKeyAndPasskey: "安全密钥或 Passkey" securityKey: "安全密钥" @@ -413,13 +425,13 @@ lastUsed: "最后使用:" lastUsedAt: "最后使用: {t}" unregister: "删除账户" passwordLessLogin: "无密码登录" -passwordLessLoginDescription: "不使用密码,仅使用安全密钥或Passkey登录" +passwordLessLoginDescription: "不使用密码,仅使用安全密钥或 Passkey 登录" resetPassword: "重置密码" newPasswordIs: "新的密码是「{password}」" reduceUiAnimation: "减少UI动画" share: "分享" notFound: "未找到" -notFoundDescription: "没有与指定URL对应的页面。" +notFoundDescription: "没有与指定 URL 对应的页面。" uploadFolder: "默认上传文件夹" cacheClear: "清空缓存" markAsReadAllNotifications: "将所有通知标为已读" @@ -436,7 +448,7 @@ text: "文本" enable: "启用" next: "下一个" retype: "重新输入" -noteOf: "{user}的帖子" +noteOf: "{user} 的帖子" quoteAttached: "已引用" quoteQuestion: "是否引用此链接内容?" noMessagesYet: "现在没有新的聊天" @@ -468,8 +480,8 @@ disableDrawer: "不显示抽屉菜单" showNoteActionsOnlyHover: "仅在悬停时显示帖子操作" noHistory: "没有历史记录" signinHistory: "登录历史" -enableAdvancedMfm: "启用扩展MFM" -enableAnimatedMfm: "启用MFM动画" +enableAdvancedMfm: "启用扩展 MFM" +enableAnimatedMfm: "启用 MFM 动画" doing: "正在进行" category: "类别" tags: "标签" @@ -479,7 +491,7 @@ existingAccount: "现有的账户" regenerate: "重新生成" fontSize: "字体大小" mediaListWithOneImageAppearance: "仅一张图片的媒体列表高度" -limitTo: "上限为{x}" +limitTo: "上限为 {x}" noFollowRequests: "没有关注申请" openImageInNewTab: "在新标签页中打开图片" dashboard: "管理面板" @@ -499,20 +511,20 @@ showFeaturedNotesInTimeline: "在时间线上显示热门推荐" objectStorage: "对象存储" useObjectStorage: "使用对象存储" objectStorageBaseUrl: "Base URL" -objectStorageBaseUrlDesc: "这里是用于参考的URL,如果您正在使用CDN或反向代理,请指定其URL,例如S3:“https://.s3.amazonaws.com”,GCS:“https://storage.googleapis.com/”" +objectStorageBaseUrlDesc: "这里是用于参考的 URL,如果您正在使用 CDN 或反向代理,请指定其 URL,例如 S3:“https://.s3.amazonaws.com”,GCS:“https://storage.googleapis.com/”" objectStorageBucket: "存储桶" objectStorageBucketDesc: "请指定使用的对象存储服务的存储桶名称。" objectStoragePrefix: "前缀" objectStoragePrefixDesc: "文件将存储在此前缀的目录下。" objectStorageEndpoint: "端点" -objectStorageEndpointDesc: "如果你使用AWS S3请留空。否则请根据你使用的服务商的说明来进行设置,指定端点形式为“”或“:”。" +objectStorageEndpointDesc: "如果你使用 AWS S3 请留空。否则请根据你使用的服务商的说明来进行设置,指定端点形式为“”或“:”。" objectStorageRegion: "可用区" -objectStorageRegionDesc: "指定一个可用区,例如“xx-east-1”。 如果您的对象存储服务没有可用区概念,请将其留空或填写“us-east-1”。" -objectStorageUseSSL: "使用SSL" -objectStorageUseSSLDesc: "如果不使用https进行API连接,请关闭。" +objectStorageRegionDesc: "指定一个可用区,例如“xx-east-1”。 如果您的对象存储服务没有可用区概念,请将其留空或填写“us-east-1”。如果引用 AWS 的配置文件或环境变量,则留空。" +objectStorageUseSSL: "使用 SSL" +objectStorageUseSSLDesc: "如果不使用 https 进行 API 连接,请关闭。" objectStorageUseProxy: "使用代理" -objectStorageUseProxyDesc: "如果您不使用代理进行API连接,请将其关闭。" -objectStorageSetPublicRead: "上传时设置为public-read" +objectStorageUseProxyDesc: "如果您不使用代理进行 API 连接,请将其关闭。" +objectStorageSetPublicRead: "上传时设置为 public-read" s3ForcePathStyleDesc: "启用 s3ForcePathStyle 会强制将存储桶名称指定为 URL 中路径的一部分,而不是主机名。使用自托管 Minio 等时可能需要启用。" serverLogs: "服务器日志" deleteAll: "全部删除" @@ -541,8 +553,8 @@ state: "状态" sort: "排序" ascendingOrder: "升序" descendingOrder: "降序" -scratchpad: "AiScript控制台" -scratchpadDescription: "AiScript控制台为AiScript提供了实验环境。您可以编写代码以与Misskey交互,运行它并查看结果。" +scratchpad: "AiScript 控制台" +scratchpadDescription: "AiScript 控制台为 AiScript 提供了实验环境。您可以编写代码与 Misskey 交互,运行并查看结果。" output: "输出" script: "脚本" disablePagesScript: "禁用页面脚本" @@ -550,7 +562,7 @@ updateRemoteUser: "更新远程用户信息" deleteAllFiles: "删除所有文件" deleteAllFilesConfirm: "要删除所有文件吗?" removeAllFollowing: "取消所有关注" -removeAllFollowingDescription: "取消{host}的所有关注者。当服务器不再存在时执行。" +removeAllFollowingDescription: "取消 {host} 的所有关注者。当服务器不再存在时执行。" userSuspended: "该用户已被冻结。" userSilenced: "该用户已被禁言。" yourAccountSuspendedTitle: "账户已被冻结" @@ -587,7 +599,7 @@ manage: "管理" plugins: "插件" preferencesBackups: "备份设置" deck: "Deck" -undeck: "取消Deck" +undeck: "取消 Deck" useBlurEffectForModal: "对话框使用模糊效果" useFullReactionPicker: "使用全功能的回应工具栏" width: "宽度" @@ -600,7 +612,7 @@ permission: "权限" enableAll: "启用全部" disableAll: "禁用全部" tokenRequested: "允许访问账户" -pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限" +pluginTokenRequestedDescription: "此插件将能够拥有这里设置的权限" notificationType: "通知类型" edit: "编辑" emailServer: "邮件服务器" @@ -608,20 +620,20 @@ enableEmail: "启用发送邮件功能" emailConfigInfo: "用于确认电子邮件和密码重置" email: "邮箱" emailAddress: "电子邮件地址" -smtpConfig: "SMTP服务器设置" +smtpConfig: "SMTP 服务器设置" smtpHost: "主机名" smtpPort: "端口" smtpUser: "用户名" smtpPass: "密码" -emptyToDisableSmtpAuth: "用户名和密码留空可以禁用SMTP验证" +emptyToDisableSmtpAuth: "用户名和密码留空可以禁用 SMTP 验证" smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS" -smtpSecureInfo: "使用STARTTLS时关闭。" +smtpSecureInfo: "使用 STARTTLS 时关闭。" testEmail: "邮件发送测试" wordMute: "文字屏蔽" regexpError: "正则表达式错误" regexpErrorDescription: "{tab} 屏蔽文字的第 {line} 行的正则表达式有错误:" instanceMute: "被屏蔽的服务器" -userSaysSomething: "{name}说了什么,但是被屏蔽词过滤了" +userSaysSomething: "{name} 说了什么,但是被屏蔽词过滤了" makeActive: "启用" display: "显示" copy: "复制" @@ -640,19 +652,20 @@ other: "其他" regenerateLoginToken: "重新生成登录令牌" regenerateLoginTokenDescription: "重新生成用于登录的内部令牌。通常您不需要这样做。重新生成后,您将在所有设备上登出。" setMultipleBySeparatingWithSpace: "您可以使用空格分隔多个项目。" -fileIdOrUrl: "文件ID或者URL" +fileIdOrUrl: "文件 ID 或者 URL" behavior: "行为" sample: "示例" abuseReports: "举报" reportAbuse: "举报" -reportAbuseOf: "举报{name}" -fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子,请同时填写URL地址。" +reportAbuseRenote: "举报转帖" +reportAbuseOf: "举报 {name}" +fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子,请同时填写 URL 地址。" abuseReported: "内容已发送。感谢您提交信息。" reporter: "举报者" reporteeOrigin: "举报来源" reporterOrigin: "举报者来源" forwardReport: "将该举报信息转发给远程服务器" -forwardReportIsAnonymous: "勾选则在远程服务器上显示的举报者是匿名的系统账号,而不是您的账号。" +forwardReportIsAnonymous: "在远程实例上显示的报告者是匿名的系统账号,而不是您的账号。" send: "发送" abuseMarkAsResolved: "处理完毕" openInNewTab: "在新标签页中打开" @@ -660,7 +673,7 @@ openInSideView: "在侧边栏中打开" defaultNavigationBehaviour: "默认导航" editTheseSettingsMayBreakAccount: "编辑这些设置可以会损坏您的账号" instanceTicker: "帖子的服务器来源" -waitingFor: "等待{x}" +waitingFor: "等待 {x}" random: "随机" system: "系统" switchUi: "切换界面" @@ -670,9 +683,10 @@ createNew: "新建" optional: "可选" createNewClip: "新建便签" unclip: "移除便签" -confirmToUnclipAlreadyClippedNote: "本帖已包含在便签\"{name}\"里。您想要将本帖从该便签中移除吗?" +confirmToUnclipAlreadyClippedNote: "本帖已包含在便签 \"{name}\" 里。您想要将本帖从该便签中移除吗?" public: "公开" -i18nInfo: "Misskey已经被志愿者们翻译成了各种语言。如果你也有兴趣,可以通过{link}帮助翻译。" +private: "私密" +i18nInfo: "Misskey 已经被志愿者们翻译成了各种语言。如果你也有兴趣,可以通过 {link} 帮助翻译。" manageAccessTokens: "管理 Access Tokens" accountInfo: "账户信息" notesCount: "帖子数量" @@ -718,14 +732,14 @@ center: "中央" wide: "宽" narrow: "窄" reloadToApplySetting: "页面刷新后设置才会生效。是否现在刷新页面?" -needReloadToApply: "重启后应用才会生效。" +needReloadToApply: "重新载入后应用才会生效。" showTitlebar: "显示标题栏" clearCache: "清除缓存" -onlineUsersCount: "{n}人在线" -nUsers: "{n}用户" -nNotes: "{n}帖子" +onlineUsersCount: "{n} 人在线" +nUsers: "{n} 用户" +nNotes: "{n} 帖子" sendErrorReports: "发送错误报告" -sendErrorReportsDescription: "启用后,如果出现问题,可以与Misskey共享详细的错误信息,从而帮助提高软件的质量。" +sendErrorReportsDescription: "启用后,如果出现问题,可以与 Misskey 共享详细的错误信息,从而帮助提高软件的质量。错误信息包括操作系统版本、浏览器类型、行为历史记录等。" myTheme: "我的主题" backgroundColor: "背景" accentColor: "强调色" @@ -755,7 +769,7 @@ emailNotification: "邮件通知" publish: "发布" inChannelSearch: "频道内搜索" useReactionPickerForContextMenu: "单击右键打开回应工具栏" -typingUsers: "{users}正在输入" +typingUsers: "{users} 正在输入" jumpToSpecifiedDate: "跳转到特定日期" showingPastTimeline: "显示过去的时间线" clear: "清除" @@ -789,7 +803,7 @@ administration: "管理" accounts: "账户" switch: "切换" noMaintainerInformationWarning: "管理人员信息未设置。" -noBotProtectionWarning: "Bot保护未设置。" +noBotProtectionWarning: "Bot 防御未设置。" configure: "设置" postToGallery: "发送到图库" postToHashtag: "投稿到这个标签" @@ -817,9 +831,9 @@ received: "收取" searchResult: "搜索结果" hashtags: "话题标签" troubleshooting: "故障排除" -useBlurEffect: "在UI上使用模糊效果" +useBlurEffect: "在 UI 上使用模糊效果" learnMore: "更多信息" -misskeyUpdated: "Misskey更新完成!" +misskeyUpdated: "Misskey 更新完成!" whatIsNew: "显示更新信息" translate: "翻译" translatedFrom: "从 {x} 翻译" @@ -828,7 +842,7 @@ usernameInfo: "在服务器上唯一标识您的帐户的名称。您可以使 aiChanMode: "小蓝模式" devMode: "开发者模式" keepCw: "回复时维持隐藏内容" -pubSub: "Pub/Sub账户" +pubSub: "Pub/Sub 账户" lastCommunication: "最近通信" resolved: "已解决" unresolved: "未解决" @@ -853,7 +867,7 @@ ffVisibilityDescription: "您可以设置您的关注/关注者信息的公开 continueThread: "查看更多帖子" deleteAccountConfirm: "将要删除账户。是否确认?" incorrectPassword: "密码错误" -voteConfirm: "确定投给“{choice}” ?" +voteConfirm: "确定投给 “{choice}” ?" hide: "隐藏" useDrawerReactionPickerForMobile: "在移动设备上使用抽屉显示" welcomeBackWithName: "欢迎回来,{name}" @@ -868,30 +882,30 @@ numberOfColumn: "列数" searchByGoogle: "Google" instanceDefaultLightTheme: "服务器默认浅色主题" instanceDefaultDarkTheme: "服务器默认深色主题" -instanceDefaultThemeDescription: "以对象格式键入主题代码" +instanceDefaultThemeDescription: "以对象格式输入主题代码" mutePeriod: "屏蔽期限" period: "截止时间" indefinitely: "永久" -tenMinutes: "10分钟" -oneHour: "1小时" -oneDay: "1天" -oneWeek: "1周" +tenMinutes: "10 分钟" +oneHour: "1 小时" +oneDay: "1 天" +oneWeek: "1 周" oneMonth: "1 个月" reflectMayTakeTime: "可能需要一些时间才能体现出效果。" failedToFetchAccountInformation: "获取账户信息失败" -rateLimitExceeded: "已超過速率限制" -cropImage: "剪裁图像" +rateLimitExceeded: "已超过速率限制" +cropImage: "裁剪图像" cropImageAsk: "是否要裁剪图像?" cropYes: "去裁剪" cropNo: "就这样吧!" file: "文件" -recentNHours: "最近{n}小时" -recentNDays: "最近{n}天" +recentNHours: "最近 {n} 小时" +recentNDays: "最近 {n} 天" noEmailServerWarning: "电子邮件服务器未设置。" thereIsUnresolvedAbuseReportWarning: "有未解决的报告" recommended: "推荐" check: "检查" -driveCapOverrideLabel: "變更此用戶的雲端硬碟容量上限" +driveCapOverrideLabel: "更改此用户的网盘容量上限" driveCapOverrideCaption: "设定为 0 以下则会解除此限制。" requireAdminForView: "需要使用管理员账户登录才能查看。" isSystemAccount: "该账号由系统自动创建和管理。" @@ -899,7 +913,7 @@ typeToConfirm: "输入 {x} 以确认操作。" deleteAccount: "删除账户" document: "文档" numberOfPageCache: "缓存页数" -numberOfPageCacheDescription: "设置较高的值会更方便用户,但服务器负载和内存使用量会增加。" +numberOfPageCacheDescription: "设置较高的值会更方便用户,但设备的负载和内存使用量会增加。" logoutConfirm: "是否确认登出?" lastActiveDate: "最后活跃时间" statusbar: "状态栏" @@ -921,7 +935,7 @@ cannotUploadBecauseNoFreeSpace: "因为已无可用空间,无法上传。" cannotUploadBecauseExceedsFileSizeLimit: "无法上传文件,超过文件大小限制。" beta: "测试" enableAutoSensitive: "自动 NSFW 识别" -enableAutoSensitiveDescription: "如果可用,请使用机器学习在媒体上自动设置 NSFW 标志。即使关闭此功能,也可能会根据服务器自动设置。" +enableAutoSensitiveDescription: "使用机器学习在可用时自动使用 NSFW 标记来标记媒体。即使您关闭此功能,根据服务器的不同,它仍然可能会自动设置。" activeEmailValidationDescription: "开启用户的电子邮件地址验证,判断它是一次性的电子邮件地址,还是可以实际通信的地址。关闭时,则只检查字符串是否正确。" navbar: "导航栏" shuffle: "随机" @@ -938,7 +952,7 @@ windowMaximize: "最大化" windowMinimize: "最小化" windowRestore: "还原" caption: "标题" -loggedInAsBot: "以Bot账户登录" +loggedInAsBot: "以 Bot 账户登录" tools: "工具" cannotLoad: "无法加载" numberOfProfileView: "个人资料展示次数" @@ -948,8 +962,8 @@ numberOfLikes: "点赞数" show: "显示" neverShow: "不再显示" remindMeLater: "稍后提醒我" -didYouLikeMisskey: "您喜欢Misskey吗?" -pleaseDonate: "Misskey是{host}所使用的免费软件。为了今后也能够维持Misskey的开发,请在有余力的情况下进行捐助!" +didYouLikeMisskey: "您喜欢 Misskey 吗?" +pleaseDonate: "Misskey 是 {host} 所使用的免费软件。为了今后也能够维持 Misskey 的开发,请在有余力的情况下进行捐助!" roles: "角色" role: "角色" noRole: "角色不存在" @@ -963,11 +977,11 @@ youCannotCreateAnymore: "抱歉,您无法再创建更多了。" cannotPerformTemporary: "暂时不可用" cannotPerformTemporaryDescription: "因操作过于频繁,暂时不可用,请稍后再试。" invalidParamError: "参数错误" -invalidParamErrorDescription: "请求参数出现问题。通常是因为bug造成的,但也可能是输入文字数量过多之类的原因。" +invalidParamErrorDescription: "请求参数出现问题。通常是因为 bug 造成的,但也可能是输入文字数量过多之类的原因。" permissionDeniedError: "操作被拒绝" permissionDeniedErrorDescription: "本账户没有执行该操作的权限。" -preset: "預設值" -selectFromPresets: "從預設值中選擇" +preset: "预设值" +selectFromPresets: "从预设值中选择" achievements: "成就" gotInvalidResponseError: "服务器无应答" gotInvalidResponseErrorDescription: "您的网络连接可能出现了问题, 或是远程服务器暂时不可用. 请稍后重试。" @@ -998,7 +1012,7 @@ rolesAssignedToMe: "指派给自己的角色" resetPasswordConfirm: "确定重置密码?" sensitiveWords: "敏感词" sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。" -sensitiveWordsDescription2: "用空格分割关键词作为AND格式,用斜线包裹关键字来构成正则表达式。" +sensitiveWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。" notesSearchNotAvailable: "帖子检索不可用" license: "许可信息" unfavoriteConfirm: "确定要取消收藏吗?" @@ -1010,8 +1024,8 @@ retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加" enableChartsForRemoteUser: "生成远程用户的图表" enableChartsForFederatedInstances: "生成远程服务器的图表" showClipButtonInNoteFooter: "在贴文下方显示便签按钮" -largeNoteReactions: "使用大图标来显示回应" -noteIdOrUrl: "帖子ID或URL" +reactionsDisplaySize: "回应显示大小" +noteIdOrUrl: "帖子 ID 或 URL" video: "视频" videos: "视频" dataSaver: "省流量模式" @@ -1041,14 +1055,14 @@ preservedUsernames: "保留的用户名" preservedUsernamesDescription: "列出需要保留的用户名,使用换行来作为分割。被指定的用户名在建立账户时无法使用,但由管理员所创建的账户不受该限制。此外,现有的账户也不会受到影响。" createNoteFromTheFile: "从文件创建帖子" archive: "归档" -channelArchiveConfirmTitle: "要将{name}归档吗?" +channelArchiveConfirmTitle: "要将 {name} 归档吗?" channelArchiveConfirmDescription: "归档后,在频道列表与搜索结果中不会显示,也无法发布新的贴文。" thisChannelArchived: "该频道已被归档。" displayOfNote: "显示帖子" initialAccountSetting: "初始设置" youFollowing: "正在关注" -preventAiLearning: "拒绝接受生成式AI的学习" -preventAiLearningDescription: "要求文章生成AI或图像生成AI不能够以发布的帖子和图像等内容作为学习对象。这是通过在HTML响应中包含noai标志来实现的,这不能完全阻止AI学习你的发布内容,并不是所有AI都会遵守这类请求。" +preventAiLearning: "拒绝接受生成式 AI 的学习" +preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。" options: "选项" specifyUser: "用户指定" failedToPreviewUrl: "无法预览" @@ -1059,8 +1073,58 @@ rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "角色必须是公开的 cancelReactionConfirm: "要取消回应吗?" changeReactionConfirm: "要更改回应吗?" later: "一会再说" -goToMisskey: "去往Misskey" +goToMisskey: "去往 Misskey" +additionalEmojiDictionary: "表情符号追加字典" installed: "已安装" +branding: "品牌" +enableServerMachineStats: "公开服务器硬件统计信息" +enableIdenticonGeneration: "启用生成用户 Identicon" +turnOffToImprovePerformance: "关闭该选项可以提高性能。" +createInviteCode: "发行邀请码" +createWithOptions: "使用选项来创建" +createCount: "发行数" +inviteCodeCreated: "已创建邀请码" +inviteLimitExceeded: "可供发行的邀请码已达上限。" +createLimitRemaining: "可供发行的邀请码:剩余{limit}个" +inviteLimitResetCycle: "可以在{time}内发行最多{limit}个邀请码。" +expirationDate: "有效日期" +noExpirationDate: "不设置有效日期" +inviteCodeUsedAt: "邀请码被使用的日期和时间" +registeredUserUsingInviteCode: "使用了邀请码的用户" +waitingForMailAuth: "等待验证电子邮件" +inviteCodeCreator: "发行邀请码的用户" +usedAt: "使用时间" +unused: "未使用" +used: "已使用" +expired: "已过期" +doYouAgree: "你同意吗?" +beSureToReadThisAsItIsImportant: "请好好阅读,这真的很重要。" +iHaveReadXCarefullyAndAgree: "我已经仔细阅读并同意了「{x}」的内容。" +dialog: "对话框" +icon: "头像" +forYou: "您的" +currentAnnouncements: "现在的公告" +pastAnnouncements: "过去的公告" +youHaveUnreadAnnouncements: "您有未读的公告" +useSecurityKey: "请根据浏览器或设备的提示,使用安全密钥或通行密钥。" +replies: "回复" +renotes: "转发" +loadReplies: "查看回复" +loadConversation: "查看对话" +pinnedList: "已置顶的列表" +keepScreenOn: "保持设备屏幕开启" +verifiedLink: "已验证的链接" +notifyNotes: "打开发帖通知" +unnotifyNotes: "关闭发帖通知" +_announcement: + forExistingUsers: "仅限现有用户" + forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。" + needConfirmationToRead: "需要确认才能标记为已读" + needConfirmationToReadDescription: "若启用,则会在标记已读时会显示确认对话框。此外,它也会不受批量已读操作的影响。" + end: "结束公告" + tooManyActiveAnnouncementDescription: "若有大量活动公告,可能会造成用户体验可能下降。请考虑归档已完成的公告。" + readConfirmTitle: "标记为已读?" + readConfirmText: "阅读“{title}”的内容并将其标记为已读。" _initialAccountSetting: accountCreated: "账户创建完成了!" letsStartAccountSetup: "来进行帐户的初始设置吧。" @@ -1070,28 +1134,35 @@ _initialAccountSetting: theseSettingsCanEditLater: "也可以在稍后修改这里的设置。" youCanEditMoreSettingsInSettingsPageLater: "还可以在「设置」页面进行其它各种设置,稍后就来确认一下看看吧。" followUsers: "为了建立属于你自己的时间线,试着去关注你感兴趣的用户吧。" - pushNotificationDescription: "启用推送通知的话,就可以在设备上受到来自{name}的通知了。" + pushNotificationDescription: "启用推送通知的话,就可以在设备上接收来自 {name} 的通知了。" initialAccountSettingCompleted: "初始设定已经完成了!" - haveFun: "希望{name}在这里玩得开心!" - ifYouNeedLearnMore: "关于{name}(Misskey)的使用方法,详见{link}。" + haveFun: "希望 {name} 在这里玩得开心!" + ifYouNeedLearnMore: "关于 {name}(Misskey) 的使用方法,详见 {link}。" skipAreYouSure: "要跳过初始设置吗?" laterAreYouSure: "要稍后再进行初始设定吗?" _serverRules: description: "在新用户注册前显示服务器的简单规则。推荐显示服务条款的主要内容。" +_serverSettings: + iconUrl: "图标 URL" + appIconDescription: "指定当 {host} 显示为 app 时的图标。" + appIconUsageExample: "例如:作为书签添加到 PWA 或手机主屏幕的时候" + appIconStyleRecommendation: "因为有可能会被裁切为圆形或者圆角矩形,建议使用边缘带有留白背景的图标。" + appIconResolutionMustBe: "分辨率必须为 {resolution}。" + manifestJsonOverride: "覆盖 mainfest.json" _accountMigration: moveFrom: "从别的账号迁移到此账户" moveFromSub: "为另一个账户建立别名" moveFromLabel: "迁移前的账户" - moveFromDescription: "如果迁移时需要继承其他账户的关注者,请在此创造别名。此操作需要在实行迁移之前完成!请如已下输入需要迁移的账户:@person@instance.com" + moveFromDescription: "如果迁移时需要继承其他账户的关注者,你需要创建一个别名。此操作需要在迁移前完成!\n请像这样输入要迁移的账户:@username@server.example.com\n如果要删除,请将输入字段留空,并保存(不推荐)。" moveTo: "把这个账户迁移到新的账户" moveToLabel: "迁移后的账户" moveCannotBeUndone: "一旦迁移账户,就无法撤销。" - moveAccountDescription: "此操作无法取消。请先确认您已在迁移后的账户上,为此账户创造了别名。创造别名后,请如以下输入您的迁移后的账户:@person@instance.com" + moveAccountDescription: "\n迁移到新帐户。\n ・现有的关注者自动关注新帐户\n ・此帐户的所有关注者都将被删除\n ・您将无法再使用此帐户发帖。\n关注者迁移是自动的,但关注中迁移必须手动完成。请在迁移前在此帐户上导出关注列表,并在迁移后立即在目标帐户上执行导入。\n屏蔽列表也是如此,因此您必须手动迁移它。\n(此描述适用于该服务器(Misskey v13.12.0 或更高版本)。其他 ActivityPub 软件(例如 Mastodon)的行为可能有所不同。)" moveAccountHowTo: "要进行账户迁移,请现在目标账户中为此账户建立一个别名。\n建立别名后,请像这样输入目标账户:@username@server.example.com" startMigration: "迁移" - migrationConfirm: "确定要把此账户迁移到{account}吗?一旦确定后,此操作无法取消,此账户也无法以原来的状态使用。\n同时,请确认迁移后的账户,已创造别名。" + migrationConfirm: "确定要把此账户迁移到 {account} 吗?一旦确定后,此操作无法取消,此账户也无法以原来的状态使用。\n同时,请确认迁移后的账户,已创造别名。" movedAndCannotBeUndone: "该账户已被迁移。\n迁移操作无法撤销。" - postMigrationNote: "这个账户的关注会在迁移操作后的24小时后解除。该账户的「关注中」和「关注者」皆会变为0。由于不会解除关注关系,你的关注者仍然可以继续查看该账户发补给关注者的帖子。" + postMigrationNote: "这个账户的关注会在迁移操作后的 24 小时后解除。该账户的「关注中」和「关注者」皆会变为 0。由于不会解除关注关系,你的关注者仍然可以继续查看该账户发补给关注者的帖子。" movedTo: "迁移后的账户" _achievements: earnedAt: "达成时间" @@ -1099,103 +1170,103 @@ _achievements: _notes1: title: "初来乍到" description: "第一次发帖" - flavor: "祝您在Misskey玩的愉快~" + flavor: "祝您在 Misskey 玩的愉快~" _notes10: title: "一些帖子" - description: "发布了10篇帖子" + description: "发布了 10 篇帖子" _notes100: title: "很多帖子" - description: "发布了100篇帖子" + description: "发布了 100 篇帖子" _notes500: title: "满是帖子" - description: "发布了500篇帖子" + description: "发布了 500 篇帖子" _notes1000: title: "积帖成山" - description: "发布了1,000篇帖子" + description: "发布了 1,000 篇帖子" _notes5000: title: "帖如泉涌" - description: "发布了5,000篇帖子" + description: "发布了 5,000 篇帖子" _notes10000: title: "超级帖" - description: "发布了10,000篇帖子" + description: "发布了 10,000 篇帖子" _notes20000: title: "还想要更多帖子" - description: "发布了20,000篇帖子" + description: "发布了 20,000 篇帖子" _notes30000: title: "帖子帖子帖子" - description: "发布了30,000篇帖子" + description: "发布了 30,000 篇帖子" _notes40000: title: "帖子工厂" - description: "发布了40,000篇帖子" + description: "发布了 40,000 篇帖子" _notes50000: title: "帖子星球" - description: "发布了50,000篇帖子" + description: "发布了 50,000 篇帖子" _notes60000: title: "帖子类星体" - description: "发布了60,000篇帖子" + description: "发布了 60,000 篇帖子" _notes70000: title: "帖子黑洞" - description: "发布了70,000篇帖子" + description: "发布了 70,000 篇帖子" _notes80000: title: "帖子星系" - description: "发布了80,000篇帖子" + description: "发布了 80,000 篇帖子" _notes90000: title: "帖子起源" - description: "发布了90,000篇帖子" + description: "发布了 90,000 篇帖子" _notes100000: title: "ALL YOUR NOTE ARE BELONG TO US" - description: "发布了100,000篇帖子" + description: "发布了 100,000 篇帖子" flavor: "真的有那么多可以写的东西吗?" _login3: title: "初学者 I" - description: "连续登录3天" - flavor: "今天开始我就是Misskist!" + description: "累计登录 3 天" + flavor: "今天开始我就是 Misskist!" _login7: title: "初学者 II" - description: "连续登录7天" + description: "累计登录 7 天" flavor: "您开始习惯了吗?" _login15: title: "初学者 III" - description: "连续登录15天" + description: "累计登录 15 天" _login30: title: "Misskist Ⅰ" - description: "连续登录30天" + description: "累计登录 30 天" _login60: title: "Misskist Ⅱ" - description: "连续登录60天" + description: "累计登录 60 天" _login100: title: "Misskist Ⅲ" - description: "总登入100天" - flavor: "那个用户,是Misskist喔" + description: "累计登入 100 天" + flavor: "那个用户,是 Misskist 喔" _login200: title: "定期联系Ⅰ" - description: "总登录天数200天" + description: "累计登录 200 天" _login300: title: "定期联系Ⅱ" - description: "总登录天数300天" + description: "累计登录 300 天" _login400: title: "定期联系Ⅲ" - description: "总登录天数400天" + description: "累计登录 400 天" _login500: title: "老熟人Ⅰ" - description: "总登录天数500天" + description: "累计登录 500 天" flavor: "诸君,我喜欢贴文" _login600: title: "老熟人Ⅱ" - description: "总登录天数600天" + description: "累计登录 600 天" _login700: title: "老熟人Ⅲ" - description: "总登录天数700天" + description: "累计登录 700 天" _login800: - title: "帖子大师Ⅰ" - description: "总登录天数800天" + title: "帖子大师 Ⅰ" + description: "累计登录 800 天" _login900: - title: "帖子大师Ⅱ" - description: "总登录天数900天" + title: "帖子大师 Ⅱ" + description: "累计登录 900 天" _login1000: - title: "帖子大师Ⅲ" - description: "总登录天数1000天" - flavor: "感谢您使用Misskey!" + title: "帖子大师 Ⅲ" + description: "累计登录 1000 天" + flavor: "感谢您使用 Misskey!" _noteClipped1: title: "忍不住要收藏到便签" description: "第一次将贴文贴进便签" @@ -1217,56 +1288,56 @@ _achievements: description: "第一次关注别人" _following10: title: "关注,跟随" - description: "关注超过10人" + description: "关注超过 10 人" _following50: title: "我的朋友很多" - description: "关注超过50人" + description: "关注超过 50 人" _following100: - title: "我的朋友很多" - description: "关注超过100人" + title: "胜友如云" + description: "关注超过 100 人" _following300: title: "朋友成群" - description: "关注数超过300" + description: "关注数超过 300" _followers1: title: "最初的关注者" description: "第一次被关注" _followers10: title: "关注我吧!" - description: "拥有超过10名关注者" + description: "拥有超过 10 名关注者" _followers50: title: "三五成群" - description: "拥有超过50名关注者" + description: "拥有超过 50 名关注者" _followers100: title: "胜友如云" - description: "拥有超过100名关注者" + description: "拥有超过 100 名关注者" _followers300: title: "排列成行" - description: "拥有超过300名关注者" + description: "拥有超过 300 名关注者" _followers500: title: "信号塔" - description: "拥有超过500名关注者" + description: "拥有超过 500 名关注者" _followers1000: title: "大影响家" - description: "拥有超过1000名关注者" + description: "拥有超过 1000 名关注者" _collectAchievements30: title: "成就收藏家" - description: "获得超过30个成就" + description: "获得超过 30 个成就" _viewAchievements3min: title: "成就爱好者" description: "盯着成就看三分钟" _iLoveMisskey: title: "I Love Misskey" - description: "发布\"I ❤ #Misskey\"帖子" + description: "发布 \"I ❤ #Misskey\" 帖子" flavor: "感谢您使用 Misskey ! by 开发团队" _foundTreasure: title: "寻宝" description: "发现了隐藏的宝藏" _client30min: title: "休息一下!" - description: "启动客户端超过30分钟" + description: "启动客户端超过 30 分钟" _client60min: - title: "Misskey重度依赖" - description: "启动客户端超过60分钟" + title: "Misskey 重度依赖" + description: "启动客户端超过 60 分钟" _noteDeletedWithin1min: title: "欲言又止" description: "发帖后一分钟内就将其删除" @@ -1276,20 +1347,20 @@ _achievements: flavor: "差不多该去睡了喔。" _postedAt0min0sec: title: "报时" - description: "在0点发布一篇帖子" + description: "在 0 点发布一篇帖子" flavor: "嘣 嘣 嘣 Biu——!" _selfQuote: title: "自我引用" description: "引用了自己的帖子" _htl20npm: title: "流动的时间线" - description: "在首页时间线的流速超过20npm" + description: "在首页时间线的流速超过 20npm" _viewInstanceChart: title: "分析师" description: "查看了服务器信息中的图表" _outputHelloWorldOnScratchpad: title: "Hello, world!" - description: "在AiScript控制台中输出 hello world" + description: "在 AiScript 控制台中输出 hello world" _open3windows: title: "多窗口" description: "打开了三个或更多的窗口" @@ -1298,25 +1369,25 @@ _achievements: description: "试图对网盘中的文件夹进行循环嵌套" _reactWithoutRead: title: "有好好读过吗?" - description: "在含有100字以上的帖子被发出三秒内做出回应" + description: "在含有 100 字以上的帖子被发出三秒内做出回应" _clickedClickHere: title: "点这里" description: "点了这里" _justPlainLucky: title: "超高校级的幸运" - description: "每10秒有0.01的概率自动获得" + description: "每 10 秒有 0.01 的概率自动获得" _setNameToSyuilo: title: "像神一样呐" - description: "将名称设定为syuilo" + description: "将名称设定为 syuilo" _passedSinceAccountCreated1: title: "一周年" - description: "账户创建时间超过1年" + description: "账户创建时间超过 1 年" _passedSinceAccountCreated2: title: "二周年" - description: "账户创建时间超过2年" + description: "账户创建时间超过 2 年" _passedSinceAccountCreated3: title: "三周年" - description: "账户创建时间超过3年" + description: "账户创建时间超过 3 年" _loggedInOnBirthday: title: "生日快乐" description: "在生日当天登录" @@ -1330,8 +1401,11 @@ _achievements: flavor: "是不是软件有问题?" _brainDiver: title: "Brain Diver" - description: "发布了包含Brain Diver链接的帖子" + description: "发布了包含 Brain Diver 链接的帖子" flavor: "Misskey-Misskey La-Tu-Ma" + _smashTestNotificationButton: + title: "过度测试" + description: "短时间内连续测试通知" _role: new: "创建角色" edit: "编辑角色" @@ -1352,7 +1426,7 @@ _role: baseRole: "基本角色" useBaseValue: "使用基本角色的值" chooseRoleToAssign: "选择要分配的角色" - iconUrl: "图标URL" + iconUrl: "图标 URL" asBadge: "作为徽章显示" descriptionOfAsBadge: "开启后,用户名旁边将会出现角色图标。" isExplorable: "公开角色时间线" @@ -1371,9 +1445,12 @@ _role: ltlAvailable: "查看本地时间线" canPublicNote: "允许公开发帖" canInvite: "发放服务器邀请码" + inviteLimit: "可发行邀请码的数量" + inviteLimitCycle: "邀请码的发行间隔" + inviteExpirationTime: "邀请码的有效日期" canManageCustomEmojis: "管理自定义表情符号" driveCapacity: "网盘容量" - alwaysMarkNsfw: "总是将文件标记为NSFW" + alwaysMarkNsfw: "总是将文件标记为 NSFW" pinMax: "帖子置顶数量限制" antennaMax: "可创建的最大天线数量" wordMuteMax: "屏蔽词的字数限制" @@ -1401,7 +1478,7 @@ _role: or: "符合以下任一条件" not: "不符合以下任何条件" _sensitiveMediaDetection: - description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。" + description: "使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。" sensitivity: "检测敏感度" sensitivityDescription: "敏感度较低,则误检(假阳性)会减少;敏感度较高,则漏检(假阴性)会减少。" setSensitiveFlagAutomatically: "自动设置 NSFW 标签" @@ -1433,9 +1510,10 @@ _ad: back: "返回" reduceFrequencyOfThisAd: "减少此广告的频率" hide: "不显示" + timezoneinfo: "星期几是由服务器的时区所指定的。" _forgotPassword: - enterEmail: "请输入您验证账号时用的电子邮箱地址,密码重置链接将发送至该邮箱上。" - ifNoEmail: "如果您没有使用电子邮件地址进行验证,请联系管理员。" + enterEmail: "请输入您设置的电子邮箱地址,密码重置链接将发送至该邮箱上。" + ifNoEmail: "如果您没有设置电子邮件地址,请联系管理员。" contactAdmin: "该服务器不支持发送电子邮件。如果您想重设密码,请联系管理员。" _gallery: my: "我的图库" @@ -1459,8 +1537,8 @@ _preferencesBackups: save: "覆盖存档" inputName: "请输入备份的名称" cannotSave: "无法保存" - nameAlreadyExists: "备份名称\"{name}\"已经存在,请指定其他名称。" - applyConfirm: "您是否要将备份\"{name}\"应用到当前设备上?当前设备现有配置将被丢弃。" + nameAlreadyExists: "备份名称 \"{name}\" 已经存在,请指定其他名称。" + applyConfirm: "您是否要将备份 \"{name}\" 应用到当前设备上?当前设备现有配置将被丢弃。" saveConfirm: "您确定要覆盖保存 {name} 吗?" deleteConfirm: "您确定要删除 {name} 吗?" renameConfirm: "您确定要把“{old}”改为“{new}”吗?" @@ -1476,18 +1554,18 @@ _registry: domain: "域" createKey: "创建键" _aboutMisskey: - about: "Misskey是由syuilo于2014年开发的开源软件。" + about: "Misskey 是由 syuilo 于 2014 年开发的开源软件。" contributors: "主要贡献者" allContributors: "全体贡献者" source: "源代码" - translation: "翻译Misskey" - donate: "赞助Misskey" - morePatrons: "还有很多其他的人也在支持我们,非常感谢🥰" + translation: "翻译 Misskey" + donate: "赞助 Misskey" + morePatrons: "还有很多其它的人也在支持我们,非常感谢🥰" patrons: "支持者" -_nsfw: - respect: "隐藏敏感内容" - ignore: "不隐藏敏感内容" - force: "总是隐藏内容" +_displayOfSensitiveMedia: + respect: "隐藏敏感媒体" + ignore: "显示敏感媒体" + force: "隐藏所有内容" _instanceTicker: none: "不显示" remote: "仅远程用户" @@ -1504,8 +1582,8 @@ _channel: featured: "热点" owned: "管理中" following: "正在关注" - usersCount: "有{n}人参与" - notesCount: "有{n}个帖子" + usersCount: "有 {n} 人参与" + notesCount: "有 {n} 个帖子" nameAndDescription: "名称与描述" nameOnly: "仅名称" _menuDisplay: @@ -1515,16 +1593,16 @@ _menuDisplay: hide: "隐藏" _wordMute: muteWords: "禁用词" - muteWordsDescription: "使用空格分隔表示AND逻辑,使用换行符分隔表示OR逻辑。" - muteWordsDescription2: "将关键字用斜线括起来表示正则表达式。" + muteWordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。" + muteWordsDescription2: "正则表达式用斜线包裹" softDescription: "隐藏时间线中指定条件的帖子。" hardDescription: "防止将具有指定条件的帖子添加到时间线。 即使您更改条件,未添加的帖文也会被排除在外。" soft: "软屏蔽" hard: "硬屏蔽" mutedNotes: "被屏蔽的帖子" _instanceMute: - instanceMuteDescription: "屏蔽配置服务器中的所有帖子和转帖,包括这些服务器上的用户回复。" - instanceMuteDescription2: "设置时用换行符来分隔" + instanceMuteDescription: "屏蔽服务器中的所有帖子和转帖,包括这些服务器上的用户回复。" + instanceMuteDescription2: "一行一个" title: "隐藏服务器已设置的帖子。" heading: "屏蔽服务器" _theme: @@ -1556,7 +1634,7 @@ _theme: lighten: "浅色" inputConstantName: "请输入常量名称" importInfo: "您可以在此处粘贴主题代码,将其导入到编辑器中" - deleteConstantConfirm: "确定要删除常量{const}吗?" + deleteConstantConfirm: "确定要删除常量 {const} 吗?" keys: accent: "强调色" bg: "背景" @@ -1588,8 +1666,8 @@ _theme: cwBg: "隐藏内容按钮背景" cwFg: "隐藏内容按钮文本" cwHoverBg: "隐藏内容按钮背景(悬停)" - toastBg: "Toast通知背景" - toastFg: "Toast通知文本" + toastBg: "Toast 通知背景" + toastFg: "Toast 通知文本" buttonBg: "按钮背景" buttonHoverBg: "按钮背景(悬停)" inputBorder: "输入框边框" @@ -1612,13 +1690,13 @@ _sfx: _ago: future: "未来" justNow: "最近" - secondsAgo: "{n}秒前" - minutesAgo: "{n}分前" - hoursAgo: "{n}小时前" - daysAgo: "{n}日前" - weeksAgo: "{n}周前" - monthsAgo: "{n}月前" - yearsAgo: "{n}年前" + secondsAgo: "{n} 秒前" + minutesAgo: "{n} 分前" + hoursAgo: "{n} 小时前" + daysAgo: "{n} 日前" + weeksAgo: "{n} 周前" + monthsAgo: "{n} 月前" + yearsAgo: "{n} 年前" invalid: "没有" _time: second: "秒" @@ -1626,7 +1704,7 @@ _time: hour: "小时" day: "日" _timelineTutorial: - title: "Misskey的使用方法" + title: "Misskey 的使用方法" step1_1: "这个画面是「时间线」。{name}的投稿会按照帖子的发布时间顺序来显示。" step1_2: "时间线有许多种类,比如在「首页时间线」中展现的是你关注的人的贴文;而在「本地时间线」中展现的是{name}里全部用户的贴文。" step2_1: "那么接下来,试着写一些什么东西来发布吧!你可以通过点击屏幕上的铅笔图标来打开投稿页面。" @@ -1638,21 +1716,20 @@ _timelineTutorial: _2fa: alreadyRegistered: "此设备已被注册" registerTOTP: "开始设置认证应用" - passwordToTOTP: "请输入您的密码" - step1: "首先,在您的设备上安装验证应用,例如{a}或{b}。" + step1: "首先,在您的设备上安装验证应用,例如 {a} 或 {b}。" step2: "然后,扫描屏幕上显示的二维码。" - step2Click: "通过点击QR码,您可以使用设备上安装的身份验证器应用程序或密钥环进行注册" - step2Url: "在桌面应用程序中输入以下URL:" + step2Click: "通过点击二维码,您可以使用设备上安装的身份验证器应用程序或密钥环进行注册" + step2Uri: "如果使用桌面应用程序的话,请输入下面的 URI" step3Title: "输入验证码" step3: "输入您的应用提供的动态口令以完成设置。" + setupCompleted: "设置完成" step4: "从现在开始,任何登录操作都将要求您提供动态口令。" securityKeyNotSupported: "您的浏览器不支持安全密钥。" - registerTOTPBeforeKey: "要注册安全密钥或Passkey,请先设置验证器应用程序。" + registerTOTPBeforeKey: "要注册安全密钥或 Passkey,请先设置验证器应用程序。" securityKeyInfo: "注册兼容 WebAuthn 的密钥,例如支持 FIDO2 的硬件安全密钥、设备上的生物识别功能、PIN 码以及 Passkey 等。" - chromePasskeyNotSupported: "目前不支持 Chrome 的Passkey。" - registerSecurityKey: "注册安全密钥或Passkey" + registerSecurityKey: "注册安全密钥或 Passkey" securityKeyName: "输入密钥名称" - tapSecurityKey: "请按照浏览器说明操作来注册安全密钥或Passkey。" + tapSecurityKey: "请按照浏览器说明操作来注册安全密钥或 Passkey。" removeKey: "删除安全密钥" removeKeyConfirm: "您确定要删除 {name} 吗?" whyTOTPOnlyRenew: "如果注册了安全密钥,则无法取消验证器应用程序上的设置。" @@ -1660,6 +1737,11 @@ _2fa: renewTOTPConfirm: "当前验证器应用程序的验证码将不再有效" renewTOTPOk: "重新配置" renewTOTPCancel: "不用,谢谢" + checkBackupCodesBeforeCloseThisWizard: "在关闭此窗口前,请确认下面的备用代码" + backupCodes: "备用代码" + backupCodesDescription: "如果无法使用认证应用,可以使用以下的备用代码来访问账户。请务必将这些代码保存在安全的地方。每个代码仅可使用一次。" + backupCodeUsedWarning: "已使用备用代码。如果无法使用认证应用,请尽快重新设定。" + backupCodesExhaustedWarning: "已使用完所有的备用代码。如果无法使用认证应用,将无法再访问您的账户。请再次设定认证应用。" _permissions: "read:account": "查看账户信息" "write:account": "更改帐户信息" @@ -1693,11 +1775,15 @@ _permissions: "write:gallery": "操作图库" "read:gallery-likes": "读取喜欢的图片" "write:gallery-likes": "操作喜欢的图片" + "read:flash": "查看 Play" + "write:flash": "编辑 Play" + "read:flash-likes": "查看 Play 的点赞" + "write:flash-likes": "编辑 Play 的点赞列表" _auth: shareAccessTitle: "应用程序授权许可" - shareAccess: "您要授权允许“{name}”访问您的帐户吗?" + shareAccess: "您要授权允许 “{name}” 访问您的帐户吗?" shareAccessAsk: "您确定要授权此应用程序访问您的帐户吗?" - permission: "{name}需要以下权限" + permission: "{name} 需要以下权限" permissionAsk: "这个应用程序需要以下权限" pleaseGoBack: "请返回到应用程序" callback: "回到应用程序" @@ -1725,12 +1811,12 @@ _widgets: calendar: "日历" trends: "趋势" clock: "时钟" - rss: "RSS阅读器" + rss: "RSS 阅读器" rssTicker: "RSS Ticker" activity: "活动" photos: "照片" digitalClock: "数字时钟" - unixClock: "UNIX时钟" + unixClock: "UNIX 时钟" federation: "联合" instanceCloud: "服务器云" postForm: "投稿窗口" @@ -1739,7 +1825,7 @@ _widgets: onlineUsers: "在线用户" jobQueue: "作业队列" serverMetric: "服务器指标" - aiscript: "AiScript控制台" + aiscript: "AiScript 控制台" aiscriptApp: "AiScript App" aichan: "小蓝" userList: "用户列表" @@ -1749,11 +1835,11 @@ _widgets: _cw: hide: "隐藏" show: "查看更多" - chars: "{count}个字符" + chars: "{count} 个字符" files: "{count} 个文件" _poll: noOnlyOneChoice: "需要至少两个选项" - choiceN: "选择{n}" + choiceN: "选择 {n}" noMore: "无法再添加更多了" canMultipleVote: "允许多个投票" expiration: "截止时间" @@ -1763,16 +1849,16 @@ _poll: deadlineDate: "截止日期" deadlineTime: "小时" duration: "时长" - votesCount: "{n}票" - totalVotes: "总票数{n}" + votesCount: "{n} 票" + totalVotes: "总票数 {n}" vote: "投票" showResult: "显示结果" voted: "已投票" closed: "已截止" - remainingDays: "{d}天{h}小时后截止" - remainingHours: "{h}小时{m}分后截止" - remainingMinutes: "{m}分{s}秒后截止" - remainingSeconds: "{s}秒后截止" + remainingDays: "{d} 天 {h} 小时后截止" + remainingHours: "{h} 小时 {m} 分后截止" + remainingMinutes: "{m} 分 {s} 秒后截止" + remainingSeconds: "{s} 秒后截止" _visibility: public: "公开" publicDescription: "您的帖子将出现在全局时间线上" @@ -1807,6 +1893,7 @@ _profile: metadataContent: "内容" changeAvatar: "修改头像" changeBanner: "修改横幅" + verifiedLinkDescription: "如果将内容设置为 URL,当链接所指向的网页内包含自己的个人资料链接时,可以显示一个已验证图标。" _exportOrImport: allNotes: "所有帖子" favoritedNotes: "收藏的帖子" @@ -1848,16 +1935,16 @@ _timelines: social: "社交" global: "全局" _play: - new: "创建Play" - edit: "编辑Play" - created: "创建了一个Play" - updated: "更新了Play" - deleted: "删除了Play" - pageSetting: "Play设置" - editThisPage: "编辑此Play" + new: "创建 Play" + edit: "编辑 Play" + created: "创建了一个 Play" + updated: "更新了 Play" + deleted: "删除了 Play" + pageSetting: "Play 设置" + editThisPage: "编辑此 Play" viewSource: "查看源代码" - my: "我的Play" - liked: "点赞的Play" + my: "我的 Play" + liked: "点赞的 Play" featured: "热门" title: "标题" script: "脚本" @@ -1870,8 +1957,8 @@ _pages: updated: "页面已更新" deleted: "该页面已被删除" pageSetting: "页面设置" - nameAlreadyExists: "该页面URL已存在" - invalidNameTitle: "无效的页面URL" + nameAlreadyExists: "该页面 URL 已存在" + invalidNameTitle: "无效的页面 URL" invalidNameText: "请确认该项不为空" editThisPage: "编辑此页面" viewSource: "查看源代码" @@ -1886,7 +1973,7 @@ _pages: content: "页面内容" variables: "变量" title: "标题" - url: "页面URL" + url: "页面 URL" summary: "页面摘要" alignCenter: "居中" hideTitleWhenPinned: "置顶时隐藏标题" @@ -1908,7 +1995,7 @@ _pages: button: "按钮" note: "嵌入的帖子" _note: - id: "帖子ID" + id: "帖子 ID" idDescription: "您也可以通过粘贴帖子的URL来进行设置。" detailed: "显示详细信息" _relayStatus: @@ -1925,9 +2012,14 @@ _notification: youReceivedFollowRequest: "您有新的关注请求" yourFollowRequestAccepted: "您的关注请求已通过" pollEnded: "问卷调查结果已生成。" + newNote: "新的帖子" unreadAntennaNote: "天线 {name}" emptyPushNotificationMessage: "推送通知已更新" achievementEarned: "获得成就" + testNotification: "测试通知" + checkNotificationBehavior: "检查通知显示" + sendTestNotification: "发送测试通知" + notificationWillBeDisplayedLikeThis: "通知将会这样表示" _types: all: "全部" follow: "关注中" @@ -1962,6 +2054,9 @@ _deck: introduction: "将各列进行组合以创建您自己的界面!" introduction2: "您可以随时通过屏幕右侧的 + 来添加列" widgetsIntroduction: "从列菜单中,选择“小工具编辑”来添加小工具" + useSimpleUiForNonRootPages: "用简易UI表示非根页面" + usedAsMinWidthWhenFlexible: "「自适应宽度」被启用的时候,这就是最小的宽度" + flexible: "自适应宽度" _columns: main: "主列" widgets: "小工具" @@ -1986,7 +2081,7 @@ _webhookSettings: createWebhook: "创建 Webhook" name: "名称" secret: "密钥" - events: "何时运行Webhook" + events: "何时运行 Webhook" active: "已启用" _events: follow: "关注时" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 04c14b2c65..a61e3b242b 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -1,9 +1,9 @@ --- _lang_: "繁體中文" -headlineMisskey: "貼文連繫網路" -introMisskey: "歡迎! Misskey是一個開放原始碼且去中心化的社群網路。\n透過「貼文」分享周邊新鮮事,並告訴其他人您的想法!📡\n透過「反應」功能,對大家的貼文表達情感!👍\n一起來探索這個新的世界吧!🚀" -poweredByMisskeyDescription: "{name}是使用開放原始碼平台Misskey的服務之一(稱為 Misskey 伺服器)。\n" -monthAndDay: "{month}月 {day}日" +headlineMisskey: "貼文連繫網絡" +introMisskey: "歡迎!Misskey 是一個開放原始碼且去中心化的社群網路服務。\n發布「貼文」向身邊的人分享您的想法!📡\n利用「反應」表達您對貼文的感覺!👍\n讓我們一起探索新的世界吧!🚀" +poweredByMisskeyDescription: "{name}是開放原始碼平臺 Misskey 的伺服器之一。" +monthAndDay: "{month} 月 {day} 日" search: "搜尋" notifications: "通知" username: "使用者名稱" @@ -15,8 +15,8 @@ gotIt: "知道了" cancel: "取消" noThankYou: "現在不要" enterUsername: "輸入使用者名稱" -renotedBy: "{user} 轉發了" -noNotes: "無貼文。" +renotedBy: "{user} 轉發" +noNotes: "無貼文" noNotifications: "沒有通知" instance: "伺服器" settings: "設定" @@ -26,7 +26,7 @@ otherSettings: "其他設定" openInWindow: "在新視窗開啟" profile: "個人檔案" timeline: "時間軸" -noAccountDescription: "此用戶還沒有自我介紹" +noAccountDescription: "此使用者尚未自我介紹" login: "登入" loggingIn: "登入中" logout: "登出" @@ -45,15 +45,20 @@ pin: "置頂" unpin: "取消置頂" copyContent: "複製內容" copyLink: "複製連結" +copyLinkRenote: "複製轉貼連結" delete: "刪除" deleteAndEdit: "刪除並編輯" deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有反應、轉發和回覆也將會消失。" addToList: "加入至清單" +addToAntenna: "新增至天線" sendMessage: "發送訊息" copyRSS: "複製RSS" copyUsername: "複製使用者名稱" -copyUserId: "複製使用者ID" -copyNoteId: "複製貼文ID" +copyUserId: "複製使用者 ID" +copyNoteId: "複製貼文 ID" +copyFileId: "複製檔案ID" +copyFolderId: "複製資料夾ID" +copyProfileUrl: "複製個人資料網址" searchUser: "搜尋使用者" reply: "回覆" loadMore: "載入更多" @@ -72,8 +77,8 @@ files: "檔案" download: "下載" driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。\n" unfollowConfirm: "確定要取消追隨{name}嗎?" -exportRequested: "已請求匯出。這可能會花一點時間。結束後檔案將會被放到雲端裡。" -importRequested: "已請求匯入。這可能會花一點時間" +exportRequested: "已請求匯出。這可能會花一點時間。匯出的檔案將會被放到雲端裡。" +importRequested: "已請求匯入。這可能會花一點時間。" lists: "清單" noLists: "你沒有任何清單" note: "貼文" @@ -87,9 +92,9 @@ error: "錯誤" somethingHappened: "發生錯誤" retry: "重試" pageLoadError: "載入頁面失敗" -pageLoadErrorDescription: "這通常是因為網路錯誤或是瀏覽器快取殘留的原因。請先清除瀏覽器快取,稍後再重試" +pageLoadErrorDescription: "這通常是網路錯誤或瀏覽器快取殘留而引起的。請先清除瀏覽器快取,稍後再重試。" serverIsDead: "伺服器沒有回應。請稍等片刻再試。" -youShouldUpgradeClient: "請重新載入以使用新版本的客戶端顯示此頁面" +youShouldUpgradeClient: "請重新載入以使用新版客戶端顯示此頁面。" enterListName: "輸入清單名稱" privacy: "隱私" makeFollowManuallyApprove: "手動審核追隨請求" @@ -111,13 +116,13 @@ inChannelQuote: "在頻道內引用" pinnedNote: "已置頂的貼文" pinned: "置頂" you: "您" -clickToShow: "按一下以顯示" +clickToShow: "點擊查看" sensitive: "敏感內容" add: "新增" reaction: "反應" reactions: "反應" reactionSetting: "在選擇器中顯示反應" -reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。" +reactionSettingDescription2: "拖動以交換,點擊以刪除,按下「+」以新增。" rememberNoteVisibility: "記住貼文可見性" attachCancel: "移除附件" markAsSensitive: "標記為敏感內容" @@ -131,13 +136,15 @@ block: "封鎖" unblock: "解除封鎖" suspend: "凍結" unsuspend: "解除凍結" -blockConfirm: "確定要封鎖此用戶?" -unblockConfirm: "確定解除封鎖此用戶?" +blockConfirm: "確定要封鎖此使用者嗎?" +unblockConfirm: "確定要解除封鎖此使用者嗎?" suspendConfirm: "確定凍結此帳戶?" unsuspendConfirm: "確定解凍此帳戶?" selectList: "選擇清單" +editList: "編輯清單" selectChannel: "選擇頻道" selectAntenna: "選擇天線" +editAntenna: "編輯天線" selectWidget: "選擇小工具" editWidgets: "編輯小工具" editWidgetsExit: "完成" @@ -146,18 +153,21 @@ emoji: "表情符號" emojis: "表情符號" emojiName: "表情符號名稱" emojiUrl: "表情符號URL" -addEmoji: "加入表情符號" +addEmoji: "新增表情符號" settingGuide: "推薦設定" cacheRemoteFiles: "快取遠端檔案" -cacheRemoteFilesDescription: "禁用此設定會停止遠端檔案的緩存,從而節省儲存空間,但資料會因直接連線從而產生額外連接數據。" +cacheRemoteFilesDescription: "禁用此設定會停止建立遠端檔案快取,從而節省伺服器儲存空間,但會因從遠端讀取資料而增加網路數據用量。" +youCanCleanRemoteFilesCache: "按檔案管理的🗑️按鈕,將快取全部刪除。" +cacheRemoteSensitiveFiles: "快取遠端的敏感檔案" +cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取遠端的敏感檔案,而是直接連結。" flagAsBot: "此使用者是機器人" -flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整Misskey內部系統將本帳戶識別為機器人" -flagAsCat: "喵~~~~~~~~~~~~~~!!!!!!!!!!!!" +flagAsBotDescription: "標記本帳戶由程式控制,防止其他程式與本帳戶產生無限互動的行為。" +flagAsCat: "此帳戶是一隻貓,喵~~~!!!" flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示" flagShowTimelineReplies: "在時間軸上顯示貼文的回覆" -flagShowTimelineRepliesDescription: "啟用時,時間線除了顯示用戶的貼文以外,還會顯示用戶對其他貼文的回覆。" +flagShowTimelineRepliesDescription: "啟用時,時間軸除了顯示使用者的貼文以外,還會顯示使用者對其他貼文的回覆。" autoAcceptFollowed: "自動允許來自追隨中使用者的追隨請求" -addAccount: "添加帳戶" +addAccount: "新增帳戶" reloadAccountsList: "更新帳戶清單的資訊" loginFailed: "登入失敗" showOnRemote: "轉到所在實例顯示" @@ -169,12 +179,12 @@ searchWith: "搜尋: {q}" youHaveNoLists: "你沒有任何清單" followConfirm: "你真的要追隨{name}嗎?" proxyAccount: "代理帳戶" -proxyAccountDescription: "代理帳戶是在某些情況下充當其他伺服器用戶的帳戶。例如,當使用者將一個來自其他伺服器的帳戶放在列表中時,由於沒有其他使用者追隨該帳戶,該指令不會傳送到該伺服器上,因此會由代理帳戶追隨。" +proxyAccountDescription: "代理帳戶是在特定條件下充當遠端追隨者的帳戶。例如,當使用者新增遠端使用者至其列表時,若沒有本地使用者追隨該遠端使用者,則其活動將不會傳送至伺服器,此時便會由代理帳戶代為追隨以解決問題。" host: "主機" selectUser: "選取使用者" recipient: "收件人" annotation: "註解" -federation: "站台聯邦" +federation: "聯邦宇宙" instances: "伺服器" registeredAt: "初次觀測" latestRequestReceivedAt: "上次收到的請求" @@ -189,10 +199,10 @@ operations: "操作" software: "軟體" version: "版本" metadata: "元資料" -withNFiles: "{n}個檔案" +withNFiles: "{n} 個檔案" monitor: "監視器" jobQueue: "佇列" -cpuAndMemory: "CPU及記憶體用量" +cpuAndMemory: "CPU 及記憶體" network: "網路" disk: "硬碟" instanceInfo: "伺服器資訊" @@ -205,8 +215,8 @@ clearCachedFilesConfirm: "確定要清除所有遠端暫存資料嗎?" blockedInstances: "已封鎖的伺服器" blockedInstancesDescription: "請逐行輸入需要封鎖的伺服器。已封鎖的伺服器將無法與本伺服器進行通訊。" muteAndBlock: "靜音和封鎖" -mutedUsers: "已靜音用戶" -blockedUsers: "已封鎖用戶" +mutedUsers: "被靜音的使用者" +blockedUsers: "被封鎖的使用者" noUsers: "沒有任何使用者" editProfile: "編輯個人檔案" noteDeleteConfirm: "確定刪除此貼文嗎?" @@ -228,7 +238,7 @@ publishing: "直播中" notResponding: "沒有回應" instanceFollowing: "追隨的伺服器" instanceFollowers: "伺服器的追隨者" -instanceUsers: "用戶" +instanceUsers: "伺服器使用者" changePassword: "修改密碼" security: "安全性" retypedNotMatch: "兩次輸入不一致。" @@ -238,7 +248,7 @@ newPasswordRetype: "確認密碼" attachFile: "上傳附件" more: "更多!" featured: "精選" -usernameOrUserId: "使用者名稱或使用者ID" +usernameOrUserId: "使用者名稱或使用者 ID" noSuchUser: "使用者不存在" lookup: "查詢" announcements: "公告" @@ -252,18 +262,18 @@ saved: "已儲存" messaging: "聊天" upload: "上傳" keepOriginalUploading: "保留原圖" -keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時,瀏覽器會在上傳時生成一張用於web發布的圖片。" +keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時,瀏覽器會在上傳時生成適用於網路傳送的版本。" fromDrive: "從雲端空間" -fromUrl: "從URL" +fromUrl: "從 URL" uploadFromUrl: "從網址上傳" -uploadFromUrlDescription: "您要上傳的文件的URL" +uploadFromUrlDescription: "您要上傳的檔案網址" uploadFromUrlRequested: "已請求上傳" uploadFromUrlMayTakeTime: "還需要一些時間才能完成上傳。" explore: "探索" messageRead: "已讀" noMoreHistory: "沒有更多歷史紀錄" startMessaging: "開始聊天" -nUsersRead: "{n}人已讀" +nUsersRead: "{n} 人已讀" agreeTo: "我同意{0}" agree: "同意" agreeBelow: "同意以下內容" @@ -271,22 +281,22 @@ basicNotesBeforeCreateAccount: "基本注意事項" termsOfService: "服務條款" start: "開始" home: "首頁" -remoteUserCaution: "由於該使用者來自遠端實例,因此資訊可能非即時的。" +remoteUserCaution: "由於該使用者來自其他實例,因此其資訊可能不完整。" activity: "動態" images: "圖片" image: "圖片" birthday: "生日" -yearsOld: "{age}歲" +yearsOld: "{age} 歲" registeredDate: "註冊日期" location: "位置" theme: "外觀主題" themeForLightMode: "在淺色模式下使用的主題" -themeForDarkMode: "在黑暗模式下使用的主題" +themeForDarkMode: "在深色模式下使用的主題" light: "淺色" -dark: "黑暗" -lightThemes: "明亮主題" -darkThemes: "黑暗主題" -syncDeviceDarkMode: "將黑暗模式與設備設置同步" +dark: "深色" +lightThemes: "淺色主題" +darkThemes: "深色主題" +syncDeviceDarkMode: "同步至此裝置的深色模式設定" drive: "雲端硬碟" fileName: "檔案名稱" selectFile: "選擇檔案" @@ -311,7 +321,7 @@ copyUrl: "複製URL" rename: "重新命名" avatar: "大頭貼" banner: "橫幅" -nsfw: "敏感內容" +displayOfSensitiveMedia: "顯示敏感媒體" whenServerDisconnected: "與伺服器的連接中斷時" disconnectedFromServer: "與伺服器中斷連線" reload: "重新整理" @@ -326,31 +336,30 @@ instanceName: "伺服器名稱" instanceDescription: "伺服器介紹" maintainerName: "管理員名稱" maintainerEmail: "管理員郵箱" -tosUrl: "服務條款URL" +tosUrl: "服務條款 URL" thisYear: "本年" thisMonth: "本月" today: "本日" -dayX: "{day}日" -monthX: "{month}月" -yearX: "{year}年" +dayX: "{day} 日" +monthX: "{month} 月" +yearX: "{year} 年" pages: "頁面" integration: "整合" -connectService: "己連結" -disconnectService: "己斷開 " -enableLocalTimeline: "開啟本地時間軸" +connectService: "已連結" +disconnectService: "已斷開 " +enableLocalTimeline: "啟用本地時間軸" enableGlobalTimeline: "啟用全域時間軸" -disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審查員仍可以繼續使用。" +disablingTimelinesInfo: "為了方便,即使您關閉了時間軸功能,管理員和審查員仍可以繼續使用。" registration: "註冊" -enableRegistration: "開啟新使用者註冊" +enableRegistration: "開放新使用者註冊" invite: "邀請" -driveCapacityPerLocalAccount: "每個本地用戶的雲端空間大小" +driveCapacityPerLocalAccount: "每個本地使用者的雲端硬碟容量" driveCapacityPerRemoteAccount: "每個非本地用戶的雲端空間大小" inMb: "以Mbps為單位" -iconUrl: "圖標URL" bannerUrl: "橫幅圖片URL" backgroundImageUrl: "背景圖片的來源網址 " basicInfo: "基本資訊" -pinnedUsers: "置頂用戶" +pinnedUsers: "置頂使用者" pinnedUsersDescription: "在「探索」頁面中使用換行標記想要置頂的使用者。" pinnedPages: "釘選頁面" pinnedPagesDescription: "輸入要固定至實例首頁的頁面路徑,以換行符分隔。" @@ -368,26 +377,26 @@ turnstile: "Turnstile" enableTurnstile: "啟用 Turnstile" turnstileSiteKey: "網站金鑰" turnstileSecretKey: "金鑰" -avoidMultiCaptchaConfirm: "使用多種驗證方式可能會造成干擾,您要關閉其他驗證方式嗎?您可以按“取消”保留多種驗證方式。" +avoidMultiCaptchaConfirm: "使用多種驗證方式可能會造成干擾,您要關閉其他驗證方式嗎?您可以按「取消」保留多種驗證方式。" antennas: "天線" manageAntennas: "管理天線" name: "名稱" antennaSource: "接收來源" antennaKeywords: "包含關鍵字" antennaExcludeKeywords: "排除關鍵字" -antennaKeywordsDescription: "用空格分隔指定AND、用換行符分隔指定OR" +antennaKeywordsDescription: "空格代表「以及」(AND),換行代表「或者」(OR)" notifyAntenna: "通知有新貼文" withFileAntenna: "僅帶有附件的貼文" -enableServiceworker: "開啟 ServiceWorker" -antennaUsersDescription: "指定用換行符分隔的用戶名" +enableServiceworker: "啟用瀏覽器的推播通知" +antennaUsersDescription: "填寫使用者名稱,以換行分隔" caseSensitive: "區分大小寫" withReplies: "包含回覆" connectedTo: "您的帳戶已連接到以下社交帳戶" notesAndReplies: "貼文與回覆" withFiles: "附件" silence: "禁言" -silenceConfirm: "確定要靜音此使用者嗎?" -unsilence: "解除靜音" +silenceConfirm: "確定要禁言此使用者嗎?" +unsilence: "解除禁言" unsilenceConfirm: "確定要解除禁言嗎?" popularUsers: "熱門使用者" recentlyUpdatedUsers: "最近發文的使用者" @@ -401,25 +410,28 @@ about: "關於" aboutMisskey: "關於 Misskey" administrator: "管理員" token: "權杖" -2fa: "雙因素驗證" +2fa: "雙重驗證" +setupOf2fa: "設定雙重驗證" totp: "驗證應用程式" totpDescription: "以驗證應用程式輸入一次性密碼" moderator: "審查員" moderation: "審查" -nUsersMentioned: "提到了{n}" -securityKeyAndPasskey: "安全金鑰・Passkey" +moderationNote: "管理筆記" +addModerationNote: "新增管理筆記" +nUsersMentioned: "被提及到 {n} 次" +securityKeyAndPasskey: "安全金鑰、Passkey" securityKey: "安全金鑰" lastUsed: "上次使用" -lastUsedAt: "最後使用:{t}" +lastUsedAt: "上次使用:{t}" unregister: "註銷帳戶" passwordLessLogin: "設置無密碼登入" passwordLessLoginDescription: "不使用密碼,以安全金鑰或 Passkey 登入" -resetPassword: "重置密碼" +resetPassword: "重設密碼" newPasswordIs: "新密碼為「{password}」" reduceUiAnimation: "減少介面的動態視覺" share: "分享" -notFound: "找不到" -notFoundDescription: "找不到與指定URL回應的頁面" +notFound: "查無項目" +notFoundDescription: "查無此頁" uploadFolder: "預設上傳資料夾" cacheClear: "清除快取" markAsReadAllNotifications: "標記所有通知為已讀" @@ -468,18 +480,18 @@ disableDrawer: "不顯示下拉式選單" showNoteActionsOnlyHover: "僅在游標停留時顯示貼文的操作選項" noHistory: "沒有歷史紀錄" signinHistory: "登入歷史" -enableAdvancedMfm: "啟用高級MFM" -enableAnimatedMfm: "啟用MFM動畫" +enableAdvancedMfm: "啟用進階 MFM" +enableAnimatedMfm: "啟用 MFM 動畫" doing: "正在進行" category: "類別" tags: "標籤" docSource: "文件來源" createAccount: "建立帳戶" existingAccount: "現有帳戶" -regenerate: "再生" +regenerate: "再次生成" fontSize: "字體大小" -mediaListWithOneImageAppearance: "僅1枚圖片的媒體列表高度" -limitTo: "上限為{x}" +mediaListWithOneImageAppearance: "只有一張圖片時的媒體列表高度" +limitTo: "上限為 {x}" noFollowRequests: "沒有追隨您的請求" openImageInNewTab: "於新分頁中開啟圖片" dashboard: "儀表板" @@ -487,7 +499,7 @@ local: "本地" remote: "遠端" total: "合計" weekOverWeekChanges: "與上週相比" -dayOverDayChanges: "與前一日相比" +dayOverDayChanges: "與昨日相比" appearance: "外觀" clientSettings: "客戶端設定" accountSettings: "帳戶設定" @@ -496,35 +508,35 @@ promote: "推廣" numberOfDays: "有效天數" hideThisNote: "隱藏此貼文" showFeaturedNotesInTimeline: "在時間軸上顯示熱門推薦" -objectStorage: "Object Storage (物件儲存)" -useObjectStorage: "使用Object Storage" +objectStorage: "對象存儲" +useObjectStorage: "使用對象存儲" objectStorageBaseUrl: "Base URL" -objectStorageBaseUrlDesc: "引用時的URL。如果您使用的是CDN或反向代理,请指定其URL,例如S3:“https://.s3.amazonaws.com”,GCS:“https://storage.googleapis.com/”" +objectStorageBaseUrlDesc: "用於引用的 URL。如果您使用的是 CDN 或反向代理,請指定其 URL,例如 S3(https://.s3.amazonaws.com)、GCS(https://storage.googleapis.com/)。" objectStorageBucket: "儲存空間(Bucket)" -objectStorageBucketDesc: "請指定您正在使用的服務的存儲桶名稱。 " +objectStorageBucketDesc: "請填寫所用服務的儲存空間(Bucket)名稱。 " objectStoragePrefix: "前綴" -objectStoragePrefixDesc: "它存儲在此前綴目錄下。" +objectStoragePrefixDesc: "它儲存在此前綴目錄下。" objectStorageEndpoint: "端點(Endpoint)" -objectStorageEndpointDesc: "如要使用AWS S3,請留空。否則請依照你使用的服務商的說明書進行設定,以''或 ':'的形式設定端點(Endpoint)。" +objectStorageEndpointDesc: "如使用 AWS S3,請留空。如使用其他服務,請按照其說明文件以「」或「:」的形式設定端點(Endpoint)。" objectStorageRegion: "地域(Region)" -objectStorageRegionDesc: "指定一個分區,例如“xx-east-1”。 如果您使用的服務沒有分區的概念,請留空或填寫“us-east-1”。" -objectStorageUseSSL: "使用SSL" -objectStorageUseSSLDesc: "如果不使用https進行API連接,請關閉" +objectStorageRegionDesc: "請填寫一個分區,例如「xx-east-1」。 如果您使用的服務不設分區,請留空或填寫「us-east-1」。" +objectStorageUseSSL: "使用 SSL" +objectStorageUseSSLDesc: "請在不使用 https 連接 API 時關閉" objectStorageUseProxy: "使用網路代理" -objectStorageUseProxyDesc: "如果不使用代理進行API連接,請關閉" -objectStorageSetPublicRead: "上傳時設定為\"public-read\"" -s3ForcePathStyleDesc: "啟用 s3ForcePathStyle 會強制將儲存槽名稱指定為 URL 中路徑的一部分,而不是主機名。 使用自託管 Minio 之類的可能需要啟用。" +objectStorageUseProxyDesc: "請在不使用網路代理連接 API 時關閉" +objectStorageSetPublicRead: "上傳時設定為「public-read」" +s3ForcePathStyleDesc: "啟用 s3ForcePathStyle 將強制填寫儲存空間(Bucket)名稱至 URL 路徑內,而非寫入主機名。 使用如 Minio 等自行託管服務時可能需要啟用。" serverLogs: "伺服器日誌" deleteAll: "刪除所有記錄" showFixedPostForm: "於時間軸頁頂顯示「發送貼文」方框" showFixedPostFormInChannel: "於時間軸頁頂顯示「發送貼文」方框(頻道)" -newNoteRecived: "發現新的貼文" +newNoteRecived: "發現新貼文" sounds: "音效" sound: "音效" listen: "聆聽" none: "無" showInPage: "在頁面中顯示" -popout: "彈出型窗口" +popout: "彈出式視窗" volume: "音量" masterVolume: "主音量" details: "詳細資訊" @@ -534,7 +546,7 @@ recentUsed: "最近使用" install: "安裝" uninstall: "解除安裝" installedApps: "已授權的應用程式" -nothing: "未發現" +nothing: "查無項目" installedDate: "安裝時間" lastUsedDate: "最後上線日期" state: "狀態" @@ -542,19 +554,19 @@ sort: "排序" ascendingOrder: "昇冪" descendingOrder: "降冪" scratchpad: "暫存記憶體" -scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。" +scratchpadDescription: "AiScript 控制臺為 AiScript 的實驗環境。您可以在此編寫、執行和確認程式碼與 Misskey 互動的結果。" output: "輸出" script: "腳本" -disablePagesScript: "停用頁面的AiScript腳本" +disablePagesScript: "停用頁面的 AiScript 腳本" updateRemoteUser: "更新遠端使用者資訊" deleteAllFiles: "刪除所有檔案" -deleteAllFilesConfirm: "要删除所有檔案嗎?" +deleteAllFilesConfirm: "要刪除所有檔案嗎?" removeAllFollowing: "解除所有追隨" removeAllFollowingDescription: "解除{host}所有的追隨。在伺服器不再存在時執行。" -userSuspended: "該使用者已被停用" -userSilenced: "該用戶已被禁言。" +userSuspended: "該使用者已被停用。" +userSilenced: "該使用者已被禁言。" yourAccountSuspendedTitle: "帳戶已被凍結" -yourAccountSuspendedDescription: "由於違反了伺服器的服務條款或其他原因,該帳戶已被凍結。 您可以與管理員連繫以了解更多訊息。 請不要創建一個新的帳戶。" +yourAccountSuspendedDescription: "該帳戶已因違反伺服器服務條款或其他原因而被凍結。您可以向管理員查詢更多資訊。請不要建立新帳戶。" tokenRevoked: "權杖無效" tokenRevokedDescription: "登入權杖失效,請重新登入。" accountDeleted: "帳戶已被刪除" @@ -567,28 +579,28 @@ relays: "中繼" addRelay: "新增中繼" inboxUrl: "收件夾URL" addedRelays: "已加入的中繼" -serviceworkerInfo: "您需要啟用推送通知" -deletedNote: "已删除的貼文" -invisibleNote: "私密的貼文" +serviceworkerInfo: "您需要啟用推送通知。" +deletedNote: "已刪除的貼文" +invisibleNote: "隱藏的貼文" enableInfiniteScroll: "啟用自動滾動頁面模式" visibility: "可見性" poll: "投票" useCw: "隱藏內容" -enablePlayer: "打開播放器" +enablePlayer: "開啟播放器" disablePlayer: "關閉播放器" expandTweet: "展開推文" themeEditor: "主題編輯器" description: "描述" -describeFile: "添加標題 " -enterFileDescription: "輸入標題 " +describeFile: "新增標題" +enterFileDescription: "輸入標題" author: "作者" -leaveConfirm: "有未保存的更改。要放棄嗎?" +leaveConfirm: "尚未儲存修改。要放棄嗎?" manage: "管理" plugins: "外掛" preferencesBackups: "備份設定檔" deck: "多欄模式" undeck: "取消多欄模式" -useBlurEffectForModal: "在模態框使用模糊效果" +useBlurEffectForModal: "在對話框使用模糊效果" useFullReactionPicker: "使用全尺寸的反應選擇器" width: "寬度" height: "高度" @@ -608,14 +620,14 @@ enableEmail: "啟用發送電郵功能" emailConfigInfo: "用於確認電郵地址及密碼重置" email: "電子郵件" emailAddress: "電郵地址" -smtpConfig: "SMTP伺服器設定" +smtpConfig: "SMTP 伺服器設定" smtpHost: "主機" smtpPort: "埠" smtpUser: "使用者名稱" smtpPass: "密碼" emptyToDisableSmtpAuth: "留空使用者名稱和密碼以關閉SMTP驗證。" smtpSecure: "在 SMTP 連接中使用隱式 SSL/TLS" -smtpSecureInfo: "使用STARTTLS時關閉。" +smtpSecureInfo: "使用 STARTTLS 時關閉。" testEmail: "測試郵件發送" wordMute: "被靜音的文字" regexpError: "正規表達式錯誤" @@ -638,16 +650,17 @@ useGlobalSetting: "使用全域設定" useGlobalSettingDesc: "啟用時,將使用帳戶通知設定。停用時,則可以單獨設定。" other: "其他" regenerateLoginToken: "重新產生登入權杖" -regenerateLoginTokenDescription: "重新產生用於登入的內部權杖。一般情況下是不需要這樣做的。一旦重產,所有裝置將會被登出。" +regenerateLoginTokenDescription: "重新產生用於登入的內部權杖。一般情況下是不需要這樣做的。重新產生後,所有裝置將會被登出。" setMultipleBySeparatingWithSpace: "您可以使用空格分隔多個項目。" -fileIdOrUrl: "檔案ID或URL" +fileIdOrUrl: "檔案 ID 或 URL" behavior: "行為" sample: "範例" abuseReports: "檢舉" reportAbuse: "檢舉" +reportAbuseRenote: "檢舉轉發貼文" reportAbuseOf: "檢舉{name}" -fillAbuseReportDescription: "請填寫檢舉的詳細理由。可以的話,請附上針對的URL網址。" -abuseReported: "回報已送出。感謝您的報告。" +fillAbuseReportDescription: "請填寫檢舉的詳細理由。如有需要,請附上相關 URL。" +abuseReported: "檢舉完成。感謝您的報告。" reporter: "檢舉者" reporteeOrigin: "檢舉來源" reporterOrigin: "檢舉者來源" @@ -657,13 +670,13 @@ send: "發送" abuseMarkAsResolved: "處理完畢" openInNewTab: "在新分頁中開啟" openInSideView: "在側欄中開啟" -defaultNavigationBehaviour: "默認導航" +defaultNavigationBehaviour: "預設導航" editTheseSettingsMayBreakAccount: "修改這些設定可能會毀損您的帳戶" instanceTicker: "貼文的實例來源" waitingFor: "等待{x}" random: "隨機" system: "系統" -switchUi: "切換界面" +switchUi: "切換介面" desktop: "桌面" clip: "摘錄" createNew: "新建" @@ -672,7 +685,8 @@ createNewClip: "建立新摘錄" unclip: "解除摘錄" confirmToUnclipAlreadyClippedNote: "此貼文已包含在摘錄「{name}」中。 你想將貼文從這個摘錄中排除嗎?" public: "公開" -i18nInfo: "Misskey已經被志願者們翻譯成各種語言版本,如果想要幫忙的話,可以進入{link}幫助翻譯。" +private: "私密" +i18nInfo: "Misskey 已被志願者們翻譯成各種語言版本。您可以瀏覽 {link} 幫助翻譯。" manageAccessTokens: "管理存取權杖" accountInfo: "帳戶資訊" notesCount: "貼文數量" @@ -680,7 +694,7 @@ repliesCount: "回覆數量" renotesCount: "轉發數量" repliedCount: "回覆數量" renotedCount: "轉發次數" -followingCount: "正在追隨的用戶數量" +followingCount: "正在追隨的使用者數量" followersCount: "追隨者數量" sentReactionsCount: "反應發送次數" receivedReactionsCount: "收到反應次數" @@ -693,7 +707,7 @@ driveUsage: "雲端硬碟使用量" noCrawle: "拒絕搜尋引擎索引" noCrawleDescription: "要求網路搜尋引擎不要索引你的個人資料頁、貼文及頁面等。" lockedAccountInfo: "即使你通過了追隨者請求,除非你將貼文的可見性設定為 「追隨者」,否則任何人都能看見你的貼文。" -alwaysMarkSensitive: "默認將圖像/影像標記為敏感內容" +alwaysMarkSensitive: "預設將多媒體標記為敏感內容" loadRawImages: "以原始圖檔顯示附件圖檔的縮圖" disableShowingAnimatedImages: "不播放動態圖檔" verificationEmailSent: "已發送驗證電子郵件。請點擊進入電子郵件中的鏈接完成驗證。" @@ -711,7 +725,7 @@ thisIsExperimentalFeature: "這是實驗性的功能。可能會有變更規格 developer: "開發者" makeExplorable: "使自己的帳戶能夠在「探索」頁面中顯示" makeExplorableDescription: "如果關閉,帳戶將不會被顯示在「探索」頁面中。" -showGapBetweenNotesInTimeline: "分開顯示時間線上的貼文。" +showGapBetweenNotesInTimeline: "分開顯示時間軸上的貼文。" duplicate: "複製" left: "左" center: "置中" @@ -721,16 +735,16 @@ reloadToApplySetting: "設定將會在頁面重新載入之後生效。要現在 needReloadToApply: "必須重新載入才會生效。" showTitlebar: "顯示標題列" clearCache: "清除快取資料" -onlineUsersCount: "{n}人正在線上" -nUsers: "{n}用戶" -nNotes: "{n}貼文" +onlineUsersCount: "{n} 人上線" +nUsers: "{n} 使用者" +nNotes: "{n} 貼文" sendErrorReports: "傳送錯誤報告" -sendErrorReportsDescription: "啟用後,問題報告將傳送至開發者以提升軟體品質。問題報告可能包括OS版本,瀏覽器類型,行為歷史記錄等。" +sendErrorReportsDescription: "傳送問題報告至開發者以提升軟體品質。問題報告可能包括作業系統版本,瀏覽器類型,行為歷史記錄等。" myTheme: "我的佈景主題" backgroundColor: "背景" accentColor: "重點色彩" textColor: "文字" -saveAs: "另存為..." +saveAs: "另存新檔" advanced: "進階" advancedSettings: "進階設定" value: "數值" @@ -752,32 +766,32 @@ editCode: "編輯代碼" apply: "套用" receiveAnnouncementFromInstance: "接收由本實例發出的電郵通知" emailNotification: "郵件通知" -publish: "發佈" +publish: "發布" inChannelSearch: "頻道内搜尋" useReactionPickerForContextMenu: "點擊右鍵開啟反應工具欄" -typingUsers: "{users}輸入中..." +typingUsers: "{users}輸入中" jumpToSpecifiedDate: "跳轉到特定日期" -showingPastTimeline: "顯示過往的時間線" +showingPastTimeline: "顯示過往的時間軸" clear: "清除" markAllAsRead: "全部標示為已讀" goBack: "返回" unlikeConfirm: "要取消按讚嗎?" -fullView: "全熒幕顯示" -quitFullView: "退出全熒幕顯示" -addDescription: "添加描述" -userPagePinTip: "在貼文的選單中選擇\"置頂\",即可置頂該貼文至您的個人檔案頁面。" +fullView: "全螢幕顯示" +quitFullView: "退出全螢幕顯示" +addDescription: "新增描述" +userPagePinTip: "在貼文的選單中選擇「置頂」,即可置頂該貼文至您的個人檔案頁面。" notSpecifiedMentionWarning: "此貼文有未指定的提及" info: "資訊" -userInfo: "用戶資料" +userInfo: "使用者資訊" unknown: "未知" -onlineStatus: "在線狀態" -hideOnlineStatus: "隱藏在線狀態" -hideOnlineStatusDescription: "隱藏在線狀態後,可能會降低檢索等功能的便利性。" +onlineStatus: "上線狀態" +hideOnlineStatus: "隱藏上線狀態" +hideOnlineStatusDescription: "隱藏上線狀態後,可能會降低搜尋等功能的便利性。" online: "線上" active: "最近活躍" offline: "離線" notRecommended: "不推薦" -botProtection: "Bot防護" +botProtection: "Bot 防護" instanceBlocking: "已封鎖的實例" selectAccount: "選擇帳戶" switchAccount: "切換帳戶" @@ -788,11 +802,11 @@ user: "使用者" administration: "管理" accounts: "帳戶" switch: "切換" -noMaintainerInformationWarning: "尚未設定管理員信息。" -noBotProtectionWarning: "尚未設定Bot防護。" +noMaintainerInformationWarning: "尚未設定管理員訊息。" +noBotProtectionWarning: "尚未設定 Bot 防護。" configure: "設定" postToGallery: "發佈到相簿" -postToHashtag: "以此主題標籤發布" +postToHashtag: "以此主題標籤發佈" gallery: "相簿" recentPosts: "最新貼文" popularPosts: "熱門的貼文" @@ -809,8 +823,8 @@ emailNotConfiguredWarning: "沒有設定電子郵件地址" ratio: "%" previewNoteText: "預覽文本" customCss: "自定義 CSS" -customCssWarn: "這個設定必須由具備相關知識的人員操作,不當的設定可能导致客戶端無法正常使用。" -global: "公開" +customCssWarn: "這個設定必須由具備相關知識的人員操作,不當的設定可能導致客戶端無法正常使用。" +global: "全域" squareAvatars: "頭像以方形顯示" sent: "發送" received: "收取" @@ -827,7 +841,7 @@ accountDeletionInProgress: "正在刪除帳戶" usernameInfo: "在伺服器上您的帳戶是唯一的識別名稱。您可以使用字母 (a ~ z, A ~ Z)、數字 (0 ~ 9) 和下底線 (_)。之後帳戶名是不能更改的。" aiChanMode: "小藍模式" devMode: "開發者模式" -keepCw: "保持CW" +keepCw: "保持隱藏內容" pubSub: "Pub/Sub 帳戶" lastCommunication: "最近的通信" resolved: "已解決" @@ -841,15 +855,15 @@ off: "關閉" emailRequiredForSignup: "註冊帳戶需要電子郵件地址" unread: "未讀" filter: "篩選" -controlPanel: "控制台" +controlPanel: "控制臺" manageAccounts: "管理帳戶" makeReactionsPublic: "將反應設為公開" makeReactionsPublicDescription: "將您做過的反應設為公開可見。" classic: "經典" muteThread: "將貼文串設為靜音" unmuteThread: "將貼文串的靜音解除" -ffVisibility: "連接的公開範圍" -ffVisibilityDescription: "您可以設定您的關注/關注者資訊的公開範圍" +ffVisibility: "連繫的可見性" +ffVisibilityDescription: "您可以設定追隨或追隨者資訊的公開範圍" continueThread: "查看更多貼文" deleteAccountConfirm: "將要刪除帳戶。是否確定?" incorrectPassword: "密碼錯誤。" @@ -868,15 +882,15 @@ numberOfColumn: "列數" searchByGoogle: "搜尋" instanceDefaultLightTheme: "實例預設的淺色主題" instanceDefaultDarkTheme: "實例預設的深色主題" -instanceDefaultThemeDescription: "輸入物件形式的主题代碼" +instanceDefaultThemeDescription: "輸入物件形式的主題代碼" mutePeriod: "靜音的期限" period: "期限" indefinitely: "無期限" -tenMinutes: "10分鐘" -oneHour: "1小時" -oneDay: "1天" -oneWeek: "1週" -oneMonth: "1個月" +tenMinutes: "十分鐘" +oneHour: "一小時" +oneDay: "一天" +oneWeek: "一週" +oneMonth: "一個月" reflectMayTakeTime: "可能需要一些時間才會出現效果。" failedToFetchAccountInformation: "取得帳戶資訊失敗" rateLimitExceeded: "已超過速率限制" @@ -885,14 +899,14 @@ cropImageAsk: "要剪裁圖片嗎?" cropYes: "裁剪" cropNo: "使用原圖" file: "檔案" -recentNHours: "過去{n}小時" -recentNDays: "過去{n}天" +recentNHours: "過去 {n} 小時" +recentNDays: "過去 {n} 天" noEmailServerWarning: "尚未設定電子郵件伺服器。" thereIsUnresolvedAbuseReportWarning: "有尚未處理的檢舉。" recommended: "推薦" check: "檢查" driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限" -driveCapOverrideCaption: "如果指定0以下的值,就會被取消。" +driveCapOverrideCaption: "如果指定 0 以下的值,就會被取消。" requireAdminForView: "必須以管理員帳戶登入才可以檢視。" isSystemAccount: "由系統自動建立與管理的帳戶。" typeToConfirm: "要執行這項操作,請輸入 {x} " @@ -919,21 +933,21 @@ failedToUpload: "上傳失敗" cannotUploadBecauseInappropriate: "由於判定可能包含不適當的內容,因此無法上傳。" cannotUploadBecauseNoFreeSpace: "由於雲端硬碟沒有可用空間,因此無法上傳。" cannotUploadBecauseExceedsFileSizeLimit: "由於超過了檔案大小的限制,無法上傳。" -beta: "Beta" -enableAutoSensitive: "自動NSFW判定" -enableAutoSensitiveDescription: "如果可用,請利用機器學習在媒體上自動設置 NSFW 旗標。 即使關閉此功能,依實例而定也可能會自動設置。" -activeEmailValidationDescription: "積極地驗證用戶的電子郵件地址,判斷它是否為免洗地址,或者它是否可以通信。 若關閉,則只會檢查字元是否正確。" +beta: "測試版" +enableAutoSensitive: "自動 NSFW 判定" +enableAutoSensitiveDescription: "如果可用,它將使用機器學習技術判斷多媒體內容是否需要標記 NSFW。即使關閉此功能,也可能會依實例規則而自動啟用。" +activeEmailValidationDescription: "積極驗證使用者的電郵地址,以判斷它是否可以通訊。關閉此選項代表只會檢查地址是否符合格式。" navbar: "導覽列" shuffle: "隨機" account: "帳戶" move: "移動 " pushNotification: "推播通知" subscribePushNotification: "啟用推播通知" -unsubscribePushNotification: "停止推播通知" +unsubscribePushNotification: "停用推播通知" pushNotificationAlreadySubscribed: "推播通知啟用中" pushNotificationNotSupported: "瀏覽器或實例不支援推播通知" -sendPushNotificationReadMessage: "通知與訊息如果已讀的話,就將推播通知刪除" -sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」通知將立刻顯示。可能會增加設備的電池消耗。" +sendPushNotificationReadMessage: "如果已閱讀通知與訊息,就刪除推播通知" +sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」通知將立刻顯示。可能會更消耗裝置電池。" windowMaximize: "最大化" windowMinimize: "最小化" windowRestore: "復原" @@ -948,8 +962,8 @@ numberOfLikes: "讚數" show: "檢視" neverShow: "不再顯示" remindMeLater: "以後再說" -didYouLikeMisskey: "您是否喜愛Misskey呢?" -pleaseDonate: "Misskey是由{host}使用的免費軟體。請贊助我們,讓開發能夠持續!" +didYouLikeMisskey: "您喜歡 Misskey 嗎?" +pleaseDonate: "Misskey 是由 {host} 使用的免費軟體。請贊助我們,讓開發得以持續!" roles: "角色" role: "角色" noRole: "沒有角色" @@ -961,23 +975,23 @@ color: "顏色" manageCustomEmojis: "管理自訂表情符號" youCannotCreateAnymore: "您無法再建立更多了。" cannotPerformTemporary: "暫時無法進行" -cannotPerformTemporaryDescription: "由於超過操作次數限制,暫時無法進行。請過一段時間之後再嘗試。" +cannotPerformTemporaryDescription: "由於超過操作次數限制,因此暫時無法進行。請稍後再嘗試。" invalidParamError: "參數錯誤" -invalidParamErrorDescription: "請求參數有問題。通常是bug造成的,但也有輸入的字元數過多之類的可能性。" +invalidParamErrorDescription: "請求參數有問題。這可能是漏洞或輸入過多字元所致。" permissionDeniedError: "操作被拒絕" -permissionDeniedErrorDescription: "本帳號沒有執行這個操作的權限。" +permissionDeniedErrorDescription: "此帳戶沒有執行這個操作的權限。" preset: "預設值" selectFromPresets: "從預設值中選擇" achievements: "成就" gotInvalidResponseError: "伺服器的回應無效" gotInvalidResponseErrorDescription: "伺服器可能已關閉或者在維護中,請稍後再試。" thisPostMayBeAnnoying: "這篇貼文可能會造成別人的困擾。" -thisPostMayBeAnnoyingHome: "發布到首頁" +thisPostMayBeAnnoyingHome: "發佈到首頁" thisPostMayBeAnnoyingCancel: "退出" -thisPostMayBeAnnoyingIgnore: "直接發布貼文" +thisPostMayBeAnnoyingIgnore: "直接發佈貼文" collapseRenotes: "省略顯示已看過的轉發貼文" internalServerError: "內部伺服器錯誤" -internalServerErrorDescription: "內部伺服器發生了非預期的錯誤。" +internalServerErrorDescription: "內部伺服器出現意外錯誤。" copyErrorInfo: "複製錯誤資訊" joinThisServer: "在此伺服器上註冊" exploreOtherServers: "探索其他伺服器" @@ -987,7 +1001,7 @@ disableFederationConfirmWarn: "即使停止了聯邦功能,貼文也不會變 disableFederationOk: "停止聯邦功能" invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。" emailNotSupported: "這個伺服器不支援寄送郵件" -postToTheChannel: "發布到頻道" +postToTheChannel: "發佈到頻道" cannotBeChangedLater: "之後不能變更。" reactionAcceptance: "接受表情反應" likeOnly: "僅限讚" @@ -998,7 +1012,7 @@ rolesAssignedToMe: "指派給自己的角色" resetPasswordConfirm: "重設密碼?" sensitiveWords: "敏感詞" sensitiveWordsDescription: "將含有設定詞彙的貼文可見性設為發送至首頁。可以用換行來進行複數的設定。" -sensitiveWordsDescription2: "用空格分隔關鍵詞構成AND格式,用斜線包圍關鍵字構成正規表達式。" +sensitiveWordsDescription2: "空格代表「以及」(AND),斜線包圍關鍵字代表使用正規表達式。" notesSearchNotAvailable: "無法使用搜尋貼文功能。" license: "授權" unfavoriteConfirm: "要取消收錄我的最愛嗎?" @@ -1007,10 +1021,10 @@ drivecleaner: "雲端硬碟清掃器" retryAllQueuesNow: "立刻重試所有佇列" retryAllQueuesConfirmTitle: "要現在重試嗎?" retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。" -enableChartsForRemoteUser: "生成遠端用戶的圖表" +enableChartsForRemoteUser: "生成遠端使用者的圖表" enableChartsForFederatedInstances: "生成遠端伺服器的圖表" -showClipButtonInNoteFooter: "將摘錄添加至貼文" -largeNoteReactions: "將貼文的反應放大顯示" +showClipButtonInNoteFooter: "新增摘錄至貼文" +reactionsDisplaySize: "表情回應的顯示尺寸" noteIdOrUrl: "貼文ID或URL" video: "影片" videos: "影片" @@ -1030,36 +1044,87 @@ rightTop: "右上" leftBottom: "左下" rightBottom: "右下" stackAxis: "堆疊方向" -vertical: "縱向" -horizontal: "側向" +vertical: "直向" +horizontal: "橫向" position: "位置" serverRules: "伺服器規則" pleaseConfirmBelowBeforeSignup: "在本伺服器註冊之前,請確認下列事項。" pleaseAgreeAllToContinue: "必須全部勾選「同意」才能繼續。" continue: "繼續" preservedUsernames: "保留的使用者名稱" -preservedUsernamesDescription: "換行列舉要保留的使用者名稱。此處指定的使用者名稱,在建立帳戶時無法使用,但由管理者所建立的帳戶不受此限。此外,既有的帳戶也不受影響。" +preservedUsernamesDescription: "換行列舉要保留的使用者名稱。此處出現的名稱將在註冊時禁用,但由管理者建立帳戶則不受此限。此外,既有的帳戶也不受影響。" createNoteFromTheFile: "由此檔案建立貼文" archive: "封存" channelArchiveConfirmTitle: "要封存{name}嗎?" -channelArchiveConfirmDescription: "封存以後,在頻道列表與搜索結果中不會顯示,也無法發布新的貼文。" +channelArchiveConfirmDescription: "封存後,將不會在頻道列表與搜尋結果中顯示,也無法發佈新貼文。" thisChannelArchived: "這個頻道已被封存。" displayOfNote: "顯示貼文" initialAccountSetting: "初始設定" youFollowing: "追隨中" preventAiLearning: "拒絕接受生成式AI的訓練" -preventAiLearningDescription: "要求外部的文章生成式AI或圖像生成式AI不以發布的貼文和圖像等內容為學習對象。這是透過在HTML響應中包含noai旗標來實現的,但不能完全防止AI的學習,因為這要看該AI是否遵守這個要求。" +preventAiLearningDescription: "要求站外生成式 AI 不使用您發佈的內容訓練模型。此功能會使伺服器於 HTML 回應新增「noai」標籤,而因為要視乎 AI 會否遵守該標籤,所以此功能無法完全阻止所有 AI 使用您的內容。" options: "選項" specifyUser: "指定使用者" failedToPreviewUrl: "無法預覽" update: "更新" -rolesThatCanBeUsedThisEmojiAsReaction: "可以當成反應使用的角色" -rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "如果是未指定角色的情況,則任何人都可以被當成反應來使用。" -rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "角色必須是公開的角色。" -cancelReactionConfirm: "要取消做出的反應嗎?" -changeReactionConfirm: "要變更做出的反應嗎?" +rolesThatCanBeUsedThisEmojiAsReaction: "可以使用此表情符號為反應的角色" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "如沒有指定角色,任何人都可使用此表情回應。" +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "必須為公開角色。" +cancelReactionConfirm: "要取消此反應嗎?" +changeReactionConfirm: "要更改反應嗎?" later: "稍後再說" -goToMisskey: "往Misskey" +goToMisskey: "往 Misskey" +additionalEmojiDictionary: "表情符號的附加辭典" +installed: "已安裝" +branding: "品牌宣傳" +enableServerMachineStats: "公佈伺服器的機器資訊" +enableIdenticonGeneration: "啟用生成使用者的 Identicon " +turnOffToImprovePerformance: "關閉時會提高性能。" +createInviteCode: "建立邀請碼" +createWithOptions: "使用選項建立" +createCount: "建立數" +inviteCodeCreated: "已建立邀請碼" +inviteLimitExceeded: "可建立的邀請碼已達上限。" +createLimitRemaining: "可建立的邀請碼:剩餘 {limit} 個" +inviteLimitResetCycle: "可以在 {time} 內建立最多 {limit} 個邀請碼。" +expirationDate: "有效日期" +noExpirationDate: "不設有效日期" +inviteCodeUsedAt: "使用邀請碼的日期和時間" +registeredUserUsingInviteCode: "用了邀請碼的使用者" +waitingForMailAuth: "等待電子郵件認證" +inviteCodeCreator: "建立了邀請碼的使用者" +usedAt: "使用的日期和時間" +unused: "未使用" +used: "已使用" +expired: "過期" +doYouAgree: "你同意嗎?" +beSureToReadThisAsItIsImportant: "重要,請務必閱讀。" +iHaveReadXCarefullyAndAgree: "我已仔細閱讀並同意「{x}」的内容。" +dialog: "對話方塊" +icon: "圖示" +forYou: "給您" +currentAnnouncements: "最新公告" +pastAnnouncements: "歷史公告" +youHaveUnreadAnnouncements: "有未讀的公告。" +useSecurityKey: "請按照瀏覽器或設備上的說明使用安全金鑰或 Passkey。" +replies: "回覆" +renotes: "轉發" +loadReplies: "閱覽回覆" +loadConversation: "閱覽對話" +pinnedList: "已置頂的清單" +keepScreenOn: "保持設備螢幕開啟" +verifiedLink: "已驗證連結" +notifyNotes: "開啟貼文通知" +unnotifyNotes: "關閉貼文通知" +_announcement: + forExistingUsers: "僅限既有的使用者" + forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。" + needConfirmationToRead: "必須確認才能標記為已讀" + needConfirmationToReadDescription: "啟用代表此公告將顯示對話方塊以確認是否標記為已讀,同時不會受「標記所有公告為已讀」功能影響。" + end: "結束公告" + tooManyActiveAnnouncementDescription: "有過多公告可能會影響使用者體驗。請考慮歸檔已結束的公告。" + readConfirmTitle: "標記為已讀嗎?" + readConfirmText: "閱讀「{title}」的內容並標記為已讀。" _initialAccountSetting: accountCreated: "帳戶已建立完成!" letsStartAccountSetup: "來進行帳戶的初始設定吧。" @@ -1072,11 +1137,18 @@ _initialAccountSetting: pushNotificationDescription: "啟用推送通知,就可以在設備上接收{name}的通知。" initialAccountSettingCompleted: "初始設定完成了!" haveFun: "盡情享受{name}吧!" - ifYouNeedLearnMore: "關於如何使用{name}(Misskey)的詳細資訊,請見{link}。" + ifYouNeedLearnMore: "請瀏覽{link}以更瞭解{name}(Misskey)的使用方法。" skipAreYouSure: "要略過初始設定嗎?" laterAreYouSure: "稍後再重新進行初始設定嗎?" _serverRules: - description: "設定伺服器的簡要規則,在新的註冊之前顯示。建議的內容是使用條款的摘要。" + description: "設定在註冊頁面顯示的伺服器簡要規則。建議是服務條款的摘要。" +_serverSettings: + iconUrl: "圖示的 URL" + appIconDescription: "指定顯示 {host} 為應用程式時的圖示。" + appIconUsageExample: "例如:漸進式網路應用程式(PWA)、於手機桌面新增書籤" + appIconStyleRecommendation: "因為可能會裁剪成圓形或圓角,所以建議用單色填滿邊框及背景。" + appIconResolutionMustBe: "解析度必須為 {resolution}。" + manifestJsonOverride: "覆寫 manifest.json" _accountMigration: moveFrom: "從其他帳戶遷移到這個帳戶" moveFromSub: "為另一個帳戶建立別名" @@ -1085,116 +1157,116 @@ _accountMigration: moveTo: "將這個帳戶遷移至新的帳戶" moveToLabel: "要遷移到的帳戶:" moveCannotBeUndone: "一旦遷移帳戶,就無法取消。" - moveAccountDescription: "這個操作不可撤銷。首先,請確認已在要遷移到的帳戶中為這個帳戶建立了一個別名。建立別名之後,像這樣輸入你要遷移到的帳戶:@person@instance.com" + moveAccountDescription: "遷移至新帳戶。\n ・此帳戶的追隨者將自動追隨新帳戶;\n ・此帳戶的所有追隨者將被取消追隨;\n ・此帳戶不能再發文。\n\n雖然會自動遷移您追隨者,但必須手動遷移您追隨的帳戶。請在遷移前匯出此帳戶的「追隨中」名單,並在遷移後自行匯入。\n列表名單、靜音名單及封鎖名單也必須如此處理。\n\n(此說明適用於本伺服器,以及運行 Misskey v13.12.0 或更新版本的其他伺服器;如 Mastodon 等使用 ActivityPub 協定的其他軟體或有不同的處理方式。)" moveAccountHowTo: "要遷移帳戶,首先要在目標帳戶中為此帳戶建立一個別名。\n 建立別名後,像這樣輸入目標帳戶:@username@server.example.com" startMigration: "遷移" migrationConfirm: "確定要將這個帳戶遷移至 {account} 嗎?一旦遷移就無法撤銷,也就無法以原來的狀態使用這個帳戶。\n另外,請確認在要遷移到的帳戶已經建立了一個別名。" movedAndCannotBeUndone: "帳戶已遷移。\n遷移無法撤消。" - postMigrationNote: "在遷移操作後的24小時之後解除此帳戶的追隨。此帳戶的追隨中、追隨者數量變為0。由於不會解除追隨者,你的追隨者仍然可以繼續檢視這個帳戶發布給追隨者的貼文。" + postMigrationNote: "在完成遷移的 24 小時後解除此帳戶的追隨。此帳戶的追隨中、追隨者數量變為 0。由於不會解除追隨者,你的追隨者仍然可以繼續檢視這個帳戶發布給追隨者的貼文。" movedTo: "要遷移到的帳戶:" _achievements: earnedAt: "獲得日期" _types: _notes1: - title: "just setting up my msky" + title: "歡迎!" description: "發出了第一則貼文" - flavor: "祝您的Misskey生活愉快!" + flavor: "祝您的 Misskey 生活愉快!" _notes10: title: "若干貼文" - description: "發表了10則貼文" + description: "發佈了十篇貼文" _notes100: title: "許多貼文" - description: "發表了100則貼文" + description: "發佈了一百篇貼文" _notes500: title: "滿滿的貼文" - description: "發表了500則貼文" + description: "發佈了五百篇貼文" _notes1000: title: "堆積如山的貼文" - description: "發表了1000則貼文" + description: "發佈了一千篇貼文" _notes5000: title: "滔滔不絕的貼文" - description: "發表了5000則貼文" + description: "發佈了五千篇貼文" _notes10000: title: "超級貼文" - description: "發表了10000則貼文" + description: "發佈了一萬篇貼文" _notes20000: - title: "需要更多的貼文" - description: "發表了20000則貼文" + title: "需要更多貼文" + description: "發佈了兩萬篇貼文" _notes30000: title: "貼文貼文貼文" - description: "發表了30000則貼文" + description: "發佈了三萬篇貼文" _notes40000: title: "貼文工廠" - description: "發表了40000則貼文" + description: "發佈了四萬篇貼文" _notes50000: title: "貼文星球" - description: "發表了50000則貼文" + description: "發佈了五萬篇貼文" _notes60000: title: "貼文類星體" - description: "發表了60000則貼文" + description: "發佈了六萬篇貼文" _notes70000: title: "貼文黑洞" - description: "發表了70000則貼文" + description: "發佈了七萬篇貼文" _notes80000: title: "貼文銀河" - description: "發表了80000則貼文" + description: "發佈了八萬篇貼文" _notes90000: title: "貼文宇宙" - description: "發表了90000則貼文" + description: "發佈了九萬篇貼文" _notes100000: title: "ALL YOUR NOTE ARE BELONG TO US" - description: "發表了100,000則貼文" + description: "發佈了十萬篇貼文" flavor: "有這麼多東西要寫嗎?" _login3: title: "初學者Ⅰ" - description: "總登入天數為3天" - flavor: "從今天開始,我就是Misskist" + description: "總登入天數為三天" + flavor: "從今天開始,我就是 Misskist" _login7: title: "初學者ⅠⅠ" - description: "總登入天數為7天" + description: "總登入天數為七天" flavor: "您開始習慣了嗎?" _login15: title: "初學者ⅠⅠⅠ" - description: "總登入天數為15天" + description: "總登入天數為十五天" _login30: title: "Misskist Ⅰ" - description: "總登入天數為30天" + description: "總登入天數為三十天" _login60: title: "Misskist ⅠⅠ" - description: "總登入天數為60天" + description: "總登入天數為六十天" _login100: title: "Misskist ⅠⅠⅠ" - description: "總登入天數為100天" - flavor: "辣個 Misskist 用戶" + description: "總登入天數為一百天" + flavor: "凶暴的 Misskist" _login200: title: "普通Ⅰ" - description: "總登入天數為200天" + description: "總登入天數為兩百天" _login300: - title: "普通IⅠ" - description: "總登入天數為300天" + title: "普通ⅠⅠ" + description: "總登入天數為三百天" _login400: - title: "普通IIⅠ" - description: "總登入天數為400天" + title: "普通ⅠⅠⅠ" + description: "總登入天數為四百天" _login500: title: "老兵Ⅰ" - description: "總登入天數為500天" + description: "總登入天數為五百天" flavor: "諸君,我喜歡貼文" _login600: title: "老兵ⅠⅠ" - description: "總登入天數為600天" + description: "總登入天數為六百天" _login700: title: "老兵ⅠⅠⅠ" - description: "總登入天數為700天" + description: "總登入天數為七百天" _login800: title: "貼文大師Ⅰ" - description: "總登入天數為800天" + description: "總登入天數為八百天" _login900: title: "貼文大師ⅠⅠ" - description: "總登入天數為900天" + description: "總登入天數為九百天" _login1000: title: "貼文大師ⅠⅠⅠ" - description: "總登入天數為1,000天" - flavor: "感謝您使用Misskey!" + description: "總登入天數為一千天" + flavor: "感謝您使用 Misskey!" _noteClipped1: title: "忍不住要收進摘錄裡" description: "第一次將貼文收進摘錄" @@ -1208,7 +1280,7 @@ _achievements: title: "有備而來" description: "設定了個人檔案" _markedAsCat: - title: "吾輩乃貓是也" + title: "我是貓" description: "已將帳戶設定為貓" flavor: "還沒有名字。" _following1: @@ -1221,7 +1293,7 @@ _achievements: title: "朋友很多" description: "追隨超過50人了" _following100: - title: "100位朋友" + title: "一百位朋友" description: "追隨超過100人了" _following300: title: "朋友過多" @@ -1230,7 +1302,7 @@ _achievements: title: "第一個追隨者" description: "第一次被追隨" _followers10: - title: "Follow me!" + title: "追隨我吧!" description: "追隨者超過10人了" _followers50: title: "成群結隊" @@ -1239,24 +1311,24 @@ _achievements: title: "熱門人物" description: "追隨者超過100人了" _followers300: - title: "請排成一排" + title: "請排隊" description: "追隨者超過300人了" _followers500: - title: "基地台" - description: "超過500名追隨者了" + title: "基地臺" + description: "超過五百名追隨者了" _followers1000: - title: "影響者" - description: "超過1000名追隨者了" + title: "星光熠熠" + description: "超過一千名追隨者了" _collectAchievements30: title: "成就收藏家" - description: "獲得30個以上的成就" + description: "獲得三十個以上的成就" _viewAchievements3min: - title: "喜愛成就" - description: "看成就列表要花3分鐘以上" + title: "成就發燒友" + description: "看著成就列表超過三分鐘" _iLoveMisskey: title: "I Love Misskey" - description: "發布「I ❤ #Misskey」" - flavor: "感謝您使用Misskey! by 開發團隊" + description: "發佈「I ❤ #Misskey」" + flavor: "感謝您使用 Misskey!by 開發團隊" _foundTreasure: title: "尋寶" description: "發現了隱藏的寶藏" @@ -1264,34 +1336,34 @@ _achievements: title: "休息一下" description: "客戶端啟動已超過30分鐘" _client60min: - title: "Misskey看太多" + title: "Misskey 看太多" description: "客戶端啟動已超過60分鐘" _noteDeletedWithin1min: - title: "現在沒有了" - description: "發文後1分鐘內刪文" + title: "欲言又止" + description: "發文後一分鐘內刪文" _postedAtLateNight: - title: "夜行性" + title: "夜貓子" description: "在深夜發佈貼文" flavor: "該去睡覺了。" _postedAt0min0sec: title: "報時" - description: "在0分0秒發佈貼文" - flavor: "啵.啵.啵.嗶ー" + description: "在零分零秒發佈貼文" + flavor: "啵、啵、啵、嗶ーー" _selfQuote: title: "自我引用" description: "引用了自己的貼文" _htl20npm: - title: "流動的TL" - description: "在首頁時間軸的流速超過20npm" + title: "源源不絕" + description: "首頁時間軸在一分鐘內出現超過二十篇貼文" _viewInstanceChart: title: "分析師" description: "顯示了實例的圖表" _outputHelloWorldOnScratchpad: - title: "Hello world!" - description: "在暫存記憶體輸出了 hello world" + title: "Hello, world!" + description: "在 AiScript 控制臺輸出了「hello world」" _open3windows: title: "多重視窗" - description: "開啟了3個以上的視窗" + description: "開啟過三個以上的視窗" _driveFolderCircularReference: title: "循環引用" description: "試圖遞迴套入雲端硬碟資料夾" @@ -1303,34 +1375,37 @@ _achievements: description: "已點擊這裡了" _justPlainLucky: title: "只是運氣好" - description: "每10秒有0.01%的機率獲得" + description: "每十秒有二萬分之一(0.005%)的機率獲得" _setNameToSyuilo: - title: "神的情結" + title: "神與您同在" description: "將名稱設定為 syuilo" _passedSinceAccountCreated1: - title: "一周年" - description: "自建立帳戶開始過了1年" + title: "一週年" + description: "帳戶加入時間已超過一年" _passedSinceAccountCreated2: - title: "二周年" - description: "自建立帳戶開始過了2年" + title: "二週年" + description: "帳戶加入時間已超過兩年" _passedSinceAccountCreated3: - title: "三周年" - description: "自建立帳戶開始過了3年" + title: "三週年" + description: "帳戶加入時間已超過三年" _loggedInOnBirthday: title: "生日快樂" description: "在生日當天登入了" _loggedInOnNewYearsDay: title: "新年快樂" description: "在元旦當天登入了" - flavor: "今年也請對敝實例多多指教" + flavor: "今年也請您多多指教!" _cookieClicked: title: "點擊餅乾的遊戲" description: "點擊了餅乾" flavor: "是不是軟體有問題?" _brainDiver: title: "Brain Driver" - description: "發佈了Brain Driver的連結" + description: "發佈一篇含歌曲《Brain Driver》連結的貼文" flavor: "Misskey-Misskey La-Tu-Ma" + _smashTestNotificationButton: + title: "過度測試" + description: "極短時間內連續測試通知" _role: new: "建立角色" edit: "編輯角色" @@ -1353,8 +1428,8 @@ _role: chooseRoleToAssign: "選擇要指派的角色" iconUrl: "圖示的URL" asBadge: "顯示為徽章" - descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。" - isExplorable: "公開角色時間軸" + descriptionOfAsBadge: "開啟的話,角色圖示會顯示在使用者名稱旁邊。" + isExplorable: "讓使用者更容易找到您" descriptionOfIsExplorable: "若開啟則公開角色時間軸。若角色不是公開的,則無法公開時間軸。" displayOrder: "顯示順序" descriptionOfDisplayOrder: "數字越大,顯示在UI上的越上面。" @@ -1370,13 +1445,16 @@ _role: ltlAvailable: "瀏覽本地時間軸" canPublicNote: "允許公開貼文" canInvite: "發行實例邀請碼" + inviteLimit: "可建立邀請碼的數量" + inviteLimitCycle: "邀請碼的發放間隔" + inviteExpirationTime: "邀請碼的有效日期" canManageCustomEmojis: "管理自訂表情符號" driveCapacity: "雲端硬碟容量" alwaysMarkNsfw: "總是將檔案標記為NSFW" pinMax: "置頂貼文的最大數量" antennaMax: "可建立的天線數量" wordMuteMax: "靜音文字的最大字數" - webhookMax: "可建立的Webhook數量" + webhookMax: "可建立的 Webhook 數量" clipMax: "可建立的摘錄數量" noteEachClipsMax: "摘錄內貼文的最大數量" userListMax: "可建立的使用者清單數量" @@ -1388,43 +1466,43 @@ _role: _condition: isLocal: "本地使用者" isRemote: "遠端使用者" - createdLessThan: "自建立帳戶開始~以內" - createdMoreThan: "自建立帳戶開始~經過" + createdLessThan: "帳戶加入時間不超過" + createdMoreThan: "帳戶加入時間已超過" followersLessThanOrEq: "追隨者人數在~以下" followersMoreThanOrEq: "追隨者人數在~以上" followingLessThanOrEq: "追隨人數在~以下" followingMoreThanOrEq: "追隨人數在~以上" - notesLessThanOrEq: "發布數在~以下" - notesMoreThanOrEq: "發布數在~以上" - and: "~和~" + notesLessThanOrEq: "貼文數在~以下" + notesMoreThanOrEq: "貼文數在~以上" + and: "~及~" or: "~或~" not: "~否" _sensitiveMediaDetection: description: "您可以使用機器學習自動檢測敏感媒體並將其用於審查。 伺服器的負荷會稍微增加。" sensitivity: "檢測敏感度" sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。" - setSensitiveFlagAutomatically: "設定 NSFW 旗標" + setSensitiveFlagAutomatically: "設定 NSFW 標籤" setSensitiveFlagAutomaticallyDescription: "即使將此設定關閉,判定結果也會保留在內部。" analyzeVideos: "啟用影片分析" analyzeVideosDescription: "除了靜止影像以外,也分析影片。伺服器的負荷會稍微增加。" _emailUnavailable: - used: "已經在使用中" + used: "已被使用" format: "格式無效" disposable: "不是永久可用的地址" mx: "郵件伺服器不正確" smtp: "郵件伺服器沒有應答" _ffVisibility: - public: "發佈" - followers: "只有關注你的用戶能看到" + public: "公開" + followers: "只有關注您的使用者能看到" private: "私密" _signup: almostThere: "即將完成" emailAddressInfo: "請輸入您所使用的電子郵件地址。電子郵件地址不會被公開。" - emailSent: "已將確認郵件發送至您輸入的電子郵件地址 ({email})。請開啟電子郵件中的連結以完成帳戶創建。" + emailSent: "已發送確認郵件至您輸入的電子郵件地址({email})。請開啟電子郵件中的連結完成註冊。" _accountDelete: accountDelete: "刪除帳戶" - mayTakeTime: "刪除帳戶的處理負荷較大,如果帳戶產生的內容數量上傳的檔案數量較多的話,就需要花费一段時間才能完成。" - sendEmail: "帳戶删除完成後,將向註冊地電子郵件地址發送通知。" + mayTakeTime: "刪除帳戶的處理負荷較大,如果帳戶發佈的內容以及上傳的檔案數量較多,則需要一段時間才能完成。" + sendEmail: "帳戶刪除完成後,將向其電子郵件地址發送通知。" requestAccountDelete: "刪除帳戶請求" started: "已開始刪除作業。" inProgress: "正在刪除" @@ -1432,6 +1510,7 @@ _ad: back: "返回" reduceFrequencyOfThisAd: "降低此廣告的頻率 " hide: "隱藏" + timezoneinfo: "星期幾是由伺服器的時區指定的。" _forgotPassword: enterEmail: "請輸入您的帳戶註冊的電子郵件地址。 密碼重置連結將被發送到該電子郵件地址。" ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。 " @@ -1439,8 +1518,8 @@ _forgotPassword: _gallery: my: "我的貼文" liked: "喜歡的貼文" - like: "讚" - unlike: "收回喜歡" + like: "讚好" + unlike: "收回讚好" _email: _follow: title: "您有新的追隨者" @@ -1448,7 +1527,7 @@ _email: title: "收到追隨請求" _plugin: install: "安裝外掛組件" - installWarn: "請不要安裝來源不明的外掛組件。" + installWarn: "請不要安裝來源不明的外掛。" manage: "管理外掛" _preferencesBackups: list: "已備份的設定檔" @@ -1458,7 +1537,7 @@ _preferencesBackups: save: "覆蓋存檔" inputName: "輸入備份檔名稱" cannotSave: "無法儲存" - nameAlreadyExists: "備份檔名稱「{name}」已經存在。請指定不同的名稱。" + nameAlreadyExists: "備份檔名稱「{name}」已經存在。請填寫其他名稱。" applyConfirm: "將備份檔「{name}」套用在現在的裝置嗎?現在的裝置設定將會消失。" saveConfirm: "要覆蓋存檔{name}嗎?" deleteConfirm: "要刪除{name}嗎?" @@ -1475,18 +1554,18 @@ _registry: domain: "域" createKey: "新增機碼" _aboutMisskey: - about: "Misskey是由syuilo自2014年起開發的開源軟體。" + about: "Misskey 是由 syuilo 自 2014 年起開發的開放原始碼軟體。" contributors: "主要貢獻者" allContributors: "全體貢獻人員" source: "原始碼" - translation: "翻譯Misskey" - donate: "贊助Misskey" + translation: "翻譯 Misskey" + donate: "贊助 Misskey" morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰" patrons: "贊助者" -_nsfw: - respect: "隱藏敏感內容" - ignore: "不隱藏敏感內容" - force: "隱藏所有內容" +_displayOfSensitiveMedia: + respect: "隱藏被標記為敏感的多媒體內容" + ignore: "不隱藏被標記為敏感的多媒體內容" + force: "隱藏所有多媒體內容" _instanceTicker: none: "隱藏" remote: "向遠端使用者顯示" @@ -1503,28 +1582,28 @@ _channel: featured: "熱門貼文" owned: "管理中" following: "追隨中" - usersCount: "有{n}人參與" - notesCount: "有{n}個貼文" + usersCount: "有 {n} 人參與" + notesCount: "有 {n} 篇貼文" nameAndDescription: "名稱與說明" nameOnly: "僅名稱" _menuDisplay: - sideFull: "側向" - sideIcon: "側向(圖示)" + sideFull: "橫向" + sideIcon: "橫向(圖示)" top: "頂部" hide: "隱藏" _wordMute: muteWords: "加入靜音文字" - muteWordsDescription: "用空格分隔指定AND,用換行分隔指定OR。" - muteWordsDescription2: "將關鍵字用斜線括起來表示正規表達式。" - softDescription: "隱藏時間軸中指定條件的貼文。" - hardDescription: "具有指定條件的貼文將不添加到時間軸。 即使您更改條件,未被添加的貼文也會被排除在外。" + muteWordsDescription: "空格代表「以及」(AND),換行代表「或者」(OR)。" + muteWordsDescription2: "用斜線包圍關鍵字代表正規表達式。" + softDescription: "隱藏時間軸中符合特定條件的貼文。" + hardDescription: "符合特定條件的貼文將不會新增至時間軸。 即使您更改條件,未被新增的貼文也會被排除在外。" soft: "軟性靜音" hard: "硬性靜音" mutedNotes: "已靜音的貼文" _instanceMute: - instanceMuteDescription: "包括對被靜音實例上的用戶的回覆,被設定的實例上所有貼文及轉發都會被靜音。" - instanceMuteDescription2: "設定時以換行進行分隔" - title: "被設定的實例,貼文將被隱藏。" + instanceMuteDescription: "包括對被靜音實例上的使用者的回覆,被設定的實例上所有貼文及轉發都會被靜音。" + instanceMuteDescription2: "換行以分隔" + title: "將隱藏被設定的實例貼文。" heading: "將實例靜音" _theme: explore: "取得佈景主題" @@ -1549,13 +1628,13 @@ _theme: func: "函数" funcKind: "功能類型" argument: "參數" - basedProp: "要基於的屬性的名稱 " + basedProp: "基於的屬性名稱 " alpha: "透明度" darken: "暗度" lighten: "亮度" - inputConstantName: "請輸入常數的名稱" + inputConstantName: "請輸入常數名稱" importInfo: "您可以在此貼上主題代碼,將其匯入編輯器中" - deleteConstantConfirm: "確定要删除常數{const}嗎?" + deleteConstantConfirm: "確定要刪除常數{const}嗎?" keys: accent: "重點色彩" bg: "背景" @@ -1563,14 +1642,14 @@ _theme: focus: "聚焦" indicator: "指標" panel: "面板" - shadow: "陰影" + shadow: "影子" header: "標題" navBg: "側邊欄的背景 " navFg: "側邊欄的文字" - navHoverFg: "側邊欄文字(懸停) " - navActive: "側邊欄文本 (活動)" + navHoverFg: "側邊欄文字(懸浮) " + navActive: "側邊欄文字(活動)" navIndicator: "側邊欄指示符" - link: "鏈接" + link: "連結" hashtag: "標籤" mention: "提到" mentionMe: "提到了我" @@ -1578,15 +1657,15 @@ _theme: modalBg: "對話框背景" divider: "分割線" scrollbarHandle: "捲動條" - scrollbarHandleHover: "捲動條 (漂浮)" + scrollbarHandleHover: "捲動條(懸浮)" dateLabelFg: "日期標籤文字" infoBg: "資訊背景" infoFg: "資訊內容" infoWarnBg: "警告背景" - infoWarnFg: "警告字元" - cwBg: "CW 按鈕背景" - cwFg: "CW 按鈕文本" - cwHoverBg: "CW 按鈕背景 (漂浮)" + infoWarnFg: "警告文字" + cwBg: "隱藏內容按鈕背景" + cwFg: "隱藏內容按鈕文字" + cwHoverBg: "隱藏內容按鈕背景(懸浮)" toastBg: "通知背景" toastFg: "通知文本" buttonBg: "按鈕背景" @@ -1595,11 +1674,11 @@ _theme: listItemHoverBg: "列表物品背景 (漂浮)" driveFolderBg: "雲端硬碟文件夾背景" wallpaperOverlay: "壁紙覆蓋層" - badge: "獎章" + badge: "徽章" messageBg: "私訊背景" - accentDarken: "強調色(偏暗)" - accentLighten: "強調色(明亮)" - fgHighlighted: "高亮顯示文本" + accentDarken: "強調色(黑暗)" + accentLighten: "強調色(明亮)" + fgHighlighted: "突顯文字" _sfx: note: "貼文" noteMy: "我的貼文" @@ -1611,47 +1690,46 @@ _sfx: _ago: future: "未來" justNow: "剛剛" - secondsAgo: "{n}秒前" - minutesAgo: "{n}分鐘前" - hoursAgo: "{n}小時前" - daysAgo: "{n}天前" - weeksAgo: "{n}周前" - monthsAgo: "{n}個月前" - yearsAgo: "{n}年前" - invalid: "未發現" + secondsAgo: "{n} 秒前" + minutesAgo: "{n} 分鐘前 " + hoursAgo: "{n} 小時前" + daysAgo: "{n} 天前" + weeksAgo: "{n} 週前" + monthsAgo: "{n} 個月前" + yearsAgo: "{n} 年前" + invalid: "無" _time: second: "秒" minute: "分鐘" hour: "小時" day: "日" _timelineTutorial: - title: "Misskey的使用方法" - step1_1: "這個畫面是「時間軸」。發布到{name}的「貼文」按照時間順序顯示。" - step1_2: "時間軸有多種類型,例如在「首頁時間軸」中流動的是您追蹤的人的貼文;而在「本地時間軸」流動的是{name}全體的貼文。" - step2_1: "試試看,發布個貼文吧!按畫面上鉛筆圖示的按鈕開啟表格。" - step2_2: "初次貼文的內容,建議包括自我介紹以及「開始使用{name}」。" + title: "Misskey 的使用方法" + step1_1: "這個畫面是「時間軸」。發佈到{name}的「貼文」會按照時間順序顯示。" + step1_2: "時間軸有多種類型,例如「首頁時間軸」是您追蹤帳戶的貼文、「本地時間軸」是{name}內所有帳戶的貼文。" + step2_1: "不如現在就嘗試發文吧!按鉛筆圖示的按鈕開啟發文頁面。" + step2_2: "您可以在第一篇貼文裡寫自我介紹,或是「我來到 {name} 了」之類的話。" step3_1: "貼文發出去了嗎?" - step3_2: "如果你的貼文出現在時間軸上,就代表發文成功。" + step3_2: "如果您的貼文出現在時間軸上,就代表發文成功。" step4_1: "可以對貼文標記「反應」。" - step4_2: "點擊貼文的「+」圖示,即可選擇喜好的表情符號來標記反應。" + step4_2: "點擊貼文的「+」圖示,即可選擇表情符號來反應。" _2fa: - alreadyRegistered: "此設備已經被註冊過了" + alreadyRegistered: "此裝置已被註冊過了" registerTOTP: "開始設定驗證應用程式" - passwordToTOTP: "請輸入密碼" - step1: "首先,在您的設備上安裝二步驗證程式,例如{a}或{b}。" - step2: "然後,掃描螢幕上的QR code。" - step2Click: "點擊QR code,可以使用設備上安裝的驗證應用程式或金鑰環進行註冊。" - step2Url: "在桌面版應用中,請輸入以下的URL:" + step1: "首先,在您的裝置上安裝驗證程式,例如 {a} 或 {b}。" + step2: "然後,掃描螢幕上的 QR 碼。" + step2Click: "您可以點擊 QR 碼,以使用裝置上的驗證應用程式或金鑰環註冊。" + step2Uri: "使用桌面版應用程式時,請輸入以下的 URI" step3Title: "輸入驗證碼" - step3: "輸入您的App提供的權杖以完成設定。" + step3: "輸入應用程式所提供的權杖以完成設定。" + setupCompleted: "設定完成" step4: "從現在開始,任何登入操作都將要求您提供權杖。" securityKeyNotSupported: "您的瀏覽器不支援安全金鑰。" - registerTOTPBeforeKey: "要註冊安全金鑰・Passkey,請先設定驗證應用程式。" - securityKeyInfo: "您可以設定使用支援FIDO2的硬體安全鎖、終端設備的指纹認證或者PIN碼來登入。" - chromePasskeyNotSupported: "目前不支援Chrome的Passkey。" - registerSecurityKey: "註冊安全金鑰・Passkey" + registerTOTPBeforeKey: "如要註冊安全金鑰或 Passkey,請先設定驗證應用程式。" + securityKeyInfo: "您可以設定使用支援 FIDO2 的硬體安全鎖、終端設備的指紋認證,或者 PIN 碼來登入。" + registerSecurityKey: "註冊安全金鑰或 Passkey" securityKeyName: "輸入金鑰名稱" - tapSecurityKey: "按照瀏覽器的說明操作,註冊安全金鑰和Passkey。" + tapSecurityKey: "按照瀏覽器的說明註冊安全金鑰或 Passkey。" removeKey: "刪除安全金鑰" removeKeyConfirm: "要刪除{name}嗎?" whyTOTPOnlyRenew: "如果註冊了安全金鑰,則無法解除驗證應用程式的設定。" @@ -1659,19 +1737,24 @@ _2fa: renewTOTPConfirm: "目前驗證應用程式的驗證碼將無法使用。" renewTOTPOk: "重設" renewTOTPCancel: "現在不要" + checkBackupCodesBeforeCloseThisWizard: "請先確認下列備用驗證碼,再關閉此精靈視窗。" + backupCodes: "備用驗證碼" + backupCodesDescription: "如果驗證應用程式不能用了,可以使用以下的備用驗證碼存取您的帳戶。請務必妥善保管這個驗證碼。每個驗證碼只能使用一次。" + backupCodeUsedWarning: "已使用備用驗證碼。如果無法使用驗證應用程式,請盡快重新設定。" + backupCodesExhaustedWarning: "已使用所有備用驗證碼。如果無法使用驗證應用程式,則將無法再存取您的帳戶。請重新設定您的驗證應用程式。" _permissions: "read:account": "查看我的帳戶資訊" "write:account": "更改我的帳戶資訊" - "read:blocks": "已封鎖用戶名單" - "write:blocks": "編輯已封鎖用戶名單" + "read:blocks": "查看封鎖名單" + "write:blocks": "編輯封鎖名單" "read:drive": "存取雲端硬碟" "write:drive": "編輯雲端硬碟的檔案" "read:favorites": "瀏覽我的最愛" "write:favorites": "編輯我的最愛列表" - "read:following": "查看追隨中的用戶資訊" - "write:following": "追隨/解除追隨" + "read:following": "查看追隨中的使用者資訊" + "write:following": "追隨/解除追隨" "read:messaging": "顯示訊息" - "write:messaging": "撰寫或刪除私人訊息" + "write:messaging": "撰寫或刪除訊息" "read:mutes": "顯示已靜音列表" "write:mutes": "編輯已靜音列表" "write:notes": "撰寫或刪除貼文" @@ -1692,10 +1775,14 @@ _permissions: "write:gallery": "操作圖庫" "read:gallery-likes": "讀取喜歡的圖片" "write:gallery-likes": "操作喜歡的圖片" + "read:flash": "檢視 Play" + "write:flash": "編輯 Play" + "read:flash-likes": "檢視 Play 的讚" + "write:flash-likes": "編輯 Play 的讚" _auth: shareAccessTitle: "應用程式的存取權限" shareAccess: "要授權「“{name}”」存取您的帳戶嗎?" - shareAccessAsk: "您確定要授權這個應用程式使用您的帳戶嗎?" + shareAccessAsk: "您確定要授權這個應用程式存取您的帳戶嗎?" permission: "{name}要求以下的權限" permissionAsk: "此應用程式需要以下權限" pleaseGoBack: "請返回至應用程式" @@ -1722,23 +1809,23 @@ _widgets: notifications: "通知" timeline: "時間軸" calendar: "行事曆" - trends: "發燒貼文" + trends: "熱門貼文" clock: "時鐘" - rss: "RSS閱讀器" - rssTicker: "RSS跑馬燈" + rss: "RSS 閱讀器" + rssTicker: "RSS 跑馬燈" activity: "動態" photos: "照片" digitalClock: "電子時鐘" - unixClock: "UNIX時間" - federation: "站台聯邦" + unixClock: "UNIX 時間" + federation: "聯邦宇宙" instanceCloud: "實例雲" - postForm: "發佈窗口" + postForm: "發文視窗" slideshow: "幻燈片" button: "按鈕" - onlineUsers: "線上的用戶" + onlineUsers: "上線使用者" jobQueue: "佇列" - serverMetric: "服務器指標 " - aiscript: "AiScript控制台" + serverMetric: "伺服器指標 " + aiscript: "AiScript 控制臺" aiscriptApp: "AiScript App" aichan: "小藍" userList: "使用者列表" @@ -1748,39 +1835,39 @@ _widgets: _cw: hide: "隱藏" show: "瀏覽更多" - chars: "{count}字元" + chars: "{count} 個字元" files: "{count} 個檔案" _poll: - noOnlyOneChoice: "至少需要兩個選項。" - choiceN: "選擇{n}" + noOnlyOneChoice: "需要至少兩個選項。" + choiceN: "選項 {n}" noMore: "沒辦法再添加選項了" canMultipleVote: "可以多次投票" expiration: "期限" infinite: "無期限" at: "結束時間" - after: "進度指定 " + after: "指定時效" deadlineDate: "截止日期" deadlineTime: "小時" duration: "時長" - votesCount: "{n}票" - totalVotes: "一共{n}票" + votesCount: "{n} 票" + totalVotes: "合共 {n} 票" vote: "投票" showResult: "顯示結果" voted: "已投票" closed: "已結束" - remainingDays: "{d}天{h}小時後結束" - remainingHours: "{h}小時{m}分後結束" - remainingMinutes: "{m}分{s}秒後結束" - remainingSeconds: "{s}秒後截止" + remainingDays: "{d} 天 {h} 小時後結束" + remainingHours: "{h} 小時 {m} 分後結束" + remainingMinutes: "{m} 分 {s} 秒後結束" + remainingSeconds: "{s} 秒後截止" _visibility: public: "公開" - publicDescription: "發布給所有用戶 " + publicDescription: "發佈給所有使用者" home: "首頁" - homeDescription: "僅發送至首頁的時間軸" + homeDescription: "僅發布至首頁的時間軸" followers: "追隨者" - followersDescription: "僅發送至關注者" + followersDescription: "僅發布至關注者" specified: "指定使用者" - specifiedDescription: "僅發送至指定使用者" + specifiedDescription: "僅發布至指定使用者" disableFederation: "停用聯邦" disableFederationDescription: "不要傳遞給其他實例" _postForm: @@ -1788,24 +1875,25 @@ _postForm: quotePlaceholder: "引用此貼文..." channelPlaceholder: "發佈到頻道" _placeholders: - a: "今天過得如何?" - b: "有什麼新鮮事嗎?" + a: "今天過得如何?" + b: "有什麼新鮮事嗎?" c: "有什麼新鮮想法嗎?" - d: "想要發布些什麼嗎?" - e: "寫些什麼吧..." - f: "期待你發佈的內容..." + d: "想要發佈些什麼嗎?" + e: "寫些什麼吧……" + f: "靜待發文……" _profile: name: "名稱" username: "使用者名稱" description: "關於我" youCanIncludeHashtags: "你也可以在「關於我」中加上 #tag" - metadata: "進階資訊" - metadataEdit: "編輯進階資訊" + metadata: "附加資訊" + metadataEdit: "編輯附加資訊" metadataDescription: "可以在個人資料中以表格形式顯示其他資訊。" metadataLabel: "標籤" metadataContent: "内容" changeAvatar: "更換大頭貼" changeBanner: "變更橫幅圖像" + verifiedLinkDescription: "如果輸入包含您個人資料的網站 URL,欄位旁邊將出現驗證圖示。" _exportOrImport: allNotes: "所有貼文" favoritedNotes: "「我的最愛」貼文" @@ -1813,51 +1901,51 @@ _exportOrImport: muteList: "靜音" blockingList: "封鎖" userLists: "清單" - excludeMutingUsers: "排除被靜音的用戶" + excludeMutingUsers: "排除被靜音的使用者" excludeInactiveUsers: "排除不活躍帳戶" _charts: - federation: "站台聯邦" + federation: "聯邦宇宙" apRequest: "請求" - usersIncDec: "使用者増減" + usersIncDec: "使用者增減" usersTotal: "使用者合共" activeUsers: "活躍使用者" notesIncDec: "貼文増減" localNotesIncDec: "本地貼文増減" remoteNotesIncDec: "遠端貼文數目增减" notesTotal: "貼文合共" - filesIncDec: "檔案増減" - filesTotal: "累計檔案" - storageUsageIncDec: "儲存空間的増減" - storageUsageTotal: "已使用的儲存空間合共" + filesIncDec: "檔案增減" + filesTotal: "檔案總數" + storageUsageIncDec: "儲存空間增減" + storageUsageTotal: "儲存空間用量" _instanceCharts: requests: "請求" - users: "使用者増減" - usersTotal: "總計使用者" - notes: "貼文増減" + users: "使用者增減" + usersTotal: "使用者總數" + notes: "貼文增減" notesTotal: "累計貼文" - ff: "追隨/追隨者的増減" - ffTotal: "追隨/追隨者累計" - cacheSize: "增加或減少快取用量" - cacheSizeTotal: "快取大小總計" - files: "檔案數量的増減" - filesTotal: "檔案數量總計" + ff: "追隨/追隨者增減" + ffTotal: "追隨/追隨者總數" + cacheSize: "快取用量增減" + cacheSizeTotal: "快取用量總數" + files: "檔案總數增減" + filesTotal: "檔案總數累計" _timelines: home: "首頁" local: "本地" social: "社交" global: "公開" _play: - new: "新增Play" - edit: "編輯Play" - created: "已新增Play" - updated: "已更新Play" - deleted: "已刪除Play" + new: "新增 Play" + edit: "編輯 Play" + created: "已新增Play " + updated: "已更新Play " + deleted: "已刪除 Play" pageSetting: "Play設定" - editThisPage: "編輯這個Play" + editThisPage: "編輯此 Play" viewSource: "檢視原始碼" - my: "自己的Play" - liked: "按了讚的Play" - featured: "人氣" + my: "自己的 Play" + liked: "按讚的 Play" + featured: "熱門" title: "標題" script: "腳本" summary: "描述" @@ -1869,17 +1957,17 @@ _pages: updated: "頁面已更新" deleted: "頁面已被刪除" pageSetting: "頁面設定" - nameAlreadyExists: "指定的頁面URL已經存在" - invalidNameTitle: "指定的頁面URL無效" + nameAlreadyExists: "該頁面 URL 已存在" + invalidNameTitle: "無效的頁面 URL" invalidNameText: "請確定是否為非空白" editThisPage: "編輯此頁面" viewSource: "檢視原始碼" viewPage: "顯示頁面" - like: "喜歡" - unlike: "收回喜歡" + like: "讚好" + unlike: "收回讚好" my: "我的頁面" - liked: "已喜歡的頁面" - featured: "人氣" + liked: "已讚好的頁面" + featured: "熱門" inspector: "面板檢查" contents: "內容" content: "頁面方塊" @@ -1900,7 +1988,7 @@ _pages: inputBlocks: "輸入" specialBlocks: "特殊" blocks: - text: "字串" + text: "文字" textarea: "字串區域" section: "區段" image: "圖片" @@ -1924,9 +2012,14 @@ _notification: youReceivedFollowRequest: "您有新的追隨請求" yourFollowRequestAccepted: "您的追隨請求已通過" pollEnded: "問卷調查已產生結果" + newNote: "新的貼文" unreadAntennaNote: "天線 {name}" emptyPushNotificationMessage: "推送通知已更新" achievementEarned: "獲得成就" + testNotification: "通知測試" + checkNotificationBehavior: "確認通知的顯示行為" + sendTestNotification: "發送測試通知" + notificationWillBeDisplayedLikeThis: "通知會以這樣的方式顯示" _types: all: "全部 " follow: "追隨中" @@ -1941,7 +2034,7 @@ _notification: achievementEarned: "獲得成就" app: "應用程式通知" _actions: - followBack: "回關" + followBack: "追隨回去" reply: "回覆" renote: "轉發" _deck: @@ -1958,9 +2051,12 @@ _deck: profile: "個人檔案" newProfile: "新建個人檔案" deleteProfile: "刪除個人檔案" - introduction: "組合欄位來製作屬於自己的介面吧!" - introduction2: "您可以隨時透過按畫面右方的 + 來添加欄位。" - widgetsIntroduction: "請從欄位的選單中,選擇「編輯小工具」來添加小工具" + introduction: "組合多個欄位,製作屬於自己的介面吧!" + introduction2: "您可以隨時按畫面右方的「+」新增欄位。" + widgetsIntroduction: "請從欄位選單中選擇「編輯小工具」新增小工具。" + useSimpleUiForNonRootPages: "用簡易介面顯示非根頁面" + usedAsMinWidthWhenFlexible: "如果啟用「自動調整寬度」,此為最小寬度" + flexible: "自動調整寬度" _columns: main: "主列" widgets: "小工具" @@ -1973,24 +2069,24 @@ _deck: direct: "指定使用者" roleTimeline: "角色時間軸" _dialog: - charactersExceeded: "已超過最大字數!現在 {current} / 限制 {max}" - charactersBelow: "低於最少字數!現在 {current} / 限制 {max}" + charactersExceeded: "您的貼文太長了!現時字數 {current}/限制字數 {max}" + charactersBelow: "您的貼文太短了!現時字數 {current}/限制字數 {min}" _disabledTimeline: - title: "停用的時間軸" - description: "目前的角色無法使用這個時間軸。" + title: "時間軸已停用" + description: "目前角色無法使用這個時間軸。" _drivecleaner: - orderBySizeDesc: "檔案由大到小" - orderByCreatedAtAsc: "依照加入的日期順序" + orderBySizeDesc: "按大小降序排列" + orderByCreatedAtAsc: "按新增日期降序排列" _webhookSettings: createWebhook: "建立 Webhook" name: "名稱" - secret: "秘密" - events: "什麼時候運行Webhook" + secret: "密鑰" + events: "何時運行 Webhook" active: "已啟用" _events: follow: "當你追隨時" followed: "當被追隨時" - note: "當發布貼文時" + note: "當發佈貼文時" reply: "當收到回覆時" renote: "當被轉發時" reaction: "當獲得反應時" diff --git a/package.json b/package.json index 81029514c3..2022abc711 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "misskey", - "version": "13.13.0-beta.7", + "version": "2023.9.0-rc.1", "codename": "nasubi", "repository": { "type": "git", "url": "https://github.com/misskey-dev/misskey.git" }, - "packageManager": "pnpm@8.6.0", + "packageManager": "pnpm@8.7.6", "workspaces": [ "packages/frontend", "packages/backend", @@ -15,17 +15,17 @@ "private": true, "scripts": { "build-pre": "node ./scripts/build-pre.js", - "build": "pnpm build-pre && pnpm -r build && pnpm gulp", + "build-assets": "node ./scripts/build-assets.mjs", + "build": "pnpm build-pre && pnpm -r build && pnpm build-assets", "build-storybook": "pnpm --filter frontend build-storybook", - "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", + "start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js", + "start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js", "init": "pnpm migrate", "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", - "dev": "node ./scripts/dev.js", + "dev": "node ./scripts/dev.mjs", "lint": "pnpm -r lint", "cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts", "cy:run": "pnpm cypress run", @@ -34,7 +34,6 @@ "jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage", "test": "pnpm -r test", "test-and-coverage": "pnpm -r test-and-coverage", - "format": "pnpm exec gulp format", "clean": "node ./scripts/clean.js", "clean-all": "node ./scripts/clean-all.js", "cleanall": "pnpm clean-all" @@ -44,24 +43,20 @@ "lodash": "4.17.21" }, "dependencies": { - "execa": "5.1.1", - "gulp": "4.0.2", - "gulp-cssnano": "2.1.3", - "gulp-rename": "2.0.0", - "gulp-replace": "1.1.4", - "gulp-terser": "2.1.0", + "execa": "8.0.1", + "cssnano": "6.0.1", "js-yaml": "4.1.0", - "typescript": "5.1.3" + "postcss": "8.4.30", + "terser": "5.20.0", + "typescript": "5.2.2" }, "devDependencies": { - "@types/gulp": "4.0.10", - "@types/gulp-rename": "2.0.1", - "@typescript-eslint/eslint-plugin": "5.59.8", - "@typescript-eslint/parser": "5.59.8", + "@typescript-eslint/eslint-plugin": "6.7.2", + "@typescript-eslint/parser": "6.7.2", "cross-env": "7.0.3", - "cypress": "12.13.0", - "eslint": "8.41.0", - "start-server-and-test": "2.0.0" + "cypress": "13.2.0", + "eslint": "8.50.0", + "start-server-and-test": "2.0.1" }, "optionalDependencies": { "@tensorflow/tfjs-core": "4.4.0" diff --git a/packages/backend/.swcrc b/packages/backend/.swcrc index 08d4222d01..d9f047b6ac 100644 --- a/packages/backend/.swcrc +++ b/packages/backend/.swcrc @@ -11,13 +11,13 @@ "decoratorMetadata": true }, "experimental": { - "keepImportAssertions": true + "keepImportAttributes": true }, "baseUrl": "src", "paths": { "@/*": ["*"] }, - "target": "es2021" + "target": "es2022" }, "minify": false } diff --git a/packages/backend/assets/avatar.png b/packages/backend/assets/avatar.png new file mode 100644 index 0000000000..1b95a0c560 Binary files /dev/null and b/packages/backend/assets/avatar.png differ diff --git a/packages/backend/check_connect.js b/packages/backend/check_connect.js index ef0a350fbf..ea988a7f69 100644 --- a/packages/backend/check_connect.js +++ b/packages/backend/check_connect.js @@ -1,15 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Redis from 'ioredis'; import { loadConfig } from './built/config.js'; const config = loadConfig(); -const redis = new Redis({ - port: config.redis.port, - host: config.redis.host, - family: config.redis.family == null ? 0 : config.redis.family, - password: config.redis.pass, - keyPrefix: `${config.redis.prefix}:`, - db: config.redis.db ?? 0, -}); +const redis = new Redis(config.redis); redis.on('connect', () => redis.disconnect()); redis.on('error', (e) => { diff --git a/packages/backend/migration/1000000000000-Init.js b/packages/backend/migration/1000000000000-Init.js index 1140be7e84..6f04b52ae1 100644 --- a/packages/backend/migration/1000000000000-Init.js +++ b/packages/backend/migration/1000000000000-Init.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class Init1000000000000 { async up(queryRunner) { diff --git a/packages/backend/migration/1556348509290-Pages.js b/packages/backend/migration/1556348509290-Pages.js index 50caa2ce91..05d801227b 100644 --- a/packages/backend/migration/1556348509290-Pages.js +++ b/packages/backend/migration/1556348509290-Pages.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class Pages1556348509290 { async up(queryRunner) { diff --git a/packages/backend/migration/1556746559567-UserProfile.js b/packages/backend/migration/1556746559567-UserProfile.js index 50a9d1a8be..7cc1ba0083 100644 --- a/packages/backend/migration/1556746559567-UserProfile.js +++ b/packages/backend/migration/1556746559567-UserProfile.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class UserProfile1556746559567 { async up(queryRunner) { diff --git a/packages/backend/migration/1557476068003-PinnedUsers.js b/packages/backend/migration/1557476068003-PinnedUsers.js index d9cce25435..12f0b8fc6a 100644 --- a/packages/backend/migration/1557476068003-PinnedUsers.js +++ b/packages/backend/migration/1557476068003-PinnedUsers.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class PinnedUsers1557476068003 { async up(queryRunner) { diff --git a/packages/backend/migration/1557761316509-AddSomeUrls.js b/packages/backend/migration/1557761316509-AddSomeUrls.js index ab8736f7cc..244f64f8ef 100644 --- a/packages/backend/migration/1557761316509-AddSomeUrls.js +++ b/packages/backend/migration/1557761316509-AddSomeUrls.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class AddSomeUrls1557761316509 { async up(queryRunner) { diff --git a/packages/backend/migration/1557932705754-ObjectStorageSetting.js b/packages/backend/migration/1557932705754-ObjectStorageSetting.js index 19a0b9d5cd..736dcafaac 100644 --- a/packages/backend/migration/1557932705754-ObjectStorageSetting.js +++ b/packages/backend/migration/1557932705754-ObjectStorageSetting.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class ObjectStorageSetting1557932705754 { async up(queryRunner) { diff --git a/packages/backend/migration/1558072954435-PageLike.js b/packages/backend/migration/1558072954435-PageLike.js index 31b08418a9..d9502a6e03 100644 --- a/packages/backend/migration/1558072954435-PageLike.js +++ b/packages/backend/migration/1558072954435-PageLike.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class PageLike1558072954435 { async up(queryRunner) { diff --git a/packages/backend/migration/1558103093633-UserGroup.js b/packages/backend/migration/1558103093633-UserGroup.js index b670b31c3d..b3cc6eb949 100644 --- a/packages/backend/migration/1558103093633-UserGroup.js +++ b/packages/backend/migration/1558103093633-UserGroup.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class UserGroup1558103093633 { async up(queryRunner) { diff --git a/packages/backend/migration/1558257926829-UserGroupInvite.js b/packages/backend/migration/1558257926829-UserGroupInvite.js index e48bd3a7ff..a87173cdfe 100644 --- a/packages/backend/migration/1558257926829-UserGroupInvite.js +++ b/packages/backend/migration/1558257926829-UserGroupInvite.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class UserGroupInvite1558257926829 { async up(queryRunner) { diff --git a/packages/backend/migration/1558266512381-UserListJoining.js b/packages/backend/migration/1558266512381-UserListJoining.js index 3398aed139..bc94b7f425 100644 --- a/packages/backend/migration/1558266512381-UserListJoining.js +++ b/packages/backend/migration/1558266512381-UserListJoining.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class UserListJoining1558266512381 { async up(queryRunner) { diff --git a/packages/backend/migration/1561706992953-webauthn.js b/packages/backend/migration/1561706992953-webauthn.js index b007ffef14..fa9b1188ca 100644 --- a/packages/backend/migration/1561706992953-webauthn.js +++ b/packages/backend/migration/1561706992953-webauthn.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class webauthn1561706992953 { async up(queryRunner) { diff --git a/packages/backend/migration/1561873850023-ChartIndexes.js b/packages/backend/migration/1561873850023-ChartIndexes.js index 3ce53567fc..c7e93ba7b7 100644 --- a/packages/backend/migration/1561873850023-ChartIndexes.js +++ b/packages/backend/migration/1561873850023-ChartIndexes.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class ChartIndexes1561873850023 { async up(queryRunner) { diff --git a/packages/backend/migration/1562422242907-PasswordLessLogin.js b/packages/backend/migration/1562422242907-PasswordLessLogin.js index b73c7db4d3..3df3a6f5f5 100644 --- a/packages/backend/migration/1562422242907-PasswordLessLogin.js +++ b/packages/backend/migration/1562422242907-PasswordLessLogin.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class PasswordLessLogin1562422242907 { async up(queryRunner) { diff --git a/packages/backend/migration/1562444565093-PinnedPage.js b/packages/backend/migration/1562444565093-PinnedPage.js index 9a999a9150..329d49bbed 100644 --- a/packages/backend/migration/1562444565093-PinnedPage.js +++ b/packages/backend/migration/1562444565093-PinnedPage.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class PinnedPage1562444565093 { async up(queryRunner) { diff --git a/packages/backend/migration/1562448332510-PageTitleHideOption.js b/packages/backend/migration/1562448332510-PageTitleHideOption.js index 8fc78d202f..e41db08090 100644 --- a/packages/backend/migration/1562448332510-PageTitleHideOption.js +++ b/packages/backend/migration/1562448332510-PageTitleHideOption.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class PageTitleHideOption1562448332510 { async up(queryRunner) { diff --git a/packages/backend/migration/1562869971568-ModerationLog.js b/packages/backend/migration/1562869971568-ModerationLog.js index dd66d16eec..2eb3015d5c 100644 --- a/packages/backend/migration/1562869971568-ModerationLog.js +++ b/packages/backend/migration/1562869971568-ModerationLog.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class ModerationLog1562869971568 { async up(queryRunner) { diff --git a/packages/backend/migration/1563757595828-UsedUsername.js b/packages/backend/migration/1563757595828-UsedUsername.js index 8972df297d..91d9d36b9d 100644 --- a/packages/backend/migration/1563757595828-UsedUsername.js +++ b/packages/backend/migration/1563757595828-UsedUsername.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class UsedUsername1563757595828 { async up(queryRunner) { diff --git a/packages/backend/migration/1565634203341-room.js b/packages/backend/migration/1565634203341-room.js index 679940f244..c2e5fca863 100644 --- a/packages/backend/migration/1565634203341-room.js +++ b/packages/backend/migration/1565634203341-room.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class room1565634203341 { async up(queryRunner) { diff --git a/packages/backend/migration/1571220798684-CustomEmojiCategory.js b/packages/backend/migration/1571220798684-CustomEmojiCategory.js index 37c07366e1..f211af67be 100644 --- a/packages/backend/migration/1571220798684-CustomEmojiCategory.js +++ b/packages/backend/migration/1571220798684-CustomEmojiCategory.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class CustomEmojiCategory1571220798684 { async up(queryRunner) { diff --git a/packages/backend/migration/1572760203493-nodeinfo.js b/packages/backend/migration/1572760203493-nodeinfo.js index 54d5f914a4..c281b0b2db 100644 --- a/packages/backend/migration/1572760203493-nodeinfo.js +++ b/packages/backend/migration/1572760203493-nodeinfo.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class nodeinfo1572760203493 { async up(queryRunner) { diff --git a/packages/backend/migration/1576269851876-TalkFederationId.js b/packages/backend/migration/1576269851876-TalkFederationId.js index 35861d571f..045f9ddb04 100644 --- a/packages/backend/migration/1576269851876-TalkFederationId.js +++ b/packages/backend/migration/1576269851876-TalkFederationId.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class TalkFederationId1576269851876 { constructor() { diff --git a/packages/backend/migration/1576869585998-ProxyRemoteFiles.js b/packages/backend/migration/1576869585998-ProxyRemoteFiles.js index d6d134be40..0dde1ae70c 100644 --- a/packages/backend/migration/1576869585998-ProxyRemoteFiles.js +++ b/packages/backend/migration/1576869585998-ProxyRemoteFiles.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class ProxyRemoteFiles1576869585998 { constructor() { diff --git a/packages/backend/migration/1579267006611-v12.js b/packages/backend/migration/1579267006611-v12.js index 7f6318a192..86f9da7e7a 100644 --- a/packages/backend/migration/1579267006611-v12.js +++ b/packages/backend/migration/1579267006611-v12.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class v121579267006611 { constructor() { diff --git a/packages/backend/migration/1579270193251-v12-2.js b/packages/backend/migration/1579270193251-v12-2.js index c51ce63066..2593aca573 100644 --- a/packages/backend/migration/1579270193251-v12-2.js +++ b/packages/backend/migration/1579270193251-v12-2.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class v1221579270193251 { constructor() { diff --git a/packages/backend/migration/1579282808087-v12-3.js b/packages/backend/migration/1579282808087-v12-3.js index aeb4f5a873..a816b2e82e 100644 --- a/packages/backend/migration/1579282808087-v12-3.js +++ b/packages/backend/migration/1579282808087-v12-3.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class v1231579282808087 { constructor() { diff --git a/packages/backend/migration/1579544426412-v12-4.js b/packages/backend/migration/1579544426412-v12-4.js index f1e093413e..600dc270a5 100644 --- a/packages/backend/migration/1579544426412-v12-4.js +++ b/packages/backend/migration/1579544426412-v12-4.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class v1241579544426412 { constructor() { diff --git a/packages/backend/migration/1579977526288-v12-5.js b/packages/backend/migration/1579977526288-v12-5.js index 6d2b5c584a..73f3343347 100644 --- a/packages/backend/migration/1579977526288-v12-5.js +++ b/packages/backend/migration/1579977526288-v12-5.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class v1251579977526288 { constructor() { diff --git a/packages/backend/migration/1579993013959-v12-6.js b/packages/backend/migration/1579993013959-v12-6.js index 3941c1391d..5009e0aa88 100644 --- a/packages/backend/migration/1579993013959-v12-6.js +++ b/packages/backend/migration/1579993013959-v12-6.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class v1261579993013959 { constructor() { diff --git a/packages/backend/migration/1580069531114-v12-7.js b/packages/backend/migration/1580069531114-v12-7.js index 4b4790cb7d..ff943ffa6b 100644 --- a/packages/backend/migration/1580069531114-v12-7.js +++ b/packages/backend/migration/1580069531114-v12-7.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class v1271580069531114 { constructor() { diff --git a/packages/backend/migration/1580148575182-v12-8.js b/packages/backend/migration/1580148575182-v12-8.js index cc30200c14..20b77b391f 100644 --- a/packages/backend/migration/1580148575182-v12-8.js +++ b/packages/backend/migration/1580148575182-v12-8.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class v1281580148575182 { constructor() { diff --git a/packages/backend/migration/1580154400017-v12-9.js b/packages/backend/migration/1580154400017-v12-9.js index 3715798f19..f78dc47456 100644 --- a/packages/backend/migration/1580154400017-v12-9.js +++ b/packages/backend/migration/1580154400017-v12-9.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class v1291580154400017 { constructor() { diff --git a/packages/backend/migration/1580276619901-v12-10.js b/packages/backend/migration/1580276619901-v12-10.js index d5decb882e..09fa27ae83 100644 --- a/packages/backend/migration/1580276619901-v12-10.js +++ b/packages/backend/migration/1580276619901-v12-10.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class v12101580276619901 { constructor() { diff --git a/packages/backend/migration/1580331224276-v12-11.js b/packages/backend/migration/1580331224276-v12-11.js index 129720adbf..f118c34937 100644 --- a/packages/backend/migration/1580331224276-v12-11.js +++ b/packages/backend/migration/1580331224276-v12-11.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class v12111580331224276 { constructor() { diff --git a/packages/backend/migration/1580508795118-v12-12.js b/packages/backend/migration/1580508795118-v12-12.js index c5cec23a36..4fba933a08 100644 --- a/packages/backend/migration/1580508795118-v12-12.js +++ b/packages/backend/migration/1580508795118-v12-12.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class v12121580508795118 { constructor() { diff --git a/packages/backend/migration/1580543501339-v12-13.js b/packages/backend/migration/1580543501339-v12-13.js index 2fa490392d..9344516309 100644 --- a/packages/backend/migration/1580543501339-v12-13.js +++ b/packages/backend/migration/1580543501339-v12-13.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class v12131580543501339 { constructor() { diff --git a/packages/backend/migration/1580864313253-v12-14.js b/packages/backend/migration/1580864313253-v12-14.js index a3756ad029..5034492a70 100644 --- a/packages/backend/migration/1580864313253-v12-14.js +++ b/packages/backend/migration/1580864313253-v12-14.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class v12141580864313253 { constructor() { diff --git a/packages/backend/migration/1581526429287-user-group-invitation.js b/packages/backend/migration/1581526429287-user-group-invitation.js index 181b0aba86..fc81813807 100644 --- a/packages/backend/migration/1581526429287-user-group-invitation.js +++ b/packages/backend/migration/1581526429287-user-group-invitation.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class userGroupInvitation1581526429287 { constructor() { diff --git a/packages/backend/migration/1581695816408-user-group-antenna.js b/packages/backend/migration/1581695816408-user-group-antenna.js index 267b58cd9b..8a212c092a 100644 --- a/packages/backend/migration/1581695816408-user-group-antenna.js +++ b/packages/backend/migration/1581695816408-user-group-antenna.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class userGroupAntenna1581695816408 { constructor() { diff --git a/packages/backend/migration/1581708415836-drive-user-folder-id-index.js b/packages/backend/migration/1581708415836-drive-user-folder-id-index.js index 43c2ce6cee..6594078db8 100644 --- a/packages/backend/migration/1581708415836-drive-user-folder-id-index.js +++ b/packages/backend/migration/1581708415836-drive-user-folder-id-index.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class driveUserFolderIdIndex1581708415836 { constructor() { diff --git a/packages/backend/migration/1581979837262-promo.js b/packages/backend/migration/1581979837262-promo.js index 4813a5f480..585564a400 100644 --- a/packages/backend/migration/1581979837262-promo.js +++ b/packages/backend/migration/1581979837262-promo.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class promo1581979837262 { constructor() { diff --git a/packages/backend/migration/1582019042083-featured-injecttion.js b/packages/backend/migration/1582019042083-featured-injecttion.js index 7f8790b01b..d270006277 100644 --- a/packages/backend/migration/1582019042083-featured-injecttion.js +++ b/packages/backend/migration/1582019042083-featured-injecttion.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class featuredInjecttion1582019042083 { constructor() { diff --git a/packages/backend/migration/1582210532752-antenna-exclude.js b/packages/backend/migration/1582210532752-antenna-exclude.js index ff8d7b80d8..12eee2364c 100644 --- a/packages/backend/migration/1582210532752-antenna-exclude.js +++ b/packages/backend/migration/1582210532752-antenna-exclude.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class antennaExclude1582210532752 { constructor() { diff --git a/packages/backend/migration/1582875306439-note-reaction-length.js b/packages/backend/migration/1582875306439-note-reaction-length.js index e99501f012..a4413c9533 100644 --- a/packages/backend/migration/1582875306439-note-reaction-length.js +++ b/packages/backend/migration/1582875306439-note-reaction-length.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class noteReactionLength1582875306439 { constructor() { diff --git a/packages/backend/migration/1585361548360-miauth.js b/packages/backend/migration/1585361548360-miauth.js index e59aa3b6ef..d073fa3d26 100644 --- a/packages/backend/migration/1585361548360-miauth.js +++ b/packages/backend/migration/1585361548360-miauth.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class miauth1585361548360 { constructor() { diff --git a/packages/backend/migration/1585385921215-custom-notification.js b/packages/backend/migration/1585385921215-custom-notification.js index c3ddb2be17..a3336e0eca 100644 --- a/packages/backend/migration/1585385921215-custom-notification.js +++ b/packages/backend/migration/1585385921215-custom-notification.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class customNotification1585385921215 { constructor() { diff --git a/packages/backend/migration/1585772678853-ap-url.js b/packages/backend/migration/1585772678853-ap-url.js index 5fb809ff53..f67f5a4542 100644 --- a/packages/backend/migration/1585772678853-ap-url.js +++ b/packages/backend/migration/1585772678853-ap-url.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class apUrl1585772678853 { constructor() { diff --git a/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js b/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js index e13bb217e3..16f7599b80 100644 --- a/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js +++ b/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class AddObjectStorageUseProxy1586624197029 { constructor() { diff --git a/packages/backend/migration/1586641139527-remote-reaction.js b/packages/backend/migration/1586641139527-remote-reaction.js index 5b23103a17..666bb42ca6 100644 --- a/packages/backend/migration/1586641139527-remote-reaction.js +++ b/packages/backend/migration/1586641139527-remote-reaction.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class remoteReaction1586641139527 { constructor() { diff --git a/packages/backend/migration/1586708940386-pageAiScript.js b/packages/backend/migration/1586708940386-pageAiScript.js index eed616c111..3d0d0ab915 100644 --- a/packages/backend/migration/1586708940386-pageAiScript.js +++ b/packages/backend/migration/1586708940386-pageAiScript.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class pageAiScript1586708940386 { constructor() { diff --git a/packages/backend/migration/1588044505511-hCaptcha.js b/packages/backend/migration/1588044505511-hCaptcha.js index a33dbd7133..22cc6672c5 100644 --- a/packages/backend/migration/1588044505511-hCaptcha.js +++ b/packages/backend/migration/1588044505511-hCaptcha.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class hCaptcha1588044505511 { constructor() { diff --git a/packages/backend/migration/1589023282116-pubRelay.js b/packages/backend/migration/1589023282116-pubRelay.js index 48a1028d39..ed010699e1 100644 --- a/packages/backend/migration/1589023282116-pubRelay.js +++ b/packages/backend/migration/1589023282116-pubRelay.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class pubRelay1589023282116 { constructor() { diff --git a/packages/backend/migration/1595075960584-blurhash.js b/packages/backend/migration/1595075960584-blurhash.js index f24d3722cf..967676531f 100644 --- a/packages/backend/migration/1595075960584-blurhash.js +++ b/packages/backend/migration/1595075960584-blurhash.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class blurhash1595075960584 { constructor() { diff --git a/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js b/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js index f18f6f972a..7df079ac05 100644 --- a/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js +++ b/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class blurhashForAvatarBanner1595077605646 { constructor() { diff --git a/packages/backend/migration/1595676934834-instance-icon-url.js b/packages/backend/migration/1595676934834-instance-icon-url.js index df9d8199bd..6bccff082b 100644 --- a/packages/backend/migration/1595676934834-instance-icon-url.js +++ b/packages/backend/migration/1595676934834-instance-icon-url.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class instanceIconUrl1595676934834 { constructor() { diff --git a/packages/backend/migration/1595771249699-word-mute.js b/packages/backend/migration/1595771249699-word-mute.js index e8e4ac838b..cfd0a5ccc1 100644 --- a/packages/backend/migration/1595771249699-word-mute.js +++ b/packages/backend/migration/1595771249699-word-mute.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class wordMute1595771249699 { constructor() { diff --git a/packages/backend/migration/1595782306083-word-mute2.js b/packages/backend/migration/1595782306083-word-mute2.js index ab1e40a041..64acf2b721 100644 --- a/packages/backend/migration/1595782306083-word-mute2.js +++ b/packages/backend/migration/1595782306083-word-mute2.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class wordMute21595782306083 { constructor() { diff --git a/packages/backend/migration/1596548170836-channel.js b/packages/backend/migration/1596548170836-channel.js index 242db7d45a..a26991d4d8 100644 --- a/packages/backend/migration/1596548170836-channel.js +++ b/packages/backend/migration/1596548170836-channel.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class channel1596548170836 { constructor() { diff --git a/packages/backend/migration/1596786425167-channel2.js b/packages/backend/migration/1596786425167-channel2.js index 4b17048fef..4e87b11bb5 100644 --- a/packages/backend/migration/1596786425167-channel2.js +++ b/packages/backend/migration/1596786425167-channel2.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class channel21596786425167 { constructor() { diff --git a/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js b/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js index 07283e31df..93e6f186d5 100644 --- a/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js +++ b/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class objectStorageSetPublicRead1597230137744 { constructor() { diff --git a/packages/backend/migration/1597236229720-IncludingNotificationTypes.js b/packages/backend/migration/1597236229720-IncludingNotificationTypes.js index f498fa7d9a..bda702d999 100644 --- a/packages/backend/migration/1597236229720-IncludingNotificationTypes.js +++ b/packages/backend/migration/1597236229720-IncludingNotificationTypes.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class IncludingNotificationTypes1597236229720 { constructor() { diff --git a/packages/backend/migration/1597385880794-add-sensitive-index.js b/packages/backend/migration/1597385880794-add-sensitive-index.js index 8c5c040ba0..ffb94895d7 100644 --- a/packages/backend/migration/1597385880794-add-sensitive-index.js +++ b/packages/backend/migration/1597385880794-add-sensitive-index.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class addSensitiveIndex1597385880794 { constructor() { diff --git a/packages/backend/migration/1597459042300-channel-unread.js b/packages/backend/migration/1597459042300-channel-unread.js index 3157ab7793..5b94d8296a 100644 --- a/packages/backend/migration/1597459042300-channel-unread.js +++ b/packages/backend/migration/1597459042300-channel-unread.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class channelUnread1597459042300 { constructor() { diff --git a/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js b/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js index 2bd8aee358..543e511404 100644 --- a/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js +++ b/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class ChannelNoteIdDescIndex1597893996136 { constructor() { diff --git a/packages/backend/migration/1600353287890-mutingNotificationTypes.js b/packages/backend/migration/1600353287890-mutingNotificationTypes.js index ed3eb7d146..4e0b8ad6eb 100644 --- a/packages/backend/migration/1600353287890-mutingNotificationTypes.js +++ b/packages/backend/migration/1600353287890-mutingNotificationTypes.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class mutingNotificationTypes1600353287890 { constructor() { diff --git a/packages/backend/migration/1603094348345-refine-abuse-user-report.js b/packages/backend/migration/1603094348345-refine-abuse-user-report.js index 4918032a2b..4e052e07c2 100644 --- a/packages/backend/migration/1603094348345-refine-abuse-user-report.js +++ b/packages/backend/migration/1603094348345-refine-abuse-user-report.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class refineAbuseUserReport1603094348345 { constructor() { diff --git a/packages/backend/migration/1603095701770-refine-abuse-user-report2.js b/packages/backend/migration/1603095701770-refine-abuse-user-report2.js index 64e92672f2..2eb205c6e0 100644 --- a/packages/backend/migration/1603095701770-refine-abuse-user-report2.js +++ b/packages/backend/migration/1603095701770-refine-abuse-user-report2.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class refineAbuseUserReport21603095701770 { constructor() { diff --git a/packages/backend/migration/1603776877564-instance-theme-color.js b/packages/backend/migration/1603776877564-instance-theme-color.js index 92440d3f64..5f83bc14e6 100644 --- a/packages/backend/migration/1603776877564-instance-theme-color.js +++ b/packages/backend/migration/1603776877564-instance-theme-color.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class instanceThemeColor1603776877564 { constructor() { diff --git a/packages/backend/migration/1603781553011-instance-favicon.js b/packages/backend/migration/1603781553011-instance-favicon.js index f607c49ffb..758b86408f 100644 --- a/packages/backend/migration/1603781553011-instance-favicon.js +++ b/packages/backend/migration/1603781553011-instance-favicon.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class instanceFavicon1603781553011 { constructor() { diff --git a/packages/backend/migration/1604821689616-delete-auto-watch.js b/packages/backend/migration/1604821689616-delete-auto-watch.js index 4706e8bae9..917ef5b10c 100644 --- a/packages/backend/migration/1604821689616-delete-auto-watch.js +++ b/packages/backend/migration/1604821689616-delete-auto-watch.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class deleteAutoWatch1604821689616 { constructor() { diff --git a/packages/backend/migration/1605408848373-clip-description.js b/packages/backend/migration/1605408848373-clip-description.js index edd5505b30..fedc603b3c 100644 --- a/packages/backend/migration/1605408848373-clip-description.js +++ b/packages/backend/migration/1605408848373-clip-description.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class clipDescription1605408848373 { constructor() { diff --git a/packages/backend/migration/1605408971051-comments.js b/packages/backend/migration/1605408971051-comments.js index 400efd5e70..8ab16859d2 100644 --- a/packages/backend/migration/1605408971051-comments.js +++ b/packages/backend/migration/1605408971051-comments.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class comments1605408971051 { constructor() { diff --git a/packages/backend/migration/1605585339718-instance-pinned-pages.js b/packages/backend/migration/1605585339718-instance-pinned-pages.js index 56ccd44c8e..e6f3c2a785 100644 --- a/packages/backend/migration/1605585339718-instance-pinned-pages.js +++ b/packages/backend/migration/1605585339718-instance-pinned-pages.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class instancePinnedPages1605585339718 { constructor() { diff --git a/packages/backend/migration/1605965516823-instance-images.js b/packages/backend/migration/1605965516823-instance-images.js index 710c75981d..848b53f1ba 100644 --- a/packages/backend/migration/1605965516823-instance-images.js +++ b/packages/backend/migration/1605965516823-instance-images.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class instanceImages1605965516823 { constructor() { diff --git a/packages/backend/migration/1606191203881-no-crawle.js b/packages/backend/migration/1606191203881-no-crawle.js index b9ada4354e..5c878f5a24 100644 --- a/packages/backend/migration/1606191203881-no-crawle.js +++ b/packages/backend/migration/1606191203881-no-crawle.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class noCrawle1606191203881 { constructor() { diff --git a/packages/backend/migration/1607151207216-instance-pinned-clip.js b/packages/backend/migration/1607151207216-instance-pinned-clip.js index 9a4195e74c..67db39fede 100644 --- a/packages/backend/migration/1607151207216-instance-pinned-clip.js +++ b/packages/backend/migration/1607151207216-instance-pinned-clip.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class instancePinnedClip1607151207216 { constructor() { diff --git a/packages/backend/migration/1607353487793-isExplorable.js b/packages/backend/migration/1607353487793-isExplorable.js index d9f1ff4c69..95ee07e917 100644 --- a/packages/backend/migration/1607353487793-isExplorable.js +++ b/packages/backend/migration/1607353487793-isExplorable.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class isExplorable1607353487793 { constructor() { diff --git a/packages/backend/migration/1610277136869-registry.js b/packages/backend/migration/1610277136869-registry.js index 184c062ddb..c5fe2c5a62 100644 --- a/packages/backend/migration/1610277136869-registry.js +++ b/packages/backend/migration/1610277136869-registry.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class registry1610277136869 { constructor() { diff --git a/packages/backend/migration/1610277585759-registry2.js b/packages/backend/migration/1610277585759-registry2.js index 591bafae31..f734a235b0 100644 --- a/packages/backend/migration/1610277585759-registry2.js +++ b/packages/backend/migration/1610277585759-registry2.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class registry21610277585759 { constructor() { diff --git a/packages/backend/migration/1610283021566-registry3.js b/packages/backend/migration/1610283021566-registry3.js index e0289f17ee..c94546c732 100644 --- a/packages/backend/migration/1610283021566-registry3.js +++ b/packages/backend/migration/1610283021566-registry3.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class registry31610283021566 { constructor() { diff --git a/packages/backend/migration/1611354329133-followersUri.js b/packages/backend/migration/1611354329133-followersUri.js index 669ddb480e..7e5f8c3093 100644 --- a/packages/backend/migration/1611354329133-followersUri.js +++ b/packages/backend/migration/1611354329133-followersUri.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class followersUri1611354329133 { constructor() { diff --git a/packages/backend/migration/1611397665007-gallery.js b/packages/backend/migration/1611397665007-gallery.js index f49b2df468..cd5c39cc10 100644 --- a/packages/backend/migration/1611397665007-gallery.js +++ b/packages/backend/migration/1611397665007-gallery.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class gallery1611397665007 { constructor() { diff --git a/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js b/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js index e4d3c0e8ec..c0b1da1e53 100644 --- a/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js +++ b/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class objectStorageS3ForcePathStyle1611547387175 { constructor() { diff --git a/packages/backend/migration/1612619156584-announcement-email.js b/packages/backend/migration/1612619156584-announcement-email.js index bcc718d1c2..f8277725f7 100644 --- a/packages/backend/migration/1612619156584-announcement-email.js +++ b/packages/backend/migration/1612619156584-announcement-email.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class announcementEmail1612619156584 { constructor() { diff --git a/packages/backend/migration/1613155914446-emailNotificationTypes.js b/packages/backend/migration/1613155914446-emailNotificationTypes.js index cd49924d2d..3afe491e48 100644 --- a/packages/backend/migration/1613155914446-emailNotificationTypes.js +++ b/packages/backend/migration/1613155914446-emailNotificationTypes.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class emailNotificationTypes1613155914446 { constructor() { diff --git a/packages/backend/migration/1613181457597-user-lang.js b/packages/backend/migration/1613181457597-user-lang.js index d2cd06848e..33e363477f 100644 --- a/packages/backend/migration/1613181457597-user-lang.js +++ b/packages/backend/migration/1613181457597-user-lang.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class userLang1613181457597 { constructor() { diff --git a/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js b/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js index f2e2c5d357..9c75c0ae54 100644 --- a/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js +++ b/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class useBigintForDriveUsage1613503367223 { constructor() { diff --git a/packages/backend/migration/1615965918224-chart-v2.js b/packages/backend/migration/1615965918224-chart-v2.js index 86fa5b0c00..2c0cacd1d9 100644 --- a/packages/backend/migration/1615965918224-chart-v2.js +++ b/packages/backend/migration/1615965918224-chart-v2.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class chartV21615965918224 { constructor() { diff --git a/packages/backend/migration/1615966519402-chart-v2-2.js b/packages/backend/migration/1615966519402-chart-v2-2.js index c62f1b875c..8d6ebf6a81 100644 --- a/packages/backend/migration/1615966519402-chart-v2-2.js +++ b/packages/backend/migration/1615966519402-chart-v2-2.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class chartV221615966519402 { constructor() { diff --git a/packages/backend/migration/1618637372000-user-last-active-date.js b/packages/backend/migration/1618637372000-user-last-active-date.js index 6c77ace467..8b4652898d 100644 --- a/packages/backend/migration/1618637372000-user-last-active-date.js +++ b/packages/backend/migration/1618637372000-user-last-active-date.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class userLastActiveDate1618637372000 { constructor() { diff --git a/packages/backend/migration/1618639857000-user-hide-online-status.js b/packages/backend/migration/1618639857000-user-hide-online-status.js index e63c8ae11f..1f19a7ebb4 100644 --- a/packages/backend/migration/1618639857000-user-hide-online-status.js +++ b/packages/backend/migration/1618639857000-user-hide-online-status.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class userHideOnlineStatus1618639857000 { constructor() { diff --git a/packages/backend/migration/1619942102890-password-reset.js b/packages/backend/migration/1619942102890-password-reset.js index 922d225dc9..9898011774 100644 --- a/packages/backend/migration/1619942102890-password-reset.js +++ b/packages/backend/migration/1619942102890-password-reset.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class passwordReset1619942102890 { constructor() { diff --git a/packages/backend/migration/1620019354680-ad.js b/packages/backend/migration/1620019354680-ad.js index c96d2bfb33..1ae66d71f4 100644 --- a/packages/backend/migration/1620019354680-ad.js +++ b/packages/backend/migration/1620019354680-ad.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class ad1620019354680 { constructor() { diff --git a/packages/backend/migration/1620364649428-ad2.js b/packages/backend/migration/1620364649428-ad2.js index db1c3e1de1..b9b26be076 100644 --- a/packages/backend/migration/1620364649428-ad2.js +++ b/packages/backend/migration/1620364649428-ad2.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class ad21620364649428 { constructor() { diff --git a/packages/backend/migration/1621479946000-add-note-indexes.js b/packages/backend/migration/1621479946000-add-note-indexes.js index dcf97fa4dc..299c1f6c02 100644 --- a/packages/backend/migration/1621479946000-add-note-indexes.js +++ b/packages/backend/migration/1621479946000-add-note-indexes.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class addNoteIndexes1621479946000 { constructor() { diff --git a/packages/backend/migration/1622679304522-user-profile-description-length.js b/packages/backend/migration/1622679304522-user-profile-description-length.js index 22f6c1c5d9..988456fe7d 100644 --- a/packages/backend/migration/1622679304522-user-profile-description-length.js +++ b/packages/backend/migration/1622679304522-user-profile-description-length.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class userProfileDescriptionLength1622679304522 { constructor() { diff --git a/packages/backend/migration/1622681548499-log-message-length.js b/packages/backend/migration/1622681548499-log-message-length.js index ac16c0e1ba..e1fa22c88b 100644 --- a/packages/backend/migration/1622681548499-log-message-length.js +++ b/packages/backend/migration/1622681548499-log-message-length.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class logMessageLength1622681548499 { constructor() { diff --git a/packages/backend/migration/1626509500668-fix-remote-file-proxy.js b/packages/backend/migration/1626509500668-fix-remote-file-proxy.js index 30c562007b..906e49cabb 100644 --- a/packages/backend/migration/1626509500668-fix-remote-file-proxy.js +++ b/packages/backend/migration/1626509500668-fix-remote-file-proxy.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class fixRemoteFileProxy1626509500668 { constructor() { diff --git a/packages/backend/migration/1629004542760-chart-reindex.js b/packages/backend/migration/1629004542760-chart-reindex.js index a7d459276d..f1d08ecfe4 100644 --- a/packages/backend/migration/1629004542760-chart-reindex.js +++ b/packages/backend/migration/1629004542760-chart-reindex.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class chartReindex1629004542760 { constructor() { diff --git a/packages/backend/migration/1629024377804-deepl-integration.js b/packages/backend/migration/1629024377804-deepl-integration.js index 19c49ffcde..465f1bcca9 100644 --- a/packages/backend/migration/1629024377804-deepl-integration.js +++ b/packages/backend/migration/1629024377804-deepl-integration.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class deeplIntegration1629024377804 { constructor() { diff --git a/packages/backend/migration/1629288472000-fix-channel-userId.js b/packages/backend/migration/1629288472000-fix-channel-userId.js index 02a1199b09..9f946ad550 100644 --- a/packages/backend/migration/1629288472000-fix-channel-userId.js +++ b/packages/backend/migration/1629288472000-fix-channel-userId.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class fixChannelUserId1629288472000 { constructor() { diff --git a/packages/backend/migration/1629512953000-user-is-deleted.js b/packages/backend/migration/1629512953000-user-is-deleted.js index a7848d5690..78bbd8bbee 100644 --- a/packages/backend/migration/1629512953000-user-is-deleted.js +++ b/packages/backend/migration/1629512953000-user-is-deleted.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class isUserDeleted1629512953000 { constructor() { diff --git a/packages/backend/migration/1629778475000-deepl-integration2.js b/packages/backend/migration/1629778475000-deepl-integration2.js index 699f06c768..b719dcf57f 100644 --- a/packages/backend/migration/1629778475000-deepl-integration2.js +++ b/packages/backend/migration/1629778475000-deepl-integration2.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class deeplIntegration21629778475000 { constructor() { diff --git a/packages/backend/migration/1629833361000-AddShowTLReplies.js b/packages/backend/migration/1629833361000-AddShowTLReplies.js index 5d4c938a7b..00aef6aeb8 100644 --- a/packages/backend/migration/1629833361000-AddShowTLReplies.js +++ b/packages/backend/migration/1629833361000-AddShowTLReplies.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class addShowTLReplies1629833361000 { constructor() { diff --git a/packages/backend/migration/1629968054000_userInstanceBlocks.js b/packages/backend/migration/1629968054000_userInstanceBlocks.js index 1f202d9f66..e8168e372e 100644 --- a/packages/backend/migration/1629968054000_userInstanceBlocks.js +++ b/packages/backend/migration/1629968054000_userInstanceBlocks.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class userInstanceBlocks1629968054000 { constructor() { diff --git a/packages/backend/migration/1633068642000-email-required-for-signup.js b/packages/backend/migration/1633068642000-email-required-for-signup.js index d592f3ca21..230227d364 100644 --- a/packages/backend/migration/1633068642000-email-required-for-signup.js +++ b/packages/backend/migration/1633068642000-email-required-for-signup.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class emailRequiredForSignup1633068642000 { constructor() { diff --git a/packages/backend/migration/1633071909016-user-pending.js b/packages/backend/migration/1633071909016-user-pending.js index 17cf5c11be..f0d037967f 100644 --- a/packages/backend/migration/1633071909016-user-pending.js +++ b/packages/backend/migration/1633071909016-user-pending.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class userPending1633071909016 { constructor() { diff --git a/packages/backend/migration/1634486652000-user-public-reactions.js b/packages/backend/migration/1634486652000-user-public-reactions.js index e741122491..09870c79c6 100644 --- a/packages/backend/migration/1634486652000-user-public-reactions.js +++ b/packages/backend/migration/1634486652000-user-public-reactions.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class userPublicReactions1634486652000 { constructor() { diff --git a/packages/backend/migration/1634902659689-delete-log.js b/packages/backend/migration/1634902659689-delete-log.js index 555a0020c3..e4e625536b 100644 --- a/packages/backend/migration/1634902659689-delete-log.js +++ b/packages/backend/migration/1634902659689-delete-log.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class deleteLog1634902659689 { constructor() { diff --git a/packages/backend/migration/1635500777168-note-thread-mute.js b/packages/backend/migration/1635500777168-note-thread-mute.js index a790cace33..9f376c4795 100644 --- a/packages/backend/migration/1635500777168-note-thread-mute.js +++ b/packages/backend/migration/1635500777168-note-thread-mute.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class noteThreadMute1635500777168 { constructor() { diff --git a/packages/backend/migration/1636197624383-ff-visibility.js b/packages/backend/migration/1636197624383-ff-visibility.js index 89028f3c22..aa089d42ac 100644 --- a/packages/backend/migration/1636197624383-ff-visibility.js +++ b/packages/backend/migration/1636197624383-ff-visibility.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class ffVisibility1636197624383 { constructor() { diff --git a/packages/backend/migration/1636697408073-remove-via-mobile.js b/packages/backend/migration/1636697408073-remove-via-mobile.js index 36e96fd21e..c014ceb921 100644 --- a/packages/backend/migration/1636697408073-remove-via-mobile.js +++ b/packages/backend/migration/1636697408073-remove-via-mobile.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class removeViaMobile1636697408073 { name = 'removeViaMobile1636697408073' diff --git a/packages/backend/migration/1637320813000-forwarded-report.js b/packages/backend/migration/1637320813000-forwarded-report.js index 1e39bd5c3f..0d1f48beb4 100644 --- a/packages/backend/migration/1637320813000-forwarded-report.js +++ b/packages/backend/migration/1637320813000-forwarded-report.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class forwardedReport1637320813000 { name = 'forwardedReport1637320813000'; diff --git a/packages/backend/migration/1639325650583-chart-v3.js b/packages/backend/migration/1639325650583-chart-v3.js index e2a4e920c9..e6209e2b70 100644 --- a/packages/backend/migration/1639325650583-chart-v3.js +++ b/packages/backend/migration/1639325650583-chart-v3.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class chartV31639325650583 { name = 'chartV31639325650583' diff --git a/packages/backend/migration/1642611822809-emoji-url.js b/packages/backend/migration/1642611822809-emoji-url.js index d38f8cc08c..212fc957ad 100644 --- a/packages/backend/migration/1642611822809-emoji-url.js +++ b/packages/backend/migration/1642611822809-emoji-url.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class emojiUrl1642611822809 { name = 'emojiUrl1642611822809' diff --git a/packages/backend/migration/1642613870898-drive-file-webpublic-type.js b/packages/backend/migration/1642613870898-drive-file-webpublic-type.js index 15434f7d0c..e50770fff3 100644 --- a/packages/backend/migration/1642613870898-drive-file-webpublic-type.js +++ b/packages/backend/migration/1642613870898-drive-file-webpublic-type.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class driveFileWebpublicType1642613870898 { name = 'driveFileWebpublicType1642613870898' diff --git a/packages/backend/migration/1643963705770-chart-v4.js b/packages/backend/migration/1643963705770-chart-v4.js index 8b320c2b41..af0bd18e58 100644 --- a/packages/backend/migration/1643963705770-chart-v4.js +++ b/packages/backend/migration/1643963705770-chart-v4.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class chartV41643963705770 { name = 'chartV41643963705770' diff --git a/packages/backend/migration/1643966656277-chart-v5.js b/packages/backend/migration/1643966656277-chart-v5.js index df84002f78..b3389a6539 100644 --- a/packages/backend/migration/1643966656277-chart-v5.js +++ b/packages/backend/migration/1643966656277-chart-v5.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class chartV51643966656277 { name = 'chartV51643966656277' diff --git a/packages/backend/migration/1643967331284-chart-v6.js b/packages/backend/migration/1643967331284-chart-v6.js index 119198f4a5..1197bdd717 100644 --- a/packages/backend/migration/1643967331284-chart-v6.js +++ b/packages/backend/migration/1643967331284-chart-v6.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class chartV61643967331284 { name = 'chartV61643967331284' diff --git a/packages/backend/migration/1644010796173-convert-hard-mutes.js b/packages/backend/migration/1644010796173-convert-hard-mutes.js index 207a759b8e..1a5316ac05 100644 --- a/packages/backend/migration/1644010796173-convert-hard-mutes.js +++ b/packages/backend/migration/1644010796173-convert-hard-mutes.js @@ -1,5 +1,9 @@ -import RE2 from 're2'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ +import RE2 from 're2'; export class convertHardMutes1644010796173 { name = 'convertHardMutes1644010796173' diff --git a/packages/backend/migration/1644058404077-chart-v7.js b/packages/backend/migration/1644058404077-chart-v7.js index f05ad003db..a850d5f48f 100644 --- a/packages/backend/migration/1644058404077-chart-v7.js +++ b/packages/backend/migration/1644058404077-chart-v7.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class chartV71644058404077 { name = 'chartV71644058404077' diff --git a/packages/backend/migration/1644059847460-chart-v8.js b/packages/backend/migration/1644059847460-chart-v8.js index a5339c0ebd..2e20159ba9 100644 --- a/packages/backend/migration/1644059847460-chart-v8.js +++ b/packages/backend/migration/1644059847460-chart-v8.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class chartV81644059847460 { name = 'chartV81644059847460' @@ -14,9 +17,9 @@ export class chartV81644059847460 { await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___local_users" TYPE integer USING "___local_users"::integer`); await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___remote_users" TYPE integer USING "___remote_users"::integer`); } - + async down(queryRunner) { - + await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___local_users" TYPE bigint USING "___local_users"::bigint`); await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___remote_users" TYPE bigint USING "___remote_users"::bigint`); await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___local_users" TYPE bigint USING "___local_users"::bigint`); diff --git a/packages/backend/migration/1644060125705-chart-v9.js b/packages/backend/migration/1644060125705-chart-v9.js index da35d42315..d1d9469ea2 100644 --- a/packages/backend/migration/1644060125705-chart-v9.js +++ b/packages/backend/migration/1644060125705-chart-v9.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class chartV91644060125705 { name = 'chartV91644060125705' @@ -14,9 +17,9 @@ export class chartV91644060125705 { await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" ALTER COLUMN "___local_users" TYPE integer USING "___local_users"::integer`); await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" ALTER COLUMN "___remote_users" TYPE integer USING "___remote_users"::integer`); } - + async down(queryRunner) { - + await queryRunner.query(`ALTER TABLE "__chart__hashtag" ALTER COLUMN "___local_users" TYPE bigint USING "___local_users"::bigint`); await queryRunner.query(`ALTER TABLE "__chart__hashtag" ALTER COLUMN "___remote_users" TYPE bigint USING "___remote_users"::bigint`); await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" ALTER COLUMN "___local_users" TYPE bigint USING "___local_users"::bigint`); diff --git a/packages/backend/migration/1644073149413-chart-v10.js b/packages/backend/migration/1644073149413-chart-v10.js index 7260bbeca4..466ae59837 100644 --- a/packages/backend/migration/1644073149413-chart-v10.js +++ b/packages/backend/migration/1644073149413-chart-v10.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class chartV101644073149413 { name = 'chartV101644073149413' diff --git a/packages/backend/migration/1644095659741-chart-v11.js b/packages/backend/migration/1644095659741-chart-v11.js index 309fff1d9a..5c98e25d86 100644 --- a/packages/backend/migration/1644095659741-chart-v11.js +++ b/packages/backend/migration/1644095659741-chart-v11.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class chartV111644095659741 { name = 'chartV111644095659741' diff --git a/packages/backend/migration/1644328606241-chart-v12.js b/packages/backend/migration/1644328606241-chart-v12.js index c3c7e44f95..2a7272fd22 100644 --- a/packages/backend/migration/1644328606241-chart-v12.js +++ b/packages/backend/migration/1644328606241-chart-v12.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class chartV121644328606241 { name = 'chartV121644328606241' diff --git a/packages/backend/migration/1644331238153-chart-v13.js b/packages/backend/migration/1644331238153-chart-v13.js index 639f7b4e20..7e33b0a8e9 100644 --- a/packages/backend/migration/1644331238153-chart-v13.js +++ b/packages/backend/migration/1644331238153-chart-v13.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class chartV131644331238153 { name = 'chartV131644331238153' diff --git a/packages/backend/migration/1644344266289-chart-v14.js b/packages/backend/migration/1644344266289-chart-v14.js index a0d9cfc38c..2050d54591 100644 --- a/packages/backend/migration/1644344266289-chart-v14.js +++ b/packages/backend/migration/1644344266289-chart-v14.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class chartV141644344266289 { name = 'chartV141644344266289' diff --git a/packages/backend/migration/1644395759931-instance-theme-color.js b/packages/backend/migration/1644395759931-instance-theme-color.js index 8f335ad210..ac842e4fe5 100644 --- a/packages/backend/migration/1644395759931-instance-theme-color.js +++ b/packages/backend/migration/1644395759931-instance-theme-color.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class instanceThemeColor1644395759931 { name = 'instanceThemeColor1644395759931' diff --git a/packages/backend/migration/1644481657998-chart-v15.js b/packages/backend/migration/1644481657998-chart-v15.js index b50ca87c40..ad5589df8b 100644 --- a/packages/backend/migration/1644481657998-chart-v15.js +++ b/packages/backend/migration/1644481657998-chart-v15.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class chartV151644481657998 { name = 'chartV151644481657998' diff --git a/packages/backend/migration/1644551208096-following-indexes.js b/packages/backend/migration/1644551208096-following-indexes.js index 276473ff6c..795b8e900e 100644 --- a/packages/backend/migration/1644551208096-following-indexes.js +++ b/packages/backend/migration/1644551208096-following-indexes.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class followingIndexes1644551208096 { name = 'followingIndexes1644551208096' diff --git a/packages/backend/migration/1645340161439-remove-max-note-text-length.js b/packages/backend/migration/1645340161439-remove-max-note-text-length.js index c88cb70bfb..84eaeddfa4 100644 --- a/packages/backend/migration/1645340161439-remove-max-note-text-length.js +++ b/packages/backend/migration/1645340161439-remove-max-note-text-length.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class removeMaxNoteTextLength1645340161439 { name = 'removeMaxNoteTextLength1645340161439' diff --git a/packages/backend/migration/1645599900873-federation-chart-pubsub.js b/packages/backend/migration/1645599900873-federation-chart-pubsub.js index fd7cb6d5a1..4f9f501cca 100644 --- a/packages/backend/migration/1645599900873-federation-chart-pubsub.js +++ b/packages/backend/migration/1645599900873-federation-chart-pubsub.js @@ -1,4 +1,7 @@ - +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class federationChartPubsub1645599900873 { name = 'federationChartPubsub1645599900873' diff --git a/packages/backend/migration/1646143552768-instance-default-theme.js b/packages/backend/migration/1646143552768-instance-default-theme.js index 029354fd92..3532916304 100644 --- a/packages/backend/migration/1646143552768-instance-default-theme.js +++ b/packages/backend/migration/1646143552768-instance-default-theme.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class instanceDefaultTheme1646143552768 { name = 'instanceDefaultTheme1646143552768' diff --git a/packages/backend/migration/1646387162108-mute-expires-at.js b/packages/backend/migration/1646387162108-mute-expires-at.js index c8be8f3c54..868f5c87ef 100644 --- a/packages/backend/migration/1646387162108-mute-expires-at.js +++ b/packages/backend/migration/1646387162108-mute-expires-at.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class muteExpiresAt1646387162108 { name = 'muteExpiresAt1646387162108' diff --git a/packages/backend/migration/1646549089451-poll-ended-notification.js b/packages/backend/migration/1646549089451-poll-ended-notification.js index 38a38ce64d..fa7327ff9c 100644 --- a/packages/backend/migration/1646549089451-poll-ended-notification.js +++ b/packages/backend/migration/1646549089451-poll-ended-notification.js @@ -1,3 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class pollEndedNotification1646549089451 { name = 'pollEndedNotification1646549089451' diff --git a/packages/backend/migration/1646633030285-chart-federation-active.js b/packages/backend/migration/1646633030285-chart-federation-active.js index 952289c8f8..b9863746ad 100644 --- a/packages/backend/migration/1646633030285-chart-federation-active.js +++ b/packages/backend/migration/1646633030285-chart-federation-active.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class chartFederationActive1646633030285 { name = 'chartFederationActive1646633030285' diff --git a/packages/backend/migration/1646655454495-remove-instance-drive-columns.js b/packages/backend/migration/1646655454495-remove-instance-drive-columns.js index a0ee1b2c43..8fd96ed4c6 100644 --- a/packages/backend/migration/1646655454495-remove-instance-drive-columns.js +++ b/packages/backend/migration/1646655454495-remove-instance-drive-columns.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class removeInstanceDriveColumns1646655454495 { name = 'removeInstanceDriveColumns1646655454495' diff --git a/packages/backend/migration/1646732390560-chart-federation-active-sub-pub.js b/packages/backend/migration/1646732390560-chart-federation-active-sub-pub.js index c9a847cbcf..1b28d012ae 100644 --- a/packages/backend/migration/1646732390560-chart-federation-active-sub-pub.js +++ b/packages/backend/migration/1646732390560-chart-federation-active-sub-pub.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class chartFederationActiveSubPub1646732390560 { name = 'chartFederationActiveSubPub1646732390560' diff --git a/packages/backend/migration/1648548247382-webhook.js b/packages/backend/migration/1648548247382-webhook.js index aea369a5cc..fc2a691918 100644 --- a/packages/backend/migration/1648548247382-webhook.js +++ b/packages/backend/migration/1648548247382-webhook.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class webhook1648548247382 { name = 'webhook1648548247382' diff --git a/packages/backend/migration/1648816172177-webhook-2.js b/packages/backend/migration/1648816172177-webhook-2.js index 2feb68d611..a7bccff82d 100644 --- a/packages/backend/migration/1648816172177-webhook-2.js +++ b/packages/backend/migration/1648816172177-webhook-2.js @@ -1,3 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class webhook21648816172177 { name = 'webhook21648816172177' diff --git a/packages/backend/migration/1651224615271-foreign-key.js b/packages/backend/migration/1651224615271-foreign-key.js index 535d21731a..12e4646329 100644 --- a/packages/backend/migration/1651224615271-foreign-key.js +++ b/packages/backend/migration/1651224615271-foreign-key.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class foreignKeyReports1651224615271 { name = 'foreignKeyReports1651224615271' diff --git a/packages/backend/migration/1652859567549-uniform-themecolor.js b/packages/backend/migration/1652859567549-uniform-themecolor.js index 8da1fd7fbb..422e63dfec 100644 --- a/packages/backend/migration/1652859567549-uniform-themecolor.js +++ b/packages/backend/migration/1652859567549-uniform-themecolor.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import tinycolor from 'tinycolor2'; export class uniformThemecolor1652859567549 { diff --git a/packages/backend/migration/1655368940105-nsfw-detection.js b/packages/backend/migration/1655368940105-nsfw-detection.js index 9268f43407..ad37ff6f83 100644 --- a/packages/backend/migration/1655368940105-nsfw-detection.js +++ b/packages/backend/migration/1655368940105-nsfw-detection.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class nsfwDetection1655368940105 { name = 'nsfwDetection1655368940105' diff --git a/packages/backend/migration/1655371960534-nsfw-detection-2.js b/packages/backend/migration/1655371960534-nsfw-detection-2.js index aac6f37dad..e6cc266178 100644 --- a/packages/backend/migration/1655371960534-nsfw-detection-2.js +++ b/packages/backend/migration/1655371960534-nsfw-detection-2.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class nsfwDetection21655371960534 { name = 'nsfwDetection21655371960534' diff --git a/packages/backend/migration/1655388169582-nsfw-detection-3.js b/packages/backend/migration/1655388169582-nsfw-detection-3.js index a5c80cf968..40362cc20c 100644 --- a/packages/backend/migration/1655388169582-nsfw-detection-3.js +++ b/packages/backend/migration/1655388169582-nsfw-detection-3.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class nsfwDetection31655388169582 { name = 'nsfwDetection31655388169582' diff --git a/packages/backend/migration/1655393015659-nsfw-detection-4.js b/packages/backend/migration/1655393015659-nsfw-detection-4.js index e780732623..d74fe9c929 100644 --- a/packages/backend/migration/1655393015659-nsfw-detection-4.js +++ b/packages/backend/migration/1655393015659-nsfw-detection-4.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class nsfwDetection41655393015659 { name = 'nsfwDetection41655393015659' diff --git a/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js b/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js index f257cd112f..7e97f9dc74 100644 --- a/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js +++ b/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class driveCapacityOverrideMb1655813815729 { name = 'driveCapacityOverrideMb1655813815729' diff --git a/packages/backend/migration/1655918165614-user-ip.js b/packages/backend/migration/1655918165614-user-ip.js index 2294fbaf19..ccb3ceb49d 100644 --- a/packages/backend/migration/1655918165614-user-ip.js +++ b/packages/backend/migration/1655918165614-user-ip.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class userIp1655918165614 { name = 'userIp1655918165614' diff --git a/packages/backend/migration/1656122560740-file-ip.js b/packages/backend/migration/1656122560740-file-ip.js index b59e7a911f..dc02df0e68 100644 --- a/packages/backend/migration/1656122560740-file-ip.js +++ b/packages/backend/migration/1656122560740-file-ip.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class fileIp1656122560740 { name = 'fileIp1656122560740' diff --git a/packages/backend/migration/1656251734807-nsfw-detection-5.js b/packages/backend/migration/1656251734807-nsfw-detection-5.js index 6f0c536907..06da9251b1 100644 --- a/packages/backend/migration/1656251734807-nsfw-detection-5.js +++ b/packages/backend/migration/1656251734807-nsfw-detection-5.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class nsfwDetection51656251734807 { name = 'nsfwDetection51656251734807' diff --git a/packages/backend/migration/1656328812281-ip-2.js b/packages/backend/migration/1656328812281-ip-2.js index b0ee1ebfc7..1b53e697de 100644 --- a/packages/backend/migration/1656328812281-ip-2.js +++ b/packages/backend/migration/1656328812281-ip-2.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class ip21656328812281 { name = 'ip21656328812281' diff --git a/packages/backend/migration/1656408772602-nsfw-detection-6.js b/packages/backend/migration/1656408772602-nsfw-detection-6.js index 7ef223a4c6..0adc8bb793 100644 --- a/packages/backend/migration/1656408772602-nsfw-detection-6.js +++ b/packages/backend/migration/1656408772602-nsfw-detection-6.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class nsfwDetection61656408772602 { name = 'nsfwDetection61656408772602' diff --git a/packages/backend/migration/1656772790599-user-moderation-note.js b/packages/backend/migration/1656772790599-user-moderation-note.js index 133bcffe1a..63a993851f 100644 --- a/packages/backend/migration/1656772790599-user-moderation-note.js +++ b/packages/backend/migration/1656772790599-user-moderation-note.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class userModerationNote1656772790599 { name = 'userModerationNote1656772790599' diff --git a/packages/backend/migration/1657346559800-active-email-validation.js b/packages/backend/migration/1657346559800-active-email-validation.js index f8e03eeb07..44b1f3f4fa 100644 --- a/packages/backend/migration/1657346559800-active-email-validation.js +++ b/packages/backend/migration/1657346559800-active-email-validation.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class activeEmailValidation1657346559800 { name = 'activeEmailValidation1657346559800' diff --git a/packages/backend/migration/1664694635394-turnstile.js b/packages/backend/migration/1664694635394-turnstile.js index 4a33443950..3ec6da9136 100644 --- a/packages/backend/migration/1664694635394-turnstile.js +++ b/packages/backend/migration/1664694635394-turnstile.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class turnstile1664694635394 { name = 'turnstile1664694635394' diff --git a/packages/backend/migration/1665091090561-add-renote-muting.js b/packages/backend/migration/1665091090561-add-renote-muting.js index d2ed2bd2e9..a22d7037f3 100644 --- a/packages/backend/migration/1665091090561-add-renote-muting.js +++ b/packages/backend/migration/1665091090561-add-renote-muting.js @@ -1,3 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export class addRenoteMuting1665091090561 { constructor() { @@ -12,5 +16,9 @@ export class addRenoteMuting1665091090561 { } async down(queryRunner) { + await queryRunner.query(`DROP INDEX "IDX_renote_muting_muterId"`); + await queryRunner.query(`DROP INDEX "IDX_renote_muting_muteeId"`); + await queryRunner.query(`DROP INDEX "IDX_renote_muting_createdAt"`); + await queryRunner.query(`DROP TABLE "renote_muting"`); } } diff --git a/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js b/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js index 2265b00617..a317468ac9 100644 --- a/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js +++ b/packages/backend/migration/1669138716634-whetherPushNotifyToSendReadMessage.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class whetherPushNotifyToSendReadMessage1669138716634 { name = 'whetherPushNotifyToSendReadMessage1669138716634' diff --git a/packages/backend/migration/1671924750884-RetentionAggregation.js b/packages/backend/migration/1671924750884-RetentionAggregation.js index ed81a4b5e9..5057bf1060 100644 --- a/packages/backend/migration/1671924750884-RetentionAggregation.js +++ b/packages/backend/migration/1671924750884-RetentionAggregation.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class RetentionAggregation1671924750884 { name = 'RetentionAggregation1671924750884' diff --git a/packages/backend/migration/1671926422832-RetentionAggregation2.js b/packages/backend/migration/1671926422832-RetentionAggregation2.js index 725429e6ef..665e24d721 100644 --- a/packages/backend/migration/1671926422832-RetentionAggregation2.js +++ b/packages/backend/migration/1671926422832-RetentionAggregation2.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class RetentionAggregation21671926422832 { name = 'RetentionAggregation21671926422832' diff --git a/packages/backend/migration/1672562400597-PerUserPvChart.js b/packages/backend/migration/1672562400597-PerUserPvChart.js index 4da6b9a8b3..1fbe1eb14a 100644 --- a/packages/backend/migration/1672562400597-PerUserPvChart.js +++ b/packages/backend/migration/1672562400597-PerUserPvChart.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class PerUserPvChart1672562400597 { name = 'PerUserPvChart1672562400597' diff --git a/packages/backend/migration/1672703171386-remove-latestRequestSentAt.js b/packages/backend/migration/1672703171386-remove-latestRequestSentAt.js index c9b28dd7e1..f053e5c20c 100644 --- a/packages/backend/migration/1672703171386-remove-latestRequestSentAt.js +++ b/packages/backend/migration/1672703171386-remove-latestRequestSentAt.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class removeLatestRequestSentAt1672703171386 { name = 'removeLatestRequestSentAt1672703171386' diff --git a/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js b/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js index 38a6769851..b71f7e1306 100644 --- a/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js +++ b/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class removeLastCommunicatedAt1672704017999 { name = 'removeLastCommunicatedAt1672704017999' diff --git a/packages/backend/migration/1672704136584-remove-latestStatus.js b/packages/backend/migration/1672704136584-remove-latestStatus.js index 937c2fe8fd..f08ed96a45 100644 --- a/packages/backend/migration/1672704136584-remove-latestStatus.js +++ b/packages/backend/migration/1672704136584-remove-latestStatus.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class removeLatestStatus1672704136584 { name = 'removeLatestStatus1672704136584' diff --git a/packages/backend/migration/1672822262496-Flash.js b/packages/backend/migration/1672822262496-Flash.js index 6c2338fab2..e45055b3cc 100644 --- a/packages/backend/migration/1672822262496-Flash.js +++ b/packages/backend/migration/1672822262496-Flash.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class Flash1672822262496 { name = 'Flash1672822262496' diff --git a/packages/backend/migration/1673336077243-PollChoiceLength.js b/packages/backend/migration/1673336077243-PollChoiceLength.js index 810c626e04..8c4a5007e4 100644 --- a/packages/backend/migration/1673336077243-PollChoiceLength.js +++ b/packages/backend/migration/1673336077243-PollChoiceLength.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class PollChoiceLength1673336077243 { name = 'PollChoiceLength1673336077243' diff --git a/packages/backend/migration/1673500412259-Role.js b/packages/backend/migration/1673500412259-Role.js index a8acedf5b7..2bf6a7f4e8 100644 --- a/packages/backend/migration/1673500412259-Role.js +++ b/packages/backend/migration/1673500412259-Role.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class Role1673500412259 { name = 'Role1673500412259' diff --git a/packages/backend/migration/1673515526953-RoleColor.js b/packages/backend/migration/1673515526953-RoleColor.js index 343eedf346..693dcfb0b6 100644 --- a/packages/backend/migration/1673515526953-RoleColor.js +++ b/packages/backend/migration/1673515526953-RoleColor.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class RoleColor1673515526953 { name = 'RoleColor1673515526953' diff --git a/packages/backend/migration/1673522856499-RoleIroiro.js b/packages/backend/migration/1673522856499-RoleIroiro.js index a1e64d49fe..10a6eef162 100644 --- a/packages/backend/migration/1673522856499-RoleIroiro.js +++ b/packages/backend/migration/1673522856499-RoleIroiro.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class RoleIroiro1673522856499 { name = 'RoleIroiro1673522856499' diff --git a/packages/backend/migration/1673524604156-RoleLastUsedAt.js b/packages/backend/migration/1673524604156-RoleLastUsedAt.js index 786ef07f5e..5bbd0c39ac 100644 --- a/packages/backend/migration/1673524604156-RoleLastUsedAt.js +++ b/packages/backend/migration/1673524604156-RoleLastUsedAt.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class RoleLastUsedAt1673524604156 { name = 'RoleLastUsedAt1673524604156' diff --git a/packages/backend/migration/1673570377815-RoleConditional.js b/packages/backend/migration/1673570377815-RoleConditional.js index 11ae4f00c6..d2b25d121e 100644 --- a/packages/backend/migration/1673570377815-RoleConditional.js +++ b/packages/backend/migration/1673570377815-RoleConditional.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class RoleConditional1673570377815 { name = 'RoleConditional1673570377815' diff --git a/packages/backend/migration/1673575973645-MetaClean.js b/packages/backend/migration/1673575973645-MetaClean.js index 11be4c1cdd..7671785d94 100644 --- a/packages/backend/migration/1673575973645-MetaClean.js +++ b/packages/backend/migration/1673575973645-MetaClean.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class MetaClean1673575973645 { name = 'MetaClean1673575973645' diff --git a/packages/backend/migration/1673783015567-Policies.js b/packages/backend/migration/1673783015567-Policies.js index 8b36921d41..4f76752c9f 100644 --- a/packages/backend/migration/1673783015567-Policies.js +++ b/packages/backend/migration/1673783015567-Policies.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class Policies1673783015567 { name = 'Policies1673783015567' diff --git a/packages/backend/migration/1673812883772-firstRetrievedAt.js b/packages/backend/migration/1673812883772-firstRetrievedAt.js index 5603bbc7c4..82990e30b6 100644 --- a/packages/backend/migration/1673812883772-firstRetrievedAt.js +++ b/packages/backend/migration/1673812883772-firstRetrievedAt.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class firstRetrievedAt1673812883772 { name = 'firstRetrievedAt1673812883772' diff --git a/packages/backend/migration/1674086433654-flashScriptLength.js b/packages/backend/migration/1674086433654-flashScriptLength.js index a4d149fe15..996fe8c691 100644 --- a/packages/backend/migration/1674086433654-flashScriptLength.js +++ b/packages/backend/migration/1674086433654-flashScriptLength.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class flashScriptLength1674086433654 { name = 'flashScriptLength1674086433654' diff --git a/packages/backend/migration/1674118260469-achievement.js b/packages/backend/migration/1674118260469-achievement.js index 131ab96f80..5d79dc669e 100644 --- a/packages/backend/migration/1674118260469-achievement.js +++ b/packages/backend/migration/1674118260469-achievement.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class achievement1674118260469 { name = 'achievement1674118260469' diff --git a/packages/backend/migration/1674255666603-loggedInDates.js b/packages/backend/migration/1674255666603-loggedInDates.js index 6d75ab6436..a6cf4b400f 100644 --- a/packages/backend/migration/1674255666603-loggedInDates.js +++ b/packages/backend/migration/1674255666603-loggedInDates.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class loggedInDates1674255666603 { name = 'loggedInDates1674255666603' diff --git a/packages/backend/migration/1675053125067-fixforeignkeyreports.js b/packages/backend/migration/1675053125067-fixforeignkeyreports.js index ca5c10b11f..d24dc5ec5a 100644 --- a/packages/backend/migration/1675053125067-fixforeignkeyreports.js +++ b/packages/backend/migration/1675053125067-fixforeignkeyreports.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class fixforeignkeyreports1675053125067 { name = 'fixforeignkeyreports1675053125067' diff --git a/packages/backend/migration/1675404035646-cleanup.js b/packages/backend/migration/1675404035646-cleanup.js index 09b22ee393..c4e4332bbc 100644 --- a/packages/backend/migration/1675404035646-cleanup.js +++ b/packages/backend/migration/1675404035646-cleanup.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class cleanup1675404035646 { name = 'cleanup1675404035646' diff --git a/packages/backend/migration/1675557528704-role-icon-badge.js b/packages/backend/migration/1675557528704-role-icon-badge.js index 0ebca088e3..ee39c07a51 100644 --- a/packages/backend/migration/1675557528704-role-icon-badge.js +++ b/packages/backend/migration/1675557528704-role-icon-badge.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class roleIconBadge1675557528704 { name = 'roleIconBadge1675557528704' diff --git a/packages/backend/migration/1676434944993-drop-group.js b/packages/backend/migration/1676434944993-drop-group.js index c856046eb9..1db2d5818f 100644 --- a/packages/backend/migration/1676434944993-drop-group.js +++ b/packages/backend/migration/1676434944993-drop-group.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class dropGroup1676434944993 { name = 'dropGroup1676434944993' diff --git a/packages/backend/migration/1676438468213-ad3.js b/packages/backend/migration/1676438468213-ad3.js index 18f56e8d36..8347f56b95 100644 --- a/packages/backend/migration/1676438468213-ad3.js +++ b/packages/backend/migration/1676438468213-ad3.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class ad1676438468213 { name = 'ad1676438468213'; async up(queryRunner) { diff --git a/packages/backend/migration/1677054292210-ad4.js b/packages/backend/migration/1677054292210-ad4.js new file mode 100644 index 0000000000..037e21059c --- /dev/null +++ b/packages/backend/migration/1677054292210-ad4.js @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class ad1677054292210 { + name = 'ad1677054292210'; + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "ad" ADD "dayOfWeek" integer NOT NULL Default 0`); + } + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "dayOfWeek"`); + } +} diff --git a/packages/backend/migration/1677570181236-role-assignment-expires-at.js b/packages/backend/migration/1677570181236-role-assignment-expires-at.js index 3ac2edab0a..e44bca1d20 100644 --- a/packages/backend/migration/1677570181236-role-assignment-expires-at.js +++ b/packages/backend/migration/1677570181236-role-assignment-expires-at.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class roleAssignmentExpiresAt1677570181236 { name = 'roleAssignmentExpiresAt1677570181236' diff --git a/packages/backend/migration/1678164627293-per-note-reaction-acceptance.js b/packages/backend/migration/1678164627293-per-note-reaction-acceptance.js index f1765dd146..c85aafbd4c 100644 --- a/packages/backend/migration/1678164627293-per-note-reaction-acceptance.js +++ b/packages/backend/migration/1678164627293-per-note-reaction-acceptance.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class perNoteReactionAcceptance1678164627293 { name = 'perNoteReactionAcceptance1678164627293' diff --git a/packages/backend/migration/1678426061773-tweak-varchar-length.js b/packages/backend/migration/1678426061773-tweak-varchar-length.js index 984c41dba6..2541f99a19 100644 --- a/packages/backend/migration/1678426061773-tweak-varchar-length.js +++ b/packages/backend/migration/1678426061773-tweak-varchar-length.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class tweakVarcharLength1678426061773 { name = 'tweakVarcharLength1678426061773' diff --git a/packages/backend/migration/1678427401214-remove-unused.js b/packages/backend/migration/1678427401214-remove-unused.js index ee643e7776..59f42da080 100644 --- a/packages/backend/migration/1678427401214-remove-unused.js +++ b/packages/backend/migration/1678427401214-remove-unused.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class removeUnused1678427401214 { name = 'removeUnused1678427401214' diff --git a/packages/backend/migration/1678602320354-role-display-order.js b/packages/backend/migration/1678602320354-role-display-order.js index de8f6f1033..0ab7b0c3e2 100644 --- a/packages/backend/migration/1678602320354-role-display-order.js +++ b/packages/backend/migration/1678602320354-role-display-order.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class roleDisplayOrder1678602320354 { name = 'roleDisplayOrder1678602320354' diff --git a/packages/backend/migration/1678694614599-sensitive-words.js b/packages/backend/migration/1678694614599-sensitive-words.js index 6d4c5730c7..5f69424eca 100644 --- a/packages/backend/migration/1678694614599-sensitive-words.js +++ b/packages/backend/migration/1678694614599-sensitive-words.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class sensitiveWords1678694614599 { name = 'sensitiveWords1678694614599' diff --git a/packages/backend/migration/1678869617549-retention-date-key.js b/packages/backend/migration/1678869617549-retention-date-key.js index 1a31b9a750..55bf6248e6 100644 --- a/packages/backend/migration/1678869617549-retention-date-key.js +++ b/packages/backend/migration/1678869617549-retention-date-key.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class retentionDateKey1678869617549 { name = 'retentionDateKey1678869617549' diff --git a/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js b/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js index 656a921770..0054e78f88 100644 --- a/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js +++ b/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class addPropsForCustomEmoji1678945242650 { name = 'addPropsForCustomEmoji1678945242650' diff --git a/packages/backend/migration/1678953978856-clip-favorite.js b/packages/backend/migration/1678953978856-clip-favorite.js index aa5dc93a6e..13145497bb 100644 --- a/packages/backend/migration/1678953978856-clip-favorite.js +++ b/packages/backend/migration/1678953978856-clip-favorite.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class clipFavorite1678953978856 { name = 'clipFavorite1678953978856' diff --git a/packages/backend/migration/1679309757174-antenna-active.js b/packages/backend/migration/1679309757174-antenna-active.js index 69e845c142..0b2bcc69ff 100644 --- a/packages/backend/migration/1679309757174-antenna-active.js +++ b/packages/backend/migration/1679309757174-antenna-active.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class antennaActive1679309757174 { name = 'antennaActive1679309757174' diff --git a/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js b/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js index 42faab7466..68576064f2 100644 --- a/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js +++ b/packages/backend/migration/1679639483253-enableChartsForRemoteUser.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class enableChartsForRemoteUser1679639483253 { name = 'enableChartsForRemoteUser1679639483253' diff --git a/packages/backend/migration/1679651580149-cleanup.js b/packages/backend/migration/1679651580149-cleanup.js index 1f00f3cc1f..7049891cf0 100644 --- a/packages/backend/migration/1679651580149-cleanup.js +++ b/packages/backend/migration/1679651580149-cleanup.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class cleanup1679651580149 { name = 'cleanup1679651580149' diff --git a/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js b/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js index 0733339841..f3a07cbd1d 100644 --- a/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js +++ b/packages/backend/migration/1679652081809-enableChartsForFederatedInstances.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class enableChartsForFederatedInstances1679652081809 { name = 'enableChartsForFederatedInstances1679652081809' diff --git a/packages/backend/migration/1680228513388-channelFavorite.js b/packages/backend/migration/1680228513388-channelFavorite.js index afc676959a..58eb7359f2 100644 --- a/packages/backend/migration/1680228513388-channelFavorite.js +++ b/packages/backend/migration/1680228513388-channelFavorite.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class channelFavorite1680228513388 { name = 'channelFavorite1680228513388' diff --git a/packages/backend/migration/1680238118084-channelNotePining.js b/packages/backend/migration/1680238118084-channelNotePining.js index 126eae87ea..f1f192d7bb 100644 --- a/packages/backend/migration/1680238118084-channelNotePining.js +++ b/packages/backend/migration/1680238118084-channelNotePining.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class channelNotePining1680238118084 { name = 'channelNotePining1680238118084' diff --git a/packages/backend/migration/1680491187535-cleanup.js b/packages/backend/migration/1680491187535-cleanup.js index 1e609ca060..006b403bd1 100644 --- a/packages/backend/migration/1680491187535-cleanup.js +++ b/packages/backend/migration/1680491187535-cleanup.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class cleanup1680491187535 { name = 'cleanup1680491187535' diff --git a/packages/backend/migration/1680582195041-cleanup.js b/packages/backend/migration/1680582195041-cleanup.js index c587e456a5..7d941be8cf 100644 --- a/packages/backend/migration/1680582195041-cleanup.js +++ b/packages/backend/migration/1680582195041-cleanup.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class cleanup1680582195041 { name = 'cleanup1680582195041' @@ -6,6 +11,6 @@ export class cleanup1680582195041 { } async down(queryRunner) { - + } } diff --git a/packages/backend/migration/1680702787050-UserMemo.js b/packages/backend/migration/1680702787050-UserMemo.js index 7446bf8da5..104d66ce24 100644 --- a/packages/backend/migration/1680702787050-UserMemo.js +++ b/packages/backend/migration/1680702787050-UserMemo.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class UserMemo1680702787050 { name = 'UserMemo1680702787050' diff --git a/packages/backend/migration/1680775031481-avatar-url-and-banner-url.js b/packages/backend/migration/1680775031481-avatar-url-and-banner-url.js index 7c5fe7ac5e..c613ee511e 100644 --- a/packages/backend/migration/1680775031481-avatar-url-and-banner-url.js +++ b/packages/backend/migration/1680775031481-avatar-url-and-banner-url.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class AvatarUrlAndBannerUrl1680775031481 { name = 'AvatarUrlAndBannerUrl1680775031481' diff --git a/packages/backend/migration/1680931179228-account-move.js b/packages/backend/migration/1680931179228-account-move.js index 821318d1bc..203d838f57 100644 --- a/packages/backend/migration/1680931179228-account-move.js +++ b/packages/backend/migration/1680931179228-account-move.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class AccountMove1680931179228 { name = 'AccountMove1680931179228' diff --git a/packages/backend/migration/1681400427971-serverRules.js b/packages/backend/migration/1681400427971-serverRules.js index 2364e8e1d2..70a74ebfff 100644 --- a/packages/backend/migration/1681400427971-serverRules.js +++ b/packages/backend/migration/1681400427971-serverRules.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class ServerRules1681400427971 { name = 'ServerRules1681400427971' diff --git a/packages/backend/migration/1681870960239-RoleTLSetting.js b/packages/backend/migration/1681870960239-RoleTLSetting.js index 2280f44eaa..07b9bc4e35 100644 --- a/packages/backend/migration/1681870960239-RoleTLSetting.js +++ b/packages/backend/migration/1681870960239-RoleTLSetting.js @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class RoleTLSetting1681870960239 { name = 'RoleTLSetting1681870960239' async up(queryRunner) { await queryRunner.query(`ALTER TABLE "role" ADD "isExplorable" boolean NOT NULL DEFAULT false`); } - + async down(queryRunner) { await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "isExplorable"`); } diff --git a/packages/backend/migration/1682190963894-movedAt.js b/packages/backend/migration/1682190963894-movedAt.js index 1f8f030a5c..cc33da8747 100644 --- a/packages/backend/migration/1682190963894-movedAt.js +++ b/packages/backend/migration/1682190963894-movedAt.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class MovedAt1682190963894 { name = 'MovedAt1682190963894' diff --git a/packages/backend/migration/1682754135458-preservedUsernames.js b/packages/backend/migration/1682754135458-preservedUsernames.js index 46a0826f43..61723e4abd 100644 --- a/packages/backend/migration/1682754135458-preservedUsernames.js +++ b/packages/backend/migration/1682754135458-preservedUsernames.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class PreservedUsernames1682754135458 { name = 'PreservedUsernames1682754135458' diff --git a/packages/backend/migration/1682985520254-channelColor.js b/packages/backend/migration/1682985520254-channelColor.js index 294b7372b2..43f1f48334 100644 --- a/packages/backend/migration/1682985520254-channelColor.js +++ b/packages/backend/migration/1682985520254-channelColor.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class ChannelColor1682985520254 { name = 'ChannelColor1682985520254' diff --git a/packages/backend/migration/1683328299359-channelArchive.js b/packages/backend/migration/1683328299359-channelArchive.js index 83695ff537..759dcbfdae 100644 --- a/packages/backend/migration/1683328299359-channelArchive.js +++ b/packages/backend/migration/1683328299359-channelArchive.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class ChannelArchive1683328299359 { name = 'ChannelArchive1683328299359' diff --git a/packages/backend/migration/1683682889948-prevent-ai-larning.js b/packages/backend/migration/1683682889948-prevent-ai-larning.js index 9d1a19c10b..1dc3eec21f 100644 --- a/packages/backend/migration/1683682889948-prevent-ai-larning.js +++ b/packages/backend/migration/1683682889948-prevent-ai-larning.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class PreventAiLarning1683682889948 { name = 'PreventAiLarning1683682889948' diff --git a/packages/backend/migration/1683683083083-public-reactions-default-true.js b/packages/backend/migration/1683683083083-public-reactions-default-true.js index 195ea02a5e..32cbe33b2f 100644 --- a/packages/backend/migration/1683683083083-public-reactions-default-true.js +++ b/packages/backend/migration/1683683083083-public-reactions-default-true.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class PublicReactionsDefaultTrue1683683083083 { name = 'PublicReactionsDefaultTrue1683683083083' diff --git a/packages/backend/migration/1683789676867-fix-typo.js b/packages/backend/migration/1683789676867-fix-typo.js index c0dbbf0050..5cd686e2f1 100644 --- a/packages/backend/migration/1683789676867-fix-typo.js +++ b/packages/backend/migration/1683789676867-fix-typo.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class FixTypo1683789676867 { name = 'FixTypo1683789676867' diff --git a/packages/backend/migration/1683847157541-UserList.js b/packages/backend/migration/1683847157541-UserList.js index b50a50eed8..f9e79a43a1 100644 --- a/packages/backend/migration/1683847157541-UserList.js +++ b/packages/backend/migration/1683847157541-UserList.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class UserList1683847157541 { name = 'UserList1683847157541' diff --git a/packages/backend/migration/1683869758873-UserListFavorites.js b/packages/backend/migration/1683869758873-UserListFavorites.js index ac9c4c42b9..aef4597a75 100644 --- a/packages/backend/migration/1683869758873-UserListFavorites.js +++ b/packages/backend/migration/1683869758873-UserListFavorites.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class UserListFavorites1683869758873 { name = 'UserListFavorites1683869758873' diff --git a/packages/backend/migration/1684206886988-remove-showTimelineReplies.js b/packages/backend/migration/1684206886988-remove-showTimelineReplies.js index 690653bd7c..a0798f85c6 100644 --- a/packages/backend/migration/1684206886988-remove-showTimelineReplies.js +++ b/packages/backend/migration/1684206886988-remove-showTimelineReplies.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class RemoveShowTimelineReplies1684206886988 { name = 'RemoveShowTimelineReplies1684206886988' diff --git a/packages/backend/migration/1684386446061-emoji-improve.js b/packages/backend/migration/1684386446061-emoji-improve.js index 40b0a2bc5e..7bded84cc9 100644 --- a/packages/backend/migration/1684386446061-emoji-improve.js +++ b/packages/backend/migration/1684386446061-emoji-improve.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class EmojiImprove1684386446061 { name = 'EmojiImprove1684386446061' diff --git a/packages/backend/migration/1685973839966-errorImageUrl.js b/packages/backend/migration/1685973839966-errorImageUrl.js new file mode 100644 index 0000000000..c4a1567b9b --- /dev/null +++ b/packages/backend/migration/1685973839966-errorImageUrl.js @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class ErrorImageUrl1685973839966 { + name = 'ErrorImageUrl1685973839966' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "errorImageUrl"`); + await queryRunner.query(`ALTER TABLE "meta" ADD "serverErrorImageUrl" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "notFoundImageUrl" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "infoImageUrl" character varying(1024)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "infoImageUrl"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "notFoundImageUrl"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "serverErrorImageUrl"`); + await queryRunner.query(`ALTER TABLE "meta" ADD "errorImageUrl" character varying(1024) DEFAULT 'https://xn--931a.moe/aiart/yubitun.png'`); + } +} diff --git a/packages/backend/migration/1688280713783-add-meta-options.js b/packages/backend/migration/1688280713783-add-meta-options.js new file mode 100644 index 0000000000..ade8378c00 --- /dev/null +++ b/packages/backend/migration/1688280713783-add-meta-options.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddMetaOptions1688280713783 { + name = 'AddMetaOptions1688280713783' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableServerMachineStats" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "meta" ADD "enableIdenticonGeneration" boolean NOT NULL DEFAULT true`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableIdenticonGeneration"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableServerMachineStats"`); + } +} diff --git a/packages/backend/migration/1688720440658-refactor-invite-system.js b/packages/backend/migration/1688720440658-refactor-invite-system.js new file mode 100644 index 0000000000..20f178612d --- /dev/null +++ b/packages/backend/migration/1688720440658-refactor-invite-system.js @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class RefactorInviteSystem1688720440658 { + name = 'RefactorInviteSystem1688720440658' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "expiresAt" TIMESTAMP WITH TIME ZONE`); + await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "usedAt" TIMESTAMP WITH TIME ZONE`); + await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "pendingUserId" character varying(32)`); + await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "createdById" character varying(32)`); + await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "usedById" character varying(32)`); + await queryRunner.query(`ALTER TABLE "registration_ticket" ADD CONSTRAINT "UQ_b6f93f2f30bdbb9a5ebdc7c7189" UNIQUE ("usedById")`); + await queryRunner.query(`ALTER TABLE "registration_ticket" ADD CONSTRAINT "FK_beba993576db0261a15364ea96e" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "registration_ticket" ADD CONSTRAINT "FK_b6f93f2f30bdbb9a5ebdc7c7189" FOREIGN KEY ("usedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "registration_ticket" DROP CONSTRAINT "FK_b6f93f2f30bdbb9a5ebdc7c7189"`); + await queryRunner.query(`ALTER TABLE "registration_ticket" DROP CONSTRAINT "FK_beba993576db0261a15364ea96e"`); + await queryRunner.query(`ALTER TABLE "registration_ticket" DROP CONSTRAINT "UQ_b6f93f2f30bdbb9a5ebdc7c7189"`); + await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "usedById"`); + await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "createdById"`); + await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "pendingUserId"`); + await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "usedAt"`); + await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "expiresAt"`); + } +} diff --git a/packages/backend/migration/1688880985544-add-index-to-relations.js b/packages/backend/migration/1688880985544-add-index-to-relations.js new file mode 100644 index 0000000000..6daac20329 --- /dev/null +++ b/packages/backend/migration/1688880985544-add-index-to-relations.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddIndexToRelations1688880985544 { + name = 'AddIndexToRelations1688880985544' + + async up(queryRunner) { + await queryRunner.query(`CREATE INDEX "IDX_beba993576db0261a15364ea96" ON "registration_ticket" ("createdById") `); + await queryRunner.query(`CREATE INDEX "IDX_b6f93f2f30bdbb9a5ebdc7c718" ON "registration_ticket" ("usedById") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_b6f93f2f30bdbb9a5ebdc7c718"`); + await queryRunner.query(`DROP INDEX "public"."IDX_beba993576db0261a15364ea96"`); + } +} diff --git a/packages/backend/migration/1689102832143-nsfw-cache.js b/packages/backend/migration/1689102832143-nsfw-cache.js new file mode 100644 index 0000000000..419588296e --- /dev/null +++ b/packages/backend/migration/1689102832143-nsfw-cache.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class NsfwCache1689102832143 { + name = 'NsfwCache1689102832143' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "cacheRemoteSensitiveFiles" boolean NOT NULL DEFAULT true`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "cacheRemoteSensitiveFiles"`); + } +} diff --git a/packages/backend/migration/1689325027964-UserBlacklistAnntena.js b/packages/backend/migration/1689325027964-UserBlacklistAnntena.js new file mode 100644 index 0000000000..ce246b20f8 --- /dev/null +++ b/packages/backend/migration/1689325027964-UserBlacklistAnntena.js @@ -0,0 +1,10 @@ +export class UserBlacklistAnntena1689325027964 { + name = 'UserBlacklistAnntena1689325027964' + + async up(queryRunner) { + await queryRunner.query(`ALTER TYPE "antenna_src_enum" ADD VALUE 'users_blacklist' AFTER 'list'`); + } + + async down(queryRunner) { + } +} diff --git a/packages/backend/migration/1690417561185-fix-renote-muting.js b/packages/backend/migration/1690417561185-fix-renote-muting.js new file mode 100644 index 0000000000..14150b0362 --- /dev/null +++ b/packages/backend/migration/1690417561185-fix-renote-muting.js @@ -0,0 +1,12 @@ +export class FixRenoteMuting1690417561185 { + name = 'FixRenoteMuting1690417561185' + + async up(queryRunner) { + await queryRunner.query(`DELETE FROM "renote_muting" WHERE "muteeId" NOT IN (SELECT "id" FROM "user")`); + await queryRunner.query(`DELETE FROM "renote_muting" WHERE "muterId" NOT IN (SELECT "id" FROM "user")`); + } + + async down(queryRunner) { + + } +} diff --git a/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js b/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js new file mode 100644 index 0000000000..7eda5debe5 --- /dev/null +++ b/packages/backend/migration/1690417561186-ChangeCacheRemoteFilesDefault.js @@ -0,0 +1,11 @@ +export class ChangeCacheRemoteFilesDefault1690417561186 { + name = 'ChangeCacheRemoteFilesDefault1690417561186' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "cacheRemoteFiles" SET DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "cacheRemoteFiles" SET DEFAULT true`); + } +} diff --git a/packages/backend/migration/1690417561187-Fix.js b/packages/backend/migration/1690417561187-Fix.js new file mode 100644 index 0000000000..e780e66d7b --- /dev/null +++ b/packages/backend/migration/1690417561187-Fix.js @@ -0,0 +1,81 @@ +export class Fix1690417561187 { + name = 'Fix1690417561187' + + async up(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_2cd3b2a6b4cf0b910b260afe08"`); + await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_createdAt"`); + await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`); + await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`); + await queryRunner.query(`COMMENT ON COLUMN "user"."isRoot" IS 'Whether the User is the root.'`); + await queryRunner.query(`COMMENT ON COLUMN "ad"."startsAt" IS 'The expired date of the Ad.'`); + await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "lastUsedAt" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "mascotImageUrl" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }'`); + await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`); + await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`); + await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`); + await queryRunner.query(`ALTER TABLE "poll" DROP CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b"`); + await queryRunner.query(`ALTER TABLE "poll" DROP CONSTRAINT "UQ_da851e06d0dfe2ef397d8b1bf1b"`); + await queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c"`); + await queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "UQ_e263909ca4fe5d57f8d4230dd5c"`); + await queryRunner.query(`ALTER TABLE "user_keypair" DROP CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6"`); + await queryRunner.query(`ALTER TABLE "user_keypair" DROP CONSTRAINT "UQ_f4853eb41ab722fe05f81cedeb6"`); + await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9"`); + await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "UQ_51cb79b5555effaf7d69ba1cff9"`); + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "FK_10c146e4b39b443ede016f6736d"`); + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "UQ_10c146e4b39b443ede016f6736d"`); + await queryRunner.query(`CREATE INDEX "IDX_3fcc2c589eaefc205e0714b99c" ON "ad" ("startsAt") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_c71faf11f0a28a5c0bb506203c" ON "channel_favorite" ("userId", "channelId") `); + await queryRunner.query(`CREATE INDEX "IDX_f7b9d338207e40e768e4a5265a" ON "instance" ("firstRetrievedAt") `); + await queryRunner.query(`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `); + await queryRunner.query(`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `); + await queryRunner.query(`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `); + await queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "FK_10c146e4b39b443ede016f6736d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "FK_10c146e4b39b443ede016f6736d"`); + await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9"`); + await queryRunner.query(`ALTER TABLE "user_keypair" DROP CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6"`); + await queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c"`); + await queryRunner.query(`ALTER TABLE "poll" DROP CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b"`); + await queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`); + await queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`); + await queryRunner.query(`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`); + await queryRunner.query(`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`); + await queryRunner.query(`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`); + await queryRunner.query(`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`); + await queryRunner.query(`DROP INDEX "public"."IDX_f7b9d338207e40e768e4a5265a"`); + await queryRunner.query(`DROP INDEX "public"."IDX_c71faf11f0a28a5c0bb506203c"`); + await queryRunner.query(`DROP INDEX "public"."IDX_3fcc2c589eaefc205e0714b99c"`); + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "UQ_10c146e4b39b443ede016f6736d" UNIQUE ("userId")`); + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "FK_10c146e4b39b443ede016f6736d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "UQ_51cb79b5555effaf7d69ba1cff9" UNIQUE ("userId")`); + await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "UQ_f4853eb41ab722fe05f81cedeb6" UNIQUE ("userId")`); + await queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "UQ_e263909ca4fe5d57f8d4230dd5c" UNIQUE ("noteId")`); + await queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "UQ_da851e06d0dfe2ef397d8b1bf1b" UNIQUE ("noteId")`); + await queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`); + await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`); + await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{admin,administrator,root,system,maintainer,host,mod,moderator,owner,superuser,staff,auth,i,me,everyone,all,mention,mentions,example,user,users,account,accounts,official,help,helps,support,supports,info,information,informations,announce,announces,announcement,announcements,notice,notification,notifications,dev,developer,developers,tech,misskey}'`); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "mascotImageUrl" SET DEFAULT '/assets/ai.png'`); + await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "lastUsedAt" SET DEFAULT '2023-04-25 06:51:20.985478+00'`); + await queryRunner.query(`COMMENT ON COLUMN "ad"."startsAt" IS NULL`); + await queryRunner.query(`COMMENT ON COLUMN "user"."isRoot" IS 'Whether the User is the admin.'`); + await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `); + await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `); + await queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `); + await queryRunner.query(`CREATE INDEX "IDX_2cd3b2a6b4cf0b910b260afe08" ON "instance" ("firstRetrievedAt") `); + } +} diff --git a/packages/backend/migration/1690569881926-user-2fa-backup-codes.js b/packages/backend/migration/1690569881926-user-2fa-backup-codes.js new file mode 100644 index 0000000000..2049df8ea2 --- /dev/null +++ b/packages/backend/migration/1690569881926-user-2fa-backup-codes.js @@ -0,0 +1,11 @@ +export class User2faBackupCodes1690569881926 { + name = 'User2faBackupCodes1690569881926' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ADD "twoFactorBackupSecret" character varying array`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twoFactorBackupSecret"`); + } +} diff --git a/packages/backend/migration/1690782653311-SensitiveChannel.js b/packages/backend/migration/1690782653311-SensitiveChannel.js new file mode 100644 index 0000000000..e76dda5180 --- /dev/null +++ b/packages/backend/migration/1690782653311-SensitiveChannel.js @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class SensitiveChannel1690782653311 { + name = 'SensitiveChannel1690782653311' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "channel" + ADD "isSensitive" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "isSensitive"`); + } +} diff --git a/packages/backend/migration/1690796169261-play-visibility.js b/packages/backend/migration/1690796169261-play-visibility.js new file mode 100644 index 0000000000..c57fa7a109 --- /dev/null +++ b/packages/backend/migration/1690796169261-play-visibility.js @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class PlayVisibility1689102832143 { + name = 'PlayVisibility1690796169261' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "public"."flash" ADD "visibility" character varying(512) DEFAULT 'public'`, undefined); + } + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "public"."flash" DROP COLUMN "visibility"`, undefined); + } +} diff --git a/packages/backend/migration/1691649257651-refine-announcement.js b/packages/backend/migration/1691649257651-refine-announcement.js new file mode 100644 index 0000000000..d8d63f3103 --- /dev/null +++ b/packages/backend/migration/1691649257651-refine-announcement.js @@ -0,0 +1,27 @@ +export class RefineAnnouncement1691649257651 { + name = 'RefineAnnouncement1691649257651' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "announcement" ADD "display" character varying(256) NOT NULL DEFAULT 'normal'`); + await queryRunner.query(`ALTER TABLE "announcement" ADD "needConfirmationToRead" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "announcement" ADD "isActive" boolean NOT NULL DEFAULT true`); + await queryRunner.query(`ALTER TABLE "announcement" ADD "forExistingUsers" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "announcement" ADD "userId" character varying(32)`); + await queryRunner.query(`CREATE INDEX "IDX_bc1afcc8ef7e9400cdc3c0a87e" ON "announcement" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_da795d3a83187e8832005ba19d" ON "announcement" ("forExistingUsers") `); + await queryRunner.query(`CREATE INDEX "IDX_fd25dfe3da37df1715f11ba6ec" ON "announcement" ("userId") `); + await queryRunner.query(`ALTER TABLE "announcement" ADD CONSTRAINT "FK_fd25dfe3da37df1715f11ba6ec8" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "announcement" DROP CONSTRAINT "FK_fd25dfe3da37df1715f11ba6ec8"`); + await queryRunner.query(`DROP INDEX "public"."IDX_fd25dfe3da37df1715f11ba6ec"`); + await queryRunner.query(`DROP INDEX "public"."IDX_da795d3a83187e8832005ba19d"`); + await queryRunner.query(`DROP INDEX "public"."IDX_bc1afcc8ef7e9400cdc3c0a87e"`); + await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "forExistingUsers"`); + await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "isActive"`); + await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "needConfirmationToRead"`); + await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "display"`); + } +} diff --git a/packages/backend/migration/1691657412740-refine-announcement-2.js b/packages/backend/migration/1691657412740-refine-announcement-2.js new file mode 100644 index 0000000000..8791f99f44 --- /dev/null +++ b/packages/backend/migration/1691657412740-refine-announcement-2.js @@ -0,0 +1,11 @@ +export class RefineAnnouncement21691657412740 { + name = 'RefineAnnouncement21691657412740' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "announcement" ADD "icon" character varying(256) NOT NULL DEFAULT 'info'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "icon"`); + } +} diff --git a/packages/backend/migration/1691959191872-passkey-support.js b/packages/backend/migration/1691959191872-passkey-support.js new file mode 100644 index 0000000000..55b571d60d --- /dev/null +++ b/packages/backend/migration/1691959191872-passkey-support.js @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class PasskeySupport1691959191872 { + name = 'PasskeySupport1691959191872' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_security_key" ADD "counter" bigint NOT NULL DEFAULT '0'`); + await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."counter" IS 'The number of times the UserSecurityKey was validated.'`); + await queryRunner.query(`ALTER TABLE "user_security_key" ADD "credentialDeviceType" character varying(32)`); + await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."credentialDeviceType" IS 'The type of Backup Eligibility in authenticator data'`); + await queryRunner.query(`ALTER TABLE "user_security_key" ADD "credentialBackedUp" boolean`); + await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."credentialBackedUp" IS 'Whether or not the credential has been backed up'`); + await queryRunner.query(`ALTER TABLE "user_security_key" ADD "transports" character varying(32) array`); + await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."transports" IS 'The type of the credential returned by the browser'`); + await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."publicKey" IS 'The public key of the UserSecurityKey, hex-encoded.'`); + await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."lastUsed" IS 'Timestamp of the last time the UserSecurityKey was used.'`); + await queryRunner.query(`ALTER TABLE "user_security_key" ALTER COLUMN "lastUsed" SET DEFAULT now()`); + await queryRunner.query(`UPDATE "user_security_key" SET "id" = REPLACE(REPLACE(REPLACE(REPLACE(ENCODE(DECODE("id", 'hex'), 'base64'), E'\\n', ''), '+', '-'), '/', '_'), '=', ''), "publicKey" = REPLACE(REPLACE(REPLACE(REPLACE(ENCODE(DECODE("publicKey", 'hex'), 'base64'), E'\\n', ''), '+', '-'), '/', '_'), '=', '')`); + await queryRunner.query(`ALTER TABLE "attestation_challenge" DROP CONSTRAINT "FK_f1a461a618fa1755692d0e0d592"`); + await queryRunner.query(`DROP INDEX "IDX_47efb914aed1f72dd39a306c7b"`); + await queryRunner.query(`DROP INDEX "IDX_f1a461a618fa1755692d0e0d59"`); + await queryRunner.query(`DROP TABLE "attestation_challenge"`); + } + + async down(queryRunner) { + await queryRunner.query(`CREATE TABLE "attestation_challenge" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "challenge" character varying(64) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "registrationChallenge" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_d0ba6786e093f1bcb497572a6b5" PRIMARY KEY ("id", "userId"))`); + await queryRunner.query(`CREATE INDEX "IDX_f1a461a618fa1755692d0e0d59" ON "attestation_challenge" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_47efb914aed1f72dd39a306c7b" ON "attestation_challenge" ("challenge") `); + await queryRunner.query(`ALTER TABLE "attestation_challenge" ADD CONSTRAINT "FK_f1a461a618fa1755692d0e0d592" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."challenge" IS 'Hex-encoded sha256 hash of the challenge.'`); + await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."createdAt" IS 'The date challenge was created for expiry purposes.'`); + await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."registrationChallenge" IS 'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.'`); + await queryRunner.query(`UPDATE "user_security_key" SET "id" = ENCODE(DECODE(REPLACE(REPLACE("id" || CASE WHEN LENGTH("id") % 4 = 2 THEN '==' WHEN LENGTH("id") % 4 = 3 THEN '=' ELSE '' END, '-', '+'), '_', '/'), 'base64'), 'hex'), "publicKey" = ENCODE(DECODE(REPLACE(REPLACE("publicKey" || CASE WHEN LENGTH("publicKey") % 4 = 2 THEN '==' WHEN LENGTH("publicKey") % 4 = 3 THEN '=' ELSE '' END, '-', '+'), '_', '/'), 'base64'), 'hex')`); + await queryRunner.query(`ALTER TABLE "user_security_key" ALTER COLUMN "lastUsed" DROP DEFAULT`); + await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."lastUsed" IS 'The date of the last time the UserSecurityKey was successfully validated.'`); + await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."publicKey" IS 'Variable-length public key used to verify attestations (hex-encoded).'`); + await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."transports" IS 'The type of the credential returned by the browser'`); + await queryRunner.query(`ALTER TABLE "user_security_key" DROP COLUMN "transports"`); + await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."credentialBackedUp" IS 'Whether or not the credential has been backed up'`); + await queryRunner.query(`ALTER TABLE "user_security_key" DROP COLUMN "credentialBackedUp"`); + await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."credentialDeviceType" IS 'The type of Backup Eligibility in authenticator data'`); + await queryRunner.query(`ALTER TABLE "user_security_key" DROP COLUMN "credentialDeviceType"`); + await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."counter" IS 'The number of times the UserSecurityKey was validated.'`); + await queryRunner.query(`ALTER TABLE "user_security_key" DROP COLUMN "counter"`); + } +} diff --git a/packages/backend/migration/1694850832075-server-icons-and-manifest.js b/packages/backend/migration/1694850832075-server-icons-and-manifest.js new file mode 100644 index 0000000000..1bd8979d9b --- /dev/null +++ b/packages/backend/migration/1694850832075-server-icons-and-manifest.js @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class ServerIconsAndManifest1694850832075 { + name = 'ServerIconsAndManifest1694850832075' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "app192IconUrl" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "app512IconUrl" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "manifestJsonOverride" character varying(8192) NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "manifestJsonOverride"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "app512IconUrl"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "app192IconUrl"`); + } +} diff --git a/packages/backend/migration/1694915420864-clipped-count.js b/packages/backend/migration/1694915420864-clipped-count.js new file mode 100644 index 0000000000..1ad8e04ce0 --- /dev/null +++ b/packages/backend/migration/1694915420864-clipped-count.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class ClippedCount1694915420864 { + name = 'ClippedCount1694915420864' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "clippedCount" smallint NOT NULL DEFAULT '0'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "clippedCount"`); + } +} diff --git a/packages/backend/migration/1695260774117-verified-links.js b/packages/backend/migration/1695260774117-verified-links.js new file mode 100644 index 0000000000..18e0571d81 --- /dev/null +++ b/packages/backend/migration/1695260774117-verified-links.js @@ -0,0 +1,11 @@ +export class VerifiedLinks1695260774117 { + name = 'VerifiedLinks1695260774117' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ADD "verifiedLinks" character varying array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "verifiedLinks"`); + } +} diff --git a/packages/backend/migration/1695288787870-following-notify.js b/packages/backend/migration/1695288787870-following-notify.js new file mode 100644 index 0000000000..e7e2194b15 --- /dev/null +++ b/packages/backend/migration/1695288787870-following-notify.js @@ -0,0 +1,13 @@ +export class FollowingNotify1695288787870 { + name = 'FollowingNotify1695288787870' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "following" ADD "notify" character varying(32)`); + await queryRunner.query(`CREATE INDEX "IDX_5108098457488634a4768e1d12" ON "following" ("notify") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_5108098457488634a4768e1d12"`); + await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "notify"`); + } +} diff --git a/packages/backend/migration/1695440131671-short-name.js b/packages/backend/migration/1695440131671-short-name.js new file mode 100644 index 0000000000..2c37297fc1 --- /dev/null +++ b/packages/backend/migration/1695440131671-short-name.js @@ -0,0 +1,11 @@ +export class ShortName1695440131671 { + name = 'ShortName1695440131671' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "shortName" character varying(64)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "shortName"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 56ecbc2eaf..97fbaab308 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -3,6 +3,9 @@ "main": "./index.js", "private": true, "type": "module", + "engines": { + "node": ">=18.16.0" + }, "scripts": { "start": "node ./built/index.js", "start:test": "NODE_ENV=test node ./built/index.js", @@ -25,6 +28,7 @@ "@swc/core-android-arm64": "1.3.11", "@swc/core-darwin-arm64": "1.3.56", "@swc/core-darwin-x64": "1.3.56", + "@swc/core-freebsd-x64": "1.3.11", "@swc/core-linux-arm-gnueabihf": "1.3.56", "@swc/core-linux-arm64-gnu": "1.3.56", "@swc/core-linux-arm64-musl": "1.3.56", @@ -36,91 +40,100 @@ "@tensorflow/tfjs": "4.4.0", "@tensorflow/tfjs-node": "4.4.0", "bufferutil": "^4.0.7", - "slacc-android-arm-eabi": "0.0.9", - "slacc-android-arm64": "0.0.9", - "slacc-darwin-arm64": "0.0.9", - "slacc-darwin-universal": "0.0.9", - "slacc-darwin-x64": "0.0.9", - "slacc-freebsd-x64": "0.0.9", - "slacc-linux-arm-gnueabihf": "0.0.9", - "slacc-linux-arm64-gnu": "0.0.9", - "slacc-linux-arm64-musl": "0.0.9", - "slacc-linux-x64-gnu": "0.0.9", - "slacc-win32-arm64-msvc": "0.0.9", - "slacc-win32-x64-msvc": "0.0.9", + "slacc-android-arm-eabi": "0.0.10", + "slacc-android-arm64": "0.0.10", + "slacc-darwin-arm64": "0.0.10", + "slacc-darwin-universal": "0.0.10", + "slacc-darwin-x64": "0.0.10", + "slacc-freebsd-x64": "0.0.10", + "slacc-linux-arm-gnueabihf": "0.0.10", + "slacc-linux-arm64-gnu": "0.0.10", + "slacc-linux-arm64-musl": "0.0.10", + "slacc-linux-x64-gnu": "0.0.10", + "slacc-linux-x64-musl": "0.0.10", + "slacc-win32-arm64-msvc": "0.0.10", + "slacc-win32-x64-msvc": "0.0.10", "utf-8-validate": "^6.0.3" }, "dependencies": { - "@aws-sdk/client-s3": "3.321.1", - "@aws-sdk/lib-storage": "3.321.1", - "@aws-sdk/node-http-handler": "3.321.1", - "@bull-board/api": "5.2.0", - "@bull-board/fastify": "5.2.0", - "@bull-board/ui": "5.2.0", + "@aws-sdk/client-s3": "3.412.0", + "@aws-sdk/lib-storage": "3.412.0", + "@smithy/node-http-handler": "2.1.5", + "@bull-board/api": "5.8.4", + "@bull-board/fastify": "5.8.4", + "@bull-board/ui": "5.8.4", "@discordapp/twemoji": "14.1.2", - "@fastify/accepts": "4.1.0", - "@fastify/cookie": "8.3.0", - "@fastify/cors": "8.3.0", - "@fastify/http-proxy": "9.1.0", - "@fastify/multipart": "7.6.0", - "@fastify/static": "6.10.2", - "@fastify/view": "7.4.1", - "@nestjs/common": "9.4.2", - "@nestjs/core": "9.4.2", - "@nestjs/testing": "9.4.2", + "@fastify/accepts": "4.2.0", + "@fastify/cookie": "9.1.0", + "@fastify/cors": "8.4.0", + "@fastify/express": "2.3.0", + "@fastify/http-proxy": "9.2.1", + "@fastify/multipart": "7.7.3", + "@fastify/static": "6.11.2", + "@fastify/view": "8.2.0", + "@nestjs/common": "10.2.6", + "@nestjs/core": "10.2.6", + "@nestjs/testing": "10.2.6", "@peertube/http-signature": "1.7.0", - "@sinonjs/fake-timers": "10.2.0", + "@simplewebauthn/server": "8.1.1", + "@sinonjs/fake-timers": "11.1.0", "@swc/cli": "0.1.62", - "@swc/core": "1.3.61", + "@swc/core": "1.3.87", "accepts": "1.3.8", "ajv": "8.12.0", - "archiver": "5.3.1", - "autwh": "0.1.0", + "archiver": "6.0.1", + "async-mutex": "0.4.0", "bcryptjs": "2.4.3", "blurhash": "2.0.5", - "bullmq": "3.15.0", - "cacheable-lookup": "6.1.0", - "cbor": "9.0.0", - "chalk": "5.2.0", - "chalk-template": "0.4.0", + "body-parser": "1.20.2", + "bullmq": "4.11.4", + "cacheable-lookup": "7.0.0", + "cbor": "9.0.1", + "chalk": "5.3.0", + "chalk-template": "1.1.0", "chokidar": "3.5.3", "cli-highlight": "2.1.11", "color-convert": "2.0.1", "content-disposition": "0.5.4", "date-fns": "2.30.0", "deep-email-validator": "0.1.21", - "escape-regexp": "0.0.1", - "fastify": "4.17.0", + "fastify": "4.23.2", "feed": "4.2.2", - "file-type": "18.4.0", + "file-type": "18.5.0", "fluent-ffmpeg": "2.1.2", "form-data": "4.0.0", - "got": "12.6.0", - "happy-dom": "9.20.3", + "got": "13.0.0", + "happy-dom": "10.0.3", "hpagent": "1.2.0", + "http-link-header": "1.1.1", "ioredis": "5.3.2", "ip-cidr": "3.1.0", - "is-svg": "4.3.2", + "ipaddr.js": "2.1.0", + "is-svg": "5.0.0", "js-yaml": "4.1.0", "jsdom": "22.1.0", "json5": "2.2.3", - "jsonld": "8.2.0", + "jsonld": "8.3.1", "jsrsasign": "10.8.6", - "meilisearch": "0.32.5", + "meilisearch": "0.34.2", "mfm-js": "0.23.3", + "microformats-parser": "1.5.2", "mime-types": "2.1.35", "misskey-js": "workspace:*", "ms": "3.0.0-canary.1", + "nanoid": "5.0.1", "nested-property": "4.0.0", - "node-fetch": "3.3.1", - "nodemailer": "6.9.3", + "node-fetch": "3.3.2", + "nodemailer": "6.9.5", "nsfwjs": "2.4.2", "oauth": "0.10.0", + "oauth2orize": "1.11.1", + "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", - "otpauth": "9.1.2", + "otpauth": "9.1.4", "parse5": "7.1.2", - "pg": "8.11.0", - "private-ip": "3.0.0", + "pg": "8.11.3", + "pkce-challenge": "4.0.1", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", "pug": "3.0.2", @@ -129,88 +142,85 @@ "qrcode": "1.5.3", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.19.0", + "re2": "1.20.3", "redis-lock": "0.1.4", "reflect-metadata": "0.1.13", "rename": "1.0.4", - "rndstr": "1.0.0", "rss-parser": "3.13.0", "rxjs": "7.8.1", - "s-age": "1.1.2", - "sanitize-html": "2.10.0", - "seedrandom": "3.0.5", - "semver": "7.5.1", - "sharp": "0.32.1", + "sanitize-html": "2.11.0", + "sharp": "0.32.6", "sharp-read-bmp": "github:misskey-dev/sharp-read-bmp", - "slacc": "0.0.9", + "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "summaly": "github:misskey-dev/summaly", - "systeminformation": "5.17.16", + "systeminformation": "5.21.8", "tinycolor2": "1.6.0", "tmp": "0.2.1", - "tsc-alias": "1.8.6", + "tsc-alias": "1.8.8", "tsconfig-paths": "4.2.0", "twemoji-parser": "14.0.0", - "typeorm": "0.3.16", - "typescript": "5.1.3", + "typeorm": "0.3.17", + "typescript": "5.2.2", "ulid": "2.3.0", - "unzipper": "0.10.14", - "uuid": "9.0.0", "vary": "1.1.2", - "web-push": "3.6.1", - "ws": "8.13.0", + "web-push": "3.6.6", + "ws": "8.14.2", "xev": "3.0.2" }, "devDependencies": { - "@jest/globals": "29.5.0", - "@swc/jest": "0.2.26", + "@jest/globals": "29.7.0", + "@simplewebauthn/typescript-types": "8.0.0", + "@swc/jest": "0.2.29", "@types/accepts": "1.3.5", - "@types/archiver": "5.3.2", - "@types/bcryptjs": "2.4.2", + "@types/archiver": "5.3.3", + "@types/bcryptjs": "2.4.4", + "@types/body-parser": "1.19.3", "@types/cbor": "6.0.0", - "@types/color-convert": "2.0.0", - "@types/content-disposition": "0.5.5", - "@types/escape-regexp": "0.0.1", - "@types/fluent-ffmpeg": "2.1.21", - "@types/jest": "29.5.2", - "@types/js-yaml": "4.0.5", - "@types/jsdom": "21.1.1", - "@types/jsonld": "1.5.8", - "@types/jsrsasign": "10.5.8", + "@types/color-convert": "2.0.1", + "@types/content-disposition": "0.5.6", + "@types/fluent-ffmpeg": "2.1.22", + "@types/http-link-header": "1.0.3", + "@types/jest": "29.5.5", + "@types/js-yaml": "4.0.6", + "@types/jsdom": "21.1.3", + "@types/jsonld": "1.5.10", + "@types/jsrsasign": "10.5.9", "@types/mime-types": "2.1.1", - "@types/node": "20.2.5", + "@types/ms": "0.7.31", + "@types/node": "20.6.3", "@types/node-fetch": "3.0.3", - "@types/nodemailer": "6.4.8", - "@types/oauth": "0.9.1", - "@types/pg": "8.10.1", + "@types/nodemailer": "6.4.10", + "@types/oauth": "0.9.2", + "@types/oauth2orize": "1.11.1", + "@types/oauth2orize-pkce": "0.1.0", + "@types/pg": "8.10.2", "@types/pug": "2.0.6", "@types/punycode": "2.1.0", - "@types/qrcode": "1.5.0", + "@types/qrcode": "1.5.2", "@types/random-seed": "0.3.3", "@types/ratelimiter": "3.4.4", - "@types/redis": "4.0.11", "@types/rename": "1.0.4", "@types/sanitize-html": "2.9.0", - "@types/semver": "7.5.0", + "@types/semver": "7.5.2", "@types/sharp": "0.32.0", + "@types/simple-oauth2": "5.0.4", "@types/sinonjs__fake-timers": "8.1.2", - "@types/tinycolor2": "1.4.3", - "@types/tmp": "0.2.3", - "@types/unzipper": "0.10.6", - "@types/uuid": "9.0.1", + "@types/tinycolor2": "1.4.4", + "@types/tmp": "0.2.4", "@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.59.8", - "@typescript-eslint/parser": "5.59.8", - "aws-sdk-client-mock": "2.1.1", + "@types/web-push": "3.6.0", + "@types/ws": "8.5.5", + "@typescript-eslint/eslint-plugin": "6.7.2", + "@typescript-eslint/parser": "6.7.2", + "aws-sdk-client-mock": "3.0.0", "cross-env": "7.0.3", - "eslint": "8.41.0", - "eslint-plugin-import": "2.27.5", - "execa": "6.1.0", - "jest": "29.5.0", - "jest-mock": "29.5.0" + "eslint": "8.50.0", + "eslint-plugin-import": "2.28.1", + "execa": "8.0.1", + "jest": "29.7.0", + "jest-mock": "29.7.0", + "simple-oauth2": "5.0.0" } } diff --git a/packages/backend/src/@types/hcaptcha.d.ts b/packages/backend/src/@types/hcaptcha.d.ts index afed587560..43e67dd340 100644 --- a/packages/backend/src/@types/hcaptcha.d.ts +++ b/packages/backend/src/@types/hcaptcha.d.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + declare module 'hcaptcha' { interface IVerifyResponse { success: boolean; diff --git a/packages/backend/src/@types/http-signature.d.ts b/packages/backend/src/@types/http-signature.d.ts index f2f9bfcc31..1f3b48aa54 100644 --- a/packages/backend/src/@types/http-signature.d.ts +++ b/packages/backend/src/@types/http-signature.d.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + declare module '@peertube/http-signature' { import type { IncomingMessage, ClientRequest } from 'node:http'; diff --git a/packages/backend/src/@types/os-utils.d.ts b/packages/backend/src/@types/os-utils.d.ts index 390df17d39..8c44232c14 100644 --- a/packages/backend/src/@types/os-utils.d.ts +++ b/packages/backend/src/@types/os-utils.d.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + declare module 'os-utils' { type FreeCommandCallback = (usedmem: number) => void; diff --git a/packages/backend/src/@types/package.json.d.ts b/packages/backend/src/@types/package.json.d.ts index abe5fae687..197b4b6bf0 100644 --- a/packages/backend/src/@types/package.json.d.ts +++ b/packages/backend/src/@types/package.json.d.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + declare module '*/package.json' { interface IRepository { type: string; diff --git a/packages/backend/src/@types/probe-image-size.d.ts b/packages/backend/src/@types/probe-image-size.d.ts index 416e819acb..4d312cba34 100644 --- a/packages/backend/src/@types/probe-image-size.d.ts +++ b/packages/backend/src/@types/probe-image-size.d.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + declare module 'probe-image-size' { import type { ReadStream } from 'node:fs'; diff --git a/packages/backend/src/@types/redis-lock.d.ts b/packages/backend/src/@types/redis-lock.d.ts index 9242656a98..c607d600d8 100644 --- a/packages/backend/src/@types/redis-lock.d.ts +++ b/packages/backend/src/@types/redis-lock.d.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + declare module 'redis-lock' { import type Redis from 'ioredis'; diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 406e3192bb..9f1ee9fcaa 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { setTimeout } from 'node:timers/promises'; import { Global, Inject, Module } from '@nestjs/common'; import * as Redis from 'ioredis'; @@ -41,14 +46,7 @@ const $meilisearch: Provider = { const $redis: Provider = { provide: DI.redis, useFactory: (config: Config) => { - return new Redis.Redis({ - port: config.redis.port, - host: config.redis.host, - family: config.redis.family == null ? 0 : config.redis.family, - password: config.redis.pass, - keyPrefix: `${config.redis.prefix}:`, - db: config.redis.db ?? 0, - }); + return new Redis.Redis(config.redis); }, inject: [DI.config], }; @@ -56,14 +54,7 @@ const $redis: Provider = { const $redisForPub: Provider = { provide: DI.redisForPub, useFactory: (config: Config) => { - const redis = new Redis.Redis({ - port: config.redisForPubsub.port, - host: config.redisForPubsub.host, - family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family, - password: config.redisForPubsub.pass, - keyPrefix: `${config.redisForPubsub.prefix}:`, - db: config.redisForPubsub.db ?? 0, - }); + const redis = new Redis.Redis(config.redisForPubsub); return redis; }, inject: [DI.config], @@ -72,14 +63,7 @@ const $redisForPub: Provider = { const $redisForSub: Provider = { provide: DI.redisForSub, useFactory: (config: Config) => { - const redis = new Redis.Redis({ - port: config.redisForPubsub.port, - host: config.redisForPubsub.host, - family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family, - password: config.redisForPubsub.pass, - keyPrefix: `${config.redisForPubsub.prefix}:`, - db: config.redisForPubsub.db ?? 0, - }); + const redis = new Redis.Redis(config.redisForPubsub); redis.subscribe(config.host); return redis; }, diff --git a/packages/backend/src/MainModule.ts b/packages/backend/src/MainModule.ts index fc568e883e..90aba0cc91 100644 --- a/packages/backend/src/MainModule.ts +++ b/packages/backend/src/MainModule.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Module } from '@nestjs/common'; import { ServerModule } from '@/server/ServerModule.js'; import { GlobalModule } from '@/GlobalModule.js'; diff --git a/packages/backend/src/NestLogger.ts b/packages/backend/src/NestLogger.ts index 448098b831..e18e9e88a7 100644 --- a/packages/backend/src/NestLogger.ts +++ b/packages/backend/src/NestLogger.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { LoggerService } from '@nestjs/common'; import Logger from '@/logger.js'; diff --git a/packages/backend/src/boot/common.ts b/packages/backend/src/boot/common.ts index 3995545d7f..4783a2b2da 100644 --- a/packages/backend/src/boot/common.ts +++ b/packages/backend/src/boot/common.ts @@ -1,9 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { NestFactory } from '@nestjs/core'; import { ChartManagementService } from '@/core/chart/ChartManagementService.js'; import { QueueProcessorService } from '@/queue/QueueProcessorService.js'; import { NestLogger } from '@/NestLogger.js'; import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js'; -import { JanitorService } from '@/daemons/JanitorService.js'; import { QueueStatsService } from '@/daemons/QueueStatsService.js'; import { ServerStatsService } from '@/daemons/ServerStatsService.js'; import { ServerService } from '@/server/ServerService.js'; @@ -20,7 +24,6 @@ export async function server() { if (process.env.NODE_ENV !== 'test') { app.get(ChartManagementService).start(); - app.get(JanitorService).start(); app.get(QueueStatsService).start(); app.get(ServerStatsService).start(); } diff --git a/packages/backend/src/boot/index.ts b/packages/backend/src/boot/entry.ts similarity index 94% rename from packages/backend/src/boot/index.ts rename to packages/backend/src/boot/entry.ts index f4daf30690..fc8fc2ffb4 100644 --- a/packages/backend/src/boot/index.ts +++ b/packages/backend/src/boot/entry.ts @@ -1,3 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ /** * Misskey Entry Point! diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index f5d936fadf..a45ea2bb8f 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; @@ -5,7 +10,6 @@ import * as os from 'node:os'; import cluster from 'node:cluster'; import chalk from 'chalk'; import chalkTemplate from 'chalk-template'; -import semver from 'semver'; import Logger from '@/logger.js'; import { loadConfig } from '@/config.js'; import type { Config } from '@/config.js'; @@ -31,7 +35,7 @@ function greet() { console.log(themeColor(' | |_|___ ___| |_ ___ _ _ ')); console.log(themeColor(' | | | | |_ -|_ -| \'_| -_| | |')); console.log(themeColor(' |_|_|_|_|___|___|_,_|___|_ |')); - console.log(' ' + chalk.gray(v) + themeColor(' |___|\n'.substr(v.length))); + console.log(' ' + chalk.gray(v) + themeColor(' |___|\n'.substring(v.length))); //#endregion console.log(' Misskey is an open-source decentralized microblogging platform.'); @@ -64,21 +68,34 @@ export async function masterMain() { process.exit(1); } - if (envOption.onlyServer) { - await server(); - } else if (envOption.onlyQueue) { - await jobQueue(); - } else { - await server(); - } - bootLogger.succ('Misskey initialized'); - if (!envOption.disableClustering) { + if (envOption.disableClustering) { + if (envOption.onlyServer) { + await server(); + } else if (envOption.onlyQueue) { + await jobQueue(); + } else { + await server(); + await jobQueue(); + } + } else { + if (envOption.onlyServer) { + // nop + } else if (envOption.onlyQueue) { + // nop + } else { + await server(); + } + await spawnWorkers(config.clusterLimit); } - bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); + if (envOption.onlyQueue) { + bootLogger.succ('Queue started', null, true); + } else { + bootLogger.succ(config.socket ? `Now listening on socket ${config.socket} on ${config.url}` : `Now listening on port ${config.port} on ${config.url}`, null, true); + } } function showEnvironment(): void { @@ -96,12 +113,6 @@ function showNodejsVersion(): void { const nodejsLogger = bootLogger.createSubLogger('nodejs'); nodejsLogger.info(`Version ${process.version} detected.`); - - const minVersion = fs.readFileSync(`${_dirname}/../../../../.node-version`, 'utf-8').trim(); - if (semver.lt(process.version, minVersion)) { - nodejsLogger.error(`At least Node.js ${minVersion} required!`); - process.exit(1); - } } function loadConfigBoot(): Config { diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts index ab75aaa572..0399c9fe5c 100644 --- a/packages/backend/src/boot/worker.ts +++ b/packages/backend/src/boot/worker.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import cluster from 'node:cluster'; import { envOption } from '@/env.js'; import { jobQueue, server } from './common.js'; diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 9d1945e4d4..abbfdfed8f 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -1,20 +1,31 @@ -/** - * Config loader +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only */ import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import * as yaml from 'js-yaml'; +import type { RedisOptions } from 'ioredis'; + +type RedisOptionsSource = Partial & { + host: string; + port: number; + family?: number; + pass: string; + db?: number; + prefix?: string; +}; /** - * ユーザーが設定する必要のある情報 + * 設定ファイルの型 */ -export type Source = { - repository_url?: string; - feedback_url?: string; +type Source = { url: string; - port: number; + port?: number; + socket?: string; + chmodSocket?: string; disableHsts?: boolean; db: { host: string; @@ -33,36 +44,16 @@ export type Source = { user: string; pass: string; }[]; - redis: { - host: string; - port: number; - family?: number; - pass: string; - db?: number; - prefix?: string; - }; - redisForPubsub?: { - host: string; - port: number; - family?: number; - pass: string; - db?: number; - prefix?: string; - }; - redisForJobQueue?: { - host: string; - port: number; - family?: number; - pass: string; - db?: number; - prefix?: string; - }; + redis: RedisOptionsSource; + redisForPubsub?: RedisOptionsSource; + redisForJobQueue?: RedisOptionsSource; meilisearch?: { host: string; port: string; apiKey: string; ssl?: boolean; index: string; + scope?: 'local' | 'global' | string[]; }; proxy?: string; @@ -73,12 +64,11 @@ export type Source = { maxFileSize?: number; - accesslog?: string; - clusterLimit?: number; id: string; + outgoingAddress?: string; outgoingAddressFamily?: 'ipv4' | 'ipv6' | 'dual'; deliverJobConcurrency?: number; @@ -95,12 +85,63 @@ export type Source = { videoThumbnailGenerator?: string; signToActivityPubGet?: boolean; + + perChannelMaxNoteCacheCount?: number; + perUserNotificationsMaxCount?: number; + deactivateAntennaThreshold?: number; }; -/** - * Misskeyが自動的に(ユーザーが設定した情報から推論して)設定する情報 - */ -export type Mixin = { +export type Config = { + url: string; + port: number; + socket: string | undefined; + chmodSocket: string | undefined; + disableHsts: boolean | undefined; + db: { + host: string; + port: number; + db: string; + user: string; + pass: string; + disableCache?: boolean; + extra?: { [x: string]: string }; + }; + dbReplications: boolean | undefined; + dbSlaves: { + host: string; + port: number; + db: string; + user: string; + pass: string; + }[] | undefined; + meilisearch: { + host: string; + port: string; + apiKey: string; + ssl?: boolean; + index: string; + scope?: 'local' | 'global' | string[]; + } | undefined; + proxy: string | undefined; + proxySmtp: string | undefined; + proxyBypassHosts: string[] | undefined; + allowedPrivateNetworks: string[] | undefined; + maxFileSize: number | undefined; + clusterLimit: number | undefined; + id: string; + outgoingAddress: string | undefined; + outgoingAddressFamily: 'ipv4' | 'ipv6' | 'dual' | undefined; + deliverJobConcurrency: number | undefined; + inboxJobConcurrency: number | undefined; + relashionshipJobConcurrency: number | undefined; + deliverJobPerSec: number | undefined; + inboxJobPerSec: number | undefined; + relashionshipJobPerSec: number | undefined; + deliverJobMaxAttempts: number | undefined; + inboxJobMaxAttempts: number | undefined; + proxyRemoteFiles: boolean | undefined; + signToActivityPubGet: boolean | undefined; + version: string; host: string; hostname: string; @@ -116,12 +157,14 @@ export type Mixin = { mediaProxy: string; externalMediaProxyEnabled: boolean; videoThumbnailGenerator: string | null; - redisForPubsub: NonNullable; - redisForJobQueue: NonNullable; + redis: RedisOptions & RedisOptionsSource; + redisForPubsub: RedisOptions & RedisOptionsSource; + redisForJobQueue: RedisOptions & RedisOptionsSource; + perChannelMaxNoteCacheCount: number; + perUserNotificationsMaxCount: number; + deactivateAntennaThreshold: number; }; -export type Config = Source & Mixin; - const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -139,7 +182,7 @@ const path = process.env.MISSKEY_CONFIG_YML ? resolve(dir, 'test.yml') : resolve(dir, 'default.yml'); -export function loadConfig() { +export function loadConfig(): Config { const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8')); const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json'); const clientManifest = clientManifestExists ? @@ -147,43 +190,72 @@ export function loadConfig() { : { 'src/_boot_.ts': { file: 'src/_boot_.ts' } }; const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; - const mixin = {} as Mixin; - const url = tryCreateUrl(config.url); - - config.url = url.origin; - - config.port = config.port ?? parseInt(process.env.PORT ?? '', 10); - - mixin.version = meta.version; - mixin.host = url.host; - mixin.hostname = url.hostname; - mixin.scheme = url.protocol.replace(/:$/, ''); - mixin.wsScheme = mixin.scheme.replace('http', 'ws'); - mixin.wsUrl = `${mixin.wsScheme}://${mixin.host}`; - mixin.apiUrl = `${mixin.scheme}://${mixin.host}/api`; - mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`; - mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`; - mixin.userAgent = `Misskey/${meta.version} (${config.url})`; - mixin.clientEntry = clientManifest['src/_boot_.ts']; - mixin.clientManifestExists = clientManifestExists; + const version = meta.version; + const host = url.host; + const hostname = url.hostname; + const scheme = url.protocol.replace(/:$/, ''); + const wsScheme = scheme.replace('http', 'ws'); const externalMediaProxy = config.mediaProxy ? config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy : null; - const internalMediaProxy = `${mixin.scheme}://${mixin.host}/proxy`; - mixin.mediaProxy = externalMediaProxy ?? internalMediaProxy; - mixin.externalMediaProxyEnabled = externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy; + const internalMediaProxy = `${scheme}://${host}/proxy`; + const redis = convertRedisOptions(config.redis, host); - 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; - if (config.redisForPubsub == null) config.redisForPubsub = config.redis; - if (config.redisForJobQueue == null) config.redisForJobQueue = config.redis; - - return Object.assign(config, mixin); + return { + version, + url: url.origin, + port: config.port ?? parseInt(process.env.PORT ?? '', 10), + socket: config.socket, + chmodSocket: config.chmodSocket, + disableHsts: config.disableHsts, + host, + hostname, + scheme, + wsScheme, + wsUrl: `${wsScheme}://${host}`, + apiUrl: `${scheme}://${host}/api`, + authUrl: `${scheme}://${host}/auth`, + driveUrl: `${scheme}://${host}/files`, + db: config.db, + dbReplications: config.dbReplications, + dbSlaves: config.dbSlaves, + meilisearch: config.meilisearch, + redis, + redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis, + redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis, + id: config.id, + proxy: config.proxy, + proxySmtp: config.proxySmtp, + proxyBypassHosts: config.proxyBypassHosts, + allowedPrivateNetworks: config.allowedPrivateNetworks, + maxFileSize: config.maxFileSize, + clusterLimit: config.clusterLimit, + outgoingAddress: config.outgoingAddress, + outgoingAddressFamily: config.outgoingAddressFamily, + deliverJobConcurrency: config.deliverJobConcurrency, + inboxJobConcurrency: config.inboxJobConcurrency, + relashionshipJobConcurrency: config.relashionshipJobConcurrency, + deliverJobPerSec: config.deliverJobPerSec, + inboxJobPerSec: config.inboxJobPerSec, + relashionshipJobPerSec: config.relashionshipJobPerSec, + deliverJobMaxAttempts: config.deliverJobMaxAttempts, + inboxJobMaxAttempts: config.inboxJobMaxAttempts, + proxyRemoteFiles: config.proxyRemoteFiles, + signToActivityPubGet: config.signToActivityPubGet, + mediaProxy: externalMediaProxy ?? internalMediaProxy, + externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy, + videoThumbnailGenerator: config.videoThumbnailGenerator ? + config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator + : null, + userAgent: `Misskey/${version} (${config.url})`, + clientEntry: clientManifest['src/_boot_.ts'], + clientManifestExists: clientManifestExists, + perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000, + perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 300, + deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7), + }; } function tryCreateUrl(url: string) { @@ -193,3 +265,14 @@ function tryCreateUrl(url: string) { throw new Error(`url="${url}" is not a valid URL.`); } } + +function convertRedisOptions(options: RedisOptionsSource, host: string): RedisOptions & RedisOptionsSource { + return { + ...options, + password: options.pass, + prefix: options.prefix ?? host, + family: options.family ?? 0, + keyPrefix: `${options.prefix ?? host}:`, + db: options.db ?? 0, + }; +} diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts index ee1a9a3093..716a8de382 100644 --- a/packages/backend/src/const.ts +++ b/packages/backend/src/const.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const MAX_NOTE_TEXT_LENGTH = 3000; export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index ab11785e28..ec1d013922 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -1,13 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { IsNull, In, MoreThan, Not } from 'typeorm'; import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; -import type { LocalUser, RemoteUser } from '@/models/entities/User.js'; -import type { BlockingsRepository, FollowingsRepository, InstancesRepository, Muting, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/index.js'; +import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; +import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/_.js'; import type { RelationshipJobData, ThinUser } from '@/queue/types.js'; -import type { User } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -27,9 +30,6 @@ import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; @Injectable() export class AccountMoveService { constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -71,12 +71,12 @@ export class AccountMoveService { * After delivering Move activity, its local followers unfollow the old account and then follow the new one. */ @bindThis - public async moveFromLocal(src: LocalUser, dst: LocalUser | RemoteUser): Promise { + public async moveFromLocal(src: MiLocalUser, dst: MiLocalUser | MiRemoteUser): Promise { const srcUri = this.userEntityService.getUserUri(src); const dstUri = this.userEntityService.getUserUri(dst); // add movedToUri to indicate that the user has moved - const update = {} as Partial; + const update = {} as Partial; update.alsoKnownAs = src.alsoKnownAs?.includes(dstUri) ? src.alsoKnownAs : src.alsoKnownAs?.concat([dstUri]) ?? [dstUri]; update.movedToUri = dstUri; update.movedAt = new Date(); @@ -114,7 +114,7 @@ export class AccountMoveService { } @bindThis - public async postMoveProcess(src: User, dst: User): Promise { + public async postMoveProcess(src: MiUser, dst: MiUser): Promise { // Copy blockings and mutings, and update lists try { await Promise.all([ @@ -213,7 +213,7 @@ export class AccountMoveService { * @returns Promise */ @bindThis - public async updateLists(src: ThinUser, dst: User): Promise { + public async updateLists(src: ThinUser, dst: MiUser): Promise { // Return if there is no list to be updated. const oldJoinings = await this.userListJoiningsRepository.find({ where: { @@ -260,7 +260,7 @@ export class AccountMoveService { } @bindThis - private async adjustFollowingCounts(localFollowerIds: string[], oldAccount: User): Promise { + private async adjustFollowingCounts(localFollowerIds: string[], oldAccount: MiUser): Promise { if (localFollowerIds.length === 0) return; // Set the old account's following and followers counts to 0. @@ -295,17 +295,17 @@ export class AccountMoveService { * dstユーザーのalsoKnownAsをfetchPersonしていき、本当にmovedToUrlをdstに指定するユーザーが存在するのかを調べる * * @param dst movedToUrlを指定するユーザー - * @param check + * @param check * @param instant checkがtrueであるユーザーが最初に見つかったら即座にreturnするかどうか * @returns Promise */ @bindThis public async validateAlsoKnownAs( - dst: LocalUser | RemoteUser, - check: (oldUser: LocalUser | RemoteUser | null, newUser: LocalUser | RemoteUser) => boolean | Promise = () => true, + dst: MiLocalUser | MiRemoteUser, + check: (oldUser: MiLocalUser | MiRemoteUser | null, newUser: MiLocalUser | MiRemoteUser) => boolean | Promise = () => true, instant = false, - ): Promise { - let resultUser: LocalUser | RemoteUser | null = null; + ): Promise { + let resultUser: MiLocalUser | MiRemoteUser | null = null; if (this.userEntityService.isRemoteUser(dst)) { if ((new Date()).getTime() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) { diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index b146fc66be..664700ea6b 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -1,8 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; -import type { User } from '@/models/entities/User.js'; +import type { UsersRepository } from '@/models/_.js'; +import type { MiUser } from '@/models/User.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { RelayService } from '@/core/RelayService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; @@ -12,9 +16,6 @@ import { bindThis } from '@/decorators.js'; @Injectable() export class AccountUpdateService { constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -26,7 +27,7 @@ export class AccountUpdateService { } @bindThis - public async publishToFollowers(userId: User['id']) { + public async publishToFollowers(userId: MiUser['id']) { const user = await this.usersRepository.findOneBy({ id: userId }); if (user == null) throw new Error('user not found'); diff --git a/packages/backend/src/core/AchievementService.ts b/packages/backend/src/core/AchievementService.ts index 9e223f1492..1b8718335b 100644 --- a/packages/backend/src/core/AchievementService.ts +++ b/packages/backend/src/core/AchievementService.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; -import type { User } from '@/models/entities/User.js'; +import type { UserProfilesRepository } from '@/models/_.js'; +import type { MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { NotificationService } from '@/core/NotificationService.js'; @@ -80,14 +85,12 @@ export const ACHIEVEMENT_TYPES = [ 'setNameToSyuilo', 'cookieClicked', 'brainDiver', + 'smashTestNotificationButton', ] as const; @Injectable() export class AchievementService { constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, @@ -97,7 +100,7 @@ export class AchievementService { @bindThis public async create( - userId: User['id'], + userId: MiUser['id'], type: typeof ACHIEVEMENT_TYPES[number], ): Promise { if (!ACHIEVEMENT_TYPES.includes(type)) return; diff --git a/packages/backend/src/core/AiService.ts b/packages/backend/src/core/AiService.ts index 059e335eff..4e876495a6 100644 --- a/packages/backend/src/core/AiService.ts +++ b/packages/backend/src/core/AiService.ts @@ -1,11 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import * as nsfw from 'nsfwjs'; import si from 'systeminformation'; -import type { Config } from '@/config.js'; -import { DI } from '@/di-symbols.js'; +import { Mutex } from 'async-mutex'; import { bindThis } from '@/decorators.js'; const _filename = fileURLToPath(import.meta.url); @@ -17,10 +21,9 @@ let isSupportedCpu: undefined | boolean = undefined; @Injectable() export class AiService { private model: nsfw.NSFWJS; + private modelLoadMutex: Mutex = new Mutex(); constructor( - @Inject(DI.config) - private config: Config, ) { } @@ -31,16 +34,22 @@ export class AiService { const cpuFlags = await this.getCpuFlags(); isSupportedCpu = REQUIRED_CPU_FLAGS.every(required => cpuFlags.includes(required)); } - + if (!isSupportedCpu) { console.error('This CPU cannot use TensorFlow.'); return null; } - + const tf = await import('@tensorflow/tfjs-node'); - - if (this.model == null) this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 }); - + + if (this.model == null) { + await this.modelLoadMutex.runExclusive(async () => { + if (this.model == null) { + this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 }); + } + }); + } + const buffer = await fs.promises.readFile(path); const image = await tf.node.decodeImage(buffer, 3) as any; try { diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts new file mode 100644 index 0000000000..70f37516a4 --- /dev/null +++ b/packages/backend/src/core/AnnouncementService.ts @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { MiUser } from '@/models/User.js'; +import type { AnnouncementReadsRepository, AnnouncementsRepository, MiAnnouncement, MiAnnouncementRead } from '@/models/_.js'; +import { bindThis } from '@/decorators.js'; +import { Packed } from '@/misc/json-schema.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; + +@Injectable() +export class AnnouncementService { + constructor( + @Inject(DI.announcementsRepository) + private announcementsRepository: AnnouncementsRepository, + + @Inject(DI.announcementReadsRepository) + private announcementReadsRepository: AnnouncementReadsRepository, + + private idService: IdService, + private globalEventService: GlobalEventService, + ) { + } + + @bindThis + public async getReads(userId: MiUser['id']): Promise { + return this.announcementReadsRepository.findBy({ + userId: userId, + }); + } + + @bindThis + public async getUnreadAnnouncements(user: MiUser): Promise { + const readsQuery = this.announcementReadsRepository.createQueryBuilder('read') + .select('read.announcementId') + .where('read.userId = :userId', { userId: user.id }); + + const q = this.announcementsRepository.createQueryBuilder('announcement') + .where('announcement.isActive = true') + .andWhere(new Brackets(qb => { + qb.orWhere('announcement.userId = :userId', { userId: user.id }); + qb.orWhere('announcement.userId IS NULL'); + })) + .andWhere(new Brackets(qb => { + qb.orWhere('announcement.forExistingUsers = false'); + qb.orWhere('announcement.createdAt > :createdAt', { createdAt: user.createdAt }); + })) + .andWhere(`announcement.id NOT IN (${ readsQuery.getQuery() })`); + + q.setParameters(readsQuery.getParameters()); + + return q.getMany(); + } + + @bindThis + public async create(values: Partial): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> { + const announcement = await this.announcementsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + updatedAt: null, + title: values.title, + text: values.text, + imageUrl: values.imageUrl, + icon: values.icon, + display: values.display, + forExistingUsers: values.forExistingUsers, + needConfirmationToRead: values.needConfirmationToRead, + userId: values.userId, + }).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0])); + + const packed = (await this.packMany([announcement]))[0]; + + if (values.userId) { + this.globalEventService.publishMainStream(values.userId, 'announcementCreated', { + announcement: packed, + }); + } else { + this.globalEventService.publishBroadcastStream('announcementCreated', { + announcement: packed, + }); + } + + return { + raw: announcement, + packed: packed, + }; + } + + @bindThis + public async read(user: MiUser, announcementId: MiAnnouncement['id']): Promise { + try { + await this.announcementReadsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + announcementId: announcementId, + userId: user.id, + }); + } catch (e) { + return; + } + + if ((await this.getUnreadAnnouncements(user)).length === 0) { + this.globalEventService.publishMainStream(user.id, 'readAllAnnouncements'); + } + } + + @bindThis + public async packMany( + announcements: MiAnnouncement[], + me?: { id: MiUser['id'] } | null | undefined, + options?: { + reads?: MiAnnouncementRead[]; + }, + ): Promise[]> { + const reads = me ? (options?.reads ?? await this.getReads(me.id)) : []; + return announcements.map(announcement => ({ + id: announcement.id, + createdAt: announcement.createdAt.toISOString(), + updatedAt: announcement.updatedAt?.toISOString() ?? null, + text: announcement.text, + title: announcement.title, + imageUrl: announcement.imageUrl, + icon: announcement.icon, + display: announcement.display, + needConfirmationToRead: announcement.needConfirmationToRead, + forYou: announcement.userId === me?.id, + isRead: reads.some(read => read.announcementId === announcement.id), + })); + } +} diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index d8df371916..841ce4b84a 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -1,18 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { Antenna } from '@/models/entities/Antenna.js'; -import type { Note } from '@/models/entities/Note.js'; -import type { User } from '@/models/entities/User.js'; -import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; -import { IdService } from '@/core/IdService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; +import type { MiAntenna } from '@/models/Antenna.js'; +import type { MiNote } from '@/models/Note.js'; +import type { MiUser } from '@/models/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { PushNotificationService } from '@/core/PushNotificationService.js'; import * as Acct from '@/misc/acct.js'; import type { Packed } from '@/misc/json-schema.js'; import { DI } from '@/di-symbols.js'; -import type { MutingsRepository, NotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js'; +import type { AntennasRepository, UserListJoiningsRepository } from '@/models/_.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { StreamMessages } from '@/server/api/stream/types.js'; @@ -21,7 +21,7 @@ import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class AntennaService implements OnApplicationShutdown { private antennasFetched: boolean; - private antennas: Antenna[]; + private antennas: MiAntenna[]; constructor( @Inject(DI.redis) @@ -30,12 +30,6 @@ export class AntennaService implements OnApplicationShutdown { @Inject(DI.redisForSub) private redisForSub: Redis.Redis, - @Inject(DI.mutingsRepository) - private mutingsRepository: MutingsRepository, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, @@ -43,11 +37,7 @@ export class AntennaService implements OnApplicationShutdown { private userListJoiningsRepository: UserListJoiningsRepository, private utilityService: UtilityService, - private idService: IdService, private globalEventService: GlobalEventService, - private pushNotificationService: PushNotificationService, - private noteEntityService: NoteEntityService, - private antennaEntityService: AntennaEntityService, ) { this.antennasFetched = false; this.antennas = []; @@ -86,7 +76,7 @@ export class AntennaService implements OnApplicationShutdown { } @bindThis - public async addNoteToAntennas(note: Note, noteUser: { id: User['id']; username: string; host: string | null; }): Promise { + public async addNoteToAntennas(note: MiNote, noteUser: { id: MiUser['id']; username: string; host: string | null; }): Promise { const antennas = await this.getAntennas(); const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const))); const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna); @@ -99,7 +89,7 @@ export class AntennaService implements OnApplicationShutdown { 'MAXLEN', '~', '200', '*', 'note', note.id); - + this.globalEventService.publishAntennaStream(antenna.id, 'note', note); } @@ -109,19 +99,19 @@ export class AntennaService implements OnApplicationShutdown { // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている @bindThis - public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise { + public async checkHitAntenna(antenna: MiAntenna, note: (MiNote | Packed<'Note'>), noteUser: { id: MiUser['id']; username: string; host: string | null; }): Promise { if (note.visibility === 'specified') return false; if (note.visibility === 'followers') return false; - + if (!antenna.withReplies && note.replyId != null) return false; - + if (antenna.src === 'home') { // TODO } else if (antenna.src === 'list') { const listUsers = (await this.userListJoiningsRepository.findBy({ userListId: antenna.userListId!, })).map(x => x.userId); - + if (!listUsers.includes(note.userId)) return false; } else if (antenna.src === 'users') { const accts = antenna.users.map(x => { @@ -129,33 +119,39 @@ export class AntennaService implements OnApplicationShutdown { return this.utilityService.getFullApAccount(username, host).toLowerCase(); }); if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false; + } else if (antenna.src === 'users_blacklist') { + const accts = antenna.users.map(x => { + const { username, host } = Acct.parse(x); + return this.utilityService.getFullApAccount(username, host).toLowerCase(); + }); + if (accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false; } - + const keywords = antenna.keywords // Clean up .map(xs => xs.filter(x => x !== '')) .filter(xs => xs.length > 0); - + if (keywords.length > 0) { 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 ? _text.includes(keyword) : _text.toLowerCase().includes(keyword.toLowerCase()), )); - + if (!matched) return false; } - + const excludeKeywords = antenna.excludeKeywords // Clean up .map(xs => xs.filter(x => x !== '')) .filter(xs => xs.length > 0); - + if (excludeKeywords.length > 0) { if (note.text == null && note.cw == null) return false; @@ -167,16 +163,16 @@ export class AntennaService implements OnApplicationShutdown { ? _text.includes(keyword) : _text.toLowerCase().includes(keyword.toLowerCase()), )); - + if (matched) return false; } - + if (antenna.withFile) { if (note.fileIds && note.fileIds.length === 0) return false; } - + // TODO: eval expression - + return true; } @@ -188,7 +184,7 @@ export class AntennaService implements OnApplicationShutdown { }); this.antennasFetched = true; } - + return this.antennas; } diff --git a/packages/backend/src/core/AppLockService.ts b/packages/backend/src/core/AppLockService.ts index 8dd805552b..7a1293a6de 100644 --- a/packages/backend/src/core/AppLockService.ts +++ b/packages/backend/src/core/AppLockService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { promisify } from 'node:util'; import { Inject, Injectable } from '@nestjs/common'; import redisLock from 'redis-lock'; @@ -32,11 +37,6 @@ export class AppLockService { return this.lock(`ap-object:${uri}`, timeout); } - @bindThis - public getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000): Promise<() => void> { - return this.lock(`instance:${host}`, timeout); - } - @bindThis public getChartInsertLock(lockKey: string, timeout = 30 * 1000): Promise<() => void> { return this.lock(`chart-insert:${lockKey}`, timeout); diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index de33e4c243..6ca684d53c 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js'; -import type { LocalUser, User } from '@/models/entities/User.js'; +import type { MiLocalUser, MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -11,11 +16,11 @@ import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class CacheService implements OnApplicationShutdown { - public userByIdCache: MemoryKVCache; - public localUserByNativeTokenCache: MemoryKVCache; - public localUserByIdCache: MemoryKVCache; - public uriPersonCache: MemoryKVCache; - public userProfileCache: RedisKVCache; + public userByIdCache: MemoryKVCache; + public localUserByNativeTokenCache: MemoryKVCache; + public localUserByIdCache: MemoryKVCache; + public uriPersonCache: MemoryKVCache; + public userProfileCache: RedisKVCache; public userMutingsCache: RedisKVCache>; public userBlockingCache: RedisKVCache>; public userBlockedCache: RedisKVCache>; // NOTE: 「被」Blockキャッシュ @@ -55,12 +60,43 @@ export class CacheService implements OnApplicationShutdown { ) { //this.onMessage = this.onMessage.bind(this); - this.userByIdCache = new MemoryKVCache(Infinity); - this.localUserByNativeTokenCache = new MemoryKVCache(Infinity); - this.localUserByIdCache = new MemoryKVCache(Infinity); - this.uriPersonCache = new MemoryKVCache(Infinity); + const localUserByIdCache = new MemoryKVCache(1000 * 60 * 60 * 6 /* 6h */); + this.localUserByIdCache = localUserByIdCache; - this.userProfileCache = new RedisKVCache(this.redisClient, 'userProfile', { + // ローカルユーザーならlocalUserByIdCacheにデータを追加し、こちらにはid(文字列)だけを追加する + const userByIdCache = new MemoryKVCache(1000 * 60 * 60 * 6 /* 6h */, { + toMapConverter: user => { + if (user.host === null) { + localUserByIdCache.set(user.id, user as MiLocalUser); + return user.id; + } + + return user; + }, + fromMapConverter: userOrId => typeof userOrId === 'string' ? localUserByIdCache.get(userOrId) : userOrId, + }); + this.userByIdCache = userByIdCache; + + this.localUserByNativeTokenCache = new MemoryKVCache(Infinity, { + toMapConverter: user => { + if (user === null) return null; + + localUserByIdCache.set(user.id, user); + return user.id; + }, + fromMapConverter: id => id === null ? null : localUserByIdCache.get(id), + }); + this.uriPersonCache = new MemoryKVCache(Infinity, { + toMapConverter: user => { + if (user === null) return null; + + userByIdCache.set(user.id, user); + return user.id; + }, + fromMapConverter: id => id === null ? null : userByIdCache.get(id), + }); + + this.userProfileCache = new RedisKVCache(this.redisClient, 'userProfile', { lifetime: 1000 * 60 * 30, // 30m memoryCacheLifetime: 1000 * 60, // 1m fetcher: (key) => this.userProfilesRepository.findOneByOrFail({ userId: key }), @@ -131,7 +167,7 @@ export class CacheService implements OnApplicationShutdown { const user = await this.usersRepository.findOneByOrFail({ id: body.id }); this.userByIdCache.set(user.id, user); for (const [k, v] of this.uriPersonCache.cache.entries()) { - if (v.value?.id === user.id) { + if (v.value === user.id) { this.uriPersonCache.set(k, user); } } @@ -142,7 +178,7 @@ export class CacheService implements OnApplicationShutdown { break; } case 'userTokenRegenerated': { - const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as LocalUser; + const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as MiLocalUser; this.localUserByNativeTokenCache.delete(body.oldToken); this.localUserByNativeTokenCache.set(body.newToken, user); break; @@ -161,13 +197,24 @@ export class CacheService implements OnApplicationShutdown { } @bindThis - public findUserById(userId: User['id']) { + public findUserById(userId: MiUser['id']) { return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId })); } @bindThis public dispose(): void { this.redisForSub.off('message', this.onMessage); + this.userByIdCache.dispose(); + this.localUserByNativeTokenCache.dispose(); + this.localUserByIdCache.dispose(); + this.uriPersonCache.dispose(); + this.userProfileCache.dispose(); + this.userMutingsCache.dispose(); + this.userBlockingCache.dispose(); + this.userBlockedCache.dispose(); + this.renoteMutingsCache.dispose(); + this.userFollowingsCache.dispose(); + this.userFollowingChannelsCache.dispose(); } @bindThis diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index 1a52a229c5..f64196f4fc 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; @@ -20,7 +25,7 @@ export class CaptchaService { secret, response, }); - + const res = await this.httpRequestService.send(url, { method: 'POST', body: params.toString(), @@ -28,14 +33,14 @@ export class CaptchaService { 'Content-Type': 'application/x-www-form-urlencoded', }, }, { throwErrorWhenResponseNotOk: false }); - + if (!res.ok) { throw new Error(`${res.status}`); } - + return await res.json() as CaptchaResponse; - } - + } + @bindThis public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise { if (response == null) { @@ -73,7 +78,7 @@ export class CaptchaService { if (response == null) { throw new Error('turnstile-failed: no response provided'); } - + const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => { throw new Error(`turnstile-request-failed: ${err}`); }); diff --git a/packages/backend/src/core/ClipService.ts b/packages/backend/src/core/ClipService.ts new file mode 100644 index 0000000000..3d9982e80f --- /dev/null +++ b/packages/backend/src/core/ClipService.ts @@ -0,0 +1,159 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { QueryFailedError } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { ClipsRepository, MiNote, MiClip, ClipNotesRepository, NotesRepository } from '@/models/_.js'; +import { bindThis } from '@/decorators.js'; +import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; +import { RoleService } from '@/core/RoleService.js'; +import { IdService } from '@/core/IdService.js'; +import type { MiLocalUser } from '@/models/User.js'; + +@Injectable() +export class ClipService { + public static NoSuchNoteError = class extends Error {}; + public static NoSuchClipError = class extends Error {}; + public static AlreadyAddedError = class extends Error {}; + public static TooManyClipNotesError = class extends Error {}; + public static TooManyClipsError = class extends Error {}; + + constructor( + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + @Inject(DI.clipNotesRepository) + private clipNotesRepository: ClipNotesRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private roleService: RoleService, + private idService: IdService, + ) { + } + + @bindThis + public async create(me: MiLocalUser, name: string, isPublic: boolean, description: string | null): Promise { + const currentCount = await this.clipsRepository.countBy({ + userId: me.id, + }); + if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) { + throw new ClipService.TooManyClipsError(); + } + + const clip = await this.clipsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + name: name, + isPublic: isPublic, + description: description, + }).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0])); + + return clip; + } + + @bindThis + public async update(me: MiLocalUser, clipId: MiClip['id'], name: string | undefined, isPublic: boolean | undefined, description: string | null | undefined): Promise { + const clip = await this.clipsRepository.findOneBy({ + id: clipId, + userId: me.id, + }); + + if (clip == null) { + throw new ClipService.NoSuchClipError(); + } + + await this.clipsRepository.update(clip.id, { + name: name, + description: description, + isPublic: isPublic, + }); + } + + @bindThis + public async delete(me: MiLocalUser, clipId: MiClip['id']): Promise { + const clip = await this.clipsRepository.findOneBy({ + id: clipId, + userId: me.id, + }); + + if (clip == null) { + throw new ClipService.NoSuchClipError(); + } + + await this.clipsRepository.delete(clip.id); + } + + @bindThis + public async addNote(me: MiLocalUser, clipId: MiClip['id'], noteId: MiNote['id']): Promise { + const clip = await this.clipsRepository.findOneBy({ + id: clipId, + userId: me.id, + }); + + if (clip == null) { + throw new ClipService.NoSuchClipError(); + } + + const currentCount = await this.clipNotesRepository.countBy({ + clipId: clip.id, + }); + if (currentCount > (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) { + throw new ClipService.TooManyClipNotesError(); + } + + try { + await this.clipNotesRepository.insert({ + id: this.idService.genId(), + noteId: noteId, + clipId: clip.id, + }); + } catch (e: unknown) { + if (e instanceof QueryFailedError) { + if (isDuplicateKeyValueError(e)) { + throw new ClipService.AlreadyAddedError(); + } else if (e.driverError.detail.includes('is not present in table "note".')) { + throw new ClipService.NoSuchNoteError(); + } + } + + throw e; + } + + this.clipsRepository.update(clip.id, { + lastClippedAt: new Date(), + }); + + this.notesRepository.increment({ id: noteId }, 'clippedCount', 1); + } + + @bindThis + public async removeNote(me: MiLocalUser, clipId: MiClip['id'], noteId: MiNote['id']): Promise { + const clip = await this.clipsRepository.findOneBy({ + id: clipId, + userId: me.id, + }); + + if (clip == null) { + throw new ClipService.NoSuchClipError(); + } + + const note = await this.notesRepository.findOneBy({ id: noteId }); + + if (note == null) { + throw new ClipService.NoSuchNoteError(); + } + + await this.clipNotesRepository.delete({ + noteId: noteId, + clipId: clip.id, + }); + + this.notesRepository.decrement({ id: noteId }, 'clippedCount', 1); + } +} diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index d3a1b1b024..78333e70a5 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -1,7 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Module } from '@nestjs/common'; import { AccountMoveService } from './AccountMoveService.js'; import { AccountUpdateService } from './AccountUpdateService.js'; import { AiService } from './AiService.js'; +import { AnnouncementService } from './AnnouncementService.js'; import { AntennaService } from './AntennaService.js'; import { AppLockService } from './AppLockService.js'; import { AchievementService } from './AchievementService.js'; @@ -37,7 +43,7 @@ import { RelayService } from './RelayService.js'; import { RoleService } from './RoleService.js'; import { S3Service } from './S3Service.js'; import { SignupService } from './SignupService.js'; -import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js'; +import { WebAuthnService } from './WebAuthnService.js'; import { UserBlockingService } from './UserBlockingService.js'; import { CacheService } from './CacheService.js'; import { UserFollowingService } from './UserFollowingService.js'; @@ -45,12 +51,14 @@ import { UserKeypairService } from './UserKeypairService.js'; import { UserListService } from './UserListService.js'; import { UserMutingService } from './UserMutingService.js'; import { UserSuspendService } from './UserSuspendService.js'; +import { UserAuthService } from './UserAuthService.js'; import { VideoProcessingService } from './VideoProcessingService.js'; import { WebhookService } from './WebhookService.js'; import { ProxyAccountService } from './ProxyAccountService.js'; import { UtilityService } from './UtilityService.js'; import { FileInfoService } from './FileInfoService.js'; import { SearchService } from './SearchService.js'; +import { ClipService } from './ClipService.js'; import { ChartLoggerService } from './chart/ChartLoggerService.js'; import FederationChart from './chart/charts/federation.js'; import NotesChart from './chart/charts/notes.js'; @@ -81,6 +89,7 @@ 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 { InviteCodeEntityService } from './entities/InviteCodeEntityService.js'; import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js'; import { MutingEntityService } from './entities/MutingEntityService.js'; import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js'; @@ -124,6 +133,7 @@ const $LoggerService: Provider = { provide: 'LoggerService', useExisting: Logger const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService }; const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService }; const $AiService: Provider = { provide: 'AiService', useExisting: AiService }; +const $AnnouncementService: Provider = { provide: 'AnnouncementService', useExisting: AnnouncementService }; const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService }; const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService }; const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService }; @@ -160,7 +170,7 @@ const $RelayService: Provider = { provide: 'RelayService', useExisting: RelaySer const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService }; const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service }; const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService }; -const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService }; +const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService }; const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService }; const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService }; const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService }; @@ -168,11 +178,13 @@ const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisti const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService }; const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService }; const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService }; +const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService }; const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService }; const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService }; const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService }; const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService }; const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService }; +const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService }; const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService }; const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart }; @@ -205,6 +217,7 @@ 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 $InviteCodeEntityService: Provider = { provide: 'InviteCodeEntityService', useExisting: InviteCodeEntityService }; const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService }; const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService }; const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService }; @@ -250,6 +263,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AccountMoveService, AccountUpdateService, AiService, + AnnouncementService, AntennaService, AppLockService, AchievementService, @@ -286,7 +300,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting RoleService, S3Service, SignupService, - TwoFactorAuthenticationService, + WebAuthnService, UserBlockingService, CacheService, UserFollowingService, @@ -294,11 +308,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting UserListService, UserMutingService, UserSuspendService, + UserAuthService, VideoProcessingService, WebhookService, UtilityService, FileInfoService, SearchService, + ClipService, ChartLoggerService, FederationChart, NotesChart, @@ -329,6 +345,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting GalleryPostEntityService, HashtagEntityService, InstanceEntityService, + InviteCodeEntityService, ModerationLogEntityService, MutingEntityService, RenoteMutingEntityService, @@ -369,6 +386,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AccountMoveService, $AccountUpdateService, $AiService, + $AnnouncementService, $AntennaService, $AppLockService, $AchievementService, @@ -405,7 +423,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $RoleService, $S3Service, $SignupService, - $TwoFactorAuthenticationService, + $WebAuthnService, $UserBlockingService, $CacheService, $UserFollowingService, @@ -413,11 +431,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $UserListService, $UserMutingService, $UserSuspendService, + $UserAuthService, $VideoProcessingService, $WebhookService, $UtilityService, $FileInfoService, $SearchService, + $ClipService, $ChartLoggerService, $FederationChart, $NotesChart, @@ -448,6 +468,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $GalleryPostEntityService, $HashtagEntityService, $InstanceEntityService, + $InviteCodeEntityService, $ModerationLogEntityService, $MutingEntityService, $RenoteMutingEntityService, @@ -489,6 +510,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AccountMoveService, AccountUpdateService, AiService, + AnnouncementService, AntennaService, AppLockService, AchievementService, @@ -525,7 +547,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting RoleService, S3Service, SignupService, - TwoFactorAuthenticationService, + WebAuthnService, UserBlockingService, CacheService, UserFollowingService, @@ -533,11 +555,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting UserListService, UserMutingService, UserSuspendService, + UserAuthService, VideoProcessingService, WebhookService, UtilityService, FileInfoService, SearchService, + ClipService, FederationChart, NotesChart, UsersChart, @@ -567,6 +591,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting GalleryPostEntityService, HashtagEntityService, InstanceEntityService, + InviteCodeEntityService, ModerationLogEntityService, MutingEntityService, RenoteMutingEntityService, @@ -607,6 +632,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AccountMoveService, $AccountUpdateService, $AiService, + $AnnouncementService, $AntennaService, $AppLockService, $AchievementService, @@ -643,7 +669,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $RoleService, $S3Service, $SignupService, - $TwoFactorAuthenticationService, + $WebAuthnService, $UserBlockingService, $CacheService, $UserFollowingService, @@ -651,11 +677,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $UserListService, $UserMutingService, $UserSuspendService, + $UserAuthService, $VideoProcessingService, $WebhookService, $UtilityService, $FileInfoService, $SearchService, + $ClipService, $FederationChart, $NotesChart, $UsersChart, @@ -685,6 +713,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $GalleryPostEntityService, $HashtagEntityService, $InstanceEntityService, + $InviteCodeEntityService, $ModerationLogEntityService, $MutingEntityService, $RenoteMutingEntityService, diff --git a/packages/backend/src/core/CreateSystemUserService.ts b/packages/backend/src/core/CreateSystemUserService.ts index 8f887d90f9..3419d0b497 100644 --- a/packages/backend/src/core/CreateSystemUserService.ts +++ b/packages/backend/src/core/CreateSystemUserService.ts @@ -1,13 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; -import { v4 as uuid } from 'uuid'; import { IsNull, DataSource } from 'typeorm'; import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; -import { User } from '@/models/entities/User.js'; -import { UserProfile } from '@/models/entities/UserProfile.js'; +import { MiUser } from '@/models/User.js'; +import { MiUserProfile } from '@/models/UserProfile.js'; import { IdService } from '@/core/IdService.js'; -import { UserKeypair } from '@/models/entities/UserKeypair.js'; -import { UsedUsername } from '@/models/entities/UsedUsername.js'; +import { MiUserKeypair } from '@/models/UserKeypair.js'; +import { MiUsedUsername } from '@/models/UsedUsername.js'; import { DI } from '@/di-symbols.js'; import generateNativeUserToken from '@/misc/generate-native-user-token.js'; import { bindThis } from '@/decorators.js'; @@ -23,30 +28,30 @@ export class CreateSystemUserService { } @bindThis - public async createSystemUser(username: string): Promise { - const password = uuid(); - + public async createSystemUser(username: string): Promise { + const password = randomUUID(); + // Generate hash of password const salt = await bcrypt.genSalt(8); const hash = await bcrypt.hash(password, salt); - + // Generate secret const secret = generateNativeUserToken(); - - const keyPair = await genRsaKeyPair(4096); - - let account!: User; - + + const keyPair = await genRsaKeyPair(); + + let account!: MiUser; + // Start transaction await this.db.transaction(async transactionalEntityManager => { - const exist = await transactionalEntityManager.findOneBy(User, { + const exist = await transactionalEntityManager.findOneBy(MiUser, { usernameLower: username.toLowerCase(), host: IsNull(), }); - + if (exist) throw new Error('the user is already exists'); - - account = await transactionalEntityManager.insert(User, { + + account = await transactionalEntityManager.insert(MiUser, { id: this.idService.genId(), createdAt: new Date(), username: username, @@ -57,26 +62,26 @@ export class CreateSystemUserService { isLocked: true, isExplorable: false, isBot: true, - }).then(x => transactionalEntityManager.findOneByOrFail(User, x.identifiers[0])); - - await transactionalEntityManager.insert(UserKeypair, { + }).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0])); + + await transactionalEntityManager.insert(MiUserKeypair, { publicKey: keyPair.publicKey, privateKey: keyPair.privateKey, userId: account.id, }); - - await transactionalEntityManager.insert(UserProfile, { + + await transactionalEntityManager.insert(MiUserProfile, { userId: account.id, autoAcceptFollowed: false, password: hash, }); - - await transactionalEntityManager.insert(UsedUsername, { + + await transactionalEntityManager.insert(MiUsedUsername, { createdAt: new Date(), username: username.toLowerCase(), }); }); - + return account; } } diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 3499df38b7..aa5490eba7 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -1,37 +1,35 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DataSource, In, IsNull } from 'typeorm'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import { In, IsNull } from 'typeorm'; import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import type { DriveFile } from '@/models/entities/DriveFile.js'; -import type { Emoji } from '@/models/entities/Emoji.js'; -import type { EmojisRepository, Role } from '@/models/index.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { MiEmoji } from '@/models/Emoji.js'; +import type { EmojisRepository, MiRole } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js'; import { UtilityService } from '@/core/UtilityService.js'; -import type { Config } from '@/config.js'; import { query } from '@/misc/prelude/url.js'; import type { Serialized } from '@/server/api/stream/types.js'; const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/; @Injectable() -export class CustomEmojiService { - private cache: MemoryKVCache; - public localEmojisCache: RedisSingleCache>; +export class CustomEmojiService implements OnApplicationShutdown { + private cache: MemoryKVCache; + public localEmojisCache: RedisSingleCache>; constructor( @Inject(DI.redis) private redisClient: Redis.Redis, - @Inject(DI.config) - private config: Config, - - @Inject(DI.db) - private db: DataSource, - @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, @@ -40,16 +38,16 @@ export class CustomEmojiService { private emojiEntityService: EmojiEntityService, private globalEventService: GlobalEventService, ) { - this.cache = new MemoryKVCache(1000 * 60 * 60 * 12); + this.cache = new MemoryKVCache(1000 * 60 * 60 * 12); - this.localEmojisCache = new RedisSingleCache>(this.redisClient, 'localEmojis', { + this.localEmojisCache = new RedisSingleCache>(this.redisClient, 'localEmojis', { lifetime: 1000 * 60 * 30, // 30m memoryCacheLifetime: 1000 * 60 * 3, // 3m fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))), toRedisConverter: (value) => JSON.stringify(Array.from(value.values())), fromRedisConverter: (value) => { if (!Array.isArray(JSON.parse(value))) return undefined; // 古いバージョンの壊れたキャッシュが残っていることがある(そのうち消す) - return new Map(JSON.parse(value).map((x: Serialized) => [x.name, { + return new Map(JSON.parse(value).map((x: Serialized) => [x.name, { ...x, updatedAt: x.updatedAt ? new Date(x.updatedAt) : null, }])); @@ -59,7 +57,7 @@ export class CustomEmojiService { @bindThis public async add(data: { - driveFile: DriveFile; + driveFile: MiDriveFile; name: string; category: string | null; aliases: string[]; @@ -67,8 +65,8 @@ export class CustomEmojiService { license: string | null; isSensitive: boolean; localOnly: boolean; - roleIdsThatCanBeUsedThisEmojiAsReaction: Role['id'][]; - }): Promise { + roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][]; + }): Promise { const emoji = await this.emojisRepository.insert({ id: this.idService.genId(), updatedAt: new Date(), @@ -97,15 +95,15 @@ export class CustomEmojiService { } @bindThis - public async update(id: Emoji['id'], data: { - driveFile?: DriveFile; + public async update(id: MiEmoji['id'], data: { + driveFile?: MiDriveFile; name?: string; category?: string | null; aliases?: string[]; license?: string | null; isSensitive?: boolean; localOnly?: boolean; - roleIdsThatCanBeUsedThisEmojiAsReaction?: Role['id'][]; + roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][]; }): Promise { const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() }); @@ -140,12 +138,12 @@ export class CustomEmojiService { this.globalEventService.publishBroadcastStream('emojiAdded', { emoji: updated, - }); + }); } } @bindThis - public async addAliasesBulk(ids: Emoji['id'][], aliases: string[]) { + public async addAliasesBulk(ids: MiEmoji['id'][], aliases: string[]) { const emojis = await this.emojisRepository.findBy({ id: In(ids), }); @@ -165,7 +163,7 @@ export class CustomEmojiService { } @bindThis - public async setAliasesBulk(ids: Emoji['id'][], aliases: string[]) { + public async setAliasesBulk(ids: MiEmoji['id'][], aliases: string[]) { await this.emojisRepository.update({ id: In(ids), }, { @@ -181,7 +179,7 @@ export class CustomEmojiService { } @bindThis - public async removeAliasesBulk(ids: Emoji['id'][], aliases: string[]) { + public async removeAliasesBulk(ids: MiEmoji['id'][], aliases: string[]) { const emojis = await this.emojisRepository.findBy({ id: In(ids), }); @@ -194,14 +192,14 @@ export class CustomEmojiService { } this.localEmojisCache.refresh(); - + this.globalEventService.publishBroadcastStream('emojiUpdated', { emojis: await this.emojiEntityService.packDetailedMany(ids), }); } @bindThis - public async setCategoryBulk(ids: Emoji['id'][], category: string | null) { + public async setCategoryBulk(ids: MiEmoji['id'][], category: string | null) { await this.emojisRepository.update({ id: In(ids), }, { @@ -215,9 +213,9 @@ export class CustomEmojiService { emojis: await this.emojiEntityService.packDetailedMany(ids), }); } - + @bindThis - public async setLicenseBulk(ids: Emoji['id'][], license: string | null) { + public async setLicenseBulk(ids: MiEmoji['id'][], license: string | null) { await this.emojisRepository.update({ id: In(ids), }, { @@ -233,7 +231,7 @@ export class CustomEmojiService { } @bindThis - public async delete(id: Emoji['id']) { + public async delete(id: MiEmoji['id']) { const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); await this.emojisRepository.delete(emoji.id); @@ -246,7 +244,7 @@ export class CustomEmojiService { } @bindThis - public async deleteBulk(ids: Emoji['id'][]) { + public async deleteBulk(ids: MiEmoji['id'][]) { const emojis = await this.emojisRepository.findBy({ id: In(ids), }); @@ -349,4 +347,14 @@ export class CustomEmojiService { this.cache.set(`${emoji.name} ${emoji.host}`, emoji); } } + + @bindThis + public dispose(): void { + this.cache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts index 327283106f..570bd440e4 100644 --- a/packages/backend/src/core/DeleteAccountService.ts +++ b/packages/backend/src/core/DeleteAccountService.ts @@ -1,8 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/_.js'; import { QueueService } from '@/core/QueueService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @@ -14,7 +18,6 @@ export class DeleteAccountService { private userSuspendService: UserSuspendService, private queueService: QueueService, - private globalEventService: GlobalEventService, ) { } @@ -28,11 +31,11 @@ export class DeleteAccountService { // 物理削除する前にDelete activityを送信する await this.userSuspendService.doPostSuspend(user).catch(e => {}); - + this.queueService.createDeleteAccountJob(user, { soft: false, }); - + await this.usersRepository.update(user.id, { isDeleted: true, }); diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index bd535c6032..5474272b00 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -1,9 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as fs from 'node:fs'; -import * as stream from 'node:stream'; -import * as util from 'node:util'; +import * as stream from 'node:stream/promises'; import { Inject, Injectable } from '@nestjs/common'; -import IPCIDR from 'ip-cidr'; -import PrivateIp from 'private-ip'; +import ipaddr from 'ipaddr.js'; import chalk from 'chalk'; import got, * as Got from 'got'; import { parse } from 'content-disposition'; @@ -15,7 +18,6 @@ import { StatusError } from '@/misc/status-error.js'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; -const pipeline = util.promisify(stream.pipeline); import { bindThis } from '@/decorators.js'; @Injectable() @@ -103,7 +105,7 @@ export class DownloadService { }); try { - await pipeline(req, fs.createWriteStream(path)); + await stream.pipeline(req, fs.createWriteStream(path)); } catch (e) { if (e instanceof Got.HTTPError) { throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage); @@ -123,15 +125,15 @@ export class DownloadService { public async downloadTextFile(url: string): Promise { // Create temp file const [path, cleanup] = await createTemp(); - + this.logger.info(`text file: Temp file is ${path}`); - + try { // write content at URL to temp file await this.downloadUrl(url, path); - - const text = await util.promisify(fs.readFile)(path, 'utf8'); - + + const text = await fs.promises.readFile(path, 'utf8'); + return text; } finally { cleanup(); @@ -140,13 +142,14 @@ export class DownloadService { @bindThis private isPrivateIp(ip: string): boolean { + const parsedIp = ipaddr.parse(ip); + for (const net of this.config.allowedPrivateNetworks ?? []) { - const cidr = new IPCIDR(net); - if (cidr.contains(ip)) { + if (parsedIp.match(ipaddr.parseCIDR(net))) { return false; } } - return PrivateIp(ip) ?? false; + return parsedIp.range() !== 'unicast'; } } diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 1483b55469..e015d3dc41 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -1,17 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { randomUUID } from 'node:crypto'; import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; -import { v4 as uuid } from 'uuid'; import sharp from 'sharp'; import { sharpBmp } from 'sharp-read-bmp'; import { IsNull } from 'typeorm'; import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import Logger from '@/logger.js'; -import type { RemoteUser, User } from '@/models/entities/User.js'; +import type { MiRemoteUser, MiUser } from '@/models/User.js'; import { MetaService } from '@/core/MetaService.js'; -import { DriveFile } from '@/models/entities/DriveFile.js'; +import { MiDriveFile } from '@/models/DriveFile.js'; import { IdService } from '@/core/IdService.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; @@ -22,7 +27,7 @@ import { VideoProcessingService } from '@/core/VideoProcessingService.js'; import { ImageProcessingService } from '@/core/ImageProcessingService.js'; import type { IImage } from '@/core/ImageProcessingService.js'; import { QueueService } from '@/core/QueueService.js'; -import type { DriveFolder } from '@/models/entities/DriveFolder.js'; +import type { MiDriveFolder } from '@/models/DriveFolder.js'; import { createTemp } from '@/misc/create-temp.js'; import DriveChart from '@/core/chart/charts/drive.js'; import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; @@ -40,7 +45,7 @@ import { isMimeImage } from '@/misc/is-mime-image.js'; type AddFileArgs = { /** User who wish to add file */ - user: { id: User['id']; host: User['host'] } | null; + user: { id: MiUser['id']; host: MiUser['host'] } | null; /** File path */ path: string; /** Name */ @@ -68,8 +73,8 @@ type AddFileArgs = { type UploadFromUrlArgs = { url: string; - user: { id: User['id']; host: User['host'] } | null; - folderId?: DriveFolder['id'] | null; + user: { id: MiUser['id']; host: MiUser['host'] } | null; + folderId?: MiDriveFolder['id'] | null; uri?: string | null; sensitive?: boolean; force?: boolean; @@ -133,7 +138,7 @@ export class DriveService { * @param size Size for original */ @bindThis - private async save(file: DriveFile, path: string, name: string, type: string, hash: string, size: number): Promise { + private async save(file: MiDriveFile, path: string, name: string, type: string, hash: string, size: number): Promise { // thunbnail, webpublic を必要なら生成 const alts = await this.generateAlts(path, type, !file.uri); @@ -162,7 +167,7 @@ export class DriveService { ?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`; // for original - const key = `${meta.objectStoragePrefix}/${uuid()}${ext}`; + const key = `${meta.objectStoragePrefix}/${randomUUID()}${ext}`; const url = `${ baseUrl }/${ key }`; // for alts @@ -179,7 +184,7 @@ export class DriveService { ]; if (alts.webpublic) { - webpublicKey = `${meta.objectStoragePrefix}/webpublic-${uuid()}.${alts.webpublic.ext}`; + webpublicKey = `${meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`; webpublicUrl = `${ baseUrl }/${ webpublicKey }`; this.registerLogger.info(`uploading webpublic: ${webpublicKey}`); @@ -187,7 +192,7 @@ export class DriveService { } if (alts.thumbnail) { - thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${uuid()}.${alts.thumbnail.ext}`; + thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`; thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`); @@ -212,9 +217,9 @@ export class DriveService { return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0])); } else { // use internal storage - const accessKey = uuid(); - const thumbnailAccessKey = 'thumbnail-' + uuid(); - const webpublicAccessKey = 'webpublic-' + uuid(); + const accessKey = randomUUID(); + const thumbnailAccessKey = 'thumbnail-' + randomUUID(); + const webpublicAccessKey = 'webpublic-' + randomUUID(); const url = this.internalStorageService.saveFromPath(accessKey, path); @@ -327,7 +332,7 @@ export class DriveService { this.registerLogger.debug('web image not created (not an required image)'); } } catch (err) { - this.registerLogger.warn('web image not created (an error occured)', err as Error); + this.registerLogger.warn('web image not created (an error occurred)', err as Error); } } else { if (satisfyWebpublic) this.registerLogger.info('web image not created (original satisfies webpublic)'); @@ -346,7 +351,7 @@ export class DriveService { thumbnail = await this.imageProcessingService.convertSharpToWebp(img, 498, 422); } } catch (err) { - this.registerLogger.warn('thumbnail not created (an error occured)', err as Error); + this.registerLogger.warn('thumbnail not created (an error occurred)', err as Error); } // #endregion thumbnail @@ -400,7 +405,7 @@ export class DriveService { // Expire oldest file (without avatar or banner) of remote user @bindThis - private async expireOldFile(user: RemoteUser, driveCapacity: number) { + private async expireOldFile(user: MiRemoteUser, driveCapacity: number) { const q = this.driveFilesRepository.createQueryBuilder('file') .where('file.userId = :userId', { userId: user.id }) .andWhere('file.isLink = FALSE'); @@ -446,7 +451,7 @@ export class DriveService { requestIp = null, requestHeaders = null, ext = null, - }: AddFileArgs): Promise { + }: AddFileArgs): Promise { let skipNsfwCheck = false; const instance = await this.metaService.fetch(); const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw; @@ -515,7 +520,7 @@ export class DriveService { if (isLocalUser) { throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.'); } - await this.expireOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as RemoteUser, driveCapacity - info.size); + await this.expireOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as MiRemoteUser, driveCapacity - info.size); } } //#endregion @@ -553,7 +558,7 @@ export class DriveService { const folder = await fetchFolder(); - let file = new DriveFile(); + let file = new MiDriveFile(); file.id = this.idService.genId(); file.createdAt = new Date(); file.userId = user ? user.id : null; @@ -569,9 +574,7 @@ export class DriveService { file.maybePorn = info.porn; file.isSensitive = user ? this.userEntityService.isLocalUser(user) && profile!.alwaysMarkNsfw ? true : - (sensitive !== null && sensitive !== undefined) - ? sensitive - : false + sensitive ?? false : false; if (info.sensitive && profile!.autoSensitive) file.isSensitive = true; @@ -584,9 +587,9 @@ export class DriveService { if (isLink) { file.url = url; // ローカルプロキシ用 - file.accessKey = uuid(); - file.thumbnailAccessKey = 'thumbnail-' + uuid(); - file.webpublicAccessKey = 'webpublic-' + uuid(); + file.accessKey = randomUUID(); + file.thumbnailAccessKey = 'thumbnail-' + randomUUID(); + file.webpublicAccessKey = 'webpublic-' + randomUUID(); } } @@ -611,7 +614,7 @@ export class DriveService { file = await this.driveFilesRepository.findOneBy({ uri: file.uri!, userId: user ? user.id : IsNull(), - }) as DriveFile; + }) as MiDriveFile; } else { this.registerLogger.error(err as Error); throw err; @@ -645,7 +648,7 @@ export class DriveService { } @bindThis - public async deleteFile(file: DriveFile, isExpired = false) { + public async deleteFile(file: MiDriveFile, isExpired = false) { if (file.storedInternal) { this.internalStorageService.del(file.accessKey!); @@ -672,7 +675,7 @@ export class DriveService { } @bindThis - public async deleteFileSync(file: DriveFile, isExpired = false) { + public async deleteFileSync(file: MiDriveFile, isExpired = false) { if (file.storedInternal) { this.internalStorageService.del(file.accessKey!); @@ -703,7 +706,7 @@ export class DriveService { } @bindThis - private async deletePostProcess(file: DriveFile, isExpired = false) { + private async deletePostProcess(file: MiDriveFile, isExpired = false) { // リモートファイル期限切れ削除後は直リンクにする if (isExpired && file.userHost !== null && file.uri != null) { this.driveFilesRepository.update(file.id, { @@ -713,9 +716,9 @@ export class DriveService { webpublicUrl: null, storedInternal: false, // ローカルプロキシ用 - accessKey: uuid(), - thumbnailAccessKey: 'thumbnail-' + uuid(), - webpublicAccessKey: 'webpublic-' + uuid(), + accessKey: randomUUID(), + thumbnailAccessKey: 'thumbnail-' + randomUUID(), + webpublicAccessKey: 'webpublic-' + randomUUID(), }); } else { this.driveFilesRepository.delete(file.id); @@ -766,7 +769,7 @@ export class DriveService { comment = null, requestIp = null, requestHeaders = null, - }: UploadFromUrlArgs): Promise { + }: UploadFromUrlArgs): Promise { // Create temp file const [path, cleanup] = await createTemp(); diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index 59932a5b88..c9da3f77c0 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as nodemailer from 'nodemailer'; import { Inject, Injectable } from '@nestjs/common'; import { validate as validateEmail } from 'deep-email-validator'; @@ -5,7 +10,7 @@ import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; -import type { UserProfilesRepository } from '@/models/index.js'; +import type { UserProfilesRepository } from '@/models/_.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; @@ -29,12 +34,12 @@ export class EmailService { @bindThis public async sendEmail(to: string, subject: string, html: string, text: string) { const meta = await this.metaService.fetch(true); - + const iconUrl = `${this.config.url}/static-assets/mi-white.png`; const emailSettingUrl = `${this.config.url}/settings/email`; - + const enableAuth = meta.smtpUser != null && meta.smtpUser !== ''; - + const transporter = nodemailer.createTransport({ host: meta.smtpHost, port: meta.smtpPort, @@ -46,7 +51,7 @@ export class EmailService { pass: meta.smtpPass, } : undefined, } as any); - + try { // TODO: htmlサニタイズ const info = await transporter.sendMail({ @@ -135,7 +140,7 @@ export class EmailService { `, }); - + this.logger.info(`Message sent: ${info.messageId}`); } catch (err) { this.logger.error(err as Error); @@ -149,12 +154,12 @@ export class EmailService { reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp'; }> { const meta = await this.metaService.fetch(); - + const exist = await this.userProfilesRepository.countBy({ emailVerified: true, email: emailAddress, }); - + const validated = meta.enableActiveEmailValidation ? await validateEmail({ email: emailAddress, validateRegex: true, @@ -163,9 +168,9 @@ export class EmailService { validateDisposable: true, // 捨てアドかどうかチェック validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので }) : { valid: true, reason: null }; - + const available = exist === 0 && validated.valid; - + return { available, reason: available ? null : diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts index 8b9a87a380..61806583c6 100644 --- a/packages/backend/src/core/FederatedInstanceService.ts +++ b/packages/backend/src/core/FederatedInstanceService.ts @@ -1,7 +1,12 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { InstancesRepository } from '@/models/index.js'; -import type { Instance } from '@/models/entities/Instance.js'; +import type { InstancesRepository } from '@/models/_.js'; +import type { MiInstance } from '@/models/Instance.js'; import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; @@ -9,8 +14,8 @@ import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; @Injectable() -export class FederatedInstanceService { - public federatedInstanceCache: RedisKVCache; +export class FederatedInstanceService implements OnApplicationShutdown { + public federatedInstanceCache: RedisKVCache; constructor( @Inject(DI.redis) @@ -22,7 +27,7 @@ export class FederatedInstanceService { private utilityService: UtilityService, private idService: IdService, ) { - this.federatedInstanceCache = new RedisKVCache(this.redisClient, 'federatedInstance', { + this.federatedInstanceCache = new RedisKVCache(this.redisClient, 'federatedInstance', { lifetime: 1000 * 60 * 30, // 30m memoryCacheLifetime: 1000 * 60 * 3, // 3m fetcher: (key) => this.instancesRepository.findOneBy({ host: key }), @@ -41,21 +46,21 @@ export class FederatedInstanceService { } @bindThis - public async fetch(host: string): Promise { + public async fetch(host: string): Promise { host = this.utilityService.toPuny(host); - + const cached = await this.federatedInstanceCache.get(host); if (cached) return cached; - + const index = await this.instancesRepository.findOneBy({ host }); - + if (index == null) { const i = await this.instancesRepository.insert({ id: this.idService.genId(), host, firstRetrievedAt: new Date(), }).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0])); - + this.federatedInstanceCache.set(host, i); return i; } else { @@ -65,7 +70,7 @@ export class FederatedInstanceService { } @bindThis - public async update(id: Instance['id'], data: Partial): Promise { + public async update(id: MiInstance['id'], data: Partial): Promise { const result = await this.instancesRepository.createQueryBuilder().update() .set(data) .where('id = :id', { id }) @@ -74,7 +79,17 @@ export class FederatedInstanceService { .then((response) => { return response.raw[0]; }); - + this.federatedInstanceCache.set(result.host, result); } + + @bindThis + public dispose(): void { + this.federatedInstanceCache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index 9de633350b..682acef15b 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -1,10 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import { JSDOM } from 'jsdom'; import tinycolor from 'tinycolor2'; -import type { Instance } from '@/models/entities/Instance.js'; -import type { InstancesRepository } from '@/models/index.js'; -import { AppLockService } from '@/core/AppLockService.js'; +import * as Redis from 'ioredis'; +import type { MiInstance } from '@/models/Instance.js'; import type Logger from '@/logger.js'; import { DI } from '@/di-symbols.js'; import { LoggerService } from '@/core/LoggerService.js'; @@ -37,39 +41,49 @@ export class FetchInstanceMetadataService { private logger: Logger; constructor( - @Inject(DI.instancesRepository) - private instancesRepository: InstancesRepository, - - private appLockService: AppLockService, private httpRequestService: HttpRequestService, private loggerService: LoggerService, private federatedInstanceService: FederatedInstanceService, + @Inject(DI.redis) + private redisClient: Redis.Redis, ) { this.logger = this.loggerService.getLogger('metadata', 'cyan'); } @bindThis - public async fetchInstanceMetadata(instance: Instance, force = false): Promise { - const unlock = await this.appLockService.getFetchInstanceMetadataLock(instance.host); - - if (!force) { - const _instance = await this.instancesRepository.findOneBy({ host: instance.host }); - const now = Date.now(); - if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) { - unlock(); - return; - } - } - - this.logger.info(`Fetching metadata of ${instance.host} ...`); - + public async tryLock(host: string): Promise { + const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'GET'); + return mutex !== '1'; + } + + @bindThis + public unlock(host: string): Promise<'OK'> { + return this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '0'); + } + + @bindThis + public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise { + const host = instance.host; + // Acquire mutex to ensure no parallel runs + if (!await this.tryLock(host)) return; try { + if (!force) { + const _instance = await this.federatedInstanceService.fetch(host); + const now = Date.now(); + if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) { + // unlock at the finally caluse + return; + } + } + + this.logger.info(`Fetching metadata of ${instance.host} ...`); + const [info, dom, manifest] = await Promise.all([ this.fetchNodeinfo(instance).catch(() => null), this.fetchDom(instance).catch(() => null), this.fetchManifest(instance).catch(() => null), ]); - + const [favicon, icon, themeColor, name, description] = await Promise.all([ this.fetchFaviconUrl(instance, dom).catch(() => null), this.fetchIconUrl(instance, dom, manifest).catch(() => null), @@ -77,13 +91,13 @@ export class FetchInstanceMetadataService { this.getSiteName(info, dom, manifest).catch(() => null), this.getDescription(info, dom, manifest).catch(() => null), ]); - + this.logger.succ(`Successfuly fetched metadata of ${instance.host}`); - + const updates = { infoUpdatedAt: new Date(), } as Record; - + if (info) { updates.softwareName = typeof info.software?.name === 'string' ? info.software.name.toLowerCase() : '?'; updates.softwareVersion = info.software?.version; @@ -91,27 +105,27 @@ export class FetchInstanceMetadataService { updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null; updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null; } - + if (name) updates.name = name; if (description) updates.description = description; - if (icon || favicon) updates.iconUrl = icon ?? favicon; + if (icon ?? favicon) updates.iconUrl = (icon && !icon.includes('data:image/png;base64')) ? icon : favicon; if (favicon) updates.faviconUrl = favicon; if (themeColor) updates.themeColor = themeColor; - + await this.federatedInstanceService.update(instance.id, updates); - + this.logger.succ(`Successfuly updated metadata of ${instance.host}`); } catch (e) { this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`); } finally { - unlock(); + await this.unlock(host); } } @bindThis - private async fetchNodeinfo(instance: Instance): Promise { + private async fetchNodeinfo(instance: MiInstance): Promise { this.logger.info(`Fetching nodeinfo of ${instance.host} ...`); - + try { const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo') .catch(err => { @@ -121,126 +135,126 @@ export class FetchInstanceMetadataService { throw err.statusCode ?? err.message; } }) as Record; - + if (wellknown.links == null || !Array.isArray(wellknown.links)) { throw new Error('No wellknown links'); } - + const links = wellknown.links as any[]; - - const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0'); - const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0'); - const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1'); - const link = lnik2_1 ?? lnik2_0 ?? lnik1_0; - + + const link1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0'); + const link2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0'); + const link2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1'); + const link = link2_1 ?? link2_0 ?? link1_0; + if (link == null) { throw new Error('No nodeinfo link provided'); } - + const info = await this.httpRequestService.getJson(link.href) .catch(err => { throw err.statusCode ?? err.message; }); - + this.logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`); - + return info as NodeInfo; } catch (err) { this.logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${err}`); - + throw err; } } @bindThis - private async fetchDom(instance: Instance): Promise { + private async fetchDom(instance: MiInstance): Promise { this.logger.info(`Fetching HTML of ${instance.host} ...`); - + const url = 'https://' + instance.host; - + const html = await this.httpRequestService.getHtml(url); - + const { window } = new JSDOM(html); const doc = window.document; - + return doc; } @bindThis - private async fetchManifest(instance: Instance): Promise | null> { + private async fetchManifest(instance: MiInstance): Promise | null> { const url = 'https://' + instance.host; - + const manifestUrl = url + '/manifest.json'; - + const manifest = await this.httpRequestService.getJson(manifestUrl) as Record; - + return manifest; } @bindThis - private async fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | null): Promise { + private async fetchFaviconUrl(instance: MiInstance, doc: DOMWindow['document'] | null): Promise { const url = 'https://' + instance.host; - + if (doc) { // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 const href = Array.from(doc.getElementsByTagName('link')).reverse().find(link => link.relList.contains('icon'))?.href; - + if (href) { return (new URL(href, url)).href; } } - + const faviconUrl = url + '/favicon.ico'; - + const favicon = await this.httpRequestService.send(faviconUrl, { method: 'HEAD', }, { throwErrorWhenResponseNotOk: false }); - + if (favicon.ok) { return faviconUrl; } - + return null; } @bindThis - private async fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { + private async fetchIconUrl(instance: MiInstance, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) { const url = 'https://' + instance.host; return (new URL(manifest.icons[0].src, url)).href; } - + if (doc) { const url = 'https://' + instance.host; - + // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 const links = Array.from(doc.getElementsByTagName('link')).reverse(); // https://github.com/misskey-dev/misskey/pull/8220/files/0ec4eba22a914e31b86874f12448f88b3e58dd5a#r796487559 - const href = + const href = [ links.find(link => link.relList.contains('apple-touch-icon-precomposed'))?.href, links.find(link => link.relList.contains('apple-touch-icon'))?.href, links.find(link => link.relList.contains('icon'))?.href, ] .find(href => href); - + if (href) { return (new URL(href, url)).href; } } - + return null; } @bindThis private async getThemeColor(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { const themeColor = info?.metadata?.themeColor ?? doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') ?? manifest?.theme_color; - + if (themeColor) { const color = new tinycolor(themeColor); if (color.isValid()) return color.toHexString(); } - + return null; } @@ -253,19 +267,19 @@ export class FetchInstanceMetadataService { return info.metadata.name; } } - + if (doc) { const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content'); - + if (og) { return og; } } - + if (manifest) { return manifest.name ?? manifest.short_name; } - + return null; } @@ -278,23 +292,23 @@ export class FetchInstanceMetadataService { return info.metadata.description; } } - + if (doc) { const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content'); if (meta) { return meta; } - + const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content'); if (og) { return og; } } - + if (manifest) { return manifest.name ?? manifest.short_name; } - + return null; } } diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index b6cae5ea75..fdea59a8ad 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -1,8 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as fs from 'node:fs'; import * as crypto from 'node:crypto'; import { join } from 'node:path'; -import * as stream from 'node:stream'; -import * as util from 'node:util'; +import * as stream from 'node:stream/promises'; import { Injectable } from '@nestjs/common'; import { FSWatcher } from 'chokidar'; import * as fileType from 'file-type'; @@ -16,8 +20,6 @@ import { createTempDir } from '@/misc/create-temp.js'; import { AiService } from '@/core/AiService.js'; import { bindThis } from '@/decorators.js'; -const pipeline = util.promisify(stream.pipeline); - export type FileInfo = { size: number; md5: string; @@ -161,20 +163,20 @@ export class FileInfoService { private async detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> { let sensitive = false; let porn = false; - + function judgePrediction(result: readonly predictionType[]): [sensitive: boolean, porn: boolean] { let sensitive = false; let porn = false; - + if ((result.find(x => x.className === 'Sexy')?.probability ?? 0) > sensitiveThreshold) sensitive = true; if ((result.find(x => x.className === 'Hentai')?.probability ?? 0) > sensitiveThreshold) sensitive = true; if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThreshold) sensitive = true; - + if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThresholdForPorn) porn = true; - + return [sensitive, porn]; } - + if ([ 'image/jpeg', 'image/png', @@ -253,10 +255,10 @@ export class FileInfoService { disposeOutDir(); } } - + return [sensitive, porn]; } - + private async *asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator { const watcher = new FSWatcher({ cwd, @@ -295,7 +297,7 @@ export class FileInfoService { } } } - + @bindThis private exists(path: string): Promise { return fs.promises.access(path).then(() => true, () => false); @@ -304,11 +306,11 @@ export class FileInfoService { @bindThis public fixMime(mime: string | fileType.MimeType): string { // see https://github.com/misskey-dev/misskey/pull/10686 - if (mime === "audio/x-flac") { - return "audio/flac"; + if (mime === 'audio/x-flac') { + return 'audio/flac'; } - if (mime === "audio/vnd.wave") { - return "audio/wav"; + if (mime === 'audio/vnd.wave') { + return 'audio/wav'; } return mime; @@ -355,11 +357,12 @@ export class FileInfoService { * Check the file is SVG or not */ @bindThis - public async checkSvg(path: string) { + public async checkSvg(path: string): Promise { try { const size = await this.getFileSize(path); if (size > 1 * 1024 * 1024) return false; - return isSvg(fs.readFileSync(path)); + const buffer = await fs.promises.readFile(path); + return isSvg(buffer.toString()); } catch { return false; } @@ -370,8 +373,7 @@ export class FileInfoService { */ @bindThis public async getFileSize(path: string): Promise { - const getStat = util.promisify(fs.stat); - return (await getStat(path)).size; + return (await fs.promises.stat(path)).size; } /** @@ -380,7 +382,7 @@ export class FileInfoService { @bindThis private async calcHash(path: string): Promise { const hash = crypto.createHash('md5').setEncoding('hex'); - await pipeline(fs.createReadStream(path), hash); + await stream.pipeline(fs.createReadStream(path), hash); return hash.read(); } diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 0ed5241148..4bc4f54c21 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -1,9 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import * as 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 { Antenna } from '@/models/entities/Antenna.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiNote } from '@/models/Note.js'; +import type { MiUserList } from '@/models/UserList.js'; +import type { MiAntenna } from '@/models/Antenna.js'; import type { StreamChannels, AdminStreamTypes, @@ -20,7 +25,7 @@ import type { Packed } from '@/misc/json-schema.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; -import { Role } from '@/models'; +import { MiRole } from '@/models/_.js'; @Injectable() export class GlobalEventService { @@ -56,17 +61,17 @@ export class GlobalEventService { } @bindThis - public publishMainStream(userId: User['id'], type: K, value?: MainStreamTypes[K]): void { + public publishMainStream(userId: MiUser['id'], type: K, value?: MainStreamTypes[K]): void { this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value); } @bindThis - public publishDriveStream(userId: User['id'], type: K, value?: DriveStreamTypes[K]): void { + public publishDriveStream(userId: MiUser['id'], type: K, value?: DriveStreamTypes[K]): void { this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value); } @bindThis - public publishNoteStream(noteId: Note['id'], type: K, value?: NoteStreamTypes[K]): void { + public publishNoteStream(noteId: MiNote['id'], type: K, value?: NoteStreamTypes[K]): void { this.publish(`noteStream:${noteId}`, type, { id: noteId, body: value, @@ -74,17 +79,17 @@ export class GlobalEventService { } @bindThis - public publishUserListStream(listId: UserList['id'], type: K, value?: UserListStreamTypes[K]): void { + public publishUserListStream(listId: MiUserList['id'], type: K, value?: UserListStreamTypes[K]): void { this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value); } @bindThis - public publishAntennaStream(antennaId: Antenna['id'], type: K, value?: AntennaStreamTypes[K]): void { + public publishAntennaStream(antennaId: MiAntenna['id'], type: K, value?: AntennaStreamTypes[K]): void { this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value); } @bindThis - public publishRoleTimelineStream(roleId: Role['id'], type: K, value?: RoleTimelineStreamTypes[K]): void { + public publishRoleTimelineStream(roleId: MiRole['id'], type: K, value?: RoleTimelineStreamTypes[K]): void { this.publish(`roleTimelineStream:${roleId}`, type, typeof value === 'undefined' ? null : value); } @@ -94,7 +99,7 @@ export class GlobalEventService { } @bindThis - public publishAdminStream(userId: User['id'], type: K, value?: AdminStreamTypes[K]): void { + public publishAdminStream(userId: MiUser['id'], type: K, value?: AdminStreamTypes[K]): void { this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value); } } diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts index 851e42e7ba..c72c7460ff 100644 --- a/packages/backend/src/core/HashtagService.ts +++ b/packages/backend/src/core/HashtagService.ts @@ -1,19 +1,21 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { User } from '@/models/entities/User.js'; +import type { MiUser } from '@/models/User.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { IdService } from '@/core/IdService.js'; -import type { Hashtag } from '@/models/entities/Hashtag.js'; -import type { HashtagsRepository, UsersRepository } from '@/models/index.js'; +import type { MiHashtag } from '@/models/Hashtag.js'; +import type { HashtagsRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @Injectable() export class HashtagService { constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.hashtagsRepository) private hashtagsRepository: HashtagsRepository, @@ -23,14 +25,14 @@ export class HashtagService { } @bindThis - public async updateHashtags(user: { id: User['id']; host: User['host']; }, tags: string[]) { + public async updateHashtags(user: { id: MiUser['id']; host: MiUser['host']; }, tags: string[]) { for (const tag of tags) { await this.updateHashtag(user, tag); } } @bindThis - public async updateUsertags(user: User, tags: string[]) { + public async updateUsertags(user: MiUser, tags: string[]) { for (const tag of tags) { await this.updateHashtag(user, tag, true, true); } @@ -41,7 +43,7 @@ export class HashtagService { } @bindThis - public async updateHashtag(user: { id: User['id']; host: User['host']; }, tag: string, isUserAttached = false, inc = true) { + public async updateHashtag(user: { id: MiUser['id']; host: MiUser['host']; }, tag: string, isUserAttached = false, inc = true) { tag = normalizeForSearch(tag); const index = await this.hashtagsRepository.findOneBy({ name: tag }); @@ -121,7 +123,7 @@ export class HashtagService { attachedLocalUsersCount: this.userEntityService.isLocalUser(user) ? 1 : 0, attachedRemoteUserIds: this.userEntityService.isRemoteUser(user) ? [user.id] : [], attachedRemoteUsersCount: this.userEntityService.isRemoteUser(user) ? 1 : 0, - } as Hashtag); + } as MiHashtag); } else { this.hashtagsRepository.insert({ id: this.idService.genId(), @@ -138,7 +140,7 @@ export class HashtagService { attachedLocalUsersCount: 0, attachedRemoteUserIds: [], attachedRemoteUsersCount: 0, - } as Hashtag); + } as MiHashtag); } } } diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 375aa846cb..73bb3dc7e9 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -1,5 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as http from 'node:http'; import * as https from 'node:https'; +import * as net from 'node:net'; import CacheableLookup from 'cacheable-lookup'; import fetch from 'node-fetch'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; @@ -42,21 +48,23 @@ export class HttpRequestService { errorTtl: 30, // 30secs lookup: false, // nativeのdns.lookupにfallbackしない }); - + this.http = new http.Agent({ keepAlive: true, keepAliveMsecs: 30 * 1000, - lookup: cache.lookup, - } as http.AgentOptions); - + lookup: cache.lookup as unknown as net.LookupFunction, + localAddress: config.outgoingAddress, + }); + this.https = new https.Agent({ keepAlive: true, keepAliveMsecs: 30 * 1000, - lookup: cache.lookup, - } as https.AgentOptions); - + lookup: cache.lookup as unknown as net.LookupFunction, + localAddress: config.outgoingAddress, + }); + const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128); - + this.httpAgent = config.proxy ? new HttpProxyAgent({ keepAlive: true, @@ -65,6 +73,7 @@ export class HttpRequestService { maxFreeSockets: 256, scheduling: 'lifo', proxy: config.proxy, + localAddress: config.outgoingAddress, }) : this.http; @@ -76,6 +85,7 @@ export class HttpRequestService { maxFreeSockets: 256, scheduling: 'lifo', proxy: config.proxy, + localAddress: config.outgoingAddress, }) : this.https; } @@ -87,7 +97,7 @@ export class HttpRequestService { */ @bindThis public getAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent { - if (bypassProxy || (this.config.proxyBypassHosts || []).includes(url.hostname)) { + if (bypassProxy || (this.config.proxyBypassHosts ?? []).includes(url.hostname)) { return url.protocol === 'http:' ? this.http : this.https; } else { return url.protocol === 'http:' ? this.httpAgent : this.httpsAgent; @@ -144,7 +154,7 @@ export class HttpRequestService { method: args.method ?? 'GET', headers: { 'User-Agent': this.config.userAgent, - ...(args.headers ?? {}) + ...(args.headers ?? {}), }, body: args.body, size: args.size ?? 10 * 1024 * 1024, diff --git a/packages/backend/src/core/IdService.ts b/packages/backend/src/core/IdService.ts index 8aa6ccfc4e..06c58ad8a1 100644 --- a/packages/backend/src/core/IdService.ts +++ b/packages/backend/src/core/IdService.ts @@ -1,11 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { ulid } from 'ulid'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { genAid, parseAid } from '@/misc/id/aid.js'; +import { genAidx, parseAidx } from '@/misc/id/aidx.js'; import { genMeid, parseMeid } from '@/misc/id/meid.js'; import { genMeidg, parseMeidg } from '@/misc/id/meidg.js'; -import { genObjectId } from '@/misc/id/object-id.js'; +import { genObjectId, parseObjectId } from '@/misc/id/object-id.js'; import { bindThis } from '@/decorators.js'; import { parseUlid } from '@/misc/id/ulid.js'; @@ -23,9 +29,10 @@ export class IdService { @bindThis public genId(date?: Date): string { if (!date || (date > new Date())) date = new Date(); - + switch (this.method) { case 'aid': return genAid(date); + case 'aidx': return genAidx(date); case 'meid': return genMeid(date); case 'meidg': return genMeidg(date); case 'ulid': return ulid(date.getTime()); @@ -38,7 +45,8 @@ export class IdService { public parse(id: string): { date: Date; } { switch (this.method) { case 'aid': return parseAid(id); - case 'objectid': + case 'aidx': return parseAidx(id); + case 'objectid': return parseObjectId(id); case 'meid': return parseMeid(id); case 'meidg': return parseMeidg(id); case 'ulid': return parseUlid(id); diff --git a/packages/backend/src/core/ImageProcessingService.ts b/packages/backend/src/core/ImageProcessingService.ts index 3246475d12..8e800eb8f5 100644 --- a/packages/backend/src/core/ImageProcessingService.ts +++ b/packages/backend/src/core/ImageProcessingService.ts @@ -1,7 +1,10 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import sharp from 'sharp'; -import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; export type IImage = { data: Buffer; @@ -45,8 +48,6 @@ import { Readable } from 'node:stream'; @Injectable() export class ImageProcessingService { constructor( - @Inject(DI.config) - private config: Config, ) { } diff --git a/packages/backend/src/core/InstanceActorService.ts b/packages/backend/src/core/InstanceActorService.ts index 4fb3fc5b4f..b40fd46291 100644 --- a/packages/backend/src/core/InstanceActorService.ts +++ b/packages/backend/src/core/InstanceActorService.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; -import type { LocalUser } from '@/models/entities/User.js'; -import type { UsersRepository } from '@/models/index.js'; +import type { MiLocalUser } from '@/models/User.js'; +import type { UsersRepository } from '@/models/_.js'; import { MemorySingleCache } from '@/misc/cache.js'; import { DI } from '@/di-symbols.js'; import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; @@ -11,7 +16,7 @@ const ACTOR_USERNAME = 'instance.actor' as const; @Injectable() export class InstanceActorService { - private cache: MemorySingleCache; + private cache: MemorySingleCache; constructor( @Inject(DI.usersRepository) @@ -19,24 +24,24 @@ export class InstanceActorService { private createSystemUserService: CreateSystemUserService, ) { - this.cache = new MemorySingleCache(Infinity); + this.cache = new MemorySingleCache(Infinity); } @bindThis - public async getInstanceActor(): Promise { + public async getInstanceActor(): Promise { const cached = this.cache.get(); if (cached) return cached; - + const user = await this.usersRepository.findOneBy({ host: IsNull(), username: ACTOR_USERNAME, - }) as LocalUser | undefined; - + }) as MiLocalUser | undefined; + if (user) { this.cache.set(user); return user; } else { - const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser; + const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as MiLocalUser; this.cache.set(created); return created; } diff --git a/packages/backend/src/core/InternalStorageService.ts b/packages/backend/src/core/InternalStorageService.ts index 7c03af7de7..22129bb348 100644 --- a/packages/backend/src/core/InternalStorageService.ts +++ b/packages/backend/src/core/InternalStorageService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as fs from 'node:fs'; import * as Path from 'node:path'; import { fileURLToPath } from 'node:url'; diff --git a/packages/backend/src/core/LoggerService.ts b/packages/backend/src/core/LoggerService.ts index 441c254f48..46b000ee63 100644 --- a/packages/backend/src/core/LoggerService.ts +++ b/packages/backend/src/core/LoggerService.ts @@ -1,15 +1,16 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import type { KEYWORD } from 'color-convert/conversions'; +import type { KEYWORD } from 'color-convert/conversions.js'; @Injectable() export class LoggerService { constructor( - @Inject(DI.config) - private config: Config, ) { } diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts index 5acc9ad9ad..00e1e3c1fc 100644 --- a/packages/backend/src/core/MetaService.ts +++ b/packages/backend/src/core/MetaService.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; -import { Meta } from '@/models/entities/Meta.js'; +import { MiMeta } from '@/models/Meta.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { bindThis } from '@/decorators.js'; import { StreamMessages } from '@/server/api/stream/types.js'; @@ -10,8 +15,8 @@ import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class MetaService implements OnApplicationShutdown { - private cache: Meta | undefined; - private intervalId: NodeJS.Timer; + private cache: MiMeta | undefined; + private intervalId: NodeJS.Timeout; constructor( @Inject(DI.redisForSub) @@ -54,19 +59,19 @@ export class MetaService implements OnApplicationShutdown { } @bindThis - public async fetch(noCache = false): Promise { + public async fetch(noCache = false): Promise { if (!noCache && this.cache) return this.cache; - + return await this.db.transaction(async transactionalEntityManager => { // 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する - const metas = await transactionalEntityManager.find(Meta, { + const metas = await transactionalEntityManager.find(MiMeta, { order: { id: 'DESC', }, }); - + const meta = metas[0]; - + if (meta) { this.cache = meta; return meta; @@ -74,14 +79,14 @@ export class MetaService implements OnApplicationShutdown { // metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う const saved = await transactionalEntityManager .upsert( - Meta, + MiMeta, { id: 'x', }, ['id'], ) - .then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0])); - + .then((x) => transactionalEntityManager.findOneByOrFail(MiMeta, x.identifiers[0])); + this.cache = saved; return saved; } @@ -89,9 +94,9 @@ export class MetaService implements OnApplicationShutdown { } @bindThis - public async update(data: Partial): Promise { + public async update(data: Partial): Promise { const updated = await this.db.transaction(async transactionalEntityManager => { - const metas = await transactionalEntityManager.find(Meta, { + const metas = await transactionalEntityManager.find(MiMeta, { order: { id: 'DESC', }, @@ -100,9 +105,9 @@ export class MetaService implements OnApplicationShutdown { const meta = metas[0]; if (meta) { - await transactionalEntityManager.update(Meta, meta.id, data); + await transactionalEntityManager.update(MiMeta, meta.id, data); - const metas = await transactionalEntityManager.find(Meta, { + const metas = await transactionalEntityManager.find(MiMeta, { order: { id: 'DESC', }, @@ -110,7 +115,7 @@ export class MetaService implements OnApplicationShutdown { return metas[0]; } else { - return await transactionalEntityManager.save(Meta, data); + return await transactionalEntityManager.save(MiMeta, data); } }); diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index dffee16e08..b275d1b142 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import * as parse5 from 'parse5'; @@ -5,7 +10,7 @@ import { Window } from 'happy-dom'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { intersperse } from '@/misc/prelude/array.js'; -import type { IMentionedRemoteUsers } from '@/models/entities/Note.js'; +import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js'; import type * as mfm from 'mfm-js'; @@ -27,29 +32,29 @@ export class MfmService { public fromHtml(html: string, hashtagNames?: string[]): string { // some AP servers like Pixelfed use br tags as well as newlines html = html.replace(/\r?\n/gi, '\n'); - + const dom = parse5.parseFragment(html); - + let text = ''; - + for (const n of dom.childNodes) { analyze(n); } - + return text.trim(); - + function getText(node: TreeAdapter.Node): string { if (treeAdapter.isTextNode(node)) return node.value; if (!treeAdapter.isElementNode(node)) return ''; if (node.nodeName === 'br') return '\n'; - + if (node.childNodes) { return node.childNodes.map(n => getText(n)).join(''); } - + return ''; } - + function appendChildren(childNodes: TreeAdapter.ChildNode[]): void { if (childNodes) { for (const n of childNodes) { @@ -57,35 +62,35 @@ export class MfmService { } } } - + function analyze(node: TreeAdapter.Node) { if (treeAdapter.isTextNode(node)) { text += node.value; return; } - + // Skip comment or document type node if (!treeAdapter.isElementNode(node)) return; - + switch (node.nodeName) { case 'br': { text += '\n'; break; } - + case 'a': { const txt = getText(node); const rel = node.attrs.find(x => x.name === 'rel'); const href = node.attrs.find(x => x.name === 'href'); - + // ハッシュタグ if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) { text += txt; // メンション } else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) { const part = txt.split('@'); - + if (part.length === 2 && href) { //#region ホスト名部分が省略されているので復元する const acct = `${txt}@${(new URL(href.value)).hostname}`; @@ -116,12 +121,12 @@ export class MfmService { return `[${txt}](${href.value})`; } }; - + text += generateLink(); } break; } - + case 'h1': { text += '【'; @@ -129,7 +134,7 @@ export class MfmService { text += '】\n'; break; } - + case 'b': case 'strong': { @@ -138,7 +143,7 @@ export class MfmService { text += '**'; break; } - + case 'small': { text += ''; @@ -146,7 +151,7 @@ export class MfmService { text += ''; break; } - + case 's': case 'del': { @@ -155,7 +160,7 @@ export class MfmService { text += '~~'; break; } - + case 'i': case 'em': { @@ -164,7 +169,7 @@ export class MfmService { text += ''; break; } - + // block code (
)
 				case 'pre': {
 					if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
@@ -176,7 +181,7 @@ export class MfmService {
 					}
 					break;
 				}
-	
+
 				// inline code ()
 				case 'code': {
 					text += '`';
@@ -184,7 +189,7 @@ export class MfmService {
 					text += '`';
 					break;
 				}
-	
+
 				case 'blockquote': {
 					const t = getText(node);
 					if (t) {
@@ -193,7 +198,7 @@ export class MfmService {
 					}
 					break;
 				}
-	
+
 				case 'p':
 				case 'h2':
 				case 'h3':
@@ -205,7 +210,7 @@ export class MfmService {
 					appendChildren(node.childNodes);
 					break;
 				}
-	
+
 				// other block elements
 				case 'div':
 				case 'header':
@@ -219,7 +224,7 @@ export class MfmService {
 					appendChildren(node.childNodes);
 					break;
 				}
-	
+
 				default:	// includes inline elements
 				{
 					appendChildren(node.childNodes);
@@ -234,48 +239,48 @@ export class MfmService {
 		if (nodes == null) {
 			return null;
 		}
-	
+
 		const { window } = new Window();
-	
+
 		const doc = window.document;
-	
+
 		function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
 			if (children) {
 				for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
 			}
 		}
-	
+
 		const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => any } = {
 			bold: (node) => {
 				const el = doc.createElement('b');
 				appendChildren(node.children, el);
 				return el;
 			},
-	
+
 			small: (node) => {
 				const el = doc.createElement('small');
 				appendChildren(node.children, el);
 				return el;
 			},
-	
+
 			strike: (node) => {
 				const el = doc.createElement('del');
 				appendChildren(node.children, el);
 				return el;
 			},
-	
+
 			italic: (node) => {
 				const el = doc.createElement('i');
 				appendChildren(node.children, el);
 				return el;
 			},
-	
+
 			fn: (node) => {
 				const el = doc.createElement('i');
 				appendChildren(node.children, el);
 				return el;
 			},
-	
+
 			blockCode: (node) => {
 				const pre = doc.createElement('pre');
 				const inner = doc.createElement('code');
@@ -283,21 +288,21 @@ export class MfmService {
 				pre.appendChild(inner);
 				return pre;
 			},
-	
+
 			center: (node) => {
 				const el = doc.createElement('div');
 				appendChildren(node.children, el);
 				return el;
 			},
-	
+
 			emojiCode: (node) => {
 				return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
 			},
-	
+
 			unicodeEmoji: (node) => {
 				return doc.createTextNode(node.props.emoji);
 			},
-	
+
 			hashtag: (node) => {
 				const a = doc.createElement('a');
 				a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
@@ -305,32 +310,32 @@ export class MfmService {
 				a.setAttribute('rel', 'tag');
 				return a;
 			},
-	
+
 			inlineCode: (node) => {
 				const el = doc.createElement('code');
 				el.textContent = node.props.code;
 				return el;
 			},
-	
+
 			mathInline: (node) => {
 				const el = doc.createElement('code');
 				el.textContent = node.props.formula;
 				return el;
 			},
-	
+
 			mathBlock: (node) => {
 				const el = doc.createElement('code');
 				el.textContent = node.props.formula;
 				return el;
 			},
-	
+
 			link: (node) => {
 				const a = doc.createElement('a');
 				a.setAttribute('href', node.props.url);
 				appendChildren(node.children, a);
 				return a;
 			},
-	
+
 			mention: (node) => {
 				const a = doc.createElement('a');
 				const { username, host, acct } = node.props;
@@ -340,47 +345,47 @@ export class MfmService {
 				a.textContent = acct;
 				return a;
 			},
-	
+
 			quote: (node) => {
 				const el = doc.createElement('blockquote');
 				appendChildren(node.children, el);
 				return el;
 			},
-	
+
 			text: (node) => {
 				const el = doc.createElement('span');
 				const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
-	
+
 				for (const x of intersperse('br', nodes)) {
 					el.appendChild(x === 'br' ? doc.createElement('br') : x);
 				}
-	
+
 				return el;
 			},
-	
+
 			url: (node) => {
 				const a = doc.createElement('a');
 				a.setAttribute('href', node.props.url);
 				a.textContent = node.props.url;
 				return a;
 			},
-	
+
 			search: (node) => {
 				const a = doc.createElement('a');
 				a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
 				a.textContent = node.props.content;
 				return a;
 			},
-	
+
 			plain: (node) => {
 				const el = doc.createElement('span');
 				appendChildren(node.children, el);
 				return el;
 			},
 		};
-	
+
 		appendChildren(nodes, doc.body);
-	
+
 		return `

${doc.body.innerHTML}

`; - } + } } diff --git a/packages/backend/src/core/ModerationLogService.ts b/packages/backend/src/core/ModerationLogService.ts index e0ec993bd6..0f18fa3754 100644 --- a/packages/backend/src/core/ModerationLogService.ts +++ b/packages/backend/src/core/ModerationLogService.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { ModerationLogsRepository } from '@/models/index.js'; -import type { User } from '@/models/entities/User.js'; +import type { ModerationLogsRepository } from '@/models/_.js'; +import type { MiUser } from '@/models/User.js'; import { IdService } from '@/core/IdService.js'; import { bindThis } from '@/decorators.js'; @@ -16,7 +21,7 @@ export class ModerationLogService { } @bindThis - public async log(moderator: { id: User['id'] }, type: string, info?: Record) { + public async log(moderator: { id: MiUser['id'] }, type: string, info?: Record) { await this.moderationLogsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 1c8491bf57..972319ddcf 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { setImmediate } from 'node:timers/promises'; import * as mfm from 'mfm-js'; import { In, DataSource } from 'typeorm'; @@ -7,22 +12,22 @@ import RE2 from 're2'; import { extractMentions } from '@/misc/extract-mentions.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; -import type { IMentionedRemoteUsers } from '@/models/entities/Note.js'; -import { Note } from '@/models/entities/Note.js'; -import type { ChannelFollowingsRepository, ChannelsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; -import type { DriveFile } from '@/models/entities/DriveFile.js'; -import type { App } from '@/models/entities/App.js'; +import type { IMentionedRemoteUsers } from '@/models/Note.js'; +import { MiNote } from '@/models/Note.js'; +import type { ChannelsRepository, FollowingsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { MiApp } from '@/models/App.js'; import { concat } from '@/misc/prelude/array.js'; import { IdService } from '@/core/IdService.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 type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; +import type { IPoll } from '@/models/Poll.js'; +import { MiPoll } from '@/models/Poll.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { checkWordMute } from '@/misc/check-word-mute.js'; -import type { Channel } from '@/models/entities/Channel.js'; +import type { MiChannel } from '@/models/Channel.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { MemorySingleCache } from '@/misc/cache.js'; -import type { UserProfile } from '@/models/entities/UserProfile.js'; +import type { MiUserProfile } from '@/models/UserProfile.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { DI } from '@/di-symbols.js'; @@ -49,23 +54,23 @@ import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; -const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); +const mutedWordsCache = new MemorySingleCache<{ userId: MiUserProfile['userId']; mutedWords: MiUserProfile['mutedWords']; }[]>(1000 * 60 * 5); type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; class NotificationManager { - private notifier: { id: User['id']; }; - private note: Note; + private notifier: { id: MiUser['id']; }; + private note: MiNote; private queue: { - target: LocalUser['id']; + target: MiLocalUser['id']; reason: NotificationType; }[]; constructor( private mutingsRepository: MutingsRepository, private notificationService: NotificationService, - notifier: { id: User['id']; }, - note: Note, + notifier: { id: MiUser['id']; }, + note: MiNote, ) { this.notifier = notifier; this.note = note; @@ -73,7 +78,7 @@ class NotificationManager { } @bindThis - public push(notifiee: LocalUser['id'], reason: NotificationType) { + public push(notifiee: MiLocalUser['id'], reason: NotificationType) { // 自分自身へは通知しない if (this.notifier.id === notifiee) return; @@ -114,32 +119,32 @@ class NotificationManager { } type MinimumUser = { - id: User['id']; - host: User['host']; - username: User['username']; - uri: User['uri']; + id: MiUser['id']; + host: MiUser['host']; + username: MiUser['username']; + uri: MiUser['uri']; }; type Option = { createdAt?: Date | null; name?: string | null; text?: string | null; - reply?: Note | null; - renote?: Note | null; - files?: DriveFile[] | null; + reply?: MiNote | null; + renote?: MiNote | null; + files?: MiDriveFile[] | null; poll?: IPoll | null; localOnly?: boolean | null; - reactionAcceptance?: Note['reactionAcceptance']; + reactionAcceptance?: MiNote['reactionAcceptance']; cw?: string | null; visibility?: string; visibleUsers?: MinimumUser[] | null; - channel?: Channel | null; + channel?: MiChannel | null; apMentions?: MinimumUser[] | null; apHashtags?: string[] | null; apEmojis?: string[] | null; uri?: string | null; url?: string | null; - app?: App | null; + app?: MiApp | null; }; @Injectable() @@ -177,12 +182,12 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, - @Inject(DI.channelFollowingsRepository) - private channelFollowingsRepository: ChannelFollowingsRepository, - @Inject(DI.noteThreadMutingsRepository) private noteThreadMutingsRepository: NoteThreadMutingsRepository, + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private idService: IdService, @@ -209,12 +214,12 @@ export class NoteCreateService implements OnApplicationShutdown { @bindThis public async create(user: { - id: User['id']; - username: User['username']; - host: User['host']; - createdAt: User['createdAt']; - isBot: User['isBot']; - }, data: Option, silent = false): Promise { + id: MiUser['id']; + username: MiUser['username']; + host: MiUser['host']; + createdAt: MiUser['createdAt']; + isBot: MiUser['isBot']; + }, data: Option, silent = false): Promise { // チャンネル外にリプライしたら対象のスコープに合わせる // (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで) if (data.reply && data.channel && data.reply.channelId !== data.channel.id) { @@ -332,7 +337,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.channel) { this.redisClient.xadd( `channelTimeline:${data.channel.id}`, - 'MAXLEN', '~', '1000', + 'MAXLEN', '~', this.config.perChannelMaxNoteCacheCount.toString(), '*', 'note', note.id); } @@ -346,8 +351,8 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - private async insertNote(user: { id: User['id']; host: User['host']; }, data: Option, tags: string[], emojis: string[], mentionedUsers: MinimumUser[]) { - const insert = new Note({ + private async insertNote(user: { id: MiUser['id']; host: MiUser['host']; }, data: Option, tags: string[], emojis: string[], mentionedUsers: MinimumUser[]) { + const insert = new MiNote({ id: this.idService.genId(data.createdAt!), createdAt: data.createdAt!, fileIds: data.files ? data.files.map(file => file.id) : [], @@ -362,7 +367,7 @@ export class NoteCreateService implements OnApplicationShutdown { name: data.name, text: data.text, hasPoll: data.poll != null, - cw: data.cw == null ? null : data.cw, + cw: data.cw ?? null, tags: tags.map(tag => normalizeForSearch(tag)), emojis, userId: user.id, @@ -397,7 +402,7 @@ export class NoteCreateService implements OnApplicationShutdown { const url = profile != null ? profile.url : null; return { uri: u.uri, - url: url == null ? undefined : url, + url: url ?? undefined, username: u.username, host: u.host, } as IMentionedRemoteUsers[0]; @@ -409,9 +414,9 @@ export class NoteCreateService implements OnApplicationShutdown { if (insert.hasPoll) { // Start transaction await this.db.transaction(async transactionalEntityManager => { - await transactionalEntityManager.insert(Note, insert); + await transactionalEntityManager.insert(MiNote, insert); - const poll = new Poll({ + const poll = new MiPoll({ noteId: insert.id, choices: data.poll!.choices, expiresAt: data.poll!.expiresAt, @@ -422,7 +427,7 @@ export class NoteCreateService implements OnApplicationShutdown { userHost: user.host, }); - await transactionalEntityManager.insert(Poll, poll); + await transactionalEntityManager.insert(MiPoll, poll); }); } else { await this.notesRepository.insert(insert); @@ -444,12 +449,12 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - private async postNoteCreated(note: Note, user: { - id: User['id']; - username: User['username']; - host: User['host']; - createdAt: User['createdAt']; - isBot: User['isBot']; + private async postNoteCreated(note: MiNote, user: { + id: MiUser['id']; + username: MiUser['username']; + host: MiUser['host']; + createdAt: MiUser['createdAt']; + isBot: MiUser['isBot']; }, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) { const meta = await this.metaService.fetch(); @@ -503,6 +508,20 @@ export class NoteCreateService implements OnApplicationShutdown { this.saveReply(data.reply, note); } + if (data.reply == null) { + this.followingsRepository.findBy({ + followeeId: user.id, + notify: 'normal', + }).then(followings => { + for (const following of followings) { + this.notificationService.createNotification(following.followerId, 'note', { + notifierId: user.id, + noteId: note.id, + }); + } + }); + } + // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき if (data.renote && (await this.noteEntityService.countSameRenotes(user.id, data.renote.id, note.id) === 0)) { if (!user.isBot) this.incRenoteCount(data.renote); @@ -570,12 +589,14 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.reply) { // 通知 if (data.reply.userHost === null) { - const threadMuted = await this.noteThreadMutingsRepository.findOneBy({ - userId: data.reply.userId, - threadId: data.reply.threadId ?? data.reply.id, + const isThreadMuted = await this.noteThreadMutingsRepository.exist({ + where: { + userId: data.reply.userId, + threadId: data.reply.threadId ?? data.reply.id, + }, }); - if (!threadMuted) { + if (!isThreadMuted) { nm.push(data.reply.userId, 'reply'); this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj); @@ -621,7 +642,7 @@ export class NoteCreateService implements OnApplicationShutdown { // メンションされたリモートユーザーに配送 for (const u of mentionedUsers.filter(u => this.userEntityService.isRemoteUser(u))) { - dm.addDirectRecipe(u as RemoteUser); + dm.addDirectRecipe(u as MiRemoteUser); } // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 @@ -672,7 +693,7 @@ export class NoteCreateService implements OnApplicationShutdown { // Register to search database this.index(note); } - + @bindThis private isSensitive(note: Option, sensitiveWord: string[]): boolean { if (sensitiveWord.length > 0) { @@ -699,7 +720,7 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - private incRenoteCount(renote: Note) { + private incRenoteCount(renote: MiNote) { this.notesRepository.createQueryBuilder().update() .set({ renoteCount: () => '"renoteCount" + 1', @@ -710,14 +731,16 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - private async createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, nm: NotificationManager) { + private async createMentionedEvents(mentionedUsers: MinimumUser[], note: MiNote, nm: NotificationManager) { for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) { - const threadMuted = await this.noteThreadMutingsRepository.findOneBy({ - userId: u.id, - threadId: note.threadId ?? note.id, + const isThreadMuted = await this.noteThreadMutingsRepository.exist({ + where: { + userId: u.id, + threadId: note.threadId ?? note.id, + }, }); - if (threadMuted) { + if (isThreadMuted) { continue; } @@ -740,12 +763,12 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - private saveReply(reply: Note, note: Note) { + private saveReply(reply: MiNote, note: MiNote) { this.notesRepository.increment({ id: reply.id }, 'repliesCount', 1); } @bindThis - private async renderNoteOrRenoteActivity(data: Option, note: Note) { + private async renderNoteOrRenoteActivity(data: Option, note: MiNote) { if (data.localOnly) return null; const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0) @@ -756,14 +779,14 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - private index(note: Note) { + private index(note: MiNote) { if (note.text == null && note.cw == null) return; - + this.searchService.indexNote(note); } @bindThis - private incNotesCountOfUser(user: { id: User['id']; }) { + private incNotesCountOfUser(user: { id: MiUser['id']; }) { this.usersRepository.createQueryBuilder().update() .set({ updatedAt: new Date(), @@ -774,13 +797,13 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - private async extractMentionedUsers(user: { host: User['host']; }, tokens: mfm.MfmNode[]): Promise { + private async extractMentionedUsers(user: { host: MiUser['host']; }, tokens: mfm.MfmNode[]): Promise { if (tokens == null) return []; const mentions = extractMentions(tokens); let mentionedUsers = (await Promise.all(mentions.map(m => this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null), - ))).filter(x => x != null) as User[]; + ))).filter(x => x != null) as MiUser[]; // Drop duplicate users mentionedUsers = mentionedUsers.filter((u, i, self) => diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index dd878f7bba..69fff36a02 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Brackets, In } from 'typeorm'; import { Injectable, Inject } from '@nestjs/common'; -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 type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; +import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js'; +import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/_.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { DI } from '@/di-symbols.js'; @@ -17,6 +22,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { MetaService } from '@/core/MetaService.js'; +import { SearchService } from '@/core/SearchService.js'; @Injectable() export class NoteDeleteService { @@ -41,18 +47,20 @@ export class NoteDeleteService { private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, private metaService: MetaService, + private searchService: SearchService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, private instanceChart: InstanceChart, ) {} - + /** * 投稿を削除します。 * @param user 投稿者 * @param note 投稿 */ - async delete(user: { id: User['id']; uri: User['uri']; host: User['host']; isBot: User['isBot']; }, note: Note, quiet = false) { + async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false) { const deletedAt = new Date(); + const cascadingNotes = await this.findCascadingNotes(note); // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき if (note.renoteId && (await this.noteEntityService.countSameRenotes(user.id, note.renoteId, note.id)) === 0) { @@ -71,7 +79,7 @@ export class NoteDeleteService { //#region ローカルの投稿なら削除アクティビティを配送 if (this.userEntityService.isLocalUser(user) && !note.localOnly) { - let renote: Note | null = null; + let renote: MiNote | null = null; // if deletd note is renote if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) { @@ -88,8 +96,8 @@ export class NoteDeleteService { } // also deliever delete activity to cascaded notes - const cascadingNotes = (await this.findCascadingNotes(note)).filter(note => !note.localOnly); // filter out local-only notes - for (const cascadingNote of cascadingNotes) { + const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes + for (const cascadingNote of federatedLocalCascadingNotes) { if (!cascadingNote.user) continue; if (!this.userEntityService.isLocalUser(cascadingNote.user)) continue; const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); @@ -114,6 +122,11 @@ export class NoteDeleteService { } } + for (const cascadingNote of cascadingNotes) { + this.searchService.unindexNote(cascadingNote); + } + this.searchService.unindexNote(note); + await this.notesRepository.delete({ id: note.id, userId: user.id, @@ -121,10 +134,8 @@ export class NoteDeleteService { } @bindThis - private async findCascadingNotes(note: Note) { - const cascadingNotes: Note[] = []; - - const recursive = async (noteId: string) => { + private async findCascadingNotes(note: MiNote): Promise { + const recursive = async (noteId: string): Promise => { const query = this.notesRepository.createQueryBuilder('note') .where('note.replyId = :noteId', { noteId }) .orWhere(new Brackets(q => { @@ -133,18 +144,20 @@ export class NoteDeleteService { })) .leftJoinAndSelect('note.user', 'user'); const replies = await query.getMany(); - for (const reply of replies) { - cascadingNotes.push(reply); - await recursive(reply.id); - } - }; - await recursive(note.id); - return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users + return [ + replies, + ...await Promise.all(replies.map(reply => recursive(reply.id))), + ].flat(); + }; + + const cascadingNotes: MiNote[] = await recursive(note.id); + + return cascadingNotes; } @bindThis - private async getMentionedRemoteUsers(note: Note) { + private async getMentionedRemoteUsers(note: MiNote) { const where = [] as any[]; // mention / reply / dm @@ -166,11 +179,11 @@ export class NoteDeleteService { return await this.usersRepository.find({ where, - }) as RemoteUser[]; + }) as MiRemoteUser[]; } @bindThis - private async deliverToConcerned(user: { id: LocalUser['id']; host: null; }, note: Note, content: any) { + private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, 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 3a9f832ac0..147554ee9a 100644 --- a/packages/backend/src/core/NotePiningService.ts +++ b/packages/backend/src/core/NotePiningService.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UserNotePiningsRepository, UsersRepository } from '@/models/index.js'; +import type { NotesRepository, UserNotePiningsRepository, UsersRepository } from '@/models/_.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import type { User } from '@/models/entities/User.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiNote } from '@/models/Note.js'; import { IdService } from '@/core/IdService.js'; -import type { UserNotePining } from '@/models/entities/UserNotePining.js'; +import type { MiUserNotePining } from '@/models/UserNotePining.js'; import { RelayService } from '@/core/RelayService.js'; import type { Config } from '@/config.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -44,7 +49,7 @@ export class NotePiningService { * @param noteId */ @bindThis - public async addPinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']) { + public async addPinned(user: { id: MiUser['id']; host: MiUser['host']; }, noteId: MiNote['id']) { // Fetch pinee const note = await this.notesRepository.findOneBy({ id: noteId, @@ -70,7 +75,7 @@ export class NotePiningService { createdAt: new Date(), userId: user.id, noteId: note.id, - } as UserNotePining); + } as MiUserNotePining); // Deliver to remote followers if (this.userEntityService.isLocalUser(user)) { @@ -84,7 +89,7 @@ export class NotePiningService { * @param noteId */ @bindThis - public async removePinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']) { + public async removePinned(user: { id: MiUser['id']; host: MiUser['host']; }, noteId: MiNote['id']) { // Fetch unpinee const note = await this.notesRepository.findOneBy({ id: noteId, @@ -107,7 +112,7 @@ export class NotePiningService { } @bindThis - public async deliverPinnedChange(userId: User['id'], noteId: Note['id'], isAddition: boolean) { + public async deliverPinnedChange(userId: MiUser['id'], noteId: MiNote['id'], isAddition: boolean) { const user = await this.usersRepository.findOneBy({ id: userId }); if (user == null) throw new Error('user not found'); diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts index e57e57d310..422e0192cf 100644 --- a/packages/backend/src/core/NoteReadService.ts +++ b/packages/backend/src/core/NoteReadService.ts @@ -1,13 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { setTimeout } from 'node:timers/promises'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { User } from '@/models/entities/User.js'; +import type { MiUser } from '@/models/User.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { MiNote } from '@/models/Note.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/index.js'; +import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; @Injectable() @@ -30,7 +35,7 @@ export class NoteReadService implements OnApplicationShutdown { } @bindThis - public async insertNoteUnread(userId: User['id'], note: Note, params: { + public async insertNoteUnread(userId: MiUser['id'], note: MiNote, params: { // NOTE: isSpecifiedがtrueならisMentionedは必ずfalse isSpecified: boolean; isMentioned: boolean; @@ -43,11 +48,13 @@ export class NoteReadService implements OnApplicationShutdown { //#endregion // スレッドミュート - const threadMute = await this.noteThreadMutingsRepository.findOneBy({ - userId: userId, - threadId: note.threadId ?? note.id, + const isThreadMuted = await this.noteThreadMutingsRepository.exist({ + where: { + userId: userId, + threadId: note.threadId ?? note.id, + }, }); - if (threadMute) return; + if (isThreadMuted) return; const unread = { id: this.idService.genId(), @@ -62,9 +69,9 @@ export class NoteReadService implements OnApplicationShutdown { // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => { - const exist = await this.noteUnreadsRepository.findOneBy({ id: unread.id }); + const exist = await this.noteUnreadsRepository.exist({ where: { id: unread.id } }); - if (exist == null) return; + if (!exist) return; if (params.isMentioned) { this.globalEventService.publishMainStream(userId, 'unreadMention', note.id); @@ -77,11 +84,11 @@ export class NoteReadService implements OnApplicationShutdown { @bindThis public async read( - userId: User['id'], - notes: (Note | Packed<'Note'>)[], + userId: MiUser['id'], + notes: (MiNote | Packed<'Note'>)[], ): Promise { - const readMentions: (Note | Packed<'Note'>)[] = []; - const readSpecifiedNotes: (Note | Packed<'Note'>)[] = []; + const readMentions: (MiNote | Packed<'Note'>)[] = []; + const readSpecifiedNotes: (MiNote | Packed<'Note'>)[] = []; for (const note of notes) { if (note.mentions && note.mentions.includes(userId)) { @@ -99,7 +106,7 @@ export class NoteReadService implements OnApplicationShutdown { }); // TODO: ↓まとめてクエリしたい - + this.noteUnreadsRepository.countBy({ userId: userId, isMentioned: true, @@ -109,7 +116,7 @@ export class NoteReadService implements OnApplicationShutdown { this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions'); } }); - + this.noteUnreadsRepository.countBy({ userId: userId, isSpecified: true, diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index ed47165f7b..258ae44f7d 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -1,38 +1,39 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { setTimeout } from 'node:timers/promises'; import * as Redis from 'ioredis'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { MutingsRepository, UserProfile, UserProfilesRepository, 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 type { UsersRepository } from '@/models/_.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiNotification } from '@/models/Notification.js'; import { bindThis } from '@/decorators.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { PushNotificationService } from '@/core/PushNotificationService.js'; import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; +import type { Config } from '@/config.js'; @Injectable() export class NotificationService implements OnApplicationShutdown { #shutdownController = new AbortController(); constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.redis) private redisClient: Redis.Redis, @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - - @Inject(DI.mutingsRepository) - private mutingsRepository: MutingsRepository, - private notificationEntityService: NotificationEntityService, - private userEntityService: UserEntityService, private idService: IdService, private globalEventService: GlobalEventService, private pushNotificationService: PushNotificationService, @@ -42,11 +43,11 @@ export class NotificationService implements OnApplicationShutdown { @bindThis public async readAllNotification( - userId: User['id'], + userId: MiUser['id'], force = false, ) { const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`); - + const latestNotificationIdsRes = await this.redisClient.xrevrange( `notificationTimeline:${userId}`, '+', @@ -64,17 +65,17 @@ export class NotificationService implements OnApplicationShutdown { } @bindThis - private postReadAllNotifications(userId: User['id']) { + private postReadAllNotifications(userId: MiUser['id']) { this.globalEventService.publishMainStream(userId, 'readAllNotifications'); this.pushNotificationService.pushNotification(userId, 'readAllNotifications', undefined); } @bindThis public async createNotification( - notifieeId: User['id'], - type: Notification['type'], - data: Partial, - ): Promise { + notifieeId: MiUser['id'], + type: MiNotification['type'], + data: Partial, + ): Promise { const profile = await this.cacheService.userProfileCache.fetch(notifieeId); const isMuted = profile.mutingNotificationTypes.includes(type); if (isMuted) return null; @@ -95,11 +96,11 @@ export class NotificationService implements OnApplicationShutdown { createdAt: new Date(), type: type, ...data, - } as Notification; + } as MiNotification; const redisIdPromise = this.redisClient.xadd( `notificationTimeline:${notifieeId}`, - 'MAXLEN', '~', '300', + 'MAXLEN', '~', this.config.perUserNotificationsMaxCount.toString(), '*', 'data', JSON.stringify(notification)); @@ -129,7 +130,7 @@ export class NotificationService implements OnApplicationShutdown { // TODO: locale ファイルをクライアント用とサーバー用で分けたい @bindThis - private async emailNotificationFollow(userId: User['id'], follower: User) { + private async emailNotificationFollow(userId: MiUser['id'], follower: MiUser) { /* const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); if (!userProfile.email || !userProfile.emailNotificationTypes.includes('follow')) return; @@ -141,7 +142,7 @@ export class NotificationService implements OnApplicationShutdown { } @bindThis - private async emailNotificationReceiveFollowRequest(userId: User['id'], follower: User) { + private async emailNotificationReceiveFollowRequest(userId: MiUser['id'], follower: MiUser) { /* const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); if (!userProfile.email || !userProfile.emailNotificationTypes.includes('receiveFollowRequest')) return; diff --git a/packages/backend/src/core/PollService.ts b/packages/backend/src/core/PollService.ts index 368753d9a7..940aa98347 100644 --- a/packages/backend/src/core/PollService.ts +++ b/packages/backend/src/core/PollService.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UsersRepository, PollsRepository, PollVotesRepository, User } from '@/models/index.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { NotesRepository, UsersRepository, PollsRepository, PollVotesRepository, MiUser } from '@/models/_.js'; +import type { MiNote } from '@/models/Note.js'; import { RelayService } from '@/core/RelayService.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -37,14 +42,14 @@ export class PollService { } @bindThis - public async vote(user: User, note: Note, choice: number) { + public async vote(user: MiUser, note: MiNote, choice: number) { const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); - + if (poll == null) throw new Error('poll not found'); - + // Check whether is valid choice if (poll.choices[choice] == null) throw new Error('invalid choice param'); - + // Check blocking if (note.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); @@ -52,13 +57,13 @@ export class PollService { throw new Error('blocked'); } } - + // if already voted const exist = await this.pollVotesRepository.findBy({ noteId: note.id, userId: user.id, }); - + if (poll.multiple) { if (exist.some(x => x.choice === choice)) { throw new Error('already voted'); @@ -66,7 +71,7 @@ export class PollService { } else if (exist.length !== 0) { throw new Error('already voted'); } - + // Create vote await this.pollVotesRepository.insert({ id: this.idService.genId(), @@ -75,11 +80,11 @@ export class PollService { userId: user.id, choice: choice, }); - + // Increment votes count const index = choice + 1; // In SQL, array index is 1 based await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`); - + this.globalEventService.publishNoteStream(note.id, 'pollVoted', { choice: choice, userId: user.id, @@ -87,13 +92,13 @@ export class PollService { } @bindThis - public async deliverQuestionUpdate(noteId: Note['id']) { + public async deliverQuestionUpdate(noteId: MiNote['id']) { const note = await this.notesRepository.findOneBy({ id: noteId }); if (note == null) throw new Error('note not found'); - + const user = await this.usersRepository.findOneBy({ id: note.userId }); if (user == null) throw new Error('note not found'); - + if (this.userEntityService.isLocalUser(user)) { const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user)); this.apDeliverManagerService.deliverToFollowers(user, content); diff --git a/packages/backend/src/core/ProxyAccountService.ts b/packages/backend/src/core/ProxyAccountService.ts index 780e56ef10..b1bc60701b 100644 --- a/packages/backend/src/core/ProxyAccountService.ts +++ b/packages/backend/src/core/ProxyAccountService.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/index.js'; -import type { LocalUser } from '@/models/entities/User.js'; +import type { UsersRepository } from '@/models/_.js'; +import type { MiLocalUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; @@ -16,9 +21,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 LocalUser; + return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as MiLocalUser; } } diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts index a4c569bdec..40d1deceeb 100644 --- a/packages/backend/src/core/PushNotificationService.ts +++ b/packages/backend/src/core/PushNotificationService.ts @@ -1,11 +1,16 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import push from 'web-push'; import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import type { Packed } from '@/misc/json-schema'; +import type { Packed } from '@/misc/json-schema.js'; import { getNoteSummary } from '@/misc/get-note-summary.js'; -import type { SwSubscription, SwSubscriptionsRepository } from '@/models/index.js'; +import type { MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js'; import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import { RedisKVCache } from '@/misc/cache.js'; @@ -31,7 +36,7 @@ function truncateBody(type: T, body: Pus ...body.note, // textをgetNoteSummaryしたものに置き換える text: getNoteSummary(('type' in body && body.type === 'renote') ? body.note.renote as Packed<'Note'> : body.note), - + cw: undefined, reply: undefined, renote: undefined, @@ -42,8 +47,8 @@ function truncateBody(type: T, body: Pus } @Injectable() -export class PushNotificationService { - private subscriptionsCache: RedisKVCache; +export class PushNotificationService implements OnApplicationShutdown { + private subscriptionsCache: RedisKVCache; constructor( @Inject(DI.config) @@ -57,7 +62,7 @@ export class PushNotificationService { private metaService: MetaService, ) { - this.subscriptionsCache = new RedisKVCache(this.redisClient, 'userSwSubscriptions', { + this.subscriptionsCache = new RedisKVCache(this.redisClient, 'userSwSubscriptions', { lifetime: 1000 * 60 * 60 * 1, // 1h memoryCacheLifetime: 1000 * 60 * 3, // 3m fetcher: (key) => this.swSubscriptionsRepository.findBy({ userId: key }), @@ -69,16 +74,16 @@ export class PushNotificationService { @bindThis 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; - + // アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 push.setVapidDetails(this.config.url, meta.swPublicKey, meta.swPrivateKey); - + const subscriptions = await this.subscriptionsCache.fetch(userId); - + for (const subscription of subscriptions) { if ([ 'readAllNotifications', @@ -103,7 +108,7 @@ export class PushNotificationService { //swLogger.info(err.statusCode); //swLogger.info(err.headers); //swLogger.info(err.body); - + if (err.statusCode === 410) { this.swSubscriptionsRepository.delete({ userId: userId, @@ -115,4 +120,14 @@ export class PushNotificationService { }); } } + + @bindThis + public dispose(): void { + this.subscriptionsCache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index bf50a1cded..9145726f86 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Brackets, ObjectLiteral } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { User } from '@/models/entities/User.js'; -import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/index.js'; +import type { MiUser } from '@/models/User.js'; +import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import type { SelectQueryBuilder } from 'typeorm'; @@ -60,11 +65,11 @@ export class QueryService { q.orderBy(`${q.alias}.id`, 'DESC'); } return q; - } - + } + // ここでいうBlockedは被Blockedの意 @bindThis - public generateBlockedUserQuery(q: SelectQueryBuilder, me: { id: User['id'] }): void { + public generateBlockedUserQuery(q: SelectQueryBuilder, me: { id: MiUser['id'] }): void { const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking') .select('blocking.blockerId') .where('blocking.blockeeId = :blockeeId', { blockeeId: me.id }); @@ -87,7 +92,7 @@ export class QueryService { } @bindThis - public generateBlockQueryForUsers(q: SelectQueryBuilder, me: { id: User['id'] }): void { + public generateBlockQueryForUsers(q: SelectQueryBuilder, me: { id: MiUser['id'] }): void { const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking') .select('blocking.blockeeId') .where('blocking.blockerId = :blockerId', { blockerId: me.id }); @@ -104,67 +109,67 @@ export class QueryService { } @bindThis - public generateChannelQuery(q: SelectQueryBuilder, me?: { id: User['id'] } | null): void { + public generateChannelQuery(q: SelectQueryBuilder, me?: { id: MiUser['id'] } | null): void { if (me == null) { q.andWhere('note.channelId IS NULL'); } else { q.leftJoinAndSelect('note.channel', 'channel'); - + const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing') .select('channelFollowing.followeeId') .where('channelFollowing.followerId = :followerId', { followerId: me.id }); - + q.andWhere(new Brackets(qb => { qb // チャンネルのノートではない .where('note.channelId IS NULL') // または自分がフォローしているチャンネルのノート .orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`); })); - + q.setParameters(channelFollowingQuery.getParameters()); } } @bindThis - public generateMutedNoteQuery(q: SelectQueryBuilder, me: { id: User['id'] }): void { + public generateMutedNoteQuery(q: SelectQueryBuilder, me: { id: MiUser['id'] }): void { const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted') .select('muted.noteId') .where('muted.userId = :userId', { userId: me.id }); - + q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); - + q.setParameters(mutedQuery.getParameters()); } @bindThis - public generateMutedNoteThreadQuery(q: SelectQueryBuilder, me: { id: User['id'] }): void { + public generateMutedNoteThreadQuery(q: SelectQueryBuilder, me: { id: MiUser['id'] }): void { const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted') .select('threadMuted.threadId') .where('threadMuted.userId = :userId', { userId: me.id }); - + q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); q.andWhere(new Brackets(qb => { qb .where('note.threadId IS NULL') .orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`); })); - + q.setParameters(mutedQuery.getParameters()); } @bindThis - public generateMutedUserQuery(q: SelectQueryBuilder, me: { id: User['id'] }, exclude?: User): void { + public generateMutedUserQuery(q: SelectQueryBuilder, me: { id: MiUser['id'] }, exclude?: MiUser): void { const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') .select('muting.muteeId') .where('muting.muterId = :muterId', { muterId: me.id }); - + if (exclude) { mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id }); } - + const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile') .select('user_profile.mutedInstances') .where('user_profile.userId = :muterId', { muterId: me.id }); - + // 投稿の作者をミュートしていない かつ // 投稿の返信先の作者をミュートしていない かつ // 投稿の引用元の作者をミュートしていない @@ -191,24 +196,24 @@ export class QueryService { .where('note.renoteUserHost IS NULL') .orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`); })); - + q.setParameters(mutingQuery.getParameters()); q.setParameters(mutingInstanceQuery.getParameters()); } @bindThis - public generateMutedUserQueryForUsers(q: SelectQueryBuilder, me: { id: User['id'] }): void { + public generateMutedUserQueryForUsers(q: SelectQueryBuilder, me: { id: MiUser['id'] }): void { const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') .select('muting.muteeId') .where('muting.muterId = :muterId', { muterId: me.id }); - + q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`); - + q.setParameters(mutingQuery.getParameters()); } @bindThis - public generateRepliesQuery(q: SelectQueryBuilder, withReplies: boolean, me?: Pick | null): void { + public generateRepliesQuery(q: SelectQueryBuilder, withReplies: boolean, me?: Pick | null): void { if (me == null) { q.andWhere(new Brackets(qb => { qb .where('note.replyId IS NULL') // 返信ではない @@ -234,7 +239,7 @@ export class QueryService { } @bindThis - public generateVisibilityQuery(q: SelectQueryBuilder, me?: { id: User['id'] } | null): void { + public generateVisibilityQuery(q: SelectQueryBuilder, me?: { id: MiUser['id'] } | null): void { // This code must always be synchronized with the checks in Notes.isVisibleForMe. if (me == null) { q.andWhere(new Brackets(qb => { qb @@ -245,7 +250,7 @@ export class QueryService { const followingQuery = this.followingsRepository.createQueryBuilder('following') .select('following.followeeId') .where('following.followerId = :meId'); - + q.andWhere(new Brackets(qb => { qb // 公開投稿である .where(new Brackets(qb => { qb @@ -268,20 +273,20 @@ export class QueryService { })); })); })); - + q.setParameters({ meId: me.id }); } } @bindThis - public generateMutedUserRenotesQueryForNotes(q: SelectQueryBuilder, me: { id: User['id'] }): void { + public generateMutedUserRenotesQueryForNotes(q: SelectQueryBuilder, me: { id: MiUser['id'] }): void { const mutingQuery = this.renoteMutingsRepository.createQueryBuilder('renote_muting') .select('renote_muting.muteeId') .where('renote_muting.muterId = :muterId', { muterId: me.id }); - + q.andWhere(new Brackets(qb => { qb - .where(new Brackets(qb => { + .where(new Brackets(qb => { qb.where('note.renoteId IS NOT NULL'); qb.andWhere('note.text IS NULL'); qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`); @@ -289,7 +294,7 @@ export class QueryService { .orWhere('note.renoteId IS NULL') .orWhere('note.text IS NOT NULL'); })); - + q.setParameters(mutingQuery.getParameters()); } } diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index 3384ca4577..4444dc9787 100644 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { setTimeout } from 'node:timers/promises'; import { Inject, Module, OnApplicationShutdown } from '@nestjs/common'; import * as Bull from 'bullmq'; diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 2ae8a2b754..d8c7250034 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -1,14 +1,19 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; -import { v4 as uuid } from 'uuid'; import type { IActivity } from '@/core/activitypub/type.js'; -import type { DriveFile } from '@/models/entities/DriveFile.js'; -import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js'; -import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; +import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; import type httpSignature from '@peertube/http-signature'; import type * as Bull from 'bullmq'; @@ -69,7 +74,7 @@ export class QueueService { if (content == null) return null; if (to == null) return null; - const data = { + const data: DeliverJobData = { user: { id: user.id, }, @@ -88,6 +93,40 @@ export class QueueService { }); } + /** + * ApDeliverManager-DeliverManager.execute()からinboxesを突っ込んでaddBulkしたい + * @param user `{ id: string; }` この関数ではThinUserに変換しないので前もって変換してください + * @param content IActivity | null + * @param inboxes `Map` / key: to (inbox url), value: isSharedInbox (whether it is sharedInbox) + * @returns void + */ + @bindThis + public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map) { + if (content == null) return null; + + const opts = { + attempts: this.config.deliverJobMaxAttempts ?? 12, + backoff: { + type: 'custom', + }, + removeOnComplete: true, + removeOnFail: true, + }; + + await this.deliverQueue.addBulk(Array.from(inboxes.entries(), d => ({ + name: d[0], + data: { + user, + content, + to: d[0], + isSharedInbox: d[1], + } as DeliverJobData, + opts, + }))); + + return; + } + @bindThis public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) { const data = { @@ -198,7 +237,7 @@ export class QueueService { } @bindThis - public createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']) { + public createImportFollowingJob(user: ThinUser, fileId: MiDriveFile['id']) { return this.dbQueue.add('importFollowing', { user: { id: user.id }, fileId: fileId, @@ -215,7 +254,7 @@ export class QueueService { } @bindThis - public createImportMutingJob(user: ThinUser, fileId: DriveFile['id']) { + public createImportMutingJob(user: ThinUser, fileId: MiDriveFile['id']) { return this.dbQueue.add('importMuting', { user: { id: user.id }, fileId: fileId, @@ -226,7 +265,7 @@ export class QueueService { } @bindThis - public createImportBlockingJob(user: ThinUser, fileId: DriveFile['id']) { + public createImportBlockingJob(user: ThinUser, fileId: MiDriveFile['id']) { return this.dbQueue.add('importBlocking', { user: { id: user.id }, fileId: fileId, @@ -259,7 +298,7 @@ export class QueueService { } @bindThis - public createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']) { + public createImportUserListsJob(user: ThinUser, fileId: MiDriveFile['id']) { return this.dbQueue.add('importUserLists', { user: { id: user.id }, fileId: fileId, @@ -270,7 +309,7 @@ export class QueueService { } @bindThis - public createImportCustomEmojisJob(user: ThinUser, fileId: DriveFile['id']) { + public createImportCustomEmojisJob(user: ThinUser, fileId: MiDriveFile['id']) { return this.dbQueue.add('importCustomEmojis', { user: { id: user.id }, fileId: fileId, @@ -373,7 +412,7 @@ export class QueueService { } @bindThis - public webhookDeliver(webhook: Webhook, type: typeof webhookEventTypes[number], content: unknown) { + public webhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) { const data = { type, content, @@ -382,7 +421,7 @@ export class QueueService { to: webhook.url, secret: webhook.secret, createdAt: Date.now(), - eventId: uuid(), + eventId: randomUUID(), }; return this.webhookDeliverQueue.add(webhook.id, data, { @@ -400,11 +439,11 @@ export class QueueService { this.deliverQueue.once('cleaned', (jobs, status) => { //deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); }); - this.deliverQueue.clean(0, Infinity, 'delayed'); + this.deliverQueue.clean(0, 0, 'delayed'); this.inboxQueue.once('cleaned', (jobs, status) => { //inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); }); - this.inboxQueue.clean(0, Infinity, 'delayed'); + this.inboxQueue.clean(0, 0, 'delayed'); } } diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 4b01b6af7e..d9bde502c8 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js'; +import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import type { RemoteUser, User } from '@/models/entities/User.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { MiRemoteUser, MiUser } from '@/models/User.js'; +import type { MiNote } from '@/models/Note.js'; import { IdService } from '@/core/IdService.js'; -import type { NoteReaction } from '@/models/entities/NoteReaction.js'; +import type { MiNoteReaction } from '@/models/NoteReaction.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { NotificationService } from '@/core/NotificationService.js'; @@ -90,7 +95,7 @@ export class ReactionService { } @bindThis - public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, _reaction?: string | null) { + public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) { // Check blocking if (note.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); @@ -141,7 +146,7 @@ export class ReactionService { } } - const record: NoteReaction = { + const record: MiNoteReaction = { id: this.idService.genId(), createdAt: new Date(), noteId: note.id, @@ -226,7 +231,7 @@ export class ReactionService { const dm = this.apDeliverManagerService.createDeliverManager(user, content); if (note.userHost !== null) { const reactee = await this.usersRepository.findOneBy({ id: note.userId }); - dm.addDirectRecipe(reactee as RemoteUser); + dm.addDirectRecipe(reactee as MiRemoteUser); } if (['public', 'home', 'followers'].includes(note.visibility)) { @@ -234,7 +239,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 RemoteUser); + dm.addDirectRecipe(u as MiRemoteUser); } } @@ -244,7 +249,7 @@ export class ReactionService { } @bindThis - public async delete(user: { id: User['id']; host: User['host']; isBot: User['isBot']; }, note: Note) { + public async delete(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote) { // if already unreacted const exist = await this.noteReactionsRepository.findOneBy({ noteId: note.id, @@ -284,7 +289,7 @@ export class ReactionService { const dm = this.apDeliverManagerService.createDeliverManager(user, content); if (note.userHost !== null) { const reactee = await this.usersRepository.findOneBy({ id: note.userId }); - dm.addDirectRecipe(reactee as RemoteUser); + dm.addDirectRecipe(reactee as MiRemoteUser); } dm.addFollowersRecipe(); dm.execute(); diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index 9d34d82be2..7171bf84c5 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; -import type { LocalUser, User } from '@/models/entities/User.js'; -import type { RelaysRepository, UsersRepository } from '@/models/index.js'; +import type { MiLocalUser, MiUser } from '@/models/User.js'; +import type { RelaysRepository, UsersRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { MemorySingleCache } from '@/misc/cache.js'; -import type { Relay } from '@/models/entities/Relay.js'; +import type { MiRelay } from '@/models/Relay.js'; import { QueueService } from '@/core/QueueService.js'; import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; @@ -16,7 +21,7 @@ const ACTOR_USERNAME = 'relay.actor' as const; @Injectable() export class RelayService { - private relaysCache: MemorySingleCache; + private relaysCache: MemorySingleCache; constructor( @Inject(DI.usersRepository) @@ -30,35 +35,35 @@ export class RelayService { private createSystemUserService: CreateSystemUserService, private apRendererService: ApRendererService, ) { - this.relaysCache = new MemorySingleCache(1000 * 60 * 10); + this.relaysCache = new MemorySingleCache(1000 * 60 * 10); } @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 LocalUser; - + + if (user) return user as MiLocalUser; + const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME); - return created as LocalUser; + return created as MiLocalUser; } @bindThis - public async addRelay(inbox: string): Promise { + public async addRelay(inbox: string): Promise { const relay = await this.relaysRepository.insert({ id: this.idService.genId(), inbox, status: 'requesting', }).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0])); - + const relayActor = await this.getRelayActor(); const follow = await this.apRendererService.renderFollowRelay(relay, relayActor); const activity = this.apRendererService.addContext(follow); this.queueService.deliver(relayActor, activity, relay.inbox, false); - + return relay; } @@ -67,32 +72,32 @@ export class RelayService { const relay = await this.relaysRepository.findOneBy({ inbox, }); - + if (relay == null) { throw new Error('relay not found'); } - + const relayActor = await this.getRelayActor(); const follow = this.apRendererService.renderFollowRelay(relay, relayActor); const undo = this.apRendererService.renderUndo(follow, relayActor); const activity = this.apRendererService.addContext(undo); this.queueService.deliver(relayActor, activity, relay.inbox, false); - + await this.relaysRepository.delete(relay.id); } @bindThis - public async listRelay(): Promise { + public async listRelay(): Promise { const relays = await this.relaysRepository.find(); return relays; } - + @bindThis public async relayAccepted(id: string): Promise { const result = await this.relaysRepository.update(id, { status: 'accepted', }); - + return JSON.stringify(result); } @@ -101,24 +106,24 @@ export class RelayService { const result = await this.relaysRepository.update(id, { status: 'rejected', }); - + return JSON.stringify(result); } @bindThis - public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise { + public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any): Promise { if (activity == null) return; - + const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({ status: 'accepted', })); if (relays.length === 0) return; - + const copy = deepClone(activity); if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public']; - + const signed = await this.apRendererService.attachLdSignature(copy, user); - + for (const relay of relays) { this.queueService.deliver(user, signed, relay.inbox, false); } diff --git a/packages/backend/src/core/RemoteLoggerService.ts b/packages/backend/src/core/RemoteLoggerService.ts index 3d45605836..5d13988ed7 100644 --- a/packages/backend/src/core/RemoteLoggerService.ts +++ b/packages/backend/src/core/RemoteLoggerService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index ff68c24219..75c5f14aa4 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -1,15 +1,21 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import chalk from 'chalk'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository } from '@/models/index.js'; -import type { LocalUser, RemoteUser } from '@/models/entities/User.js'; +import type { UsersRepository } from '@/models/_.js'; +import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { WebfingerService } from '@/core/WebfingerService.js'; +import { ILink, WebfingerService } from '@/core/WebfingerService.js'; import { RemoteLoggerService } from '@/core/RemoteLoggerService.js'; +import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { bindThis } from '@/decorators.js'; @@ -27,15 +33,16 @@ export class RemoteUserResolveService { private utilityService: UtilityService, private webfingerService: WebfingerService, private remoteLoggerService: RemoteLoggerService, + private apDbResolverService: ApDbResolverService, private apPersonService: ApPersonService, ) { this.logger = this.remoteLoggerService.logger.createSubLogger('resolve-user'); } @bindThis - public async resolveUser(username: string, host: string | null): Promise { + public async resolveUser(username: string, host: string | null): Promise { const usernameLower = username.toLowerCase(); - + if (host == null) { this.logger.info(`return local user: ${usernameLower}`); return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { @@ -44,11 +51,11 @@ export class RemoteUserResolveService { } else { return u; } - }) as LocalUser; + }) as MiLocalUser; } - + host = this.utilityService.toPuny(host); - + if (this.config.host === host) { this.logger.info(`return local user: ${usernameLower}`); return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { @@ -57,41 +64,57 @@ export class RemoteUserResolveService { } else { return u; } - }) as LocalUser; + }) as MiLocalUser; } - - const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null; - + + const user = await this.usersRepository.findOneBy({ usernameLower, host }) as MiRemoteUser | null; + const acctLower = `${usernameLower}@${host}`; - + if (user == null) { const self = await this.resolveSelf(acctLower); - + + if (self.href.startsWith(this.config.url)) { + const local = this.apDbResolverService.parseUri(self.href); + if (local.local && local.type === 'users') { + // the LR points to local + return (await this.apDbResolverService + .getUserFromApId(self.href) + .then((u) => { + if (u == null) { + throw new Error('local user not found'); + } else { + return u; + } + })) as MiLocalUser; + } + } + this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`); return await this.apPersonService.createPerson(self.href); } - - // ユーザー情報が古い場合は、WebFilgerからやりなおして返す + + // ユーザー情報が古い場合は、WebFingerからやりなおして返す if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する await this.usersRepository.update(user.id, { lastFetchedAt: new Date(), }); - + this.logger.info(`try resync: ${acctLower}`); const self = await this.resolveSelf(acctLower); - + if (user.uri !== self.href) { // 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}`); - + // validate uri const uri = new URL(self.href); if (uri.hostname !== host) { throw new Error('Invalid uri'); } - + await this.usersRepository.update({ usernameLower, host: host, @@ -101,25 +124,25 @@ export class RemoteUserResolveService { } else { this.logger.info(`uri is fine: ${acctLower}`); } - + await this.apPersonService.updatePerson(self.href); - + this.logger.info(`return resynced remote user: ${acctLower}`); return await this.usersRepository.findOneBy({ uri: self.href }).then(u => { if (u == null) { throw new Error('user not found'); } else { - return u as LocalUser | RemoteUser; + return u as MiLocalUser | MiRemoteUser; } }); } - + this.logger.info(`return existing remote user: ${acctLower}`); return user; } @bindThis - private async resolveSelf(acctLower: string) { + private async resolveSelf(acctLower: string): Promise { this.logger.info(`WebFinger for ${chalk.yellow(acctLower)}`); const finger = await this.webfingerService.webfinger(acctLower).catch(err => { this.logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ err.statusCode ?? err.message }`); diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index d92cdf82eb..997fcae7fb 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -1,20 +1,25 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { In } from 'typeorm'; -import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; +import type { MiRole, MiRoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/_.js'; import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js'; -import type { User } from '@/models/entities/User.js'; +import type { MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { MetaService } from '@/core/MetaService.js'; import { CacheService } from '@/core/CacheService.js'; -import type { RoleCondFormulaValue } from '@/models/entities/Role.js'; +import type { RoleCondFormulaValue } from '@/models/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 { Packed } from '@/misc/json-schema'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import type { Packed } from '@/misc/json-schema.js'; import type { OnApplicationShutdown } from '@nestjs/common'; export type RolePolicies = { @@ -22,6 +27,9 @@ export type RolePolicies = { ltlAvailable: boolean; canPublicNote: boolean; canInvite: boolean; + inviteLimit: number; + inviteLimitCycle: number; + inviteExpirationTime: number; canManageCustomEmojis: boolean; canSearchNotes: boolean; canHideAds: boolean; @@ -43,6 +51,9 @@ export const DEFAULT_POLICIES: RolePolicies = { ltlAvailable: true, canPublicNote: true, canInvite: false, + inviteLimit: 0, + inviteLimitCycle: 60 * 24 * 7, + inviteExpirationTime: 0, canManageCustomEmojis: false, canSearchNotes: false, canHideAds: false, @@ -61,8 +72,8 @@ export const DEFAULT_POLICIES: RolePolicies = { @Injectable() export class RoleService implements OnApplicationShutdown { - private rolesCache: MemorySingleCache; - private roleAssignmentByUserIdCache: MemoryKVCache; + private rolesCache: MemorySingleCache; + private roleAssignmentByUserIdCache: MemoryKVCache; public static AlreadyAssignedError = class extends Error {}; public static NotAssignedError = class extends Error {}; @@ -92,8 +103,8 @@ export class RoleService implements OnApplicationShutdown { ) { //this.onMessage = this.onMessage.bind(this); - this.rolesCache = new MemorySingleCache(1000 * 60 * 60 * 1); - this.roleAssignmentByUserIdCache = new MemoryKVCache(1000 * 60 * 60 * 1); + this.rolesCache = new MemorySingleCache(1000 * 60 * 60 * 1); + this.roleAssignmentByUserIdCache = new MemoryKVCache(1000 * 60 * 60 * 1); this.redisForSub.on('message', this.onMessage); } @@ -164,7 +175,7 @@ export class RoleService implements OnApplicationShutdown { } @bindThis - private evalCond(user: User, value: RoleCondFormulaValue): boolean { + private evalCond(user: MiUser, value: RoleCondFormulaValue): boolean { try { switch (value.type) { case 'and': { @@ -216,14 +227,19 @@ export class RoleService implements OnApplicationShutdown { } @bindThis - public async getUserRoles(userId: User['id']) { + public async getUserAssigns(userId: MiUser['id']) { 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); + return assigns; + } + + @bindThis + public async getUserRoles(userId: MiUser['id']) { const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); - const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id)); + const assigns = await this.getUserAssigns(userId); + const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id)); const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null; const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula)); return [...assignedRoles, ...matchedCondRoles]; @@ -233,7 +249,7 @@ export class RoleService implements OnApplicationShutdown { * 指定ユーザーのバッジロール一覧取得 */ @bindThis - public async getUserBadgeRoles(userId: User['id']) { + public async getUserBadgeRoles(userId: MiUser['id']) { const now = Date.now(); let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); // 期限切れのロールを除外 @@ -252,7 +268,7 @@ export class RoleService implements OnApplicationShutdown { } @bindThis - public async getUserPolicies(userId: User['id'] | null): Promise { + public async getUserPolicies(userId: MiUser['id'] | null): Promise { const meta = await this.metaService.fetch(); const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies }; @@ -279,6 +295,9 @@ export class RoleService implements OnApplicationShutdown { ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), canInvite: calc('canInvite', vs => vs.some(v => v === true)), + inviteLimit: calc('inviteLimit', vs => Math.max(...vs)), + inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)), + inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)), canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)), canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), @@ -297,19 +316,19 @@ export class RoleService implements OnApplicationShutdown { } @bindThis - public async isModerator(user: { id: User['id']; isRoot: User['isRoot'] } | null): Promise { + public async isModerator(user: { id: MiUser['id']; isRoot: MiUser['isRoot'] } | null): Promise { if (user == null) return false; return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isModerator || r.isAdministrator); } @bindThis - public async isAdministrator(user: { id: User['id']; isRoot: User['isRoot'] } | null): Promise { + public async isAdministrator(user: { id: MiUser['id']; isRoot: MiUser['isRoot'] } | null): Promise { if (user == null) return false; return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isAdministrator); } @bindThis - public async isExplorable(role: { id: Role['id']} | null): Promise { + public async isExplorable(role: { id: MiRole['id']} | null): Promise { if (role == null) return false; const check = await this.rolesRepository.findOneBy({ id: role.id }); if (check == null) return false; @@ -317,7 +336,7 @@ export class RoleService implements OnApplicationShutdown { } @bindThis - public async getModeratorIds(includeAdmins = true): Promise { + public async getModeratorIds(includeAdmins = true): Promise { const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator); const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({ @@ -328,7 +347,7 @@ export class RoleService implements OnApplicationShutdown { } @bindThis - public async getModerators(includeAdmins = true): Promise { + public async getModerators(includeAdmins = true): Promise { const ids = await this.getModeratorIds(includeAdmins); const users = ids.length > 0 ? await this.usersRepository.findBy({ id: In(ids), @@ -337,7 +356,7 @@ export class RoleService implements OnApplicationShutdown { } @bindThis - public async getAdministratorIds(): Promise { + public async getAdministratorIds(): Promise { const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); const administratorRoles = roles.filter(r => r.isAdministrator); const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({ @@ -348,7 +367,7 @@ export class RoleService implements OnApplicationShutdown { } @bindThis - public async getAdministrators(): Promise { + public async getAdministrators(): Promise { const ids = await this.getAdministratorIds(); const users = ids.length > 0 ? await this.usersRepository.findBy({ id: In(ids), @@ -357,7 +376,7 @@ export class RoleService implements OnApplicationShutdown { } @bindThis - public async assign(userId: User['id'], roleId: Role['id'], expiresAt: Date | null = null, moderator?: User): Promise { + public async assign(userId: MiUser['id'], roleId: MiRole['id'], expiresAt: Date | null = null, moderator?: MiUser): Promise { const now = new Date(); const role = await this.rolesRepository.findOneByOrFail({ id: roleId }); @@ -403,11 +422,9 @@ export class RoleService implements OnApplicationShutdown { } @bindThis - public async unassign(userId: User['id'], roleId: Role['id'], moderator?: User): Promise { + public async unassign(userId: MiUser['id'], roleId: MiRole['id'], moderator?: MiUser): Promise { const now = new Date(); - const role = await this.rolesRepository.findOneByOrFail({ id: roleId }); - const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId }); if (existing == null) { throw new RoleService.NotAssignedError(); @@ -456,7 +473,7 @@ export class RoleService implements OnApplicationShutdown { } @bindThis - public async update(role: Role, params: Partial, moderator?: User): Promise { + public async update(role: MiRole, params: Partial, moderator?: MiUser): Promise { const date = new Date(); await this.rolesRepository.update(role.id, { updatedAt: date, @@ -478,6 +495,7 @@ export class RoleService implements OnApplicationShutdown { @bindThis public dispose(): void { this.redisForSub.off('message', this.onMessage); + this.roleAssignmentByUserIdCache.dispose(); } @bindThis diff --git a/packages/backend/src/core/S3Service.ts b/packages/backend/src/core/S3Service.ts index 01ce12ffdd..df0991539d 100644 --- a/packages/backend/src/core/S3Service.ts +++ b/packages/backend/src/core/S3Service.ts @@ -1,13 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { URL } from 'node:url'; import * as http from 'node:http'; import * as https from 'node:https'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3'; import { Upload } from '@aws-sdk/lib-storage'; -import { NodeHttpHandler, NodeHttpHandlerOptions } from '@aws-sdk/node-http-handler'; -import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; -import type { Meta } from '@/models/entities/Meta.js'; +import { NodeHttpHandler, NodeHttpHandlerOptions } from '@smithy/node-http-handler'; +import type { MiMeta } from '@/models/Meta.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import type { DeleteObjectCommandInput, PutObjectCommandInput } from '@aws-sdk/client-s3'; @@ -15,15 +18,12 @@ import type { DeleteObjectCommandInput, PutObjectCommandInput } from '@aws-sdk/c @Injectable() export class S3Service { constructor( - @Inject(DI.config) - private config: Config, - private httpRequestService: HttpRequestService, ) { } @bindThis - public getS3Client(meta: Meta): S3Client { + public getS3Client(meta: MiMeta): S3Client { const u = meta.objectStorageEndpoint ? `${meta.objectStorageUseSSL ? 'https' : 'http'}://${meta.objectStorageEndpoint}` : `${meta.objectStorageUseSSL ? 'https' : 'http'}://example.net`; // dummy url to select http(s) agent @@ -50,7 +50,7 @@ export class S3Service { } @bindThis - public async upload(meta: Meta, input: PutObjectCommandInput) { + public async upload(meta: MiMeta, input: PutObjectCommandInput) { const client = this.getS3Client(meta); return new Upload({ client, @@ -62,7 +62,7 @@ export class S3Service { } @bindThis - public delete(meta: Meta, input: DeleteObjectCommandInput) { + public delete(meta: MiMeta, input: DeleteObjectCommandInput) { const client = this.getS3Client(meta); return client.send(new DeleteObjectCommand(input)); } diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index 9502afcc9b..3ef321dd32 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; -import { Note } from '@/models/entities/Note.js'; -import { User } from '@/models/index.js'; -import type { NotesRepository } from '@/models/index.js'; +import { MiNote } from '@/models/Note.js'; +import { MiUser } from '@/models/_.js'; +import type { NotesRepository } from '@/models/_.js'; import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; import { QueryService } from '@/core/QueryService.js'; import { IdService } from '@/core/IdService.js'; @@ -20,6 +25,8 @@ type Q = { op: '<', k: K, v: number } | { op: '>=', k: K, v: number } | { op: '<=', k: K, v: number } | + { op: 'is null', k: K} | + { op: 'is not null', k: K} | { op: 'and', qs: Q[] } | { op: 'or', qs: Q[] } | { op: 'not', q: Q }; @@ -45,6 +52,8 @@ function compileQuery(q: Q): string { case '<=': return `(${q.k} <= ${compileValue(q.v)})`; case 'and': return q.qs.length === 0 ? '' : `(${ q.qs.map(_q => compileQuery(_q)).join(' AND ') })`; case 'or': return q.qs.length === 0 ? '' : `(${ q.qs.map(_q => compileQuery(_q)).join(' OR ') })`; + case 'is null': return `(${q.k} IS NULL)`; + case 'is not null': return `(${q.k} IS NOT NULL)`; case 'not': return `(NOT ${compileQuery(q.q)})`; default: throw new Error('unrecognized query operator'); } @@ -52,6 +61,7 @@ function compileQuery(q: Q): string { @Injectable() export class SearchService { + private readonly meilisearchIndexScope: 'local' | 'global' | string[] = 'local'; private meilisearchNoteIndex: Index | null = null; constructor( @@ -92,15 +102,34 @@ export class SearchService { }, }); } + + if (config.meilisearch?.scope) { + this.meilisearchIndexScope = config.meilisearch.scope; + } } @bindThis - public async indexNote(note: Note): Promise { + public async indexNote(note: MiNote): Promise { if (note.text == null && note.cw == null) return; if (!['home', 'public'].includes(note.visibility)) return; if (this.meilisearch) { - this.meilisearchNoteIndex!.addDocuments([{ + switch (this.meilisearchIndexScope) { + case 'global': + break; + + case 'local': + if (note.userHost == null) break; + return; + + default: { + if (note.userHost == null) break; + if (this.meilisearchIndexScope.includes(note.userHost)) break; + return; + } + } + + await this.meilisearchNoteIndex?.addDocuments([{ id: note.id, createdAt: note.createdAt.getTime(), userId: note.userId, @@ -116,15 +145,24 @@ export class SearchService { } @bindThis - public async searchNote(q: string, me: User | null, opts: { - userId?: Note['userId'] | null; - channelId?: Note['channelId'] | null; + public async unindexNote(note: MiNote): Promise { + if (!['home', 'public'].includes(note.visibility)) return; + + if (this.meilisearch) { + this.meilisearchNoteIndex!.deleteDocument(note.id); + } + } + + @bindThis + public async searchNote(q: string, me: MiUser | null, opts: { + userId?: MiNote['userId'] | null; + channelId?: MiNote['channelId'] | null; host?: string | null; }, pagination: { - untilId?: Note['id']; - sinceId?: Note['id']; + untilId?: MiNote['id']; + sinceId?: MiNote['id']; limit?: number; - }): Promise { + }): Promise { if (this.meilisearch) { const filter: Q = { op: 'and', @@ -136,7 +174,7 @@ export class SearchService { if (opts.channelId) filter.qs.push({ op: '=', k: 'channelId', v: opts.channelId }); if (opts.host) { if (opts.host === '.') { - // TODO: Meilisearchが2023/05/07現在値がNULLかどうかのクエリが書けない + filter.qs.push({ op: 'is null', k: 'userHost' }); } else { filter.qs.push({ op: '=', k: 'userHost', v: opts.host }); } @@ -170,11 +208,19 @@ export class SearchService { .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); + if (opts.host) { + if (opts.host === '.') { + query.andWhere('user.host IS NULL'); + } else { + query.andWhere('user.host = :host', { host: opts.host }); + } + } + this.queryService.generateVisibilityQuery(query, me); if (me) this.queryService.generateMutedUserQuery(query, me); if (me) this.queryService.generateBlockedUserQuery(query, me); - return await query.take(pagination.limit).getMany(); + return await query.limit(pagination.limit).getMany(); } } } diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 29eb65fda4..dfec0cfcfe 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -1,15 +1,19 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { generateKeyPair } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { DataSource, IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsedUsernamesRepository, UsersRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; -import { User } from '@/models/entities/User.js'; -import { UserProfile } from '@/models/entities/UserProfile.js'; +import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; +import { MiUser } from '@/models/User.js'; +import { MiUserProfile } from '@/models/UserProfile.js'; import { IdService } from '@/core/IdService.js'; -import { UserKeypair } from '@/models/entities/UserKeypair.js'; -import { UsedUsername } from '@/models/entities/UsedUsername.js'; +import { MiUserKeypair } from '@/models/UserKeypair.js'; +import { MiUsedUsername } from '@/models/UsedUsername.js'; import generateUserToken from '@/misc/generate-native-user-token.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -23,9 +27,6 @@ export class SignupService { @Inject(DI.db) private db: DataSource, - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -42,41 +43,41 @@ export class SignupService { @bindThis public async signup(opts: { - username: User['username']; + username: MiUser['username']; password?: string | null; - passwordHash?: UserProfile['password'] | null; + passwordHash?: MiUserProfile['password'] | null; host?: string | null; ignorePreservedUsernames?: boolean; }) { const { username, password, passwordHash, host } = opts; let hash = passwordHash; - + // Validate username if (!this.userEntityService.validateLocalUsername(username)) { throw new Error('INVALID_USERNAME'); } - + if (password != null && passwordHash == null) { // Validate password if (!this.userEntityService.validatePassword(password)) { throw new Error('INVALID_PASSWORD'); } - + // Generate hash of password const salt = await bcrypt.genSalt(8); hash = await bcrypt.hash(password, salt); } - + // Generate secret const secret = generateUserToken(); - + // Check username duplication - if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) { + if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { throw new Error('DUPLICATED_USERNAME'); } - + // Check deleted username duplication - if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) { + if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) { throw new Error('USED_USERNAME'); } @@ -92,7 +93,7 @@ export class SignupService { const keyPair = await new Promise((res, rej) => generateKeyPair('rsa', { - modulusLength: 4096, + modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem', @@ -106,19 +107,19 @@ export class SignupService { }, (err, publicKey, privateKey) => err ? rej(err) : res([publicKey, privateKey]), )); - - let account!: User; - + + let account!: MiUser; + // Start transaction await this.db.transaction(async transactionalEntityManager => { - const exist = await transactionalEntityManager.findOneBy(User, { + const exist = await transactionalEntityManager.findOneBy(MiUser, { usernameLower: username.toLowerCase(), host: IsNull(), }); - + if (exist) throw new Error(' the username is already used'); - - account = await transactionalEntityManager.save(new User({ + + account = await transactionalEntityManager.save(new MiUser({ id: this.idService.genId(), createdAt: new Date(), username: username, @@ -127,27 +128,27 @@ export class SignupService { token: secret, isRoot: isTheFirstUser, })); - - await transactionalEntityManager.save(new UserKeypair({ + + await transactionalEntityManager.save(new MiUserKeypair({ publicKey: keyPair[0], privateKey: keyPair[1], userId: account.id, })); - - await transactionalEntityManager.save(new UserProfile({ + + await transactionalEntityManager.save(new MiUserProfile({ userId: account.id, autoAcceptFollowed: true, password: hash, })); - - await transactionalEntityManager.save(new UsedUsername({ + + await transactionalEntityManager.save(new MiUsedUsername({ createdAt: new Date(), username: username.toLowerCase(), })); }); - + this.usersChart.update(account, true); - + return { account, secret }; } } diff --git a/packages/backend/src/core/TwoFactorAuthenticationService.ts b/packages/backend/src/core/TwoFactorAuthenticationService.ts deleted file mode 100644 index dda78236e9..0000000000 --- a/packages/backend/src/core/TwoFactorAuthenticationService.ts +++ /dev/null @@ -1,445 +0,0 @@ -import * as crypto from 'node:crypto'; -import { Inject, Injectable } from '@nestjs/common'; -import * as jsrsasign from 'jsrsasign'; -import { DI } from '@/di-symbols.js'; -import type { UsersRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; -import { bindThis } from '@/decorators.js'; - -const ECC_PRELUDE = Buffer.from([0x04]); -const NULL_BYTE = Buffer.from([0]); -const PEM_PRELUDE = Buffer.from( - '3059301306072a8648ce3d020106082a8648ce3d030107034200', - 'hex', -); - -// Android Safetynet attestations are signed with this cert: -const GSR2 = `-----BEGIN CERTIFICATE----- -MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G -A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp -Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1 -MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG -A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL -v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8 -eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq -tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd -C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa -zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB -mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH -V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n -bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG -3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs -J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO -291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS -ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd -AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 -TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== ------END CERTIFICATE-----\n`; - -function base64URLDecode(source: string) { - return Buffer.from(source.replace(/\-/g, '+').replace(/_/g, '/'), 'base64'); -} - -function getCertSubject(certificate: string) { - const subjectCert = new jsrsasign.X509(); - subjectCert.readCertPEM(certificate); - - const subjectString = subjectCert.getSubjectString(); - const subjectFields = subjectString.slice(1).split('/'); - - const fields = {} as Record; - for (const field of subjectFields) { - const eqIndex = field.indexOf('='); - fields[field.substring(0, eqIndex)] = field.substring(eqIndex + 1); - } - - return fields; -} - -function verifyCertificateChain(certificates: string[]) { - let valid = true; - - for (let i = 0; i < certificates.length; i++) { - const Cert = certificates[i]; - const certificate = new jsrsasign.X509(); - certificate.readCertPEM(Cert); - - const CACert = i + 1 >= certificates.length ? Cert : certificates[i + 1]; - - const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]); - if (certStruct == null) throw new Error('certStruct is null'); - - const algorithm = certificate.getSignatureAlgorithmField(); - const signatureHex = certificate.getSignatureValueHex(); - - // Verify against CA - const Signature = new jsrsasign.KJUR.crypto.Signature({ alg: algorithm }); - Signature.init(CACert); - Signature.updateHex(certStruct); - valid = valid && !!Signature.verify(signatureHex); // true if CA signed the certificate - } - - return valid; -} - -function PEMString(pemBuffer: Buffer, type = 'CERTIFICATE') { - if (pemBuffer.length === 65 && pemBuffer[0] === 0x04) { - pemBuffer = Buffer.concat([PEM_PRELUDE, pemBuffer], 91); - type = 'PUBLIC KEY'; - } - const cert = pemBuffer.toString('base64'); - - const keyParts = []; - const max = Math.ceil(cert.length / 64); - let start = 0; - for (let i = 0; i < max; i++) { - keyParts.push(cert.substring(start, start + 64)); - start += 64; - } - - return ( - `-----BEGIN ${type}-----\n` + - keyParts.join('\n') + - `\n-----END ${type}-----\n` - ); -} - -@Injectable() -export class TwoFactorAuthenticationService { - constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - ) { - } - - @bindThis - public hash(data: Buffer) { - return crypto - .createHash('sha256') - .update(data) - .digest(); - } - - @bindThis - public verifySignin({ - publicKey, - authenticatorData, - clientDataJSON, - clientData, - signature, - challenge, - }: { - publicKey: Buffer, - authenticatorData: Buffer, - clientDataJSON: Buffer, - clientData: any, - signature: Buffer, - challenge: string - }) { - if (clientData.type !== 'webauthn.get') { - throw new Error('type is not webauthn.get'); - } - - if (this.hash(clientData.challenge).toString('hex') !== challenge) { - throw new Error('challenge mismatch'); - } - if (clientData.origin !== this.config.scheme + '://' + this.config.host) { - throw new Error('origin mismatch'); - } - - const verificationData = Buffer.concat( - [authenticatorData, this.hash(clientDataJSON)], - 32 + authenticatorData.length, - ); - - return crypto - .createVerify('SHA256') - .update(verificationData) - .verify(PEMString(publicKey), signature); - } - - @bindThis - public getProcedures() { - return { - none: { - verify({ publicKey }: { publicKey: Map }) { - const negTwo = publicKey.get(-2); - - if (!negTwo || negTwo.length !== 32) { - throw new Error('invalid or no -2 key given'); - } - const negThree = publicKey.get(-3); - if (!negThree || negThree.length !== 32) { - throw new Error('invalid or no -3 key given'); - } - - const publicKeyU2F = Buffer.concat( - [ECC_PRELUDE, negTwo, negThree], - 1 + 32 + 32, - ); - - return { - publicKey: publicKeyU2F, - valid: true, - }; - }, - }, - 'android-key': { - verify({ - attStmt, - authenticatorData, - clientDataHash, - publicKey, - rpIdHash, - credentialId, - }: { - attStmt: any, - authenticatorData: Buffer, - clientDataHash: Buffer, - publicKey: Map; - rpIdHash: Buffer, - credentialId: Buffer, - }) { - if (attStmt.alg !== -7) { - throw new Error('alg mismatch'); - } - - const verificationData = Buffer.concat([ - authenticatorData, - clientDataHash, - ]); - - const attCert: Buffer = attStmt.x5c[0]; - - const negTwo = publicKey.get(-2); - - if (!negTwo || negTwo.length !== 32) { - throw new Error('invalid or no -2 key given'); - } - const negThree = publicKey.get(-3); - if (!negThree || negThree.length !== 32) { - throw new Error('invalid or no -3 key given'); - } - - const publicKeyData = Buffer.concat( - [ECC_PRELUDE, negTwo, negThree], - 1 + 32 + 32, - ); - - if (!attCert.equals(publicKeyData)) { - throw new Error('public key mismatch'); - } - - const isValid = crypto - .createVerify('SHA256') - .update(verificationData) - .verify(PEMString(attCert), attStmt.sig); - - // TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON) - - return { - valid: isValid, - publicKey: publicKeyData, - }; - }, - }, - // what a stupid attestation - 'android-safetynet': { - verify: ({ - attStmt, - authenticatorData, - clientDataHash, - publicKey, - rpIdHash, - credentialId, - }: { - attStmt: any, - authenticatorData: Buffer, - clientDataHash: Buffer, - publicKey: Map; - rpIdHash: Buffer, - credentialId: Buffer, - }) => { - const verificationData = this.hash( - Buffer.concat([authenticatorData, clientDataHash]), - ); - - const jwsParts = attStmt.response.toString('utf-8').split('.'); - - const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8')); - const response = JSON.parse( - base64URLDecode(jwsParts[1]).toString('utf-8'), - ); - const signature = jwsParts[2]; - - if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) { - throw new Error('invalid nonce'); - } - - const certificateChain = header.x5c - .map((key: any) => PEMString(key)) - .concat([GSR2]); - - if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') { - throw new Error('invalid common name'); - } - - if (!verifyCertificateChain(certificateChain)) { - throw new Error('Invalid certificate chain!'); - } - - const signatureBase = Buffer.from( - jwsParts[0] + '.' + jwsParts[1], - 'utf-8', - ); - - const valid = crypto - .createVerify('sha256') - .update(signatureBase) - .verify(certificateChain[0], base64URLDecode(signature)); - - const negTwo = publicKey.get(-2); - - if (!negTwo || negTwo.length !== 32) { - throw new Error('invalid or no -2 key given'); - } - const negThree = publicKey.get(-3); - if (!negThree || negThree.length !== 32) { - throw new Error('invalid or no -3 key given'); - } - - const publicKeyData = Buffer.concat( - [ECC_PRELUDE, negTwo, negThree], - 1 + 32 + 32, - ); - return { - valid, - publicKey: publicKeyData, - }; - }, - }, - packed: { - verify({ - attStmt, - authenticatorData, - clientDataHash, - publicKey, - rpIdHash, - credentialId, - }: { - attStmt: any, - authenticatorData: Buffer, - clientDataHash: Buffer, - publicKey: Map; - rpIdHash: Buffer, - credentialId: Buffer, - }) { - const verificationData = Buffer.concat([ - authenticatorData, - clientDataHash, - ]); - - if (attStmt.x5c) { - const attCert = attStmt.x5c[0]; - - const validSignature = crypto - .createVerify('SHA256') - .update(verificationData) - .verify(PEMString(attCert), attStmt.sig); - - const negTwo = publicKey.get(-2); - - if (!negTwo || negTwo.length !== 32) { - throw new Error('invalid or no -2 key given'); - } - const negThree = publicKey.get(-3); - if (!negThree || negThree.length !== 32) { - throw new Error('invalid or no -3 key given'); - } - - const publicKeyData = Buffer.concat( - [ECC_PRELUDE, negTwo, negThree], - 1 + 32 + 32, - ); - - return { - valid: validSignature, - publicKey: publicKeyData, - }; - } else if (attStmt.ecdaaKeyId) { - // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation - throw new Error('ECDAA-Verify is not supported'); - } else { - if (attStmt.alg !== -7) throw new Error('alg mismatch'); - - throw new Error('self attestation is not supported'); - } - }, - }, - - 'fido-u2f': { - verify({ - attStmt, - authenticatorData, - clientDataHash, - publicKey, - rpIdHash, - credentialId, - }: { - attStmt: any, - authenticatorData: Buffer, - clientDataHash: Buffer, - publicKey: Map, - rpIdHash: Buffer, - credentialId: Buffer - }) { - const x5c: Buffer[] = attStmt.x5c; - if (x5c.length !== 1) { - throw new Error('x5c length does not match expectation'); - } - - const attCert = x5c[0]; - - // TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve - - const negTwo: Buffer = publicKey.get(-2); - - if (!negTwo || negTwo.length !== 32) { - throw new Error('invalid or no -2 key given'); - } - const negThree: Buffer = publicKey.get(-3); - if (!negThree || negThree.length !== 32) { - throw new Error('invalid or no -3 key given'); - } - - const publicKeyU2F = Buffer.concat( - [ECC_PRELUDE, negTwo, negThree], - 1 + 32 + 32, - ); - - const verificationData = Buffer.concat([ - NULL_BYTE, - rpIdHash, - clientDataHash, - credentialId, - publicKeyU2F, - ]); - - const validSignature = crypto - .createVerify('SHA256') - .update(verificationData) - .verify(PEMString(attCert), attStmt.sig); - - return { - valid: validSignature, - publicKey: publicKeyU2F, - }; - }, - }, - }; - } -} diff --git a/packages/backend/src/core/UserAuthService.ts b/packages/backend/src/core/UserAuthService.ts new file mode 100644 index 0000000000..ccf4dfc6bd --- /dev/null +++ b/packages/backend/src/core/UserAuthService.ts @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { QueryFailedError } from 'typeorm'; +import * as OTPAuth from 'otpauth'; +import { DI } from '@/di-symbols.js'; +import type { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import { bindThis } from '@/decorators.js'; +import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; +import type { MiLocalUser } from '@/models/User.js'; + +@Injectable() +export class UserAuthService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + ) { + } + + @bindThis + public async twoFactorAuthenticate(profile: MiUserProfile, token: string): Promise { + if (profile.twoFactorBackupSecret?.includes(token)) { + await this.userProfilesRepository.update({ userId: profile.userId }, { + twoFactorBackupSecret: profile.twoFactorBackupSecret.filter((secret) => secret !== token), + }); + } else { + const delta = OTPAuth.TOTP.validate({ + secret: OTPAuth.Secret.fromBase32(profile.twoFactorSecret!), + digits: 6, + token, + window: 5, + }); + + if (delta === null) { + throw new Error('authentication failed'); + } + } + } +} diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index 3ca22f8bbc..37031e341e 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -1,13 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; import { IdService } from '@/core/IdService.js'; -import type { User } from '@/models/entities/User.js'; -import type { Blocking } from '@/models/entities/Blocking.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiBlocking } from '@/models/Blocking.js'; import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; -import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; +import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/_.js'; import Logger from '@/logger.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; @@ -54,7 +58,7 @@ export class UserBlockingService implements OnModuleInit { } @bindThis - public async block(blocker: User, blockee: User, silent = false) { + public async block(blocker: MiUser, blockee: MiUser, silent = false) { await Promise.all([ this.cancelRequest(blocker, blockee, silent), this.cancelRequest(blockee, blocker, silent), @@ -70,7 +74,7 @@ export class UserBlockingService implements OnModuleInit { blockerId: blocker.id, blockee, blockeeId: blockee.id, - } as Blocking; + } as MiBlocking; await this.blockingsRepository.insert(blocking); @@ -89,7 +93,7 @@ export class UserBlockingService implements OnModuleInit { } @bindThis - private async cancelRequest(follower: User, followee: User, silent = false) { + private async cancelRequest(follower: MiUser, followee: MiUser, silent = false) { const request = await this.followRequestsRepository.findOneBy({ followeeId: followee.id, followerId: follower.id, @@ -139,7 +143,7 @@ export class UserBlockingService implements OnModuleInit { } @bindThis - private async removeFromList(listOwner: User, user: User) { + private async removeFromList(listOwner: MiUser, user: MiUser) { const userLists = await this.userListsRepository.findBy({ userId: listOwner.id, }); @@ -153,7 +157,7 @@ export class UserBlockingService implements OnModuleInit { } @bindThis - public async unblock(blocker: User, blockee: User) { + public async unblock(blocker: MiUser, blockee: MiUser) { const blocking = await this.blockingsRepository.findOneBy({ blockerId: blocker.id, blockeeId: blockee.id, @@ -187,7 +191,7 @@ export class UserBlockingService implements OnModuleInit { } @bindThis - public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise { + public async checkBlocked(blockerId: MiUser['id'], blockeeId: MiUser['id']): Promise { return (await this.cacheService.userBlockingCache.fetch(blockerId)).has(blockeeId); } } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 7d90bc2c08..5b2b0205d9 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -1,6 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; -import type { LocalUser, PartialLocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js'; +import { IsNull } from 'typeorm'; +import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/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'; @@ -13,7 +19,7 @@ import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { WebhookService } from '@/core/WebhookService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { bindThis } from '@/decorators.js'; @@ -21,22 +27,21 @@ import { UserBlockingService } from '@/core/UserBlockingService.js'; import { MetaService } from '@/core/MetaService.js'; import { CacheService } from '@/core/CacheService.js'; import type { Config } from '@/config.js'; -import Logger from '../logger.js'; -import { IsNull } from 'typeorm'; import { AccountMoveService } from '@/core/AccountMoveService.js'; +import Logger from '../logger.js'; const logger = new Logger('following/create'); -type Local = LocalUser | { - id: LocalUser['id']; - host: LocalUser['host']; - uri: LocalUser['uri'] +type Local = MiLocalUser | { + id: MiLocalUser['id']; + host: MiLocalUser['host']; + uri: MiLocalUser['uri'] }; -type Remote = RemoteUser | { - id: RemoteUser['id']; - host: RemoteUser['host']; - uri: RemoteUser['uri']; - inbox: RemoteUser['inbox']; +type Remote = MiRemoteUser | { + id: MiRemoteUser['id']; + host: MiRemoteUser['host']; + uri: MiRemoteUser['uri']; + inbox: MiRemoteUser['inbox']; }; type Both = Local | Remote; @@ -86,11 +91,11 @@ export class UserFollowingService implements OnModuleInit { } @bindThis - public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string, silent = false): Promise { + public async follow(_follower: { id: MiUser['id'] }, _followee: { id: MiUser['id'] }, requestId?: string, silent = false): Promise { const [follower, followee] = await Promise.all([ this.usersRepository.findOneByOrFail({ id: _follower.id }), this.usersRepository.findOneByOrFail({ id: _followee.id }), - ]) as [LocalUser | RemoteUser, LocalUser | RemoteUser]; + ]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser]; // check blocking const [blocking, blocked] = await Promise.all([ @@ -122,22 +127,26 @@ export class UserFollowingService implements OnModuleInit { let autoAccept = false; // 鍵アカウントであっても、既にフォローされていた場合はスルー - const following = await this.followingsRepository.findOneBy({ - followerId: follower.id, - followeeId: followee.id, + const isFollowing = await this.followingsRepository.exist({ + where: { + followerId: follower.id, + followeeId: followee.id, + }, }); - if (following) { + if (isFollowing) { autoAccept = true; } // フォローしているユーザーは自動承認オプション if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) { - const followed = await this.followingsRepository.findOneBy({ - followerId: followee.id, - followeeId: follower.id, + const isFollowed = await this.followingsRepository.exist({ + where: { + followerId: followee.id, + followeeId: follower.id, + }, }); - if (followed) autoAccept = true; + if (isFollowed) autoAccept = true; } // Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account. @@ -171,10 +180,10 @@ export class UserFollowingService implements OnModuleInit { @bindThis private async insertFollowingDoc( followee: { - id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'] + id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox'] }, follower: { - id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'] + id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox'] }, silent = false, ): Promise { @@ -206,12 +215,14 @@ export class UserFollowingService implements OnModuleInit { this.cacheService.userFollowingsCache.refresh(follower.id); - const req = await this.followRequestsRepository.findOneBy({ - followeeId: followee.id, - followerId: follower.id, + const requestExist = await this.followRequestsRepository.exist({ + where: { + followeeId: followee.id, + followerId: follower.id, + }, }); - if (req) { + if (requestExist) { await this.followRequestsRepository.delete({ followeeId: followee.id, followerId: follower.id, @@ -301,10 +312,10 @@ export class UserFollowingService implements OnModuleInit { @bindThis public async unfollow( follower: { - id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; + id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; }, followee: { - id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; + id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; }, silent = false, ): Promise { @@ -316,7 +327,7 @@ export class UserFollowingService implements OnModuleInit { where: { followerId: follower.id, followeeId: followee.id, - } + }, }); if (following === null || !following.follower || !following.followee) { @@ -347,21 +358,21 @@ export class UserFollowingService implements OnModuleInit { } if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower as PartialLocalUser, followee as PartialRemoteUser), follower)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower as MiPartialLocalUser, followee as MiPartialRemoteUser), follower)); this.queueService.deliver(follower, content, followee.inbox, false); } if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) { // local user has null host - const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower as PartialRemoteUser, followee as PartialLocalUser), followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower as MiPartialRemoteUser, followee as MiPartialLocalUser), followee)); this.queueService.deliver(followee, content, follower.inbox, false); } } @bindThis private async decrementFollowing( - follower: User, - followee: User, + follower: MiUser, + followee: MiUser, ): Promise { this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id }); @@ -406,8 +417,8 @@ export class UserFollowingService implements OnModuleInit { followerId: user.id, followee: { movedToUri: IsNull(), - } - } + }, + }, }); const nonMovedFollowers = await this.followingsRepository.count({ relations: { @@ -417,8 +428,8 @@ export class UserFollowingService implements OnModuleInit { followeeId: user.id, follower: { movedToUri: IsNull(), - } - } + }, + }, }); await this.usersRepository.update( { id: user.id }, @@ -433,10 +444,10 @@ export class UserFollowingService implements OnModuleInit { @bindThis public async createFollowRequest( follower: { - id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; + id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; }, followee: { - id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; + id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; }, requestId?: string, ): Promise { @@ -483,7 +494,7 @@ export class UserFollowingService implements OnModuleInit { } if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower as PartialLocalUser, followee as PartialRemoteUser, requestId ?? `${this.config.url}/follows/${followRequest.id}`)); + const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiPartialLocalUser, followee as MiPartialRemoteUser, requestId ?? `${this.config.url}/follows/${followRequest.id}`)); this.queueService.deliver(follower, content, followee.inbox, false); } } @@ -491,26 +502,28 @@ export class UserFollowingService implements OnModuleInit { @bindThis public async cancelFollowRequest( followee: { - id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox'] + id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox'] }, follower: { - id: User['id']; host: User['host']; uri: User['host'] + id: MiUser['id']; host: MiUser['host']; uri: MiUser['host'] }, ): Promise { if (this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower as PartialLocalUser | PartialRemoteUser, followee as PartialRemoteUser), follower)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower as MiPartialLocalUser | MiPartialRemoteUser, followee as MiPartialRemoteUser), follower)); if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので this.queueService.deliver(follower, content, followee.inbox, false); } } - const request = await this.followRequestsRepository.findOneBy({ - followeeId: followee.id, - followerId: follower.id, + const requestExist = await this.followRequestsRepository.exist({ + where: { + followeeId: followee.id, + followerId: follower.id, + }, }); - if (request == null) { + if (!requestExist) { throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found'); } @@ -527,9 +540,9 @@ export class UserFollowingService implements OnModuleInit { @bindThis public async acceptFollowRequest( followee: { - id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; + id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; }, - follower: User, + follower: MiUser, ): Promise { const request = await this.followRequestsRepository.findOneBy({ followeeId: followee.id, @@ -543,7 +556,7 @@ export class UserFollowingService implements OnModuleInit { await this.insertFollowingDoc(followee, follower); if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee as PartialLocalUser, request.requestId!), followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee as MiPartialLocalUser, request.requestId!), followee)); this.queueService.deliver(followee, content, follower.inbox, false); } @@ -555,7 +568,7 @@ export class UserFollowingService implements OnModuleInit { @bindThis public async acceptAllFollowRequests( user: { - id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; + id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; }, ): Promise { const requests = await this.followRequestsRepository.findBy({ @@ -638,7 +651,7 @@ export class UserFollowingService implements OnModuleInit { where: { followeeId: followee.id, followerId: follower.id, - } + }, }); if (!following || !following.followee || !following.follower) return; diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 72c35c529c..425a97f3f1 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -1,15 +1,20 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { User } from '@/models/entities/User.js'; -import type { UserKeypairsRepository } from '@/models/index.js'; +import type { MiUser } from '@/models/User.js'; +import type { UserKeypairsRepository } from '@/models/_.js'; import { RedisKVCache } from '@/misc/cache.js'; -import type { UserKeypair } from '@/models/entities/UserKeypair.js'; +import type { MiUserKeypair } from '@/models/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @Injectable() -export class UserKeypairService { - private cache: RedisKVCache; +export class UserKeypairService implements OnApplicationShutdown { + private cache: RedisKVCache; constructor( @Inject(DI.redis) @@ -18,7 +23,7 @@ export class UserKeypairService { @Inject(DI.userKeypairsRepository) private userKeypairsRepository: UserKeypairsRepository, ) { - this.cache = new RedisKVCache(this.redisClient, 'userKeypair', { + this.cache = new RedisKVCache(this.redisClient, 'userKeypair', { lifetime: 1000 * 60 * 60 * 24, // 24h memoryCacheLifetime: Infinity, fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }), @@ -28,7 +33,17 @@ export class UserKeypairService { } @bindThis - public async getUserKeypair(userId: User['id']): Promise { + public async getUserKeypair(userId: MiUser['id']): Promise { return await this.cache.fetch(userId); } + + @bindThis + public dispose(): void { + this.cache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index 08cc907ebf..a71d50bba5 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -1,10 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UserListJoiningsRepository, UsersRepository } from '@/models/index.js'; -import type { User } from '@/models/entities/User.js'; -import type { UserList } from '@/models/entities/UserList.js'; -import type { UserListJoining } from '@/models/entities/UserListJoining.js'; +import type { UserListJoiningsRepository } from '@/models/_.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiUserList } from '@/models/UserList.js'; +import type { MiUserListJoining } from '@/models/UserListJoining.js'; import { IdService } from '@/core/IdService.js'; -import { UserFollowingService } from '@/core/UserFollowingService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -18,15 +22,11 @@ export class UserListService { public static TooManyUsersError = class extends Error {}; constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.userListJoiningsRepository) private userListJoiningsRepository: UserListJoiningsRepository, private userEntityService: UserEntityService, private idService: IdService, - private userFollowingService: UserFollowingService, private roleService: RoleService, private globalEventService: GlobalEventService, private proxyAccountService: ProxyAccountService, @@ -35,7 +35,7 @@ export class UserListService { } @bindThis - public async push(target: User, list: UserList, me: User) { + public async push(target: MiUser, list: MiUserList, me: MiUser) { const currentCount = await this.userListJoiningsRepository.countBy({ userListId: list.id, }); @@ -48,7 +48,7 @@ export class UserListService { createdAt: new Date(), userId: target.id, userListId: list.id, - } as UserListJoining); + } as MiUserListJoining); this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target)); diff --git a/packages/backend/src/core/UserMutingService.ts b/packages/backend/src/core/UserMutingService.ts index d201ec6c04..2387c9d648 100644 --- a/packages/backend/src/core/UserMutingService.ts +++ b/packages/backend/src/core/UserMutingService.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; -import type { MutingsRepository, Muting } from '@/models/index.js'; +import type { MutingsRepository, MiMuting } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; -import type { User } from '@/models/entities/User.js'; +import type { MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; @@ -19,7 +24,7 @@ export class UserMutingService { } @bindThis - public async mute(user: User, target: User, expiresAt: Date | null = null): Promise { + public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise { await this.mutingsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), @@ -32,7 +37,7 @@ export class UserMutingService { } @bindThis - public async unmute(mutings: Muting[]): Promise { + public async unmute(mutings: MiMuting[]): Promise { if (mutings.length === 0) return; await this.mutingsRepository.delete({ diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index b197d335d8..8940a142d1 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -1,11 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Not, IsNull } from 'typeorm'; -import type { FollowingsRepository, UsersRepository } from '@/models/index.js'; -import type { User } from '@/models/entities/User.js'; +import type { FollowingsRepository } from '@/models/_.js'; +import type { MiUser } from '@/models/User.js'; import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -13,12 +17,6 @@ import { bindThis } from '@/decorators.js'; @Injectable() export class UserSuspendService { constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @@ -30,15 +28,15 @@ export class UserSuspendService { } @bindThis - public async doPostSuspend(user: { id: User['id']; host: User['host'] }): Promise { + public async doPostSuspend(user: { id: MiUser['id']; host: MiUser['host'] }): Promise { this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); - + if (this.userEntityService.isLocalUser(user)) { // 知り得る全SharedInboxにDelete配信 const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user)); - + const queue: string[] = []; - + const followings = await this.followingsRepository.find({ where: [ { followerSharedInbox: Not(IsNull()) }, @@ -46,13 +44,13 @@ export class UserSuspendService { ], select: ['followerSharedInbox', 'followeeSharedInbox'], }); - + const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); - + for (const inbox of inboxes) { if (inbox != null && !queue.includes(inbox)) queue.push(inbox); } - + for (const inbox of queue) { this.queueService.deliver(user, content, inbox, true); } @@ -60,15 +58,15 @@ export class UserSuspendService { } @bindThis - public async doPostUnsuspend(user: User): Promise { + public async doPostUnsuspend(user: MiUser): Promise { this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false }); - + if (this.userEntityService.isLocalUser(user)) { // 知り得る全SharedInboxにUndo Delete配信 const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user)); - + const queue: string[] = []; - + const followings = await this.followingsRepository.find({ where: [ { followerSharedInbox: Not(IsNull()) }, @@ -76,13 +74,13 @@ export class UserSuspendService { ], select: ['followerSharedInbox', 'followeeSharedInbox'], }); - + const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); - + for (const inbox of inboxes) { if (inbox != null && !queue.includes(inbox)) queue.push(inbox); } - + for (const inbox of queue) { this.queueService.deliver(user as any, content, inbox, true); } diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index d00708a442..d2d2776bd2 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { URL } from 'node:url'; import { toASCII } from 'punycode'; import { Inject, Injectable } from '@nestjs/common'; diff --git a/packages/backend/src/core/VideoProcessingService.ts b/packages/backend/src/core/VideoProcessingService.ts index 5869905db0..ffb7573358 100644 --- a/packages/backend/src/core/VideoProcessingService.ts +++ b/packages/backend/src/core/VideoProcessingService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import FFmpeg from 'fluent-ffmpeg'; import { DI } from '@/di-symbols.js'; @@ -21,7 +26,7 @@ export class VideoProcessingService { @bindThis public async generateVideoThumbnail(source: string): Promise { const [dir, cleanup] = await createTempDir(); - + try { await new Promise((res, rej) => { FFmpeg({ @@ -52,7 +57,7 @@ export class VideoProcessingService { query({ thumbnail: '1', url, - }) + }), ); } } diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts new file mode 100644 index 0000000000..5945dc2919 --- /dev/null +++ b/packages/backend/src/core/WebAuthnService.ts @@ -0,0 +1,252 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import * as Redis from 'ioredis'; +import { + generateAuthenticationOptions, + generateRegistrationOptions, verifyAuthenticationResponse, + verifyRegistrationResponse, +} from '@simplewebauthn/server'; +import { AttestationFormat, isoCBOR } from '@simplewebauthn/server/helpers'; +import { DI } from '@/di-symbols.js'; +import type { UserSecurityKeysRepository } from '@/models/_.js'; +import type { Config } from '@/config.js'; +import { bindThis } from '@/decorators.js'; +import { MetaService } from '@/core/MetaService.js'; +import { MiUser } from '@/models/_.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import type { + AuthenticationResponseJSON, + AuthenticatorTransportFuture, + CredentialDeviceType, + PublicKeyCredentialCreationOptionsJSON, + PublicKeyCredentialDescriptorFuture, + PublicKeyCredentialRequestOptionsJSON, + RegistrationResponseJSON, +} from '@simplewebauthn/typescript-types'; + +@Injectable() +export class WebAuthnService { + constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, + + @Inject(DI.config) + private config: Config, + + @Inject(DI.userSecurityKeysRepository) + private userSecurityKeysRepository: UserSecurityKeysRepository, + + private metaService: MetaService, + ) { + } + + @bindThis + public async getRelyingParty(): Promise<{ origin: string; rpId: string; rpName: string; rpIcon?: string; }> { + const instance = await this.metaService.fetch(); + return { + origin: this.config.url, + rpId: this.config.host, + rpName: instance.name ?? this.config.host, + rpIcon: instance.iconUrl ?? undefined, + }; + } + + @bindThis + public async initiateRegistration(userId: MiUser['id'], userName: string, userDisplayName?: string): Promise { + const relyingParty = await this.getRelyingParty(); + const keys = await this.userSecurityKeysRepository.findBy({ + userId: userId, + }); + + const registrationOptions = await generateRegistrationOptions({ + rpName: relyingParty.rpName, + rpID: relyingParty.rpId, + userID: userId, + userName: userName, + userDisplayName: userDisplayName, + attestationType: 'indirect', + excludeCredentials: keys.map(key => ({ + id: Buffer.from(key.id, 'base64url'), + type: 'public-key', + transports: key.transports ?? undefined, + })), + authenticatorSelection: { + residentKey: 'required', + userVerification: 'preferred', + }, + }); + + await this.redisClient.setex(`webauthn:challenge:${userId}`, 90, registrationOptions.challenge); + + return registrationOptions; + } + + @bindThis + public async verifyRegistration(userId: MiUser['id'], response: RegistrationResponseJSON): Promise<{ + credentialID: Uint8Array; + credentialPublicKey: Uint8Array; + attestationObject: Uint8Array; + fmt: AttestationFormat; + counter: number; + userVerified: boolean; + credentialDeviceType: CredentialDeviceType; + credentialBackedUp: boolean; + transports?: AuthenticatorTransportFuture[]; + }> { + const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`); + + if (!challenge) { + throw new IdentifiableError('7dbfb66c-9216-4e2b-9c27-cef2ac8efb84', 'challenge not found'); + } + + await this.redisClient.del(`webauthn:challenge:${userId}`); + + const relyingParty = await this.getRelyingParty(); + + let verification; + try { + verification = await verifyRegistrationResponse({ + response: response, + expectedChallenge: challenge, + expectedOrigin: relyingParty.origin, + expectedRPID: relyingParty.rpId, + requireUserVerification: true, + }); + } catch (error) { + console.error(error); + throw new IdentifiableError('5c1446f8-8ca7-4d31-9f39-656afe9c5d87', 'verification failed'); + } + + const { verified } = verification; + + if (!verified || !verification.registrationInfo) { + throw new IdentifiableError('bb333667-3832-4a80-8bb5-c505be7d710d', 'verification failed'); + } + + const { registrationInfo } = verification; + + return { + credentialID: registrationInfo.credentialID, + credentialPublicKey: registrationInfo.credentialPublicKey, + attestationObject: registrationInfo.attestationObject, + fmt: registrationInfo.fmt, + counter: registrationInfo.counter, + userVerified: registrationInfo.userVerified, + credentialDeviceType: registrationInfo.credentialDeviceType, + credentialBackedUp: registrationInfo.credentialBackedUp, + transports: response.response.transports, + }; + } + + @bindThis + public async initiateAuthentication(userId: MiUser['id']): Promise { + const keys = await this.userSecurityKeysRepository.findBy({ + userId: userId, + }); + + if (keys.length === 0) { + throw new IdentifiableError('f27fd449-9af4-4841-9249-1f989b9fa4a4', 'no keys found'); + } + + const authenticationOptions = await generateAuthenticationOptions({ + allowCredentials: keys.map(key => ({ + id: Buffer.from(key.id, 'base64url'), + type: 'public-key', + transports: key.transports ?? undefined, + })), + userVerification: 'preferred', + }); + + await this.redisClient.setex(`webauthn:challenge:${userId}`, 90, authenticationOptions.challenge); + + return authenticationOptions; + } + + @bindThis + public async verifyAuthentication(userId: MiUser['id'], response: AuthenticationResponseJSON): Promise { + const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`); + + if (!challenge) { + throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', 'challenge not found'); + } + + await this.redisClient.del(`webauthn:challenge:${userId}`); + + const key = await this.userSecurityKeysRepository.findOneBy({ + id: response.id, + userId: userId, + }); + + if (!key) { + throw new IdentifiableError('36b96a7d-b547-412d-aeed-2d611cdc8cdc', 'unknown key'); + } + + // マイグレーション + if (key.counter === 0 && key.publicKey.length === 87) { + const cert = new Uint8Array(Buffer.from(key.publicKey, 'base64url')); + if (cert[0] === 0x04) { // 前の実装ではいつも 0x04 で始まっていた + const halfLength = (cert.length - 1) / 2; + + const cborMap = new Map(); + cborMap.set(1, 2); // kty, EC2 + cborMap.set(3, -7); // alg, ES256 + cborMap.set(-1, 1); // crv, P256 + cborMap.set(-2, cert.slice(1, halfLength + 1)); // x + cborMap.set(-3, cert.slice(halfLength + 1)); // y + + const cborPubKey = Buffer.from(isoCBOR.encode(cborMap)).toString('base64url'); + await this.userSecurityKeysRepository.update({ + id: response.id, + userId: userId, + }, { + publicKey: cborPubKey, + }); + key.publicKey = cborPubKey; + } + } + + const relyingParty = await this.getRelyingParty(); + + let verification; + try { + verification = await verifyAuthenticationResponse({ + response: response, + expectedChallenge: challenge, + expectedOrigin: relyingParty.origin, + expectedRPID: relyingParty.rpId, + authenticator: { + credentialID: Buffer.from(key.id, 'base64url'), + credentialPublicKey: Buffer.from(key.publicKey, 'base64url'), + counter: key.counter, + transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined, + }, + requireUserVerification: true, + }); + } catch (error) { + console.error(error); + throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', 'verification failed'); + } + + const { verified, authenticationInfo } = verification; + + if (!verified) { + return false; + } + + await this.userSecurityKeysRepository.update({ + id: response.id, + userId: userId, + }, { + lastUsed: new Date(), + counter: authenticationInfo.newCounter, + credentialDeviceType: authenticationInfo.credentialDeviceType, + credentialBackedUp: authenticationInfo.credentialBackedUp, + }); + + return verified; + } +} diff --git a/packages/backend/src/core/WebfingerService.ts b/packages/backend/src/core/WebfingerService.ts index f58a6a10fc..3d5747aebd 100644 --- a/packages/backend/src/core/WebfingerService.ts +++ b/packages/backend/src/core/WebfingerService.ts @@ -1,17 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { URL } from 'node:url'; -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; +import { Injectable } from '@nestjs/common'; import { query as urlQuery } from '@/misc/prelude/url.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; -type ILink = { +export type ILink = { href: string; rel?: string; }; -type IWebFinger = { +export type IWebFinger = { links: ILink[]; subject: string; }; @@ -22,9 +25,6 @@ const mRegex = /^([^@]+)@(.*)/; @Injectable() export class WebfingerService { constructor( - @Inject(DI.config) - private config: Config, - private httpRequestService: HttpRequestService, ) { } diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts index 467755a072..1344f0ac97 100644 --- a/packages/backend/src/core/WebhookService.ts +++ b/packages/backend/src/core/WebhookService.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { WebhooksRepository } from '@/models/index.js'; -import type { Webhook } from '@/models/entities/Webhook.js'; +import type { WebhooksRepository } from '@/models/_.js'; +import type { MiWebhook } from '@/models/Webhook.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { StreamMessages } from '@/server/api/stream/types.js'; @@ -10,7 +15,7 @@ import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class WebhookService implements OnApplicationShutdown { private webhooksFetched = false; - private webhooks: Webhook[] = []; + private webhooks: MiWebhook[] = []; constructor( @Inject(DI.redisForSub) @@ -31,7 +36,7 @@ export class WebhookService implements OnApplicationShutdown { }); this.webhooksFetched = true; } - + return this.webhooks; } diff --git a/packages/backend/src/core/activitypub/ApAudienceService.ts b/packages/backend/src/core/activitypub/ApAudienceService.ts index 8282a6324c..440852bdf3 100644 --- a/packages/backend/src/core/activitypub/ApAudienceService.ts +++ b/packages/backend/src/core/activitypub/ApAudienceService.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; -import type { RemoteUser, User } from '@/models/entities/User.js'; +import type { MiRemoteUser, MiUser } from '@/models/User.js'; import { concat, unique } from '@/misc/prelude/array.js'; import { bindThis } from '@/decorators.js'; import { getApIds } from './type.js'; @@ -12,10 +17,12 @@ type Visibility = 'public' | 'home' | 'followers' | 'specified'; type AudienceInfo = { visibility: Visibility, - mentionedUsers: User[], - visibleUsers: User[], + mentionedUsers: MiUser[], + visibleUsers: MiUser[], }; +type GroupedAudience = Record<'public' | 'followers' | 'other', string[]>; + @Injectable() export class ApAudienceService { constructor( @@ -24,17 +31,17 @@ export class ApAudienceService { } @bindThis - public async parseAudience(actor: RemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise { + public async parseAudience(actor: MiRemoteUser, 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 User => x != null); - + )).filter((x): x is MiUser => x != null); + if (toGroups.public.length > 0) { return { visibility: 'public', @@ -42,7 +49,7 @@ export class ApAudienceService { visibleUsers: [], }; } - + if (ccGroups.public.length > 0) { return { visibility: 'home', @@ -50,7 +57,7 @@ export class ApAudienceService { visibleUsers: [], }; } - + if (toGroups.followers.length > 0) { return { visibility: 'followers', @@ -58,22 +65,22 @@ export class ApAudienceService { visibleUsers: [], }; } - + return { visibility: 'specified', mentionedUsers, visibleUsers: mentionedUsers, }; } - + @bindThis - private groupingAudience(ids: string[], actor: RemoteUser) { - const groups = { - public: [] as string[], - followers: [] as string[], - other: [] as string[], + private groupingAudience(ids: string[], actor: MiRemoteUser): GroupedAudience { + const groups: GroupedAudience = { + public: [], + followers: [], + other: [], }; - + for (const id of ids) { if (this.isPublic(id)) { groups.public.push(id); @@ -83,25 +90,23 @@ export class ApAudienceService { groups.other.push(id); } } - + groups.other = unique(groups.other); - + return groups; } - + @bindThis - private isPublic(id: string) { + private isPublic(id: string): boolean { return [ 'https://www.w3.org/ns/activitystreams#Public', - 'as#Public', + 'as:Public', 'Public', ].includes(id); } - + @bindThis - private isFollowers(id: string, actor: RemoteUser) { - return ( - id === (actor.followersUri ?? `${actor.uri}/followers`) - ); + private isFollowers(id: string, actor: MiRemoteUser): boolean { + 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 2b404ebeca..995c5dcd5f 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -1,14 +1,18 @@ -import { Inject, Injectable } from '@nestjs/common'; -import escapeRegexp from 'escape-regexp'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; +import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import { MemoryKVCache } from '@/misc/cache.js'; -import type { UserPublickey } from '@/models/entities/UserPublickey.js'; +import type { MiUserPublickey } from '@/models/UserPublickey.js'; import { CacheService } from '@/core/CacheService.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { MiNote } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; -import { LocalUser, RemoteUser } from '@/models/entities/User.js'; +import { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { getApId } from './type.js'; import { ApPersonService } from './models/ApPersonService.js'; import type { IObject } from './type.js'; @@ -30,9 +34,9 @@ export type UriParseResult = { }; @Injectable() -export class ApDbResolverService { - private publicKeyCache: MemoryKVCache; - private publicKeyByUserIdCache: MemoryKVCache; +export class ApDbResolverService implements OnApplicationShutdown { + private publicKeyCache: MemoryKVCache; + private publicKeyByUserIdCache: MemoryKVCache; constructor( @Inject(DI.config) @@ -50,38 +54,31 @@ export class ApDbResolverService { private cacheService: CacheService, private apPersonService: ApPersonService, ) { - this.publicKeyCache = new MemoryKVCache(Infinity); - this.publicKeyByUserIdCache = new MemoryKVCache(Infinity); + this.publicKeyCache = new MemoryKVCache(Infinity); + this.publicKeyByUserIdCache = new MemoryKVCache(Infinity); } @bindThis public parseUri(value: string | IObject): UriParseResult { - const uri = getApId(value); - - // the host part of a URL is case insensitive, so use the 'i' flag. - const localRegex = new RegExp('^' + escapeRegexp(this.config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i'); - const matchLocal = uri.match(localRegex); - - if (matchLocal) { - return { - local: true, - type: matchLocal[1], - id: matchLocal[2], - rest: matchLocal[3], - }; - } else { - return { - local: false, - uri, - }; - } + const separator = '/'; + + const uri = new URL(getApId(value)); + if (uri.origin !== this.config.url) return { local: false, uri: uri.href }; + + const [, type, id, ...rest] = uri.pathname.split(separator); + return { + local: true, + type, + id, + rest: rest.length === 0 ? undefined : rest.join(separator), + }; } /** * AP Note => Misskey Note in DB */ @bindThis - public async getNoteFromApId(value: string | IObject): Promise { + public async getNoteFromApId(value: string | IObject): Promise { const parsed = this.parseUri(value); if (parsed.local) { @@ -101,19 +98,21 @@ export class ApDbResolverService { * 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) { if (parsed.type !== 'users') return null; - return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({ - id: parsed.id, - }).then(x => x ?? undefined)) as LocalUser | undefined ?? null; + return await this.cacheService.userByIdCache.fetchMaybe( + parsed.id, + () => this.usersRepository.findOneBy({ id: parsed.id }).then(x => x ?? undefined), + ) as MiLocalUser | undefined ?? null; } else { - return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({ - uri: parsed.uri, - })) as RemoteUser | null; + return await this.cacheService.uriPersonCache.fetch( + parsed.uri, + () => this.usersRepository.findOneBy({ uri: parsed.uri }), + ) as MiRemoteUser | null; } } @@ -122,14 +121,14 @@ export class ApDbResolverService { */ @bindThis public async getAuthUserFromKeyId(keyId: string): Promise<{ - user: RemoteUser; - key: UserPublickey; + user: MiRemoteUser; + key: MiUserPublickey; } | null> { const key = await this.publicKeyCache.fetch(keyId, async () => { const key = await this.userPublickeysRepository.findOneBy({ keyId, }); - + if (key == null) return null; return key; @@ -138,7 +137,7 @@ export class ApDbResolverService { if (key == null) return null; return { - user: await this.cacheService.findUserById(key.userId) as RemoteUser, + user: await this.cacheService.findUserById(key.userId) as MiRemoteUser, key, }; } @@ -148,18 +147,31 @@ export class ApDbResolverService { */ @bindThis public async getAuthUserFromApId(uri: string): Promise<{ - user: RemoteUser; - key: UserPublickey | null; + user: MiRemoteUser; + key: MiUserPublickey | null; } | null> { - const user = await this.apPersonService.resolvePerson(uri) as RemoteUser; + const user = await this.apPersonService.resolvePerson(uri) as MiRemoteUser; - if (user == null) return null; - - const key = await this.publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null); + const key = await this.publicKeyByUserIdCache.fetch( + user.id, + () => this.userPublickeysRepository.findOneBy({ userId: user.id }), + v => v != null, + ); return { user, key, }; } + + @bindThis + public dispose(): void { + this.publicKeyCache.dispose(); + this.publicKeyByUserIdCache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 62a2a33a19..81003bcf1c 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -1,12 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; 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 { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; +import type { FollowingsRepository } from '@/models/_.js'; +import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; import { QueueService } from '@/core/QueueService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import type { IActivity } from '@/core/activitypub/type.js'; +import { ThinUser } from '@/queue/types.js'; interface IRecipe { type: string; @@ -18,88 +24,25 @@ interface IFollowersRecipe extends IRecipe { interface IDirectRecipe extends IRecipe { type: 'Direct'; - to: RemoteUser; + to: MiRemoteUser; } -const isFollowers = (recipe: any): recipe is IFollowersRecipe => +const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe => recipe.type === 'Followers'; -const isDirect = (recipe: any): recipe is IDirectRecipe => +const isDirect = (recipe: IRecipe): recipe is IDirectRecipe => recipe.type === 'Direct'; -@Injectable() -export class ApDeliverManagerService { - constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - - private userEntityService: UserEntityService, - private queueService: QueueService, - ) { - } - - /** - * Deliver activity to followers - * @param activity Activity - * @param from Followee - */ - @bindThis - public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: any) { - const manager = new DeliverManager( - this.userEntityService, - this.followingsRepository, - this.queueService, - actor, - activity, - ); - manager.addFollowersRecipe(); - await manager.execute(); - } - - /** - * Deliver activity to user - * @param activity Activity - * @param to Target user - */ - @bindThis - public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: any, to: RemoteUser) { - const manager = new DeliverManager( - this.userEntityService, - this.followingsRepository, - this.queueService, - actor, - activity, - ); - manager.addDirectRecipe(to); - await manager.execute(); - } - - @bindThis - public createDeliverManager(actor: { id: User['id']; host: null; }, activity: any) { - return new DeliverManager( - this.userEntityService, - this.followingsRepository, - this.queueService, - - actor, - activity, - ); - } -} - class DeliverManager { - private actor: { id: User['id']; host: null; }; - private activity: any; + private actor: ThinUser; + private activity: IActivity | null; private recipes: IRecipe[] = []; /** * Constructor + * @param userEntityService + * @param followingsRepository + * @param queueService * @param actor Actor * @param activity Activity to deliver */ @@ -108,10 +51,17 @@ class DeliverManager { private followingsRepository: FollowingsRepository, private queueService: QueueService, - actor: { id: User['id']; host: null; }, - activity: any, + actor: { id: MiUser['id']; host: null; }, + activity: IActivity | null, ) { - this.actor = actor; + // 型で弾いてはいるが一応ローカルユーザーかチェック + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (actor.host != null) throw new Error('actor.host must be null'); + + // パフォーマンス向上のためキューに突っ込むのはidのみに絞る + this.actor = { + id: actor.id, + }; this.activity = activity; } @@ -119,10 +69,10 @@ class DeliverManager { * Add recipe for followers deliver */ @bindThis - public addFollowersRecipe() { - const deliver = { + public addFollowersRecipe(): void { + const deliver: IFollowersRecipe = { type: 'Followers', - } as IFollowersRecipe; + }; this.addRecipe(deliver); } @@ -132,11 +82,11 @@ class DeliverManager { * @param to To */ @bindThis - public addDirectRecipe(to: RemoteUser) { - const recipe = { + public addDirectRecipe(to: MiRemoteUser): void { + const recipe: IDirectRecipe = { type: 'Direct', to, - } as IDirectRecipe; + }; this.addRecipe(recipe); } @@ -146,7 +96,7 @@ class DeliverManager { * @param recipe Recipe */ @bindThis - public addRecipe(recipe: IRecipe) { + public addRecipe(recipe: IRecipe): void { this.recipes.push(recipe); } @@ -154,18 +104,13 @@ class DeliverManager { * Execute delivers */ @bindThis - public async execute() { - if (!this.userEntityService.isLocalUser(this.actor)) return; - + public async execute(): Promise { // The value flags whether it is shared or not. + // key: inbox URL, value: whether it is sharedInbox const inboxes = new Map(); - /* - build inbox list - - Process follower recipes first to avoid duplication when processing - direct recipes later. - */ + // build inbox list + // Process follower recipes first to avoid duplication when processing direct recipes later. if (this.recipes.some(r => isFollowers(r))) { // followers deliver // TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう @@ -179,31 +124,87 @@ class DeliverManager { followerSharedInbox: true, followerInbox: true, }, - }) as { - followerSharedInbox: string | null; - followerInbox: string; - }[]; + }); for (const following of followers) { const inbox = following.followerSharedInbox ?? following.followerInbox; + if (inbox === null) throw new Error('inbox is null'); inboxes.set(inbox, following.followerSharedInbox != null); } } - this.recipes.filter((recipe): recipe is IDirectRecipe => - // followers recipes have already been processed - isDirect(recipe) + for (const recipe of this.recipes.filter(isDirect)) { // check that shared inbox has not been added yet - && !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox)) + if (recipe.to.sharedInbox !== null && inboxes.has(recipe.to.sharedInbox)) continue; + // check that they actually have an inbox - && recipe.to.inbox != null, - ) - .forEach(recipe => inboxes.set(recipe.to.inbox!, false)); + if (recipe.to.inbox === null) continue; + + inboxes.set(recipe.to.inbox, false); + } // deliver - for (const inbox of inboxes) { - // inbox[0]: inbox, inbox[1]: whether it is sharedInbox - this.queueService.deliver(this.actor, this.activity, inbox[0], inbox[1]); - } + this.queueService.deliverMany(this.actor, this.activity, inboxes); + } +} + +@Injectable() +export class ApDeliverManagerService { + constructor( + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, + private queueService: QueueService, + ) { + } + + /** + * Deliver activity to followers + * @param actor + * @param activity Activity + */ + @bindThis + public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity): Promise { + const manager = new DeliverManager( + this.userEntityService, + this.followingsRepository, + this.queueService, + actor, + activity, + ); + manager.addFollowersRecipe(); + await manager.execute(); + } + + /** + * Deliver activity to user + * @param actor + * @param activity Activity + * @param to Target user + */ + @bindThis + public async deliverToUser(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, to: MiRemoteUser): Promise { + const manager = new DeliverManager( + this.userEntityService, + this.followingsRepository, + this.queueService, + actor, + activity, + ); + manager.addDirectRecipe(to); + await manager.execute(); + } + + @bindThis + public createDeliverManager(actor: { id: MiUser['id']; host: null; }, activity: IActivity | null): DeliverManager { + return new DeliverManager( + this.userEntityService, + this.followingsRepository, + this.queueService, + + actor, + activity, + ); } } diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index efef777fb0..b921ee7454 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; @@ -13,18 +18,16 @@ import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js'; import { AppLockService } from '@/core/AppLockService.js'; import type Logger from '@/logger.js'; import { MetaService } from '@/core/MetaService.js'; -import { AccountMoveService } from '@/core/AccountMoveService.js'; import { IdService } from '@/core/IdService.js'; import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { CacheService } from '@/core/CacheService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { QueueService } from '@/core/QueueService.js'; -import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, } from '@/models/index.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; -import type { RemoteUser } from '@/models/entities/User.js'; -import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; +import type { MiRemoteUser } from '@/models/User.js'; +import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { ApNoteService } from './models/ApNoteService.js'; import { ApLoggerService } from './ApLoggerService.js'; import { ApDbResolverService } from './ApDbResolverService.js'; @@ -78,15 +81,13 @@ export class ApInboxService { private apNoteService: ApNoteService, private apPersonService: ApPersonService, private apQuestionService: ApQuestionService, - private accountMoveService: AccountMoveService, - private cacheService: CacheService, private queueService: QueueService, ) { this.logger = this.apLoggerService.logger; } @bindThis - public async performActivity(actor: RemoteUser, activity: IObject) { + public async performActivity(actor: MiRemoteUser, activity: IObject): Promise { if (isCollectionOrOrderedCollection(activity)) { const resolver = this.apResolverService.createResolver(); for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { @@ -107,14 +108,14 @@ export class ApInboxService { if (actor.uri) { if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { setImmediate(() => { - this.apPersonService.updatePerson(actor.uri!); + this.apPersonService.updatePerson(actor.uri); }); } } } @bindThis - public async performOneActivity(actor: RemoteUser, activity: IObject): Promise { + public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise { if (actor.isSuspended) return; if (isCreate(activity)) { @@ -151,7 +152,7 @@ export class ApInboxService { } @bindThis - private async follow(actor: RemoteUser, activity: IFollow): Promise { + private async follow(actor: MiRemoteUser, activity: IFollow): Promise { const followee = await this.apDbResolverService.getUserFromApId(activity.object); if (followee == null) { @@ -168,7 +169,7 @@ export class ApInboxService { } @bindThis - private async like(actor: RemoteUser, activity: ILike): Promise { + private async like(actor: MiRemoteUser, activity: ILike): Promise { const targetUri = getApId(activity.object); const note = await this.apNoteService.fetchNote(targetUri); @@ -186,7 +187,7 @@ export class ApInboxService { } @bindThis - private async accept(actor: RemoteUser, activity: IAccept): Promise { + private async accept(actor: MiRemoteUser, activity: IAccept): Promise { const uri = activity.id ?? activity; this.logger.info(`Accept: ${uri}`); @@ -204,7 +205,7 @@ export class ApInboxService { } @bindThis - private async acceptFollow(actor: RemoteUser, activity: IFollow): Promise { + private async acceptFollow(actor: MiRemoteUser, activity: IFollow): Promise { // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある const follower = await this.apDbResolverService.getUserFromApId(activity.actor); @@ -228,8 +229,8 @@ export class ApInboxService { } @bindThis - private async add(actor: RemoteUser, activity: IAdd): Promise { - if ('actor' in activity && actor.uri !== activity.actor) { + private async add(actor: MiRemoteUser, activity: IAdd): Promise { + if (actor.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -248,7 +249,7 @@ export class ApInboxService { } @bindThis - private async announce(actor: RemoteUser, activity: IAnnounce): Promise { + private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise { const uri = getApId(activity); this.logger.info(`Announce: ${uri}`); @@ -259,7 +260,7 @@ export class ApInboxService { } @bindThis - private async announceNote(actor: RemoteUser, activity: IAnnounce, targetUri: string): Promise { + private async announceNote(actor: MiRemoteUser, activity: IAnnounce, targetUri: string): Promise { const uri = getApId(activity); if (actor.isSuspended) { @@ -273,7 +274,7 @@ export class ApInboxService { const unlock = await this.appLockService.getApLock(uri); try { - // 既に同じURIを持つものが登録されていないかチェック + // 既に同じURIを持つものが登録されていないかチェック const exist = await this.apNoteService.fetchNote(uri); if (exist) { return; @@ -292,7 +293,7 @@ export class ApInboxService { return; } - this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode ?? err}`); + this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`); } throw err; } @@ -319,7 +320,7 @@ export class ApInboxService { } @bindThis - private async block(actor: RemoteUser, activity: IBlock): Promise { + private async block(actor: MiRemoteUser, activity: IBlock): Promise { // ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず const blockee = await this.apDbResolverService.getUserFromApId(activity.object); @@ -337,7 +338,7 @@ export class ApInboxService { } @bindThis - private async create(actor: RemoteUser, activity: ICreate): Promise { + private async create(actor: MiRemoteUser, activity: ICreate): Promise { const uri = getApId(activity); this.logger.info(`Create: ${uri}`); @@ -373,7 +374,7 @@ export class ApInboxService { } @bindThis - private async createNote(resolver: Resolver, actor: RemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { + private async createNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { const uri = getApId(note); if (typeof note === 'object') { @@ -408,8 +409,8 @@ export class ApInboxService { } @bindThis - private async delete(actor: RemoteUser, activity: IDelete): Promise { - if ('actor' in activity && actor.uri !== activity.actor) { + private async delete(actor: MiRemoteUser, activity: IDelete): Promise { + if (actor.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -420,7 +421,7 @@ export class ApInboxService { // typeが不明だけど、どうせ消えてるのでremote resolveしない formerType = undefined; } else { - const object = activity.object as IObject; + const object = activity.object; if (isTombstone(object)) { formerType = toSingle(object.formerType); } else { @@ -450,7 +451,7 @@ export class ApInboxService { } @bindThis - private async deleteActor(actor: RemoteUser, uri: string): Promise { + private async deleteActor(actor: MiRemoteUser, uri: string): Promise { this.logger.info(`Deleting the Actor: ${uri}`); if (actor.uri !== uri) { @@ -474,7 +475,7 @@ export class ApInboxService { } @bindThis - private async deleteNote(actor: RemoteUser, uri: string): Promise { + private async deleteNote(actor: MiRemoteUser, uri: string): Promise { this.logger.info(`Deleting the Note: ${uri}`); const unlock = await this.appLockService.getApLock(uri); @@ -498,12 +499,15 @@ export class ApInboxService { } @bindThis - private async flag(actor: RemoteUser, activity: IFlag): Promise { + private async flag(actor: MiRemoteUser, activity: IFlag): Promise { // objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する const uris = getApIds(activity.object); - const userIds = uris.filter(uri => uri.startsWith(this.config.url + '/users/')).map(uri => uri.split('/').pop()!); + const userIds = uris + .filter(uri => uri.startsWith(this.config.url + '/users/')) + .map(uri => uri.split('/').at(-1)) + .filter((userId): userId is string => userId !== undefined); const users = await this.usersRepository.findBy({ id: In(userIds), }); @@ -523,7 +527,7 @@ export class ApInboxService { } @bindThis - private async reject(actor: RemoteUser, activity: IReject): Promise { + private async reject(actor: MiRemoteUser, activity: IReject): Promise { const uri = activity.id ?? activity; this.logger.info(`Reject: ${uri}`); @@ -541,7 +545,7 @@ export class ApInboxService { } @bindThis - private async rejectFollow(actor: RemoteUser, activity: IFollow): Promise { + private async rejectFollow(actor: MiRemoteUser, activity: IFollow): Promise { // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある const follower = await this.apDbResolverService.getUserFromApId(activity.actor); @@ -565,8 +569,8 @@ export class ApInboxService { } @bindThis - private async remove(actor: RemoteUser, activity: IRemove): Promise { - if ('actor' in activity && actor.uri !== activity.actor) { + private async remove(actor: MiRemoteUser, activity: IRemove): Promise { + if (actor.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -585,8 +589,8 @@ export class ApInboxService { } @bindThis - private async undo(actor: RemoteUser, activity: IUndo): Promise { - if ('actor' in activity && actor.uri !== activity.actor) { + private async undo(actor: MiRemoteUser, activity: IUndo): Promise { + if (actor.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -612,18 +616,20 @@ export class ApInboxService { } @bindThis - private async undoAccept(actor: RemoteUser, activity: IAccept): Promise { + private async undoAccept(actor: MiRemoteUser, activity: IAccept): Promise { const follower = await this.apDbResolverService.getUserFromApId(activity.object); if (follower == null) { return 'skip: follower not found'; } - const following = await this.followingsRepository.findOneBy({ - followerId: follower.id, - followeeId: actor.id, + const isFollowing = await this.followingsRepository.exist({ + where: { + followerId: follower.id, + followeeId: actor.id, + }, }); - if (following) { + if (isFollowing) { await this.userFollowingService.unfollow(follower, actor); return 'ok: unfollowed'; } @@ -632,7 +638,7 @@ export class ApInboxService { } @bindThis - private async undoAnnounce(actor: RemoteUser, activity: IAnnounce): Promise { + private async undoAnnounce(actor: MiRemoteUser, activity: IAnnounce): Promise { const uri = getApId(activity); const note = await this.notesRepository.findOneBy({ @@ -647,7 +653,7 @@ export class ApInboxService { } @bindThis - private async undoBlock(actor: RemoteUser, activity: IBlock): Promise { + private async undoBlock(actor: MiRemoteUser, activity: IBlock): Promise { const blockee = await this.apDbResolverService.getUserFromApId(activity.object); if (blockee == null) { @@ -663,7 +669,7 @@ export class ApInboxService { } @bindThis - private async undoFollow(actor: RemoteUser, activity: IFollow): Promise { + private async undoFollow(actor: MiRemoteUser, activity: IFollow): Promise { const followee = await this.apDbResolverService.getUserFromApId(activity.object); if (followee == null) { return 'skip: followee not found'; @@ -673,22 +679,26 @@ export class ApInboxService { return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません'; } - const req = await this.followRequestsRepository.findOneBy({ - followerId: actor.id, - followeeId: followee.id, + const requestExist = await this.followRequestsRepository.exist({ + where: { + followerId: actor.id, + followeeId: followee.id, + }, }); - const following = await this.followingsRepository.findOneBy({ - followerId: actor.id, - followeeId: followee.id, + const isFollowing = await this.followingsRepository.exist({ + where: { + followerId: actor.id, + followeeId: followee.id, + }, }); - if (req) { + if (requestExist) { await this.userFollowingService.cancelFollowRequest(followee, actor); return 'ok: follow request canceled'; } - if (following) { + if (isFollowing) { await this.userFollowingService.unfollow(actor, followee); return 'ok: unfollowed'; } @@ -697,7 +707,7 @@ export class ApInboxService { } @bindThis - private async undoLike(actor: RemoteUser, activity: ILike): Promise { + private async undoLike(actor: MiRemoteUser, activity: ILike): Promise { const targetUri = getApId(activity.object); const note = await this.apNoteService.fetchNote(targetUri); @@ -712,8 +722,8 @@ export class ApInboxService { } @bindThis - private async update(actor: RemoteUser, activity: IUpdate): Promise { - if ('actor' in activity && actor.uri !== activity.actor) { + private async update(actor: MiRemoteUser, activity: IUpdate): Promise { + if (actor.uri !== activity.actor) { return 'skip: invalid actor'; } @@ -727,7 +737,7 @@ export class ApInboxService { }); if (isActor(object)) { - await this.apPersonService.updatePerson(actor.uri!, resolver, object); + await this.apPersonService.updatePerson(actor.uri, resolver, object); return 'ok: Person updated'; } else if (getApType(object) === 'Question') { await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err)); @@ -738,7 +748,7 @@ export class ApInboxService { } @bindThis - private async move(actor: RemoteUser, activity: IMove): Promise { + private async move(actor: MiRemoteUser, activity: IMove): Promise { // fetch the new and old accounts const targetUri = getApHrefNullable(activity.target); if (!targetUri) return 'skip: invalid activity target'; diff --git a/packages/backend/src/core/activitypub/ApLoggerService.ts b/packages/backend/src/core/activitypub/ApLoggerService.ts index eeffab1b6d..cd9597e423 100644 --- a/packages/backend/src/core/activitypub/ApLoggerService.ts +++ b/packages/backend/src/core/activitypub/ApLoggerService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { RemoteLoggerService } from '@/core/RemoteLoggerService.js'; diff --git a/packages/backend/src/core/activitypub/ApMfmService.ts b/packages/backend/src/core/activitypub/ApMfmService.ts index 6116822f7a..60868627a2 100644 --- a/packages/backend/src/core/activitypub/ApMfmService.ts +++ b/packages/backend/src/core/activitypub/ApMfmService.ts @@ -1,33 +1,32 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import * as mfm from 'mfm-js'; -import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; import { MfmService } from '@/core/MfmService.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { MiNote } from '@/models/Note.js'; +import { bindThis } from '@/decorators.js'; import { extractApHashtagObjects } from './models/tag.js'; import type { IObject } from './type.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ApMfmService { constructor( - @Inject(DI.config) - private config: Config, - private mfmService: MfmService, ) { } @bindThis - public htmlToMfm(html: string, tag?: IObject | IObject[]) { - const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null); - + public htmlToMfm(html: string, tag?: IObject | IObject[]): string { + const hashtagNames = extractApHashtagObjects(tag).map(x => x.name); return this.mfmService.fromHtml(html, hashtagNames); } @bindThis - public getNoteHtml(note: Note) { + public getNoteHtml(note: MiNote): string | null { if (!note.text) return ''; return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)); - } + } } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index d8b95ca4d1..7a9d2e21d8 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -1,32 +1,35 @@ -import { createPublicKey } from 'node:crypto'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { createPublicKey, randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; -import { In, IsNull } from 'typeorm'; -import { v4 as uuid } from 'uuid'; +import { In } from 'typeorm'; import * as mfm from 'mfm-js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import type { PartialLocalUser, LocalUser, PartialRemoteUser, 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'; -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 { PollVote } from '@/models/entities/PollVote.js'; +import type { MiPartialLocalUser, MiLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; +import type { IMentionedRemoteUsers, MiNote } from '@/models/Note.js'; +import type { MiBlocking } from '@/models/Blocking.js'; +import type { MiRelay } from '@/models/Relay.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { MiNoteReaction } from '@/models/NoteReaction.js'; +import type { MiEmoji } from '@/models/Emoji.js'; +import type { MiPoll } from '@/models/Poll.js'; +import type { MiPollVote } from '@/models/PollVote.js'; import { UserKeypairService } from '@/core/UserKeypairService.js'; import { MfmService } from '@/core/MfmService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; -import type { UserKeypair } from '@/models/entities/UserKeypair.js'; -import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js'; +import type { MiUserKeypair } from '@/models/UserKeypair.js'; +import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { isNotNull } from '@/misc/is-not-null.js'; import { LdSignatureService } from './LdSignatureService.js'; import { ApMfmService } from './ApMfmService.js'; import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; -import type { IIdentifier } from './models/identifier.js'; @Injectable() export class ApRendererService { @@ -46,9 +49,6 @@ export class ApRendererService { @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - @Inject(DI.emojisRepository) - private emojisRepository: EmojisRepository, - @Inject(DI.pollsRepository) private pollsRepository: PollsRepository, @@ -63,7 +63,7 @@ export class ApRendererService { } @bindThis - public renderAccept(object: any, user: { id: User['id']; host: null }): IAccept { + public renderAccept(object: string | IObject, user: { id: MiUser['id']; host: null }): IAccept { return { type: 'Accept', actor: this.userEntityService.genLocalUserUri(user.id), @@ -72,7 +72,7 @@ export class ApRendererService { } @bindThis - public renderAdd(user: LocalUser, target: any, object: any): IAdd { + public renderAdd(user: MiLocalUser, target: string | IObject | undefined, object: string | IObject): IAdd { return { type: 'Add', actor: this.userEntityService.genLocalUserUri(user.id), @@ -82,7 +82,7 @@ export class ApRendererService { } @bindThis - public renderAnnounce(object: any, note: Note): IAnnounce { + public renderAnnounce(object: string | IObject, note: MiNote): IAnnounce { const attributedTo = this.userEntityService.genLocalUserUri(note.userId); let to: string[] = []; @@ -118,7 +118,7 @@ export class ApRendererService { * @param block The block to be rendered. The blockee relation must be loaded. */ @bindThis - public renderBlock(block: Blocking): IBlock { + public renderBlock(block: MiBlocking): IBlock { if (block.blockee?.uri == null) { throw new Error('renderBlock: missing blockee uri'); } @@ -132,14 +132,14 @@ export class ApRendererService { } @bindThis - public renderCreate(object: IObject, note: Note): ICreate { - const activity = { + public renderCreate(object: IObject, note: MiNote): ICreate { + const activity: ICreate = { id: `${this.config.url}/notes/${note.id}/activity`, actor: this.userEntityService.genLocalUserUri(note.userId), type: 'Create', published: note.createdAt.toISOString(), object, - } as ICreate; + }; if (object.to) activity.to = object.to; if (object.cc) activity.cc = object.cc; @@ -148,7 +148,7 @@ export class ApRendererService { } @bindThis - public renderDelete(object: IObject | string, user: { id: User['id']; host: null }): IDelete { + public renderDelete(object: IObject | string, user: { id: MiUser['id']; host: null }): IDelete { return { type: 'Delete', actor: this.userEntityService.genLocalUserUri(user.id), @@ -158,7 +158,7 @@ export class ApRendererService { } @bindThis - public renderDocument(file: DriveFile): IApDocument { + public renderDocument(file: MiDriveFile): IApDocument { return { type: 'Document', mediaType: file.webpublicType ?? file.type, @@ -168,7 +168,7 @@ export class ApRendererService { } @bindThis - public renderEmoji(emoji: Emoji): IApEmoji { + public renderEmoji(emoji: MiEmoji): IApEmoji { return { id: `${this.config.url}/emojis/${emoji.name}`, type: 'Emoji', @@ -185,7 +185,7 @@ export class ApRendererService { // to anonymise reporters, the reporting actor must be a system user @bindThis - public renderFlag(user: LocalUser, object: IObject | string, content: string): IFlag { + public renderFlag(user: MiLocalUser, object: IObject | string, content: string): IFlag { return { type: 'Flag', actor: this.userEntityService.genLocalUserUri(user.id), @@ -195,7 +195,7 @@ export class ApRendererService { } @bindThis - public renderFollowRelay(relay: Relay, relayActor: LocalUser): IFollow { + public renderFollowRelay(relay: MiRelay, relayActor: MiLocalUser): IFollow { return { id: `${this.config.url}/activities/follow-relay/${relay.id}`, type: 'Follow', @@ -209,22 +209,22 @@ export class ApRendererService { * @param id Follower|Followee ID */ @bindThis - public async renderFollowUser(id: User['id']) { - const user = await this.usersRepository.findOneByOrFail({ id: id }) as PartialLocalUser | PartialRemoteUser; + public async renderFollowUser(id: MiUser['id']): Promise { + const user = await this.usersRepository.findOneByOrFail({ id: id }) as MiPartialLocalUser | MiPartialRemoteUser; return this.userEntityService.getUserUri(user); } @bindThis public renderFollow( - follower: PartialLocalUser | PartialRemoteUser, - followee: PartialLocalUser | PartialRemoteUser, + follower: MiPartialLocalUser | MiPartialRemoteUser, + followee: MiPartialLocalUser | MiPartialRemoteUser, requestId?: string, ): IFollow { return { id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`, type: 'Follow', - actor: this.userEntityService.getUserUri(follower)!, - object: this.userEntityService.getUserUri(followee)!, + actor: this.userEntityService.getUserUri(follower), + object: this.userEntityService.getUserUri(followee), }; } @@ -238,7 +238,7 @@ export class ApRendererService { } @bindThis - public renderImage(file: DriveFile): IApImage { + public renderImage(file: MiDriveFile): IApImage { return { type: 'Image', url: this.driveFileEntityService.getPublicUrl(file), @@ -248,7 +248,7 @@ export class ApRendererService { } @bindThis - public renderKey(user: LocalUser, key: UserKeypair, postfix?: string): IKey { + public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey { return { id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`, type: 'Key', @@ -261,17 +261,17 @@ export class ApRendererService { } @bindThis - public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }): Promise { + public async renderLike(noteReaction: MiNoteReaction, note: { uri: string | null }): Promise { const reaction = noteReaction.reaction; - const object = { + const object: ILike = { type: 'Like', id: `${this.config.url}/likes/${noteReaction.id}`, actor: `${this.config.url}/users/${noteReaction.userId}`, object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`, content: reaction, _misskey_reaction: reaction, - } as ILike; + }; if (reaction.startsWith(':')) { const name = reaction.replaceAll(':', ''); @@ -284,21 +284,21 @@ export class ApRendererService { } @bindThis - public renderMention(mention: PartialLocalUser | PartialRemoteUser): IApMention { + public renderMention(mention: MiPartialLocalUser | MiPartialRemoteUser): IApMention { return { type: 'Mention', - href: this.userEntityService.getUserUri(mention)!, - name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`, + href: this.userEntityService.getUserUri(mention), + name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as MiLocalUser).username}`, }; } @bindThis public renderMove( - src: PartialLocalUser | PartialRemoteUser, - dst: PartialLocalUser | PartialRemoteUser, + src: MiPartialLocalUser | MiPartialRemoteUser, + dst: MiPartialLocalUser | MiPartialRemoteUser, ): IMove { - const actor = this.userEntityService.getUserUri(src)!; - const target = this.userEntityService.getUserUri(dst)!; + const actor = this.userEntityService.getUserUri(src); + const target = this.userEntityService.getUserUri(dst); return { id: `${this.config.url}/moves/${src.id}/${dst.id}`, actor, @@ -309,23 +309,23 @@ export class ApRendererService { } @bindThis - public async renderNote(note: Note, dive = true): Promise { - const getPromisedFiles = async (ids: string[]) => { - if (!ids || ids.length === 0) return []; + public async renderNote(note: MiNote, dive = true): Promise { + const getPromisedFiles = async (ids: string[]): Promise => { + if (ids.length === 0) return []; const items = await this.driveFilesRepository.findBy({ id: In(ids) }); - return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[]; + return ids.map(id => items.find(item => item.id === id)).filter((item): item is MiDriveFile => item != null); }; let inReplyTo; - let inReplyToNote: Note | null; + let inReplyToNote: MiNote | null; if (note.replyId) { inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId }); if (inReplyToNote != null) { - const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId }); + const inReplyToUserExist = await this.usersRepository.exist({ where: { id: inReplyToNote.userId } }); - if (inReplyToUser != null) { + if (inReplyToUserExist) { if (inReplyToNote.uri) { inReplyTo = inReplyToNote.uri; } else { @@ -375,13 +375,13 @@ export class ApRendererService { id: In(note.mentions), }) : []; - const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag)); - const mentionTags = mentionedUsers.map(u => this.renderMention(u as LocalUser | RemoteUser)); + const hashtagTags = note.tags.map(tag => this.renderHashtag(tag)); + const mentionTags = mentionedUsers.map(u => this.renderMention(u as MiLocalUser | MiRemoteUser)); const files = await getPromisedFiles(note.fileIds); const text = note.text ?? ''; - let poll: Poll | null = null; + let poll: MiPoll | null = null; if (note.hasPoll) { poll = await this.pollsRepository.findOneBy({ noteId: note.id }); @@ -449,39 +449,28 @@ export class ApRendererService { } @bindThis - public async renderPerson(user: LocalUser) { + public async renderPerson(user: MiLocalUser) { const id = this.userEntityService.genLocalUserUri(user.id); - const isSystem = !!user.username.match(/\./); + const isSystem = user.username.includes('.'); const [avatar, banner, profile] = await Promise.all([ - user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : Promise.resolve(undefined), - user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : Promise.resolve(undefined), + user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : undefined, + user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : undefined, this.userProfilesRepository.findOneByOrFail({ userId: user.id }), ]); - const attachment: { + const attachment = profile.fields.map(field => ({ type: 'PropertyValue', - name: string, - value: string, - identifier?: IIdentifier, - }[] = []; - - if (profile.fields) { - for (const field of profile.fields) { - attachment.push({ - type: 'PropertyValue', - name: field.name, - value: (field.value != null && field.value.match(/^https?:/)) - ? `${new URL(field.value).href}` - : field.value, - }); - } - } + name: field.name, + value: /^https?:/.test(field.value) + ? `${new URL(field.value).href}` + : field.value, + })); const emojis = await this.getEmojis(user.emojis); const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); - const hashtagTags = (user.tags ?? []).map(tag => this.renderHashtag(tag)); + const hashtagTags = user.tags.map(tag => this.renderHashtag(tag)); const tag = [ ...apemojis, @@ -490,7 +479,7 @@ export class ApRendererService { const keypair = await this.userKeypairService.getUserKeypair(user.id); - const person = { + const person: any = { type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person', id, inbox: `${id}/inbox`, @@ -508,11 +497,11 @@ export class ApRendererService { image: banner ? this.renderImage(banner) : null, tag, manuallyApprovesFollowers: user.isLocked, - discoverable: !!user.isExplorable, + discoverable: user.isExplorable, publicKey: this.renderKey(user, keypair, '#main-key'), isCat: user.isCat, attachment: attachment.length ? attachment : undefined, - } as any; + }; if (user.movedToUri) { person.movedTo = user.movedToUri; @@ -534,7 +523,7 @@ export class ApRendererService { } @bindThis - public renderQuestion(user: { id: User['id'] }, note: Note, poll: Poll): IQuestion { + public renderQuestion(user: { id: MiUser['id'] }, note: MiNote, poll: MiPoll): IQuestion { return { type: 'Question', id: `${this.config.url}/questions/${note.id}`, @@ -552,7 +541,7 @@ export class ApRendererService { } @bindThis - public renderReject(object: any, user: { id: User['id'] }): IReject { + public renderReject(object: string | IObject, user: { id: MiUser['id'] }): IReject { return { type: 'Reject', actor: this.userEntityService.genLocalUserUri(user.id), @@ -561,7 +550,7 @@ export class ApRendererService { } @bindThis - public renderRemove(user: { id: User['id'] }, target: any, object: any): IRemove { + public renderRemove(user: { id: MiUser['id'] }, target: string | IObject | undefined, object: string | IObject): IRemove { return { type: 'Remove', actor: this.userEntityService.genLocalUserUri(user.id), @@ -579,8 +568,8 @@ export class ApRendererService { } @bindThis - 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; + public renderUndo(object: string | IObject, user: { id: MiUser['id'] }): IUndo { + const id = typeof object !== 'string' && typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined; return { type: 'Undo', @@ -592,7 +581,7 @@ export class ApRendererService { } @bindThis - public renderUpdate(object: any, user: { id: User['id'] }): IUpdate { + public renderUpdate(object: string | IObject, user: { id: MiUser['id'] }): IUpdate { return { id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`, actor: this.userEntityService.genLocalUserUri(user.id), @@ -604,7 +593,7 @@ export class ApRendererService { } @bindThis - public renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: RemoteUser): ICreate { + public renderVote(user: { id: MiUser['id'] }, vote: MiPollVote, note: MiNote, poll: MiPoll, pollOwner: MiRemoteUser): ICreate { return { id: `${this.config.url}/users/${user.id}#votes/${vote.id}/activity`, actor: this.userEntityService.genLocalUserUri(user.id), @@ -625,7 +614,7 @@ export class ApRendererService { @bindThis public addContext(x: T): T & { '@context': any; id: string; } { if (typeof x === 'object' && x.id == null) { - x.id = `${this.config.url}/${uuid()}`; + x.id = `${this.config.url}/${randomUUID()}`; } return Object.assign({ @@ -658,11 +647,11 @@ export class ApRendererService { vcard: 'http://www.w3.org/2006/vcard/ns#', }, ], - }, x as T & { id: string; }); + }, x as T & { id: string }); } @bindThis - public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise { + public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise { const keypair = await this.userKeypairService.getUserKeypair(user.id); const ldSignature = this.ldSignatureService.use(); @@ -683,13 +672,13 @@ export class ApRendererService { */ @bindThis public renderOrderedCollectionPage(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) { - const page = { + const page: any = { id, partOf, type: 'OrderedCollectionPage', totalItems, orderedItems, - } as any; + }; if (prev) page.prev = prev; if (next) page.next = next; @@ -706,7 +695,7 @@ export class ApRendererService { * @param orderedItems attached objects (optional) */ @bindThis - public renderOrderedCollection(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: IObject[]) { + public renderOrderedCollection(id: string, totalItems: number, first?: string, last?: string, orderedItems?: IObject[]) { const page: any = { id, type: 'OrderedCollection', @@ -721,8 +710,8 @@ export class ApRendererService { } @bindThis - private async getEmojis(names: string[]): Promise { - if (names == null || names.length === 0) return []; + private async getEmojis(names: string[]): Promise { + if (names.length === 0) return []; const allEmojis = await this.customEmojiService.localEmojisCache.fetch(); const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull); diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 5005612ab8..b59ce5241f 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -1,9 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as crypto from 'node:crypto'; import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import type { User } from '@/models/entities/User.js'; +import type { MiUser } from '@/models/User.js'; import { UserKeypairService } from '@/core/UserKeypairService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { LoggerService } from '@/core/LoggerService.js'; @@ -140,7 +145,7 @@ export class ApRequestService { } @bindThis - public async signedPost(user: { id: User['id'] }, url: string, object: any) { + public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown): Promise { const body = JSON.stringify(object); const keypair = await this.userKeypairService.getUserKeypair(user.id); @@ -169,7 +174,7 @@ export class ApRequestService { * @param url URL to fetch */ @bindThis - public async signedGet(url: string, user: { id: User['id'] }) { + public async signedGet(url: string, user: { id: MiUser['id'] }): Promise { const keypair = await this.userKeypairService.getUserKeypair(user.id); const req = ApRequestCreator.createSignedGet({ diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index d3e0345c9c..9ca63c9ec5 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -1,7 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { LocalUser, RemoteUser } from '@/models/entities/User.js'; +import { IsNull, Not } from 'typeorm'; +import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; -import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js'; +import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; @@ -18,7 +24,7 @@ import type { IObject, ICollection, IOrderedCollection } from './type.js'; export class Resolver { private history: Set; - private user?: LocalUser; + private user?: MiLocalUser; private logger: Logger; constructor( @@ -27,6 +33,7 @@ export class Resolver { private notesRepository: NotesRepository, private pollsRepository: PollsRepository, private noteReactionsRepository: NoteReactionsRepository, + private followRequestsRepository: FollowRequestsRepository, private utilityService: UtilityService, private instanceActorService: InstanceActorService, private metaService: MetaService, @@ -61,10 +68,6 @@ export class Resolver { @bindThis public async resolve(value: string | IObject): Promise { - if (value == null) { - throw new Error('resolvee is null (or undefined)'); - } - if (typeof value !== 'string') { return value; } @@ -104,11 +107,11 @@ export class Resolver { ? await this.apRequestService.signedGet(value, this.user) as IObject : await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject; - if (object == null || ( + if ( Array.isArray(object['@context']) ? !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' - )) { + ) { throw new Error('invalid response'); } @@ -133,7 +136,7 @@ export class Resolver { }); case 'users': return this.usersRepository.findOneByOrFail({ id: parsed.id }) - .then(user => this.apRendererService.renderPerson(user as LocalUser)); + .then(user => this.apRendererService.renderPerson(user as MiLocalUser)); case 'questions': // Polls are indexed by the note they are attached to. return Promise.all([ @@ -145,13 +148,24 @@ export class Resolver { 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'); - - return Promise.all( - [parsed.id, parsed.rest].map(id => this.usersRepository.findOneByOrFail({ id })), - ) - .then(([follower, followee]) => this.apRendererService.addContext(this.apRendererService.renderFollow(follower as LocalUser | RemoteUser, followee as LocalUser | RemoteUser, url))); + return this.followRequestsRepository.findOneBy({ id: parsed.id }) + .then(async followRequest => { + if (followRequest == null) throw new Error('resolveLocal: invalid follow request ID'); + const [follower, followee] = await Promise.all([ + this.usersRepository.findOneBy({ + id: followRequest.followerId, + host: IsNull(), + }), + this.usersRepository.findOneBy({ + id: followRequest.followeeId, + host: Not(IsNull()), + }), + ]); + if (follower == null || followee == null) { + throw new Error('resolveLocal: follower or followee does not exist'); + } + return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url)); + }); default: throw new Error(`resolveLocal: type ${parsed.type} unhandled`); } @@ -176,6 +190,9 @@ export class ApResolverService { @Inject(DI.noteReactionsRepository) private noteReactionsRepository: NoteReactionsRepository, + @Inject(DI.followRequestsRepository) + private followRequestsRepository: FollowRequestsRepository, + private utilityService: UtilityService, private instanceActorService: InstanceActorService, private metaService: MetaService, @@ -195,6 +212,7 @@ export class ApResolverService { this.notesRepository, this.pollsRepository, this.noteReactionsRepository, + this.followRequestsRepository, this.utilityService, this.instanceActorService, this.metaService, diff --git a/packages/backend/src/core/activitypub/LdSignatureService.ts b/packages/backend/src/core/activitypub/LdSignatureService.ts index 20fe2a0a77..39b5ff8abc 100644 --- a/packages/backend/src/core/activitypub/LdSignatureService.ts +++ b/packages/backend/src/core/activitypub/LdSignatureService.ts @@ -1,8 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as crypto from 'node:crypto'; import { Injectable } from '@nestjs/common'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import { CONTEXTS } from './misc/contexts.js'; +import type { JsonLdDocument } from 'jsonld'; +import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js'; // RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017 @@ -18,22 +25,21 @@ class LdSignature { @bindThis public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise { - const options = { - type: 'RsaSignature2017', - creator, - domain, - nonce: crypto.randomBytes(16).toString('hex'), - created: (created ?? new Date()).toISOString(), - } as { + const options: { type: string; creator: string; domain?: string; nonce: string; created: string; + } = { + type: 'RsaSignature2017', + creator, + nonce: crypto.randomBytes(16).toString('hex'), + created: (created ?? new Date()).toISOString(), }; - if (!domain) { - delete options.domain; + if (domain) { + options.domain = domain; } const toBeSigned = await this.createVerifyData(data, options); @@ -62,7 +68,7 @@ class LdSignature { } @bindThis - public async createVerifyData(data: any, options: any) { + public async createVerifyData(data: any, options: any): Promise { const transformedOptions = { ...options, '@context': 'https://w3id.org/identity/v1', @@ -82,7 +88,7 @@ class LdSignature { } @bindThis - public async normalize(data: any) { + public async normalize(data: JsonLdDocument): Promise { const customLoader = this.getLoader(); // XXX: Importing jsonld dynamically since Jest frequently fails to import it statically // https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595 @@ -93,14 +99,14 @@ class LdSignature { @bindThis private getLoader() { - return async (url: string): Promise => { - if (!url.match('^https?\:\/\/')) throw new Error(`Invalid URL ${url}`); + return async (url: string): Promise => { + if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`); if (this.preLoad) { if (url in CONTEXTS) { if (this.debug) console.debug(`HIT: ${url}`); return { - contextUrl: null, + contextUrl: undefined, document: CONTEXTS[url], documentUrl: url, }; @@ -110,7 +116,7 @@ class LdSignature { if (this.debug) console.debug(`MISS: ${url}`); const document = await this.fetchDocument(url); return { - contextUrl: null, + contextUrl: undefined, document: document, documentUrl: url, }; @@ -118,13 +124,17 @@ class LdSignature { } @bindThis - private async fetchDocument(url: string) { - const json = await this.httpRequestService.send(url, { - headers: { - Accept: 'application/ld+json, application/json', + private async fetchDocument(url: string): Promise { + const json = await this.httpRequestService.send( + url, + { + headers: { + Accept: 'application/ld+json, application/json', + }, + timeout: this.loderTimeout, }, - timeout: this.loderTimeout, - }, { throwErrorWhenResponseNotOk: false }).then(res => { + { throwErrorWhenResponseNotOk: false }, + ).then(res => { if (!res.ok) { throw new Error(`${res.status} ${res.statusText}`); } else { @@ -132,7 +142,7 @@ class LdSignature { } }); - return json; + return json as JsonLd; } @bindThis diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts index aee0d3629c..71c440e5cc 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -1,3 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { JsonLd } from 'jsonld/jsonld-spec.js'; + /* eslint:disable:quotemark indent */ const id_v1 = { '@context': { @@ -86,7 +93,7 @@ const id_v1 = { 'accessControl': { '@id': 'perm:accessControl', '@type': '@id' }, 'writePermission': { '@id': 'perm:writePermission', '@type': '@id' }, }, -}; +} satisfies JsonLd; const security_v1 = { '@context': { @@ -137,7 +144,7 @@ const security_v1 = { 'signatureAlgorithm': 'sec:signingAlgorithm', 'signatureValue': 'sec:signatureValue', }, -}; +} satisfies JsonLd; const activitystreams = { '@context': { @@ -517,9 +524,9 @@ const activitystreams = { '@type': '@id', }, }, -}; +} satisfies JsonLd; -export const CONTEXTS: Record = { +export const CONTEXTS: Record = { 'https://w3id.org/identity/v1': id_v1, 'https://w3id.org/security/v1': security_v1, 'https://www.w3.org/ns/activitystreams': activitystreams, diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index 0043907c21..a4cd533892 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -1,27 +1,29 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; -import type { RemoteUser } from '@/models/entities/User.js'; -import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { DriveFilesRepository } from '@/models/_.js'; +import type { MiRemoteUser } from '@/models/User.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; import { MetaService } from '@/core/MetaService.js'; import { truncate } from '@/misc/truncate.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; import { DriveService } from '@/core/DriveService.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; +import { checkHttps } from '@/misc/check-https.js'; import { ApResolverService } from '../ApResolverService.js'; import { ApLoggerService } from '../ApLoggerService.js'; -import { checkHttps } from '@/misc/check-https.js'; +import type { IObject } from '../type.js'; @Injectable() export class ApImageService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -32,21 +34,25 @@ export class ApImageService { ) { this.logger = this.apLoggerService.logger; } - + /** * Imageを作成します。 */ @bindThis - public async createImage(actor: RemoteUser, value: any): Promise { + public async createImage(actor: MiRemoteUser, value: string | IObject): Promise { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { throw new Error('actor has been suspended'); } - const image = await this.apResolverService.createResolver().resolve(value) as any; + const image = await this.apResolverService.createResolver().resolve(value); if (image.url == null) { - throw new Error('invalid image: url not privided'); + throw new Error('invalid image: url not provided'); + } + + if (typeof image.url !== 'string') { + throw new Error('invalid image: unexpected type of url: ' + JSON.stringify(image.url, null, 2)); } if (!checkHttps(image.url)) { @@ -57,29 +63,24 @@ export class ApImageService { const instance = await this.metaService.fetch(); - let file = await this.driveService.uploadFromUrl({ + // Cache if remote file cache is on AND either + // 1. remote sensitive file is also on + // 2. or the image is not sensitive + const shouldBeCached = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive); + + const file = await this.driveService.uploadFromUrl({ url: image.url, user: actor, uri: image.url, sensitive: image.sensitive, - isLink: !instance.cacheRemoteFiles, - comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH), + isLink: !shouldBeCached, + comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH), }); + if (!file.isLink || file.url === image.url) return file; - if (file.isLink) { - // URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、 - // URLを更新する - if (file.url !== image.url) { - await this.driveFilesRepository.update({ id: file.id }, { - url: image.url, - uri: image.url, - }); - - file = await this.driveFilesRepository.findOneByOrFail({ id: file.id }); - } - } - - return file; + // URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、URLを更新する + await this.driveFilesRepository.update({ id: file.id }, { url: image.url, uri: image.url }); + return await this.driveFilesRepository.findOneByOrFail({ id: file.id }); } /** @@ -89,7 +90,7 @@ export class ApImageService { * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ @bindThis - public async resolveImage(actor: RemoteUser, value: any): Promise { + public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise { // TODO // リモートサーバーからフェッチしてきて登録 diff --git a/packages/backend/src/core/activitypub/models/ApMentionService.ts b/packages/backend/src/core/activitypub/models/ApMentionService.ts index c581840ca9..9aa8ba5ede 100644 --- a/packages/backend/src/core/activitypub/models/ApMentionService.ts +++ b/packages/backend/src/core/activitypub/models/ApMentionService.ts @@ -1,38 +1,37 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; -import { DI } from '@/di-symbols.js'; -import type { User } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { MiUser } from '@/models/_.js'; import { toArray, unique } from '@/misc/prelude/array.js'; import { bindThis } from '@/decorators.js'; import { isMention } from '../type.js'; -import { ApResolverService, Resolver } from '../ApResolverService.js'; +import { Resolver } from '../ApResolverService.js'; import { ApPersonService } from './ApPersonService.js'; import type { IObject, IApMention } from '../type.js'; @Injectable() export class ApMentionService { constructor( - @Inject(DI.config) - private config: Config, - - private apResolverService: ApResolverService, private apPersonService: ApPersonService, ) { } @bindThis - public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver) { - const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href as string)); + public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver): Promise { + const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href)); - 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 User => x != null); - + )).filter((x): x is MiUser => x != null); + return mentionedUsers; } - + @bindThis public extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] { if (tags == null) return []; diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 76757f530a..573dff5b91 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -1,16 +1,21 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { forwardRef, Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { PollsRepository, EmojisRepository } from '@/models/index.js'; +import type { PollsRepository, EmojisRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; -import type { RemoteUser } from '@/models/entities/User.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { MiRemoteUser } from '@/models/User.js'; +import type { MiNote } from '@/models/Note.js'; import { toArray, toSingle, unique } from '@/misc/prelude/array.js'; -import type { Emoji } from '@/models/entities/Emoji.js'; +import type { MiEmoji } from '@/models/Emoji.js'; import { MetaService } from '@/core/MetaService.js'; import { AppLockService } from '@/core/AppLockService.js'; -import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; import type Logger from '@/logger.js'; import { IdService } from '@/core/IdService.js'; @@ -20,7 +25,6 @@ import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; -// eslint-disable-next-line @typescript-eslint/consistent-type-imports import { ApLoggerService } from '../ApLoggerService.js'; import { ApMfmService } from '../ApMfmService.js'; import { ApDbResolverService } from '../ApDbResolverService.js'; @@ -55,7 +59,7 @@ export class ApNoteService { // 循環参照のため / for circular dependency @Inject(forwardRef(() => ApPersonService)) private apPersonService: ApPersonService, - + private utilityService: UtilityService, private apAudienceService: ApAudienceService, private apMentionService: ApMentionService, @@ -72,17 +76,13 @@ export class ApNoteService { } @bindThis - public validateNote(object: IObject, uri: string) { + public validateNote(object: IObject, uri: string): Error | null { const expectHost = this.utilityService.extractDbHost(uri); - - if (object == null) { - return new Error('invalid Note: object is null'); - } - + if (!validPost.includes(getApType(object))) { return new Error(`invalid Note: invalid object type ${getApType(object)}`); } - + if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) { return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); } @@ -91,70 +91,73 @@ export class ApNoteService { if (object.attributedTo && actualHost !== expectHost) { return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); } - + return null; } - + /** * Noteをフェッチします。 * * Misskeyに対象のNoteが登録されていればそれを返します。 */ @bindThis - public async fetchNote(object: string | IObject): Promise { + public async fetchNote(object: string | IObject): Promise { return await this.apDbResolverService.getNoteFromApId(object); } - + /** * Noteを作成します。 */ @bindThis - public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { + public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { + // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); - + const object = await resolver.resolve(value); - + const entryUri = getApId(value); const err = this.validateNote(object, entryUri); if (err) { - this.logger.error(`${err.message}`, { - resolver: { - history: resolver.getHistory(), - }, - value: value, - object: object, + this.logger.error(err.message, { + resolver: { history: resolver.getHistory() }, + value, + object, }); throw new Error('invalid note'); } - + const note = object as IPost; - + this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); if (note.id && !checkHttps(note.id)) { - throw new Error('unexpected shcema of note.id: ' + note.id); + throw new Error('unexpected schema of note.id: ' + note.id); } const url = getOneApHrefNullable(note.url); if (url && !checkHttps(url)) { - throw new Error('unexpected shcema of note url: ' + url); + throw new Error('unexpected schema of note url: ' + url); } - + this.logger.info(`Creating the Note: ${note.id}`); - + // 投稿者をフェッチ - const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo!), resolver) as RemoteUser; - + if (note.attributedTo == null) { + throw new Error('invalid note.attributedTo: ' + note.attributedTo); + } + + const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as MiRemoteUser; + // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { throw new Error('actor has been suspended'); } - + const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); let visibility = noteAudience.visibility; const visibleUsers = noteAudience.visibleUsers; - + // Audience (to, cc) が指定されてなかった場合 if (visibility === 'specified' && visibleUsers.length === 0) { if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している @@ -162,81 +165,71 @@ export class ApNoteService { visibility = 'public'; } } - + const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); - const apHashtags = await extractApHashtags(note.tag); - + const apHashtags = extractApHashtags(note.tag); + // 添付ファイル // TODO: attachmentは必ずしもImageではない // TODO: attachmentは必ずしも配列ではない - // Noteがsensitiveなら添付もsensitiveにする - const limit = promiseLimit(2); - - note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : []; - const files = note.attachment - .map(attach => attach.sensitive = note.sensitive) - ? (await Promise.all(note.attachment.map(x => limit(() => this.apImageService.resolveImage(actor, x)) as Promise))) - .filter(image => image != null) - : []; - + const limit = promiseLimit(2); + const files = (await Promise.all(toArray(note.attachment).map(attach => ( + limit(() => this.apImageService.resolveImage(actor, { + ...attach, + sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする + })) + )))); + // リプライ - const reply: Note | null = note.inReplyTo - ? await this.resolveNote(note.inReplyTo, resolver).then(x => { - if (x == null) { - this.logger.warn('Specified inReplyTo, but not found'); - throw new Error('inReplyTo not found'); - } else { + const reply: MiNote | null = note.inReplyTo + ? await this.resolveNote(note.inReplyTo, { resolver }) + .then(x => { + if (x == null) { + this.logger.warn('Specified inReplyTo, but not found'); + throw new Error('inReplyTo not found'); + } + return x; - } - }).catch(async err => { - this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`); - throw err; - }) + }) + .catch(async err => { + this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`); + throw err; + }) : null; - + // 引用 - let quote: Note | undefined | null; - - if (note._misskey_quote || note.quoteUrl) { - const tryResolveNote = async (uri: string): Promise<{ - status: 'ok'; - res: Note | null; - } | { - status: 'permerror' | 'temperror'; - }> => { - if (typeof uri !== 'string' || !uri.match(/^https?:/)) return { status: 'permerror' }; + let quote: MiNote | undefined | null = null; + + if (note._misskey_quote ?? note.quoteUrl) { + const tryResolveNote = async (uri: string): Promise< + | { status: 'ok'; res: MiNote } + | { status: 'permerror' | 'temperror' } + > => { + if (!/^https?:/.test(uri)) return { status: 'permerror' }; try { const res = await this.resolveNote(uri); - if (res) { - return { - status: 'ok', - res, - }; - } else { - return { - status: 'permerror', - }; - } + if (res == null) return { status: 'permerror' }; + return { status: 'ok', res }; } catch (e) { return { status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror', }; } }; - + const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string')); - const results = await Promise.all(uris.map(uri => tryResolveNote(uri))); - - quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x); + const results = await Promise.all(uris.map(tryResolveNote)); + + quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { throw new Error('quote resolve failed'); } } } - + const cw = note.summary === '' ? null : note.summary; - + // テキストのパース let text: string | null = null; if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { @@ -246,58 +239,70 @@ export class ApNoteService { } else if (typeof note.content === 'string') { text = this.apMfmService.htmlToMfm(note.content, note.tag); } - + // vote if (reply && reply.hasPoll) { const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); - + const tryCreateVote = async (name: string, index: number): Promise => { if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) { this.logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); } else if (index >= 0) { this.logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); await this.pollService.vote(actor, reply, index); - + // リモートフォロワーにUpdate配信 this.pollService.deliverQuestionUpdate(reply.id); } return null; }; - + if (note.name) { return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name)); } } - + const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => { this.logger.info(`extractEmojis: ${e}`); - return [] as Emoji[]; + return []; }); - + const apEmojis = emojis.map(emoji => emoji.name); - + const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); - - return await this.noteCreateService.create(actor, { - createdAt: note.published ? new Date(note.published) : null, - files, - reply, - renote: quote, - name: note.name, - cw, - text, - localOnly: false, - visibility, - visibleUsers, - apMentions, - apHashtags, - apEmojis, - poll, - uri: note.id, - url: url, - }, silent); + + try { + return await this.noteCreateService.create(actor, { + createdAt: note.published ? new Date(note.published) : null, + files, + reply, + renote: quote, + name: note.name, + cw, + text, + localOnly: false, + visibility, + visibleUsers, + apMentions, + apHashtags, + apEmojis, + poll, + uri: note.id, + url: url, + }, silent); + } catch (err: any) { + if (err.name !== 'duplicated') { + throw err; + } + this.logger.info('The note is already inserted while creating itself, reading again'); + const duplicate = await this.fetchNote(value); + if (!duplicate) { + throw new Error('The note creation failed with duplication error even when there is no duplication'); + } + return duplicate; + } } - + /** * Noteを解決します。 * @@ -305,94 +310,91 @@ export class ApNoteService { * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ @bindThis - public async resolveNote(value: string | IObject, resolver?: Resolver): Promise { - const uri = typeof value === 'string' ? value : value.id; - if (uri == null) throw new Error('missing uri'); - - // ブロックしてたら中断 + public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise { + const uri = getApId(value); + + // ブロックしていたら中断 const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw new StatusError('blocked host', 451); - + if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) { + throw new StatusError('blocked host', 451); + } + const unlock = await this.appLockService.getApLock(uri); - + try { //#region このサーバーに既に登録されていたらそれを返す const exist = await this.fetchNote(uri); - - if (exist) { - return exist; - } + if (exist) return exist; //#endregion - + if (uri.startsWith(this.config.url)) { throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note'); } - + // リモートサーバーからフェッチしてきて登録 // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 - return await this.createNote(uri, resolver, true); + const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri; + return await this.createNote(createFrom, options.resolver, true); } finally { unlock(); } } - + @bindThis - public async extractEmojis(tags: IObject | IObject[], host: string): Promise { + public async extractEmojis(tags: IObject | IObject[], host: string): Promise { + // eslint-disable-next-line no-param-reassign host = this.utilityService.toPuny(host); - - if (!tags) return []; - + const eomjiTags = toArray(tags).filter(isEmoji); const existingEmojis = await this.emojisRepository.findBy({ host, - name: In(eomjiTags.map(tag => tag.name!.replaceAll(':', ''))), + name: In(eomjiTags.map(tag => tag.name.replaceAll(':', ''))), }); - + return await Promise.all(eomjiTags.map(async tag => { - const name = tag.name!.replaceAll(':', ''); + const name = tag.name.replaceAll(':', ''); tag.icon = toSingle(tag.icon); - + const exists = existingEmojis.find(x => x.name === name); - + if (exists) { - if ((tag.updated != null && exists.updatedAt == null) + if ((exists.updatedAt == null) || (tag.id != null && exists.uri == null) - || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt) - || (tag.icon!.url !== exists.originalUrl) + || (new Date(tag.updated) > exists.updatedAt) + || (tag.icon.url !== exists.originalUrl) ) { await this.emojisRepository.update({ host, name, }, { uri: tag.id, - originalUrl: tag.icon!.url, - publicUrl: tag.icon!.url, + originalUrl: tag.icon.url, + publicUrl: tag.icon.url, updatedAt: new Date(), }); - - return await this.emojisRepository.findOneBy({ - host, - name, - }) as Emoji; + + const emoji = await this.emojisRepository.findOneBy({ host, name }); + if (emoji == null) throw new Error('emoji update failed'); + return emoji; } - + return exists; } - + this.logger.info(`register emoji host=${host}, name=${name}`); - + return await this.emojisRepository.insert({ id: this.idService.genId(), host, name, uri: tag.id, - originalUrl: tag.icon!.url, - publicUrl: tag.icon!.url, + originalUrl: tag.icon.url, + publicUrl: tag.icon.url, updatedAt: new Date(), aliases: [], - } as Partial).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); + }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); })); } } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index f52ebed107..ea64883395 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -1,31 +1,35 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + 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 { BlockingsRepository, MutingsRepository, FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; +import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; -import type { LocalUser, RemoteUser } from '@/models/entities/User.js'; -import { User } from '@/models/entities/User.js'; +import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; +import { MiUser } from '@/models/User.js'; import { truncate } from '@/misc/truncate.js'; import type { CacheService } from '@/core/CacheService.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import type Logger from '@/logger.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { MiNote } from '@/models/Note.js'; import type { IdService } from '@/core/IdService.js'; import type { MfmService } from '@/core/MfmService.js'; -import type { Emoji } from '@/models/entities/Emoji.js'; import { toArray } from '@/misc/prelude/array.js'; import type { GlobalEventService } from '@/core/GlobalEventService.js'; import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import type { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; -import { UserProfile } from '@/models/entities/UserProfile.js'; -import { UserPublickey } from '@/models/entities/UserPublickey.js'; +import { MiUserProfile } from '@/models/UserProfile.js'; +import { MiUserPublickey } from '@/models/UserPublickey.js'; import type UsersChart from '@/core/chart/charts/users.js'; import type InstanceChart from '@/core/chart/charts/instance.js'; import type { HashtagService } from '@/core/HashtagService.js'; -import { UserNotePining } from '@/models/entities/UserNotePining.js'; +import { MiUserNotePining } from '@/models/UserNotePining.js'; import { StatusError } from '@/misc/status-error.js'; import type { UtilityService } from '@/core/UtilityService.js'; import type { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -48,6 +52,8 @@ import type { IActor, IObject } from '../type.js'; const nameLength = 128; const summaryLength = 2048; +type Field = Record<'name' | 'value', string>; + @Injectable() export class ApPersonService implements OnModuleInit { private utilityService: UtilityService; @@ -94,28 +100,10 @@ export class ApPersonService implements OnModuleInit { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - - //private utilityService: UtilityService, - //private userEntityService: UserEntityService, - //private idService: IdService, - //private globalEventService: GlobalEventService, - //private metaService: MetaService, - //private federatedInstanceService: FederatedInstanceService, - //private fetchInstanceMetadataService: FetchInstanceMetadataService, - //private cacheService: CacheService, - //private apResolverService: ApResolverService, - //private apNoteService: ApNoteService, - //private apImageService: ApImageService, - //private apMfmService: ApMfmService, - //private mfmService: MfmService, - //private hashtagService: HashtagService, - //private usersChart: UsersChart, - //private instanceChart: InstanceChart, - //private apLoggerService: ApLoggerService, ) { } - onModuleInit() { + onModuleInit(): void { this.utilityService = this.moduleRef.get('UtilityService'); this.userEntityService = this.moduleRef.get('UserEntityService'); this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); @@ -153,10 +141,6 @@ export class ApPersonService implements OnModuleInit { private validateActor(x: IObject, uri: string): IActor { const expectHost = this.punyHost(uri); - if (x == null) { - throw new Error('invalid Actor: object is null'); - } - if (!isActor(x)) { throw new Error(`invalid Actor type '${x.type}'`); } @@ -217,22 +201,20 @@ export class ApPersonService implements OnModuleInit { * Misskeyに対象のPersonが登録されていればそれを返し、登録がなければnullを返します。 */ @bindThis - public async fetchPerson(uri: string): Promise { - if (typeof uri !== 'string') throw new Error('uri is not string'); - - const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null; + public async fetchPerson(uri: string): Promise { + const cached = this.cacheService.uriPersonCache.get(uri) as MiLocalUser | MiRemoteUser | null | undefined; if (cached) return cached; // URIがこのサーバーを指しているならデータベースからフェッチ if (uri.startsWith(`${this.config.url}/`)) { const id = uri.split('/').pop(); - const u = await this.usersRepository.findOneBy({ id }) as LocalUser; + const u = await this.usersRepository.findOneBy({ id }) as MiLocalUser | null; if (u) this.cacheService.uriPersonCache.set(uri, u); return u; } //#region このサーバーに既に登録されていたらそれを返す - const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser; + const exist = await this.usersRepository.findOneBy({ uri }) as MiLocalUser | MiRemoteUser | null; if (exist) { this.cacheService.uriPersonCache.set(uri, exist); @@ -243,20 +225,39 @@ export class ApPersonService implements OnModuleInit { return null; } + private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise> { + const [avatar, banner] = await Promise.all([icon, image].map(img => { + if (img == null) return null; + if (user == null) throw new Error('failed to create user: user is null'); + return this.apImageService.resolveImage(user, img).catch(() => null); + })); + + return { + avatarId: avatar?.id ?? null, + bannerId: banner?.id ?? null, + avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null, + bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null, + avatarBlurhash: avatar?.blurhash ?? null, + bannerBlurhash: banner?.blurhash ?? null, + }; + } + /** * Personを作成します。 */ @bindThis - public async createPerson(uri: string, resolver?: Resolver): Promise { + public async createPerson(uri: string, resolver?: Resolver): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); if (uri.startsWith(this.config.url)) { throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user'); } + // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); - const object = await resolver.resolve(uri) as any; + const object = await resolver.resolve(uri); + if (object.id == null) throw new Error('invalid object.id: ' + object.id); const person = this.validateActor(object, uri); @@ -264,9 +265,9 @@ export class ApPersonService implements OnModuleInit { const host = this.punyHost(object.id); - const { fields } = this.analyzeAttachments(person.attachment ?? []); + const fields = this.analyzeAttachments(person.attachment ?? []); - const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32); + const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32); const isBot = getApType(object) === 'Service'; @@ -279,47 +280,58 @@ export class ApPersonService implements OnModuleInit { } // Create user - let user: RemoteUser; + let user: MiRemoteUser | null = null; + + //#region カスタム絵文字取得 + const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host) + .then(_emojis => _emojis.map(emoji => emoji.name)) + .catch(err => { + this.logger.error('error occurred while fetching user emojis', { stack: err }); + return []; + }); + //#endregion + try { - // Start transaction + // Start transaction await this.db.transaction(async transactionalEntityManager => { - user = await transactionalEntityManager.save(new User({ + user = await transactionalEntityManager.save(new MiUser({ id: this.idService.genId(), avatarId: null, bannerId: null, createdAt: new Date(), lastFetchedAt: new Date(), name: truncate(person.name, nameLength), - isLocked: !!person.manuallyApprovesFollowers, + isLocked: person.manuallyApprovesFollowers, movedToUri: person.movedTo, movedAt: person.movedTo ? new Date() : null, alsoKnownAs: person.alsoKnownAs, - isExplorable: !!person.discoverable, + isExplorable: person.discoverable, username: person.preferredUsername, - usernameLower: person.preferredUsername!.toLowerCase(), + usernameLower: person.preferredUsername?.toLowerCase(), host, inbox: person.inbox, - sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined), + sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox, followersUri: person.followers ? getApId(person.followers) : undefined, featured: person.featured ? getApId(person.featured) : undefined, uri: person.id, tags, isBot, isCat: (person as any).isCat === true, - })) as RemoteUser; + emojis, + })) as MiRemoteUser; - await transactionalEntityManager.save(new UserProfile({ + await transactionalEntityManager.save(new MiUserProfile({ userId: user.id, description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, - url: url, + url, fields, - birthday: bday ? bday[0] : null, + birthday: bday?.[0] ?? null, location: person['vcard:Address'] ?? null, userHost: host, })); if (person.publicKey) { - await transactionalEntityManager.save(new UserPublickey({ + await transactionalEntityManager.save(new MiUserPublickey({ userId: user.id, keyId: person.publicKey.id, keyPem: person.publicKey.publicKeyPem, @@ -327,24 +339,24 @@ export class ApPersonService implements OnModuleInit { } }); } catch (e) { - // duplicate key error + // duplicate key error if (isDuplicateKeyValueError(e)) { - // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 - const u = await this.usersRepository.findOneBy({ - uri: person.id, - }); + // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 + const u = await this.usersRepository.findOneBy({ uri: person.id }); + if (u == null) throw new Error('already registered'); - if (u) { - user = u as RemoteUser; - } else { - throw new Error('already registered'); - } + user = u as MiRemoteUser; } else { this.logger.error(e instanceof Error ? e : new Error(e as string)); throw e; } } + if (user == null) throw new Error('failed to create user: user is null'); + + // Register to the cache + this.cacheService.uriPersonCache.set(user.uri, user); + // Register host this.federatedInstanceService.fetch(host).then(async i => { this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); @@ -354,68 +366,34 @@ export class ApPersonService implements OnModuleInit { } }); - this.usersChart.update(user!, true); + this.usersChart.update(user, true); // ハッシュタグ更新 - this.hashtagService.updateUsertags(user!, tags); + this.hashtagService.updateUsertags(user, tags); //#region アバターとヘッダー画像をフェッチ - const [avatar, banner] = await Promise.all([ - person.icon, - person.image, - ].map(img => - img == null - ? Promise.resolve(null) - : this.apImageService.resolveImage(user!, img).catch(() => null), - )); + try { + const updates = await this.resolveAvatarAndBanner(user, person.icon, person.image); + await this.usersRepository.update(user.id, updates); + user = { ...user, ...updates }; - const avatarId = avatar ? avatar.id : null; - const bannerId = banner ? banner.id : null; - const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null; - const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null; - const avatarBlurhash = avatar ? avatar.blurhash : null; - const bannerBlurhash = banner ? banner.blurhash : null; - - await this.usersRepository.update(user!.id, { - avatarId, - bannerId, - avatarUrl, - bannerUrl, - avatarBlurhash, - bannerBlurhash, - }); - - user!.avatarId = avatarId; - user!.bannerId = bannerId; - user!.avatarUrl = avatarUrl; - user!.bannerUrl = bannerUrl; - user!.avatarBlurhash = avatarBlurhash; - user!.bannerBlurhash = bannerBlurhash; + // Register to the cache + this.cacheService.uriPersonCache.set(user.uri, user); + } catch (err) { + this.logger.error('error occurred while fetching user avatar/banner', { stack: err }); + } //#endregion - //#region カスタム絵文字取得 - const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => { - this.logger.info(`extractEmojis: ${err}`); - return [] as Emoji[]; - }); + await this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err)); - const emojiNames = emojis.map(emoji => emoji.name); - - await this.usersRepository.update(user!.id, { - emojis: emojiNames, - }); - //#endregion - - await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err)); - - return user!; + return user; } /** * Personの情報を更新します。 * Misskeyに対象のPersonが登録されていなければ無視します。 * もしアカウントの移行が確認された場合、アカウント移行処理を行います。 - * + * * @param uri URI of Person * @param resolver Resolver * @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します) @@ -426,18 +404,14 @@ export class ApPersonService implements OnModuleInit { if (typeof uri !== 'string') throw new Error('uri is not string'); // URIがこのサーバーを指しているならスキップ - if (uri.startsWith(`${this.config.url}/`)) { - return; - } + if (uri.startsWith(`${this.config.url}/`)) return; //#region このサーバーに既に登録されているか - const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null; - - if (exist === null) { - return; - } + const exist = await this.fetchPerson(uri) as MiRemoteUser | null; + if (exist === null) return; //#endregion + // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); const object = hint ?? await resolver.resolve(uri); @@ -446,27 +420,17 @@ export class ApPersonService implements OnModuleInit { this.logger.info(`Updating the Person: ${person.id}`); - // アバターとヘッダー画像をフェッチ - const [avatar, banner] = await Promise.all([ - person.icon, - person.image, - ].map(img => - img == null - ? Promise.resolve(null) - : this.apImageService.resolveImage(exist, img).catch(() => null), - )); - // カスタム絵文字取得 const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => { this.logger.info(`extractEmojis: ${e}`); - return [] as Emoji[]; + return []; }); const emojiNames = emojis.map(emoji => emoji.name); - const { fields } = this.analyzeAttachments(person.attachment ?? []); + const fields = this.analyzeAttachments(person.attachment ?? []); - const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32); + const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32); const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); @@ -479,7 +443,7 @@ export class ApPersonService implements OnModuleInit { const updates = { lastFetchedAt: new Date(), inbox: person.inbox, - sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined), + sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox, followersUri: person.followers ? getApId(person.followers) : undefined, featured: person.featured, emojis: emojiNames, @@ -487,33 +451,33 @@ export class ApPersonService implements OnModuleInit { tags, isBot: getApType(object) === 'Service', isCat: (person as any).isCat === true, - isLocked: !!person.manuallyApprovesFollowers, + isLocked: person.manuallyApprovesFollowers, movedToUri: person.movedTo ?? null, alsoKnownAs: person.alsoKnownAs ?? null, - isExplorable: !!person.discoverable, - } as Partial & Pick; + isExplorable: person.discoverable, + ...(await this.resolveAvatarAndBanner(exist, person.icon, person.image).catch(() => ({}))), + } as Partial & Pick; - const moving = + const moving = ((): boolean => { // 移行先がない→ある - (!exist.movedToUri && updates.movedToUri) || + if ( + exist.movedToUri === null && + updates.movedToUri + ) return true; + // 移行先がある→別のもの - (exist.movedToUri !== updates.movedToUri && exist.movedToUri && updates.movedToUri); + if ( + exist.movedToUri !== null && + updates.movedToUri !== null && + exist.movedToUri !== updates.movedToUri + ) return true; + // 移行先がある→ない、ない→ないは無視 + return false; + })(); if (moving) updates.movedAt = new Date(); - if (avatar) { - updates.avatarId = avatar.id; - updates.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar'); - updates.avatarBlurhash = avatar.blurhash; - } - - if (banner) { - updates.bannerId = banner.id; - updates.bannerUrl = this.driveFileEntityService.getPublicUrl(banner); - updates.bannerBlurhash = banner.blurhash; - } - // Update user await this.usersRepository.update(exist.id, updates); @@ -525,10 +489,10 @@ export class ApPersonService implements OnModuleInit { } await this.userProfilesRepository.update({ userId: exist.id }, { - url: url, + url, fields, description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, - birthday: bday ? bday[0] : null, + birthday: bday?.[0] ?? null, location: person['vcard:Address'] ?? null, }); @@ -538,11 +502,10 @@ export class ApPersonService implements OnModuleInit { this.hashtagService.updateUsertags(exist, tags); // 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする - await this.followingsRepository.update({ - followerId: exist.id, - }, { - followerSharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined), - }); + await this.followingsRepository.update( + { followerId: exist.id }, + { followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox }, + ); await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err)); @@ -579,28 +542,23 @@ export class ApPersonService implements OnModuleInit { * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ @bindThis - public async resolvePerson(uri: string, resolver?: Resolver): Promise { - if (typeof uri !== 'string') throw new Error('uri is not string'); - + public async resolvePerson(uri: string, resolver?: Resolver): Promise { //#region このサーバーに既に登録されていたらそれを返す const exist = await this.fetchPerson(uri); - - if (exist) { - return exist; - } + if (exist) return exist; //#endregion // リモートサーバーからフェッチしてきて登録 + // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); return await this.createPerson(uri, resolver); } @bindThis - public analyzeAttachments(attachments: IObject | IObject[] | undefined) { - const fields: { - name: string, - value: string - }[] = []; + // TODO: `attachments`が`IObject`だった場合、返り値が`[]`になるようだが構わないのか? + public analyzeAttachments(attachments: IObject | IObject[] | undefined): Field[] { + const fields: Field[] = []; + if (Array.isArray(attachments)) { for (const attachment of attachments.filter(isPropertyValue)) { fields.push({ @@ -610,11 +568,11 @@ export class ApPersonService implements OnModuleInit { } } - return { fields }; + return fields; } @bindThis - public async updateFeatured(userId: User['id'], resolver?: Resolver) { + public async updateFeatured(userId: MiUser['id'], resolver?: Resolver): Promise { const user = await this.usersRepository.findOneByOrFail({ id: userId }); if (!this.userEntityService.isRemoteUser(user)) return; if (!user.featured) return; @@ -632,24 +590,27 @@ export class ApPersonService implements OnModuleInit { const items = await Promise.all(toArray(unresolvedItems).map(x => _resolver.resolve(x))); // Resolve and regist Notes - const limit = promiseLimit(2); + const limit = promiseLimit(2); const featuredNotes = await Promise.all(items .filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも .slice(0, 5) - .map(item => limit(() => this.apNoteService.resolveNote(item, _resolver)))); + .map(item => limit(() => this.apNoteService.resolveNote(item, { + resolver: _resolver, + sentFrom: new URL(user.uri), + })))); await this.db.transaction(async transactionalEntityManager => { - await transactionalEntityManager.delete(UserNotePining, { userId: user.id }); + await transactionalEntityManager.delete(MiUserNotePining, { userId: user.id }); // とりあえずidを別の時間で生成して順番を維持 let td = 0; - for (const note of featuredNotes.filter(note => note != null)) { + for (const note of featuredNotes.filter((note): note is MiNote => note != null)) { td -= 1000; - transactionalEntityManager.insert(UserNotePining, { + transactionalEntityManager.insert(MiUserNotePining, { id: this.idService.genId(new Date(Date.now() + td)), createdAt: new Date(), userId: user.id, - noteId: note!.id, + noteId: note.id, }); } }); @@ -661,7 +622,7 @@ export class ApPersonService implements OnModuleInit { * @param movePreventUris ここに列挙されたURIにsrc.movedToUriが含まれる場合、移行処理はしない(無限ループ防止) */ @bindThis - private async processRemoteMove(src: RemoteUser, movePreventUris: string[] = []): Promise { + private async processRemoteMove(src: MiRemoteUser, movePreventUris: string[] = []): Promise { if (!src.movedToUri) return 'skip: no movedToUri'; if (src.uri === src.movedToUri) return 'skip: movedTo itself (src)'; // ??? if (movePreventUris.length > 10) return 'skip: too many moves'; @@ -671,7 +632,7 @@ export class ApPersonService implements OnModuleInit { if (dst && this.userEntityService.isLocalUser(dst)) { // targetがローカルユーザーだった場合データベースから引っ張ってくる - dst = await this.usersRepository.findOneByOrFail({ uri: src.movedToUri }) as LocalUser; + dst = await this.usersRepository.findOneByOrFail({ uri: src.movedToUri }) as MiLocalUser; } else if (dst) { if (movePreventUris.includes(src.movedToUri)) return 'skip: circular move'; @@ -688,7 +649,7 @@ export class ApPersonService implements OnModuleInit { // (uriが存在しなかったり応答がなかったりする場合resolvePersonはthrow Errorする) dst = await this.resolvePerson(src.movedToUri); } - + if (dst.movedToUri === dst.uri) return 'skip: movedTo itself (dst)'; // ??? if (src.movedToUri !== dst.uri) return 'skip: missmatch uri'; // ??? if (dst.movedToUri === src.uri) return 'skip: dst.movedToUri === src.uri'; diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts index 13a2f0fa5c..27bd62268b 100644 --- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts @@ -1,15 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, PollsRepository } from '@/models/index.js'; +import type { NotesRepository, PollsRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; -import type { IPoll } from '@/models/entities/Poll.js'; +import type { IPoll } from '@/models/Poll.js'; import type Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; import { isQuestion } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApResolverService } from '../ApResolverService.js'; import type { Resolver } from '../ApResolverService.js'; import type { IObject, IQuestion } from '../type.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ApQuestionService { @@ -33,33 +38,25 @@ export class ApQuestionService { @bindThis public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise { + // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); const question = await resolver.resolve(source); + if (!isQuestion(question)) throw new Error('invalid type'); - if (!isQuestion(question)) { - throw new Error('invalid type'); - } + const multiple = question.oneOf === undefined; + if (multiple && question.anyOf === undefined) throw new Error('invalid question'); - const multiple = !question.oneOf; const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null; - if (multiple && !question.anyOf) { - throw new Error('invalid question'); - } + const choices = question[multiple ? 'anyOf' : 'oneOf'] + ?.map((x) => x.name) + .filter((x): x is string => typeof x === 'string') + ?? []; - const choices = question[multiple ? 'anyOf' : 'oneOf']! - .map((x, i) => x.name!); + const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0); - const votes = question[multiple ? 'anyOf' : 'oneOf']! - .map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0); - - return { - choices, - votes, - multiple, - expiresAt, - }; + return { choices, votes, multiple, expiresAt }; } /** @@ -68,8 +65,9 @@ export class ApQuestionService { * @returns true if updated */ @bindThis - public async updateQuestion(value: any, resolver?: Resolver) { + public async updateQuestion(value: string | IObject, resolver?: Resolver): Promise { const uri = typeof value === 'string' ? value : value.id; + if (uri == null) throw new Error('uri is null'); // URIがこのサーバーを指しているならスキップ if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local'); @@ -83,6 +81,7 @@ export class ApQuestionService { //#endregion // resolve new Question object + // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); const question = await resolver.resolve(value) as IQuestion; this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); @@ -90,12 +89,14 @@ export class ApQuestionService { if (question.type !== 'Question') throw new Error('object is not a Question'); const apChoices = question.oneOf ?? question.anyOf; + if (apChoices == null) throw new Error('invalid apChoices: ' + apChoices); let changed = false; for (const choice of poll.choices) { const oldCount = poll.votes[poll.choices.indexOf(choice)]; - const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems; + const newCount = apChoices.filter(ap => ap.name === choice).at(0)?.replies?.totalItems; + if (newCount == null) throw new Error('invalid newCount: ' + newCount); if (oldCount !== newCount) { changed = true; @@ -103,9 +104,7 @@ export class ApQuestionService { } } - await this.pollsRepository.update({ noteId: note.id }, { - votes: poll.votes, - }); + await this.pollsRepository.update({ noteId: note.id }, { votes: poll.votes }); return changed; } diff --git a/packages/backend/src/core/activitypub/models/icon.ts b/packages/backend/src/core/activitypub/models/icon.ts index 50794a937d..9fed78020d 100644 --- a/packages/backend/src/core/activitypub/models/icon.ts +++ b/packages/backend/src/core/activitypub/models/icon.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export type IIcon = { type: string; mediaType?: string; diff --git a/packages/backend/src/core/activitypub/models/identifier.ts b/packages/backend/src/core/activitypub/models/identifier.ts index f6c3bb8c88..22a7b0a76e 100644 --- a/packages/backend/src/core/activitypub/models/identifier.ts +++ b/packages/backend/src/core/activitypub/models/identifier.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export type IIdentifier = { type: string; name: string; diff --git a/packages/backend/src/core/activitypub/models/tag.ts b/packages/backend/src/core/activitypub/models/tag.ts index 803846a0b0..772ea11864 100644 --- a/packages/backend/src/core/activitypub/models/tag.ts +++ b/packages/backend/src/core/activitypub/models/tag.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { toArray } from '@/misc/prelude/array.js'; import { isHashtag } from '../type.js'; import type { IObject, IApHashtag } from '../type.js'; -export function extractApHashtags(tags: IObject | IObject[] | null | undefined) { +export function extractApHashtags(tags: IObject | IObject[] | null | undefined): string[] { if (tags == null) return []; const hashtags = extractApHashtagObjects(tags); diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 625135da6c..16ff86e894 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export type Obj = { [x: string]: any }; export type ApObject = IObject | string | (IObject | string)[]; @@ -194,7 +199,6 @@ export interface IApPropertyValue extends IObject { } export const isPropertyValue = (object: IObject): object is IApPropertyValue => - object && getApType(object) === 'PropertyValue' && typeof object.name === 'string' && 'value' in object && diff --git a/packages/backend/src/core/chart/ChartLoggerService.ts b/packages/backend/src/core/chart/ChartLoggerService.ts index afd3bab5a2..bd90efec64 100644 --- a/packages/backend/src/core/chart/ChartLoggerService.ts +++ b/packages/backend/src/core/chart/ChartLoggerService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; diff --git a/packages/backend/src/core/chart/ChartManagementService.ts b/packages/backend/src/core/chart/ChartManagementService.ts index b0e9e534df..f751a68cb4 100644 --- a/packages/backend/src/core/chart/ChartManagementService.ts +++ b/packages/backend/src/core/chart/ChartManagementService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; @@ -18,7 +23,7 @@ import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class ChartManagementService implements OnApplicationShutdown { private charts; - private saveIntervalId: NodeJS.Timer; + private saveIntervalId: NodeJS.Timeout; constructor( private federationChart: FederationChart, diff --git a/packages/backend/src/core/chart/charts/active-users.ts b/packages/backend/src/core/chart/charts/active-users.ts index bc0ba25cbb..55da1469e5 100644 --- a/packages/backend/src/core/chart/charts/active-users.ts +++ b/packages/backend/src/core/chart/charts/active-users.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; -import type { User } from '@/models/entities/User.js'; +import type { MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; @@ -16,9 +21,8 @@ const year = 1000 * 60 * 60 * 24 * 365; /** * アクティブユーザーに関するチャート */ -// eslint-disable-next-line import/no-default-export @Injectable() -export default class ActiveUsersChart extends Chart { +export default class ActiveUsersChart extends Chart { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, @@ -38,7 +42,7 @@ export default class ActiveUsersChart extends Chart { } @bindThis - public async read(user: { id: User['id'], host: null, createdAt: User['createdAt'] }): Promise { + public async read(user: { id: MiUser['id'], host: null, createdAt: MiUser['createdAt'] }): Promise { await this.commit({ 'read': [user.id], 'registeredWithinWeek': (Date.now() - user.createdAt.getTime() < week) ? [user.id] : [], @@ -51,7 +55,7 @@ export default class ActiveUsersChart extends Chart { } @bindThis - public async write(user: { id: User['id'], host: null, createdAt: User['createdAt'] }): Promise { + public async write(user: { id: MiUser['id'], host: null, createdAt: MiUser['createdAt'] }): Promise { await this.commit({ 'write': [user.id], }); diff --git a/packages/backend/src/core/chart/charts/ap-request.ts b/packages/backend/src/core/chart/charts/ap-request.ts index ce377460c8..03c9b42be1 100644 --- a/packages/backend/src/core/chart/charts/ap-request.ts +++ b/packages/backend/src/core/chart/charts/ap-request.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; @@ -11,9 +16,8 @@ import type { KVs } from '../core.js'; /** * Chart about ActivityPub requests */ -// eslint-disable-next-line import/no-default-export @Injectable() -export default class ApRequestChart extends Chart { +export default class ApRequestChart extends Chart { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, diff --git a/packages/backend/src/core/chart/charts/drive.ts b/packages/backend/src/core/chart/charts/drive.ts index b63db591fb..bbcbf1a955 100644 --- a/packages/backend/src/core/chart/charts/drive.ts +++ b/packages/backend/src/core/chart/charts/drive.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @@ -12,9 +17,8 @@ import type { KVs } from '../core.js'; /** * ドライブに関するチャート */ -// eslint-disable-next-line import/no-default-export @Injectable() -export default class DriveChart extends Chart { +export default class DriveChart extends Chart { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, @@ -34,7 +38,7 @@ export default class DriveChart extends Chart { } @bindThis - public async update(file: DriveFile, isAdditional: boolean): Promise { + public async update(file: MiDriveFile, isAdditional: boolean): Promise { const fileSizeKb = file.size / 1000; await this.commit(file.userHost === null ? { 'local.incCount': isAdditional ? 1 : 0, diff --git a/packages/backend/src/core/chart/charts/entities/active-users.ts b/packages/backend/src/core/chart/charts/entities/active-users.ts index e291e37c1b..e68022ef29 100644 --- a/packages/backend/src/core/chart/charts/entities/active-users.ts +++ b/packages/backend/src/core/chart/charts/entities/active-users.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Chart from '../../core.js'; export const name = 'activeUsers'; diff --git a/packages/backend/src/core/chart/charts/entities/ap-request.ts b/packages/backend/src/core/chart/charts/entities/ap-request.ts index 3a9f3dacfd..a824515255 100644 --- a/packages/backend/src/core/chart/charts/entities/ap-request.ts +++ b/packages/backend/src/core/chart/charts/entities/ap-request.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Chart from '../../core.js'; export const name = 'apRequest'; diff --git a/packages/backend/src/core/chart/charts/entities/drive.ts b/packages/backend/src/core/chart/charts/entities/drive.ts index 4bf5bb729e..4a56bd45c5 100644 --- a/packages/backend/src/core/chart/charts/entities/drive.ts +++ b/packages/backend/src/core/chart/charts/entities/drive.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Chart from '../../core.js'; export const name = 'drive'; diff --git a/packages/backend/src/core/chart/charts/entities/federation.ts b/packages/backend/src/core/chart/charts/entities/federation.ts index a8466b0b4c..e067c71a7f 100644 --- a/packages/backend/src/core/chart/charts/entities/federation.ts +++ b/packages/backend/src/core/chart/charts/entities/federation.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Chart from '../../core.js'; export const name = 'federation'; diff --git a/packages/backend/src/core/chart/charts/entities/instance.ts b/packages/backend/src/core/chart/charts/entities/instance.ts index 06962120e2..4ea10d56d1 100644 --- a/packages/backend/src/core/chart/charts/entities/instance.ts +++ b/packages/backend/src/core/chart/charts/entities/instance.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Chart from '../../core.js'; export const name = 'instance'; diff --git a/packages/backend/src/core/chart/charts/entities/notes.ts b/packages/backend/src/core/chart/charts/entities/notes.ts index 9387dbfb2c..26e2529b17 100644 --- a/packages/backend/src/core/chart/charts/entities/notes.ts +++ b/packages/backend/src/core/chart/charts/entities/notes.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Chart from '../../core.js'; export const name = 'notes'; diff --git a/packages/backend/src/core/chart/charts/entities/per-user-drive.ts b/packages/backend/src/core/chart/charts/entities/per-user-drive.ts index 6111640ea0..aec3dd5140 100644 --- a/packages/backend/src/core/chart/charts/entities/per-user-drive.ts +++ b/packages/backend/src/core/chart/charts/entities/per-user-drive.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Chart from '../../core.js'; export const name = 'perUserDrive'; diff --git a/packages/backend/src/core/chart/charts/entities/per-user-following.ts b/packages/backend/src/core/chart/charts/entities/per-user-following.ts index 4118daa474..afb5813058 100644 --- a/packages/backend/src/core/chart/charts/entities/per-user-following.ts +++ b/packages/backend/src/core/chart/charts/entities/per-user-following.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Chart from '../../core.js'; export const name = 'perUserFollowing'; diff --git a/packages/backend/src/core/chart/charts/entities/per-user-notes.ts b/packages/backend/src/core/chart/charts/entities/per-user-notes.ts index c1fa174452..60a0b01c8e 100644 --- a/packages/backend/src/core/chart/charts/entities/per-user-notes.ts +++ b/packages/backend/src/core/chart/charts/entities/per-user-notes.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Chart from '../../core.js'; export const name = 'perUserNotes'; diff --git a/packages/backend/src/core/chart/charts/entities/per-user-pv.ts b/packages/backend/src/core/chart/charts/entities/per-user-pv.ts index 64c8ed1fb1..78d4464d7e 100644 --- a/packages/backend/src/core/chart/charts/entities/per-user-pv.ts +++ b/packages/backend/src/core/chart/charts/entities/per-user-pv.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Chart from '../../core.js'; export const name = 'perUserPv'; diff --git a/packages/backend/src/core/chart/charts/entities/per-user-reactions.ts b/packages/backend/src/core/chart/charts/entities/per-user-reactions.ts index 5e1a6c7b30..761101d479 100644 --- a/packages/backend/src/core/chart/charts/entities/per-user-reactions.ts +++ b/packages/backend/src/core/chart/charts/entities/per-user-reactions.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Chart from '../../core.js'; export const name = 'perUserReaction'; diff --git a/packages/backend/src/core/chart/charts/entities/test-grouped.ts b/packages/backend/src/core/chart/charts/entities/test-grouped.ts index 66b6e8e864..15eb1fd1f8 100644 --- a/packages/backend/src/core/chart/charts/entities/test-grouped.ts +++ b/packages/backend/src/core/chart/charts/entities/test-grouped.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Chart from '../../core.js'; export const name = 'testGrouped'; diff --git a/packages/backend/src/core/chart/charts/entities/test-intersection.ts b/packages/backend/src/core/chart/charts/entities/test-intersection.ts index a3bdcb367f..2ef63977a5 100644 --- a/packages/backend/src/core/chart/charts/entities/test-intersection.ts +++ b/packages/backend/src/core/chart/charts/entities/test-intersection.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Chart from '../../core.js'; export const name = 'testIntersection'; diff --git a/packages/backend/src/core/chart/charts/entities/test-unique.ts b/packages/backend/src/core/chart/charts/entities/test-unique.ts index b2cfb71b05..56233585db 100644 --- a/packages/backend/src/core/chart/charts/entities/test-unique.ts +++ b/packages/backend/src/core/chart/charts/entities/test-unique.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Chart from '../../core.js'; export const name = 'testUnique'; diff --git a/packages/backend/src/core/chart/charts/entities/test.ts b/packages/backend/src/core/chart/charts/entities/test.ts index 7cba21e16a..163db4e79f 100644 --- a/packages/backend/src/core/chart/charts/entities/test.ts +++ b/packages/backend/src/core/chart/charts/entities/test.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Chart from '../../core.js'; export const name = 'test'; diff --git a/packages/backend/src/core/chart/charts/entities/users.ts b/packages/backend/src/core/chart/charts/entities/users.ts index c0b83094ae..c7bffd3fd4 100644 --- a/packages/backend/src/core/chart/charts/entities/users.ts +++ b/packages/backend/src/core/chart/charts/entities/users.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Chart from '../../core.js'; export const name = 'users'; diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts index ae4eb6e48d..fc474b002b 100644 --- a/packages/backend/src/core/chart/charts/federation.ts +++ b/packages/backend/src/core/chart/charts/federation.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import type { FollowingsRepository, InstancesRepository } from '@/models/index.js'; +import type { FollowingsRepository, InstancesRepository } from '@/models/_.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; @@ -13,9 +18,8 @@ import type { KVs } from '../core.js'; /** * フェデレーションに関するチャート */ -// eslint-disable-next-line import/no-default-export @Injectable() -export default class FederationChart extends Chart { +export default class FederationChart extends Chart { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, diff --git a/packages/backend/src/core/chart/charts/instance.ts b/packages/backend/src/core/chart/charts/instance.ts index 8ca88d80e3..9df0afb02e 100644 --- a/packages/backend/src/core/chart/charts/instance.ts +++ b/packages/backend/src/core/chart/charts/instance.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import type { DriveFilesRepository, FollowingsRepository, UsersRepository, NotesRepository } from '@/models/index.js'; -import type { DriveFile } from '@/models/entities/DriveFile.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { DriveFilesRepository, FollowingsRepository, UsersRepository, NotesRepository } from '@/models/_.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { MiNote } from '@/models/Note.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -15,9 +20,8 @@ import type { KVs } from '../core.js'; /** * インスタンスごとのチャート */ -// eslint-disable-next-line import/no-default-export @Injectable() -export default class InstanceChart extends Chart { +export default class InstanceChart extends Chart { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, @@ -93,7 +97,7 @@ export default class InstanceChart extends Chart { } @bindThis - public async updateNote(host: string, note: Note, isAdditional: boolean): Promise { + public async updateNote(host: string, note: MiNote, isAdditional: boolean): Promise { await this.commit({ 'notes.total': isAdditional ? 1 : -1, 'notes.inc': isAdditional ? 1 : 0, @@ -124,7 +128,7 @@ export default class InstanceChart extends Chart { } @bindThis - public async updateDrive(file: DriveFile, isAdditional: boolean): Promise { + public async updateDrive(file: MiDriveFile, isAdditional: boolean): Promise { const fileSizeKb = file.size / 1000; await this.commit({ 'drive.totalFiles': isAdditional ? 1 : -1, diff --git a/packages/backend/src/core/chart/charts/notes.ts b/packages/backend/src/core/chart/charts/notes.ts index 23dc248fec..df3295dbac 100644 --- a/packages/backend/src/core/chart/charts/notes.ts +++ b/packages/backend/src/core/chart/charts/notes.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable, Inject } from '@nestjs/common'; import { Not, IsNull, DataSource } from 'typeorm'; -import type { NotesRepository } from '@/models/index.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { NotesRepository } from '@/models/_.js'; +import type { MiNote } from '@/models/Note.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @@ -13,9 +18,8 @@ import type { KVs } from '../core.js'; /** * ノートに関するチャート */ -// eslint-disable-next-line import/no-default-export @Injectable() -export default class NotesChart extends Chart { +export default class NotesChart extends Chart { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, @@ -46,7 +50,7 @@ export default class NotesChart extends Chart { } @bindThis - public async update(note: Note, isAdditional: boolean): Promise { + public async update(note: MiNote, isAdditional: boolean): Promise { const prefix = note.userHost === null ? 'local' : 'remote'; await this.commit({ diff --git a/packages/backend/src/core/chart/charts/per-user-drive.ts b/packages/backend/src/core/chart/charts/per-user-drive.ts index ffba04b041..18354359c8 100644 --- a/packages/backend/src/core/chart/charts/per-user-drive.ts +++ b/packages/backend/src/core/chart/charts/per-user-drive.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import type { DriveFilesRepository } from '@/models/index.js'; -import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { DriveFilesRepository } from '@/models/_.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; @@ -14,9 +19,8 @@ import type { KVs } from '../core.js'; /** * ユーザーごとのドライブに関するチャート */ -// eslint-disable-next-line import/no-default-export @Injectable() -export default class PerUserDriveChart extends Chart { +export default class PerUserDriveChart extends Chart { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, @@ -48,7 +52,7 @@ export default class PerUserDriveChart extends Chart { } @bindThis - public async update(file: DriveFile, isAdditional: boolean): Promise { + public async update(file: MiDriveFile, isAdditional: boolean): Promise { const fileSizeKb = file.size / 1000; await this.commit({ 'totalCount': isAdditional ? 1 : -1, diff --git a/packages/backend/src/core/chart/charts/per-user-following.ts b/packages/backend/src/core/chart/charts/per-user-following.ts index aea6d44a9a..79bff2cb66 100644 --- a/packages/backend/src/core/chart/charts/per-user-following.ts +++ b/packages/backend/src/core/chart/charts/per-user-following.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable, Inject } from '@nestjs/common'; import { Not, IsNull, DataSource } from 'typeorm'; -import type { User } from '@/models/entities/User.js'; +import type { MiUser } from '@/models/User.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import type { FollowingsRepository } from '@/models/index.js'; +import type { FollowingsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; @@ -14,9 +19,8 @@ import type { KVs } from '../core.js'; /** * ユーザーごとのフォローに関するチャート */ -// eslint-disable-next-line import/no-default-export @Injectable() -export default class PerUserFollowingChart extends Chart { +export default class PerUserFollowingChart extends Chart { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, @@ -57,7 +61,7 @@ export default class PerUserFollowingChart extends Chart { } @bindThis - public async update(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }, isFollow: boolean): Promise { + public async update(follower: { id: MiUser['id']; host: MiUser['host']; }, followee: { id: MiUser['id']; host: MiUser['host']; }, isFollow: boolean): Promise { const prefixFollower = this.userEntityService.isLocalUser(follower) ? 'local' : 'remote'; const prefixFollowee = this.userEntityService.isLocalUser(followee) ? 'local' : 'remote'; 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 d8966f34c1..0db0e6f07f 100644 --- a/packages/backend/src/core/chart/charts/per-user-notes.ts +++ b/packages/backend/src/core/chart/charts/per-user-notes.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import type { User } from '@/models/entities/User.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiNote } from '@/models/Note.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; @@ -14,9 +19,8 @@ import type { KVs } from '../core.js'; /** * ユーザーごとのノートに関するチャート */ -// eslint-disable-next-line import/no-default-export @Injectable() -export default class PerUserNotesChart extends Chart { +export default class PerUserNotesChart extends Chart { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, @@ -45,7 +49,7 @@ export default class PerUserNotesChart extends Chart { } @bindThis - public update(user: { id: User['id'] }, note: Note, isAdditional: boolean): void { + public update(user: { id: MiUser['id'] }, note: MiNote, isAdditional: boolean): void { this.commit({ 'total': isAdditional ? 1 : -1, 'inc': isAdditional ? 1 : 0, diff --git a/packages/backend/src/core/chart/charts/per-user-pv.ts b/packages/backend/src/core/chart/charts/per-user-pv.ts index 53c89d8a9a..cf1b4c71f6 100644 --- a/packages/backend/src/core/chart/charts/per-user-pv.ts +++ b/packages/backend/src/core/chart/charts/per-user-pv.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import type { User } from '@/models/entities/User.js'; +import type { MiUser } from '@/models/User.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @@ -12,9 +17,8 @@ import type { KVs } from '../core.js'; /** * ユーザーごとのプロフィール被閲覧数に関するチャート */ -// eslint-disable-next-line import/no-default-export @Injectable() -export default class PerUserPvChart extends Chart { +export default class PerUserPvChart extends Chart { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, @@ -34,7 +38,7 @@ export default class PerUserPvChart extends Chart { } @bindThis - public async commitByUser(user: { id: User['id'] }, key: string): Promise { + public async commitByUser(user: { id: MiUser['id'] }, key: string): Promise { await this.commit({ 'upv.user': [key], 'pv.user': 1, @@ -42,7 +46,7 @@ export default class PerUserPvChart extends Chart { } @bindThis - public async commitByVisitor(user: { id: User['id'] }, key: string): Promise { + public async commitByVisitor(user: { id: MiUser['id'] }, key: string): Promise { await this.commit({ 'upv.visitor': [key], 'pv.visitor': 1, diff --git a/packages/backend/src/core/chart/charts/per-user-reactions.ts b/packages/backend/src/core/chart/charts/per-user-reactions.ts index 7bc6d4b521..9f4f6e9651 100644 --- a/packages/backend/src/core/chart/charts/per-user-reactions.ts +++ b/packages/backend/src/core/chart/charts/per-user-reactions.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import type { User } from '@/models/entities/User.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiNote } from '@/models/Note.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -14,9 +19,8 @@ import type { KVs } from '../core.js'; /** * ユーザーごとのリアクションに関するチャート */ -// eslint-disable-next-line import/no-default-export @Injectable() -export default class PerUserReactionsChart extends Chart { +export default class PerUserReactionsChart extends Chart { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, @@ -37,7 +41,7 @@ export default class PerUserReactionsChart extends Chart { } @bindThis - public async update(user: { id: User['id'], host: User['host'] }, note: Note): Promise { + public async update(user: { id: MiUser['id'], host: MiUser['host'] }, note: MiNote): Promise { const prefix = this.userEntityService.isLocalUser(user) ? 'local' : 'remote'; this.commit({ [`${prefix}.count`]: 1, diff --git a/packages/backend/src/core/chart/charts/test-grouped.ts b/packages/backend/src/core/chart/charts/test-grouped.ts index 128967bc65..00fb872237 100644 --- a/packages/backend/src/core/chart/charts/test-grouped.ts +++ b/packages/backend/src/core/chart/charts/test-grouped.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; @@ -11,9 +16,8 @@ import type { KVs } from '../core.js'; /** * For testing */ -// eslint-disable-next-line import/no-default-export @Injectable() -export default class TestGroupedChart extends Chart { +export default class TestGroupedChart extends Chart { // eslint-disable-line import/no-default-export private total = {} as Record; constructor( diff --git a/packages/backend/src/core/chart/charts/test-intersection.ts b/packages/backend/src/core/chart/charts/test-intersection.ts index 6b4eed9062..45a7e805c5 100644 --- a/packages/backend/src/core/chart/charts/test-intersection.ts +++ b/packages/backend/src/core/chart/charts/test-intersection.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; @@ -11,9 +16,8 @@ import type { KVs } from '../core.js'; /** * For testing */ -// eslint-disable-next-line import/no-default-export @Injectable() -export default class TestIntersectionChart extends Chart { +export default class TestIntersectionChart extends Chart { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, diff --git a/packages/backend/src/core/chart/charts/test-unique.ts b/packages/backend/src/core/chart/charts/test-unique.ts index 5d2b3f8ab1..e9d38eaf13 100644 --- a/packages/backend/src/core/chart/charts/test-unique.ts +++ b/packages/backend/src/core/chart/charts/test-unique.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; @@ -11,9 +16,8 @@ import type { KVs } from '../core.js'; /** * For testing */ -// eslint-disable-next-line import/no-default-export @Injectable() -export default class TestUniqueChart extends Chart { +export default class TestUniqueChart extends Chart { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, diff --git a/packages/backend/src/core/chart/charts/test.ts b/packages/backend/src/core/chart/charts/test.ts index 238351d8b3..4dd6063b5b 100644 --- a/packages/backend/src/core/chart/charts/test.ts +++ b/packages/backend/src/core/chart/charts/test.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { AppLockService } from '@/core/AppLockService.js'; @@ -11,9 +16,8 @@ import type { KVs } from '../core.js'; /** * For testing */ -// eslint-disable-next-line import/no-default-export @Injectable() -export default class TestChart extends Chart { +export default class TestChart extends Chart { // eslint-disable-line import/no-default-export public total = 0; // publicにするのはテストのため constructor( diff --git a/packages/backend/src/core/chart/charts/users.ts b/packages/backend/src/core/chart/charts/users.ts index 7bc3602439..c2026c2aea 100644 --- a/packages/backend/src/core/chart/charts/users.ts +++ b/packages/backend/src/core/chart/charts/users.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable, Inject } from '@nestjs/common'; import { Not, IsNull, DataSource } from 'typeorm'; -import type { User } from '@/models/entities/User.js'; +import type { MiUser } from '@/models/User.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import type { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; @@ -14,9 +19,8 @@ import type { KVs } from '../core.js'; /** * ユーザー数に関するチャート */ -// eslint-disable-next-line import/no-default-export @Injectable() -export default class UsersChart extends Chart { +export default class UsersChart extends Chart { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, @@ -48,7 +52,7 @@ export default class UsersChart extends Chart { } @bindThis - public async update(user: { id: User['id'], host: User['host'] }, isAdditional: boolean): Promise { + public async update(user: { id: MiUser['id'], host: MiUser['host'] }, isAdditional: boolean): Promise { const prefix = this.userEntityService.isLocalUser(user) ? 'local' : 'remote'; await this.commit({ diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts index d352adcc1f..8d0a89f2d6 100644 --- a/packages/backend/src/core/chart/core.ts +++ b/packages/backend/src/core/chart/core.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + /** * チャートエンジン * @@ -254,7 +259,7 @@ export default abstract class Chart { private convertRawRecord(x: RawRecord): KVs { const kvs = {} as Record; for (const k of Object.keys(x).filter((k) => k.startsWith(COLUMN_PREFIX)) as (keyof Columns)[]) { - kvs[(k as string).substr(COLUMN_PREFIX.length).split(COLUMN_DELIMITER).join('.')] = x[k] as unknown as number; + kvs[(k as string).substring(COLUMN_PREFIX.length).split(COLUMN_DELIMITER).join('.')] = x[k] as unknown as number; } return kvs as KVs; } @@ -627,7 +632,7 @@ export default abstract class Chart { } // 要求された範囲の最も古い箇所に位置するログが存在しなかったら - } else if (!isTimeSame(new Date(logs[logs.length - 1].date * 1000), gt)) { + } else if (!isTimeSame(new Date(logs.at(-1)!.date * 1000), gt)) { // 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する // (隙間埋めできないため) const outdatedLog = await repository.findOne({ diff --git a/packages/backend/src/core/chart/entities.ts b/packages/backend/src/core/chart/entities.ts index b44e2e38b7..b6a1299a2f 100644 --- a/packages/backend/src/core/chart/entities.ts +++ b/packages/backend/src/core/chart/entities.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { entity as FederationChart } from './charts/entities/federation.js'; import { entity as NotesChart } from './charts/entities/notes.js'; import { entity as UsersChart } from './charts/entities/users.js'; diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts index 7f8240b8b2..0e65a10d26 100644 --- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts +++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { AbuseUserReportsRepository } from '@/models/index.js'; +import type { AbuseUserReportsRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { AbuseUserReport } from '@/models/entities/AbuseUserReport.js'; -import { UserEntityService } from './UserEntityService.js'; +import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { bindThis } from '@/decorators.js'; +import { UserEntityService } from './UserEntityService.js'; @Injectable() export class AbuseUserReportEntityService { @@ -18,7 +23,7 @@ export class AbuseUserReportEntityService { @bindThis public async pack( - src: AbuseUserReport['id'] | AbuseUserReport, + src: MiAbuseUserReport['id'] | MiAbuseUserReport, ) { const report = typeof src === 'object' ? src : await this.abuseUserReportsRepository.findOneByOrFail({ id: src }); diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts index 328511f5df..ed108f2ce5 100644 --- a/packages/backend/src/core/entities/AntennaEntityService.ts +++ b/packages/backend/src/core/entities/AntennaEntityService.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { AntennasRepository } from '@/models/index.js'; +import type { AntennasRepository } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { Antenna } from '@/models/entities/Antenna.js'; +import type { MiAntenna } from '@/models/Antenna.js'; import { bindThis } from '@/decorators.js'; @Injectable() @@ -15,7 +20,7 @@ export class AntennaEntityService { @bindThis public async pack( - src: Antenna['id'] | Antenna, + src: MiAntenna['id'] | MiAntenna, ): Promise> { const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src }); diff --git a/packages/backend/src/core/entities/AppEntityService.ts b/packages/backend/src/core/entities/AppEntityService.ts index 0b4c3935c7..14a93cda5b 100644 --- a/packages/backend/src/core/entities/AppEntityService.ts +++ b/packages/backend/src/core/entities/AppEntityService.ts @@ -1,9 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { AccessTokensRepository, AppsRepository } from '@/models/index.js'; +import type { AccessTokensRepository, AppsRepository } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { App } from '@/models/entities/App.js'; -import type { User } from '@/models/entities/User.js'; +import type { MiApp } from '@/models/App.js'; +import type { MiUser } from '@/models/User.js'; import { bindThis } from '@/decorators.js'; @Injectable() @@ -19,8 +24,8 @@ export class AppEntityService { @bindThis public async pack( - src: App['id'] | App, - me?: { id: User['id'] } | null | undefined, + src: MiApp['id'] | MiApp, + me?: { id: MiUser['id'] } | null | undefined, options?: { detail?: boolean, includeSecret?: boolean, diff --git a/packages/backend/src/core/entities/AuthSessionEntityService.ts b/packages/backend/src/core/entities/AuthSessionEntityService.ts index b7edc8494e..fd356cc89d 100644 --- a/packages/backend/src/core/entities/AuthSessionEntityService.ts +++ b/packages/backend/src/core/entities/AuthSessionEntityService.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { AuthSessionsRepository } from '@/models/index.js'; +import type { AuthSessionsRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { AuthSession } from '@/models/entities/AuthSession.js'; -import type { User } from '@/models/entities/User.js'; -import { AppEntityService } from './AppEntityService.js'; +import type { MiAuthSession } from '@/models/AuthSession.js'; +import type { MiUser } from '@/models/User.js'; import { bindThis } from '@/decorators.js'; +import { AppEntityService } from './AppEntityService.js'; @Injectable() export class AuthSessionEntityService { @@ -19,8 +24,8 @@ export class AuthSessionEntityService { @bindThis public async pack( - src: AuthSession['id'] | AuthSession, - me?: { id: User['id'] } | null | undefined, + src: MiAuthSession['id'] | MiAuthSession, + me?: { id: MiUser['id'] } | null | undefined, ) { const session = typeof src === 'object' ? src : await this.authSessionsRepository.findOneByOrFail({ id: src }); diff --git a/packages/backend/src/core/entities/BlockingEntityService.ts b/packages/backend/src/core/entities/BlockingEntityService.ts index e169c7e90a..44466e24e8 100644 --- a/packages/backend/src/core/entities/BlockingEntityService.ts +++ b/packages/backend/src/core/entities/BlockingEntityService.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { BlockingsRepository } from '@/models/index.js'; +import type { BlockingsRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { Blocking } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; +import type { MiBlocking } from '@/models/Blocking.js'; +import type { MiUser } from '@/models/User.js'; import { bindThis } from '@/decorators.js'; import { UserEntityService } from './UserEntityService.js'; @@ -20,8 +25,8 @@ export class BlockingEntityService { @bindThis public async pack( - src: Blocking['id'] | Blocking, - me?: { id: User['id'] } | null | undefined, + src: MiBlocking['id'] | MiBlocking, + me?: { id: MiUser['id'] } | null | undefined, ): Promise> { const blocking = typeof src === 'object' ? src : await this.blockingsRepository.findOneByOrFail({ id: src }); @@ -38,7 +43,7 @@ export class BlockingEntityService { @bindThis public packMany( blockings: any[], - me: { id: User['id'] }, + me: { id: MiUser['id'] }, ) { return Promise.all(blockings.map(x => this.pack(x, me))); } diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts index 15ffd44861..094de4d2d5 100644 --- a/packages/backend/src/core/entities/ChannelEntityService.ts +++ b/packages/backend/src/core/entities/ChannelEntityService.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { ChannelFavoritesRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository, NotesRepository } from '@/models/index.js'; +import type { ChannelFavoritesRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository, NotesRepository } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; -import type { Channel } from '@/models/entities/Channel.js'; +import type { } from '@/models/Blocking.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiChannel } from '@/models/Channel.js'; import { bindThis } from '@/decorators.js'; import { DriveFileEntityService } from './DriveFileEntityService.js'; import { NoteEntityService } from './NoteEntityService.js'; @@ -38,8 +43,8 @@ export class ChannelEntityService { @bindThis public async pack( - src: Channel['id'] | Channel, - me?: { id: User['id'] } | null | undefined, + src: MiChannel['id'] | MiChannel, + me?: { id: MiUser['id'] } | null | undefined, detailed?: boolean, ): Promise> { const channel = typeof src === 'object' ? src : await this.channelsRepository.findOneByOrFail({ id: src }); @@ -47,17 +52,26 @@ export class ChannelEntityService { const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null; - const hasUnreadNote = meId ? (await this.noteUnreadsRepository.findOneBy({ noteChannelId: channel.id, userId: meId })) != null : undefined; + const hasUnreadNote = meId ? await this.noteUnreadsRepository.exist({ + where: { + noteChannelId: channel.id, + userId: meId, + }, + }) : undefined; - const following = meId ? await this.channelFollowingsRepository.findOneBy({ - followerId: meId, - followeeId: channel.id, - }) : null; + const isFollowing = meId ? await this.channelFollowingsRepository.exist({ + where: { + followerId: meId, + followeeId: channel.id, + }, + }) : false; - const favorite = meId ? await this.channelFavoritesRepository.findOneBy({ - userId: meId, - channelId: channel.id, - }) : null; + const isFavorited = meId ? await this.channelFavoritesRepository.exist({ + where: { + userId: meId, + channelId: channel.id, + }, + }) : false; const pinnedNotes = channel.pinnedNoteIds.length > 0 ? await this.notesRepository.find({ where: { @@ -78,10 +92,11 @@ export class ChannelEntityService { isArchived: channel.isArchived, usersCount: channel.usersCount, notesCount: channel.notesCount, + isSensitive: channel.isSensitive, ...(me ? { - isFollowing: following != null, - isFavorited: favorite != null, + isFollowing, + isFavorited, hasUnreadNote, } : {}), diff --git a/packages/backend/src/core/entities/ClipEntityService.ts b/packages/backend/src/core/entities/ClipEntityService.ts index 33d3c53806..e141db03f1 100644 --- a/packages/backend/src/core/entities/ClipEntityService.ts +++ b/packages/backend/src/core/entities/ClipEntityService.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { ClipFavoritesRepository, ClipsRepository, User } from '@/models/index.js'; +import type { ClipFavoritesRepository, ClipsRepository, MiUser } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { Clip } from '@/models/entities/Clip.js'; +import type { } from '@/models/Blocking.js'; +import type { MiClip } from '@/models/Clip.js'; import { bindThis } from '@/decorators.js'; import { UserEntityService } from './UserEntityService.js'; @@ -23,8 +28,8 @@ export class ClipEntityService { @bindThis public async pack( - src: Clip['id'] | Clip, - me?: { id: User['id'] } | null | undefined, + src: MiClip['id'] | MiClip, + me?: { id: MiUser['id'] } | null | undefined, ): Promise> { const meId = me ? me.id : null; const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src }); @@ -39,14 +44,14 @@ export class ClipEntityService { description: clip.description, isPublic: clip.isPublic, favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }), - isFavorited: meId ? await this.clipFavoritesRepository.findOneBy({ clipId: clip.id, userId: meId }).then(x => x != null) : undefined, + isFavorited: meId ? await this.clipFavoritesRepository.exist({ where: { clipId: clip.id, userId: meId } }) : undefined, }); } @bindThis public packMany( - clips: Clip[], - me?: { id: User['id'] } | null | undefined, + clips: MiClip[], + me?: { id: MiUser['id'] } | null | undefined, ) { return Promise.all(clips.map(x => this.pack(x, me))); } diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index d82f36d971..23273b0413 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -1,14 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { DataSource, In } from 'typeorm'; +import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { Packed } from '@/misc/json-schema.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { User } from '@/models/entities/User.js'; -import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; import { appendQuery, query } from '@/misc/prelude/url.js'; import { deepClone } from '@/misc/clone.js'; +import { bindThis } from '@/decorators.js'; +import { isMimeImage } from '@/misc/is-mime-image.js'; +import { isNotNull } from '@/misc/is-not-null.js'; import { UtilityService } from '../UtilityService.js'; import { VideoProcessingService } from '../VideoProcessingService.js'; import { UserEntityService } from './UserEntityService.js'; @@ -19,9 +27,6 @@ type PackOptions = { self?: boolean, withUser?: boolean, }; -import { bindThis } from '@/decorators.js'; -import { isMimeImage } from '@/misc/is-mime-image.js'; -import { isNotNull } from '@/misc/is-not-null.js'; @Injectable() export class DriveFileEntityService { @@ -29,12 +34,6 @@ export class DriveFileEntityService { @Inject(DI.config) private config: Config, - @Inject(DI.db) - private db: DataSource, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -47,7 +46,7 @@ export class DriveFileEntityService { private videoProcessingService: VideoProcessingService, ) { } - + @bindThis public validateFileName(name: string): boolean { return ( @@ -60,7 +59,7 @@ export class DriveFileEntityService { } @bindThis - public getPublicProperties(file: DriveFile): DriveFile['properties'] { + public getPublicProperties(file: MiDriveFile): MiDriveFile['properties'] { if (file.properties.orientation != null) { const properties = deepClone(file.properties); if (file.properties.orientation >= 5) { @@ -85,7 +84,7 @@ export class DriveFileEntityService { } @bindThis - public getThumbnailUrl(file: DriveFile): string | null { + public getThumbnailUrl(file: MiDriveFile): string | null { if (file.type.startsWith('video')) { if (file.thumbnailUrl) return file.thumbnailUrl; @@ -108,7 +107,7 @@ export class DriveFileEntityService { } @bindThis - public getPublicUrl(file: DriveFile, mode?: 'avatar'): string { // static = thumbnail + public getPublicUrl(file: MiDriveFile, mode?: 'avatar'): string { // static = thumbnail // リモートかつメディアプロキシ if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) { return this.getProxiedUrl(file.uri, mode); @@ -134,7 +133,7 @@ export class DriveFileEntityService { } @bindThis - public async calcDriveUsageOf(user: User['id'] | { id: User['id'] }): Promise { + public async calcDriveUsageOf(user: MiUser['id'] | { id: MiUser['id'] }): Promise { const id = typeof user === 'object' ? user.id : user; const { sum } = await this.driveFilesRepository @@ -185,7 +184,7 @@ export class DriveFileEntityService { @bindThis public async pack( - src: DriveFile['id'] | DriveFile, + src: MiDriveFile['id'] | MiDriveFile, options?: PackOptions, ): Promise> { const opts = Object.assign({ @@ -219,7 +218,7 @@ export class DriveFileEntityService { @bindThis public async packNullable( - src: DriveFile['id'] | DriveFile, + src: MiDriveFile['id'] | MiDriveFile, options?: PackOptions, ): Promise | null> { const opts = Object.assign({ @@ -254,7 +253,7 @@ export class DriveFileEntityService { @bindThis public async packMany( - files: DriveFile[], + files: MiDriveFile[], options?: PackOptions, ): Promise[]> { const items = await Promise.all(files.map(f => this.packNullable(f, options))); @@ -263,7 +262,7 @@ export class DriveFileEntityService { @bindThis public async packManyByIdsMap( - fileIds: DriveFile['id'][], + fileIds: MiDriveFile['id'][], options?: PackOptions, ): Promise['id'], Packed<'DriveFile'> | null>> { if (fileIds.length === 0) return new Map(); @@ -278,7 +277,7 @@ export class DriveFileEntityService { @bindThis public async packManyByIds( - fileIds: DriveFile['id'][], + fileIds: MiDriveFile['id'][], options?: PackOptions, ): Promise[]> { if (fileIds.length === 0) return []; diff --git a/packages/backend/src/core/entities/DriveFolderEntityService.ts b/packages/backend/src/core/entities/DriveFolderEntityService.ts index 13929b145f..55014284bd 100644 --- a/packages/backend/src/core/entities/DriveFolderEntityService.ts +++ b/packages/backend/src/core/entities/DriveFolderEntityService.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/index.js'; +import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { DriveFolder } from '@/models/entities/DriveFolder.js'; +import type { } from '@/models/Blocking.js'; +import type { MiDriveFolder } from '@/models/DriveFolder.js'; import { bindThis } from '@/decorators.js'; @Injectable() @@ -20,7 +25,7 @@ export class DriveFolderEntityService { @bindThis public async pack( - src: DriveFolder['id'] | DriveFolder, + src: MiDriveFolder['id'] | MiDriveFolder, options?: { detail: boolean }, diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 4a18cd1b3b..5b97cfad5e 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -1,9 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { Emoji } from '@/models/entities/Emoji.js'; +import type { } from '@/models/Blocking.js'; +import type { MiEmoji } from '@/models/Emoji.js'; import { bindThis } from '@/decorators.js'; @Injectable() @@ -16,7 +21,7 @@ export class EmojiEntityService { @bindThis public async packSimple( - src: Emoji['id'] | Emoji, + src: MiEmoji['id'] | MiEmoji, ): Promise> { const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); @@ -40,7 +45,7 @@ export class EmojiEntityService { @bindThis public async packDetailed( - src: Emoji['id'] | Emoji, + src: MiEmoji['id'] | MiEmoji, ): Promise> { const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts index e52a591884..4701cddcba 100644 --- a/packages/backend/src/core/entities/FlashEntityService.ts +++ b/packages/backend/src/core/entities/FlashEntityService.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { FlashsRepository, FlashLikesRepository } from '@/models/index.js'; +import type { FlashsRepository, FlashLikesRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; -import type { Flash } from '@/models/entities/Flash.js'; +import type { } from '@/models/Blocking.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiFlash } from '@/models/Flash.js'; import { bindThis } from '@/decorators.js'; import { UserEntityService } from './UserEntityService.js'; @@ -24,8 +29,8 @@ export class FlashEntityService { @bindThis public async pack( - src: Flash['id'] | Flash, - me?: { id: User['id'] } | null | undefined, + src: MiFlash['id'] | MiFlash, + me?: { id: MiUser['id'] } | null | undefined, ): Promise> { const meId = me ? me.id : null; const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src }); @@ -40,14 +45,14 @@ export class FlashEntityService { summary: flash.summary, script: flash.script, likedCount: flash.likedCount, - isLiked: meId ? await this.flashLikesRepository.findOneBy({ flashId: flash.id, userId: meId }).then(x => x != null) : undefined, + isLiked: meId ? await this.flashLikesRepository.exist({ where: { flashId: flash.id, userId: meId } }) : undefined, }); } @bindThis public packMany( - flashs: Flash[], - me?: { id: User['id'] } | null | undefined, + flashs: MiFlash[], + me?: { id: MiUser['id'] } | null | undefined, ) { return Promise.all(flashs.map(x => this.pack(x, me))); } diff --git a/packages/backend/src/core/entities/FlashLikeEntityService.ts b/packages/backend/src/core/entities/FlashLikeEntityService.ts index 0351ec3014..2eff86217a 100644 --- a/packages/backend/src/core/entities/FlashLikeEntityService.ts +++ b/packages/backend/src/core/entities/FlashLikeEntityService.ts @@ -1,9 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { FlashLikesRepository } from '@/models/index.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 type { FlashLikesRepository } from '@/models/_.js'; +import type { } from '@/models/Blocking.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiFlashLike } from '@/models/FlashLike.js'; import { bindThis } from '@/decorators.js'; import { FlashEntityService } from './FlashEntityService.js'; @@ -19,8 +24,8 @@ export class FlashLikeEntityService { @bindThis public async pack( - src: FlashLike['id'] | FlashLike, - me?: { id: User['id'] } | null | undefined, + src: MiFlashLike['id'] | MiFlashLike, + me?: { id: MiUser['id'] } | null | undefined, ) { const like = typeof src === 'object' ? src : await this.flashLikesRepository.findOneByOrFail({ id: src }); @@ -33,7 +38,7 @@ export class FlashLikeEntityService { @bindThis public packMany( likes: any[], - me: { id: User['id'] }, + me: { id: MiUser['id'] }, ) { return Promise.all(likes.map(x => this.pack(x, me))); } diff --git a/packages/backend/src/core/entities/FollowRequestEntityService.ts b/packages/backend/src/core/entities/FollowRequestEntityService.ts index c2edc6a13a..0e0fec9f46 100644 --- a/packages/backend/src/core/entities/FollowRequestEntityService.ts +++ b/packages/backend/src/core/entities/FollowRequestEntityService.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { FollowRequestsRepository } from '@/models/index.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; -import type { FollowRequest } from '@/models/entities/FollowRequest.js'; -import { UserEntityService } from './UserEntityService.js'; +import type { FollowRequestsRepository } from '@/models/_.js'; +import type { } from '@/models/Blocking.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiFollowRequest } from '@/models/FollowRequest.js'; import { bindThis } from '@/decorators.js'; +import { UserEntityService } from './UserEntityService.js'; @Injectable() export class FollowRequestEntityService { @@ -19,8 +24,8 @@ export class FollowRequestEntityService { @bindThis public async pack( - src: FollowRequest['id'] | FollowRequest, - me?: { id: User['id'] } | null | undefined, + src: MiFollowRequest['id'] | MiFollowRequest, + me?: { id: MiUser['id'] } | null | undefined, ) { const request = typeof src === 'object' ? src : await this.followRequestsRepository.findOneByOrFail({ id: src }); diff --git a/packages/backend/src/core/entities/FollowingEntityService.ts b/packages/backend/src/core/entities/FollowingEntityService.ts index 55ba4e67ad..9f6eb51e8c 100644 --- a/packages/backend/src/core/entities/FollowingEntityService.ts +++ b/packages/backend/src/core/entities/FollowingEntityService.ts @@ -1,33 +1,38 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository } from '@/models/index.js'; +import type { FollowingsRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; -import type { Following } from '@/models/entities/Following.js'; +import type { } from '@/models/Blocking.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiFollowing } from '@/models/Following.js'; import { bindThis } from '@/decorators.js'; import { UserEntityService } from './UserEntityService.js'; -type LocalFollowerFollowing = Following & { +type LocalFollowerFollowing = MiFollowing & { followerHost: null; followerInbox: null; followerSharedInbox: null; }; -type RemoteFollowerFollowing = Following & { +type RemoteFollowerFollowing = MiFollowing & { followerHost: string; followerInbox: string; followerSharedInbox: string; }; -type LocalFolloweeFollowing = Following & { +type LocalFolloweeFollowing = MiFollowing & { followeeHost: null; followeeInbox: null; followeeSharedInbox: null; }; -type RemoteFolloweeFollowing = Following & { +type RemoteFolloweeFollowing = MiFollowing & { followeeHost: string; followeeInbox: string; followeeSharedInbox: string; @@ -44,29 +49,29 @@ export class FollowingEntityService { } @bindThis - public isLocalFollower(following: Following): following is LocalFollowerFollowing { + public isLocalFollower(following: MiFollowing): following is LocalFollowerFollowing { return following.followerHost == null; } @bindThis - public isRemoteFollower(following: Following): following is RemoteFollowerFollowing { + public isRemoteFollower(following: MiFollowing): following is RemoteFollowerFollowing { return following.followerHost != null; } @bindThis - public isLocalFollowee(following: Following): following is LocalFolloweeFollowing { + public isLocalFollowee(following: MiFollowing): following is LocalFolloweeFollowing { return following.followeeHost == null; } @bindThis - public isRemoteFollowee(following: Following): following is RemoteFolloweeFollowing { + public isRemoteFollowee(following: MiFollowing): following is RemoteFolloweeFollowing { return following.followeeHost != null; } @bindThis public async pack( - src: Following['id'] | Following, - me?: { id: User['id'] } | null | undefined, + src: MiFollowing['id'] | MiFollowing, + me?: { id: MiUser['id'] } | null | undefined, opts?: { populateFollowee?: boolean; populateFollower?: boolean; @@ -93,7 +98,7 @@ export class FollowingEntityService { @bindThis public packMany( followings: any[], - me?: { id: User['id'] } | null | undefined, + me?: { id: MiUser['id'] } | null | undefined, opts?: { populateFollowee?: boolean; populateFollower?: boolean; diff --git a/packages/backend/src/core/entities/GalleryLikeEntityService.ts b/packages/backend/src/core/entities/GalleryLikeEntityService.ts index db46045db3..e740701888 100644 --- a/packages/backend/src/core/entities/GalleryLikeEntityService.ts +++ b/packages/backend/src/core/entities/GalleryLikeEntityService.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { GalleryLikesRepository } from '@/models/index.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { GalleryLike } from '@/models/entities/GalleryLike.js'; -import { GalleryPostEntityService } from './GalleryPostEntityService.js'; +import type { GalleryLikesRepository } from '@/models/_.js'; +import type { } from '@/models/Blocking.js'; +import type { MiGalleryLike } from '@/models/GalleryLike.js'; import { bindThis } from '@/decorators.js'; +import { GalleryPostEntityService } from './GalleryPostEntityService.js'; @Injectable() export class GalleryLikeEntityService { @@ -18,7 +23,7 @@ export class GalleryLikeEntityService { @bindThis public async pack( - src: GalleryLike['id'] | GalleryLike, + src: MiGalleryLike['id'] | MiGalleryLike, me?: any, ) { const like = typeof src === 'object' ? src : await this.galleryLikesRepository.findOneByOrFail({ id: src }); diff --git a/packages/backend/src/core/entities/GalleryPostEntityService.ts b/packages/backend/src/core/entities/GalleryPostEntityService.ts index 632c75304f..bbaf70f0fd 100644 --- a/packages/backend/src/core/entities/GalleryPostEntityService.ts +++ b/packages/backend/src/core/entities/GalleryPostEntityService.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; -import type { GalleryPost } from '@/models/entities/GalleryPost.js'; +import type { } from '@/models/Blocking.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiGalleryPost } from '@/models/GalleryPost.js'; import { bindThis } from '@/decorators.js'; import { UserEntityService } from './UserEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js'; @@ -26,8 +31,8 @@ export class GalleryPostEntityService { @bindThis public async pack( - src: GalleryPost['id'] | GalleryPost, - me?: { id: User['id'] } | null | undefined, + src: MiGalleryPost['id'] | MiGalleryPost, + me?: { id: MiUser['id'] } | null | undefined, ): Promise> { const meId = me ? me.id : null; const post = typeof src === 'object' ? src : await this.galleryPostsRepository.findOneByOrFail({ id: src }); @@ -46,14 +51,14 @@ export class GalleryPostEntityService { tags: post.tags.length > 0 ? post.tags : undefined, isSensitive: post.isSensitive, likedCount: post.likedCount, - isLiked: meId ? await this.galleryLikesRepository.findOneBy({ postId: post.id, userId: meId }).then(x => x != null) : undefined, + isLiked: meId ? await this.galleryLikesRepository.exist({ where: { postId: post.id, userId: meId } }) : undefined, }); } @bindThis public packMany( - posts: GalleryPost[], - me?: { id: User['id'] } | null | undefined, + posts: MiGalleryPost[], + me?: { id: MiUser['id'] } | null | undefined, ) { return Promise.all(posts.map(x => this.pack(x, me))); } diff --git a/packages/backend/src/core/entities/HashtagEntityService.ts b/packages/backend/src/core/entities/HashtagEntityService.ts index 2cd79b8f8c..006e267b12 100644 --- a/packages/backend/src/core/entities/HashtagEntityService.ts +++ b/packages/backend/src/core/entities/HashtagEntityService.ts @@ -1,25 +1,23 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { HashtagsRepository } from '@/models/index.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { Hashtag } from '@/models/entities/Hashtag.js'; +import type { } from '@/models/Blocking.js'; +import type { MiHashtag } from '@/models/Hashtag.js'; import { bindThis } from '@/decorators.js'; -import { UserEntityService } from './UserEntityService.js'; @Injectable() export class HashtagEntityService { constructor( - @Inject(DI.hashtagsRepository) - private hashtagsRepository: HashtagsRepository, - - private userEntityService: UserEntityService, ) { } @bindThis public async pack( - src: Hashtag, + src: MiHashtag, ): Promise> { return { tag: src.name, @@ -34,7 +32,7 @@ export class HashtagEntityService { @bindThis public packMany( - hashtags: Hashtag[], + hashtags: MiHashtag[], ) { return Promise.all(hashtags.map(x => this.pack(x))); } diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 3bf84ed375..0e27e9df7f 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -1,9 +1,12 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { InstancesRepository } from '@/models/index.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { Instance } from '@/models/entities/Instance.js'; +import type { } from '@/models/Blocking.js'; +import type { MiInstance } from '@/models/Instance.js'; import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import { UtilityService } from '../UtilityService.js'; @@ -11,9 +14,6 @@ import { UtilityService } from '../UtilityService.js'; @Injectable() export class InstanceEntityService { constructor( - @Inject(DI.instancesRepository) - private instancesRepository: InstancesRepository, - private metaService: MetaService, private utilityService: UtilityService, @@ -22,7 +22,7 @@ export class InstanceEntityService { @bindThis public async pack( - instance: Instance, + instance: MiInstance, ): Promise> { const meta = await this.metaService.fetch(); return { @@ -52,7 +52,7 @@ export class InstanceEntityService { @bindThis public packMany( - instances: Instance[], + instances: MiInstance[], ) { return Promise.all(instances.map(x => this.pack(x))); } diff --git a/packages/backend/src/core/entities/InviteCodeEntityService.ts b/packages/backend/src/core/entities/InviteCodeEntityService.ts new file mode 100644 index 0000000000..914eaafe68 --- /dev/null +++ b/packages/backend/src/core/entities/InviteCodeEntityService.ts @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { RegistrationTicketsRepository } from '@/models/_.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/json-schema.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiRegistrationTicket } from '@/models/RegistrationTicket.js'; +import { bindThis } from '@/decorators.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class InviteCodeEntityService { + constructor( + @Inject(DI.registrationTicketsRepository) + private registrationTicketsRepository: RegistrationTicketsRepository, + + private userEntityService: UserEntityService, + ) { + } + + @bindThis + public async pack( + src: MiRegistrationTicket['id'] | MiRegistrationTicket, + me?: { id: MiUser['id'] } | null | undefined, + ): Promise> { + const target = typeof src === 'object' ? src : await this.registrationTicketsRepository.findOneOrFail({ + where: { + id: src, + }, + relations: ['createdBy', 'usedBy'], + }); + + return await awaitAll({ + id: target.id, + code: target.code, + expiresAt: target.expiresAt ? target.expiresAt.toISOString() : null, + createdAt: target.createdAt.toISOString(), + createdBy: target.createdBy ? await this.userEntityService.pack(target.createdBy, me) : null, + usedBy: target.usedBy ? await this.userEntityService.pack(target.usedBy, me) : null, + usedAt: target.usedAt ? target.usedAt.toISOString() : null, + used: !!target.usedAt, + }); + } + + @bindThis + public packMany( + targets: any[], + me: { id: MiUser['id'] }, + ) { + return Promise.all(targets.map(x => this.pack(x, me))); + } +} diff --git a/packages/backend/src/core/entities/ModerationLogEntityService.ts b/packages/backend/src/core/entities/ModerationLogEntityService.ts index 7058e38af9..83b024d83b 100644 --- a/packages/backend/src/core/entities/ModerationLogEntityService.ts +++ b/packages/backend/src/core/entities/ModerationLogEntityService.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { ModerationLogsRepository } from '@/models/index.js'; +import type { ModerationLogsRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { ModerationLog } from '@/models/entities/ModerationLog.js'; -import { UserEntityService } from './UserEntityService.js'; +import type { } from '@/models/Blocking.js'; +import type { MiModerationLog } from '@/models/ModerationLog.js'; import { bindThis } from '@/decorators.js'; +import { UserEntityService } from './UserEntityService.js'; @Injectable() export class ModerationLogEntityService { @@ -19,7 +24,7 @@ export class ModerationLogEntityService { @bindThis public async pack( - src: ModerationLog['id'] | ModerationLog, + src: MiModerationLog['id'] | MiModerationLog, ) { const log = typeof src === 'object' ? src : await this.moderationLogsRepository.findOneByOrFail({ id: src }); diff --git a/packages/backend/src/core/entities/MutingEntityService.ts b/packages/backend/src/core/entities/MutingEntityService.ts index 561d53292e..e3d5d2e211 100644 --- a/packages/backend/src/core/entities/MutingEntityService.ts +++ b/packages/backend/src/core/entities/MutingEntityService.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { MutingsRepository } from '@/models/index.js'; +import type { MutingsRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; -import type { Muting } from '@/models/entities/Muting.js'; +import type { } from '@/models/Blocking.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiMuting } from '@/models/Muting.js'; import { bindThis } from '@/decorators.js'; import { UserEntityService } from './UserEntityService.js'; @@ -21,8 +26,8 @@ export class MutingEntityService { @bindThis public async pack( - src: Muting['id'] | Muting, - me?: { id: User['id'] } | null | undefined, + src: MiMuting['id'] | MiMuting, + me?: { id: MiUser['id'] } | null | undefined, ): Promise> { const muting = typeof src === 'object' ? src : await this.mutingsRepository.findOneByOrFail({ id: src }); @@ -40,7 +45,7 @@ export class MutingEntityService { @bindThis public packMany( mutings: any[], - me: { id: User['id'] }, + me: { id: MiUser['id'] }, ) { return Promise.all(mutings.map(x => this.pack(x, me))); } diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 32269a4101..bf42e98ce0 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -1,15 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import { DataSource, In } from 'typeorm'; +import { In } from 'typeorm'; import * as mfm from 'mfm-js'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; import type { Packed } from '@/misc/json-schema.js'; import { nyaize } from '@/misc/nyaize.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { User } from '@/models/entities/User.js'; -import type { Note } from '@/models/entities/Note.js'; -import type { NoteReaction } from '@/models/entities/NoteReaction.js'; -import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, DriveFilesRepository } from '@/models/index.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiNote } from '@/models/Note.js'; +import type { MiNoteReaction } from '@/models/NoteReaction.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { isNotNull } from '@/misc/is-not-null.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -24,13 +29,10 @@ export class NoteEntityService implements OnModuleInit { private driveFileEntityService: DriveFileEntityService; private customEmojiService: CustomEmojiService; private reactionService: ReactionService; - + constructor( private moduleRef: ModuleRef, - @Inject(DI.db) - private db: DataSource, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -52,9 +54,6 @@ export class NoteEntityService implements OnModuleInit { @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, - //private userEntityService: UserEntityService, //private driveFileEntityService: DriveFileEntityService, //private customEmojiService: CustomEmojiService, @@ -68,9 +67,9 @@ export class NoteEntityService implements OnModuleInit { this.customEmojiService = this.moduleRef.get('CustomEmojiService'); this.reactionService = this.moduleRef.get('ReactionService'); } - + @bindThis - private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) { + private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null) { // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) let hide = false; @@ -106,16 +105,14 @@ export class NoteEntityService implements OnModuleInit { hide = false; } else { // フォロワーかどうか - const following = await this.followingsRepository.findOneBy({ - followeeId: packedNote.userId, - followerId: meId, + const isFollowing = await this.followingsRepository.exist({ + where: { + followeeId: packedNote.userId, + followerId: meId, + }, }); - if (following == null) { - hide = true; - } else { - hide = false; - } + hide = !isFollowing; } } @@ -131,7 +128,7 @@ export class NoteEntityService implements OnModuleInit { } @bindThis - private async populatePoll(note: Note, meId: User['id'] | null) { + private async populatePoll(note: MiNote, meId: MiUser['id'] | null) { const poll = await this.pollsRepository.findOneByOrFail({ noteId: note.id }); const choices = poll.choices.map(c => ({ text: c, @@ -170,8 +167,8 @@ export class NoteEntityService implements OnModuleInit { } @bindThis - private async populateMyReaction(note: Note, meId: User['id'], _hint_?: { - myReactions: Map; + private async populateMyReaction(note: MiNote, meId: MiUser['id'], _hint_?: { + myReactions: Map; }) { if (_hint_?.myReactions) { const reaction = _hint_.myReactions.get(note.id); @@ -201,7 +198,7 @@ export class NoteEntityService implements OnModuleInit { } @bindThis - public async isVisibleForMe(note: Note, meId: User['id'] | null): Promise { + public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null): Promise { // This code must always be synchronized with the checks in generateVisibilityQuery. // visibility が specified かつ自分が指定されていなかったら非表示 if (note.visibility === 'specified') { @@ -255,7 +252,7 @@ export class NoteEntityService implements OnModuleInit { } @bindThis - public async packAttachedFiles(fileIds: Note['fileIds'], packedFiles: Map | null>): Promise[]> { + public async packAttachedFiles(fileIds: MiNote['fileIds'], packedFiles: Map | null>): Promise[]> { const missingIds = []; for (const id of fileIds) { if (!packedFiles.has(id)) missingIds.push(id); @@ -271,14 +268,14 @@ export class NoteEntityService implements OnModuleInit { @bindThis public async pack( - src: Note['id'] | Note, - me?: { id: User['id'] } | null | undefined, + src: MiNote['id'] | MiNote, + me?: { id: MiUser['id'] } | null | undefined, options?: { detail?: boolean; skipHide?: boolean; _hint_?: { - myReactions: Map; - packedFiles: Map | null>; + myReactions: Map; + packedFiles: Map | null>; }; }, ): Promise> { @@ -336,12 +333,15 @@ export class NoteEntityService implements OnModuleInit { id: channel.id, name: channel.name, color: channel.color, + isSensitive: channel.isSensitive, } : undefined, mentions: note.mentions.length > 0 ? note.mentions : undefined, uri: note.uri ?? undefined, url: note.url ?? undefined, ...(opts.detail ? { + clippedCount: note.clippedCount, + reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, { detail: false, _hint_: options?._hint_, @@ -388,8 +388,8 @@ export class NoteEntityService implements OnModuleInit { @bindThis public async packMany( - notes: Note[], - me?: { id: User['id'] } | null | undefined, + notes: MiNote[], + me?: { id: MiUser['id'] } | null | undefined, options?: { detail?: boolean; skipHide?: boolean; @@ -398,7 +398,7 @@ export class NoteEntityService implements OnModuleInit { if (notes.length === 0) return []; const meId = me ? me.id : null; - const myReactionsMap = new Map(); + const myReactionsMap = new Map(); if (meId) { const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!); // パフォーマンスのためノートが作成されてから1秒以上経っていない場合はリアクションを取得しない @@ -428,7 +428,7 @@ export class NoteEntityService implements OnModuleInit { } @bindThis - public aggregateNoteEmojis(notes: Note[]) { + public aggregateNoteEmojis(notes: MiNote[]) { let emojis: { name: string | null; host: string | null; }[] = []; for (const note of notes) { emojis = emojis.concat(note.emojis @@ -457,12 +457,12 @@ export class NoteEntityService implements OnModuleInit { const query = this.notesRepository.createQueryBuilder('note') .where('note.userId = :userId', { userId }) .andWhere('note.renoteId = :renoteId', { renoteId }); - + // 指定した投稿を除く if (excludeNoteId) { query.andWhere('note.id != :excludeNoteId', { excludeNoteId }); } - + return await query.getCount(); - } + } } diff --git a/packages/backend/src/core/entities/NoteFavoriteEntityService.ts b/packages/backend/src/core/entities/NoteFavoriteEntityService.ts index 8a7727b4cd..808c8c9f69 100644 --- a/packages/backend/src/core/entities/NoteFavoriteEntityService.ts +++ b/packages/backend/src/core/entities/NoteFavoriteEntityService.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { NoteFavoritesRepository } from '@/models/index.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 { NoteEntityService } from './NoteEntityService.js'; +import type { NoteFavoritesRepository } from '@/models/_.js'; +import type { } from '@/models/Blocking.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiNoteFavorite } from '@/models/NoteFavorite.js'; import { bindThis } from '@/decorators.js'; +import { NoteEntityService } from './NoteEntityService.js'; @Injectable() export class NoteFavoriteEntityService { @@ -19,8 +24,8 @@ export class NoteFavoriteEntityService { @bindThis public async pack( - src: NoteFavorite['id'] | NoteFavorite, - me?: { id: User['id'] } | null | undefined, + src: MiNoteFavorite['id'] | MiNoteFavorite, + me?: { id: MiUser['id'] } | null | undefined, ) { const favorite = typeof src === 'object' ? src : await this.noteFavoritesRepository.findOneByOrFail({ id: src }); @@ -35,7 +40,7 @@ export class NoteFavoriteEntityService { @bindThis public packMany( favorites: any[], - me: { id: User['id'] }, + me: { id: MiUser['id'] }, ) { return Promise.all(favorites.map(x => this.pack(x, me))); } diff --git a/packages/backend/src/core/entities/NoteReactionEntityService.ts b/packages/backend/src/core/entities/NoteReactionEntityService.ts index 8f943ba24c..9701f37fdb 100644 --- a/packages/backend/src/core/entities/NoteReactionEntityService.ts +++ b/packages/backend/src/core/entities/NoteReactionEntityService.ts @@ -1,12 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { NoteReactionsRepository } from '@/models/index.js'; +import type { NoteReactionsRepository } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; import { bindThis } from '@/decorators.js'; import type { OnModuleInit } from '@nestjs/common'; -import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; -import type { NoteReaction } from '@/models/entities/NoteReaction.js'; +import type { } from '@/models/Blocking.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiNoteReaction } from '@/models/NoteReaction.js'; import type { ReactionService } from '../ReactionService.js'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; @@ -17,7 +22,7 @@ export class NoteReactionEntityService implements OnModuleInit { private userEntityService: UserEntityService; private noteEntityService: NoteEntityService; private reactionService: ReactionService; - + constructor( private moduleRef: ModuleRef, @@ -38,8 +43,8 @@ export class NoteReactionEntityService implements OnModuleInit { @bindThis public async pack( - src: NoteReaction['id'] | NoteReaction, - me?: { id: User['id'] } | null | undefined, + src: MiNoteReaction['id'] | MiNoteReaction, + me?: { id: MiUser['id'] } | null | undefined, options?: { withNote: boolean; }, diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index d76b863957..3ee7c91f3a 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { AccessTokensRepository, FollowRequestsRepository, NoteReactionsRepository, NotesRepository, User, UsersRepository } from '@/models/index.js'; +import type { AccessTokensRepository, FollowRequestsRepository, NotesRepository, MiUser, UsersRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Notification } from '@/models/entities/Notification.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { MiNotification } from '@/models/Notification.js'; +import type { MiNote } from '@/models/Note.js'; import type { Packed } from '@/misc/json-schema.js'; import { bindThis } from '@/decorators.js'; import { isNotNull } from '@/misc/is-not-null.js'; @@ -15,7 +20,7 @@ import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; -const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]); +const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]); @Injectable() export class NotificationEntityService implements OnModuleInit { @@ -32,9 +37,6 @@ export class NotificationEntityService implements OnModuleInit { @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.noteReactionsRepository) - private noteReactionsRepository: NoteReactionsRepository, - @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, @@ -55,15 +57,15 @@ export class NotificationEntityService implements OnModuleInit { @bindThis public async pack( - src: Notification, - meId: User['id'], + src: MiNotification, + meId: MiUser['id'], // eslint-disable-next-line @typescript-eslint/ban-types options: { - + }, hint?: { - packedNotes: Map>; - packedUsers: Map>; + packedNotes: Map>; + packedUsers: Map>; }, ): Promise> { const notification = src; @@ -106,8 +108,8 @@ export class NotificationEntityService implements OnModuleInit { @bindThis public async packMany( - notifications: Notification[], - meId: User['id'], + notifications: MiNotification[], + meId: MiUser['id'], ) { if (notifications.length === 0) return []; diff --git a/packages/backend/src/core/entities/PageEntityService.ts b/packages/backend/src/core/entities/PageEntityService.ts index d6da856637..e3a1e19ddd 100644 --- a/packages/backend/src/core/entities/PageEntityService.ts +++ b/packages/backend/src/core/entities/PageEntityService.ts @@ -1,12 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, PagesRepository, PageLikesRepository } from '@/models/index.js'; +import type { DriveFilesRepository, PagesRepository, PageLikesRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; -import type { Page } from '@/models/entities/Page.js'; -import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { } from '@/models/Blocking.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiPage } from '@/models/Page.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; import { bindThis } from '@/decorators.js'; import { UserEntityService } from './UserEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js'; @@ -30,13 +35,13 @@ export class PageEntityService { @bindThis public async pack( - src: Page['id'] | Page, - me?: { id: User['id'] } | null | undefined, + src: MiPage['id'] | MiPage, + me?: { id: MiUser['id'] } | null | undefined, ): Promise> { const meId = me ? me.id : null; const page = typeof src === 'object' ? src : await this.pagesRepository.findOneByOrFail({ id: src }); - const attachedFiles: Promise[] = []; + const attachedFiles: Promise[] = []; const collectFile = (xs: any[]) => { for (const x of xs) { if (x.type === 'image') { @@ -95,16 +100,16 @@ export class PageEntityService { script: page.script, eyeCatchingImageId: page.eyeCatchingImageId, eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null, - attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is DriveFile => x != null)), + attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is MiDriveFile => x != null)), likedCount: page.likedCount, - isLiked: meId ? await this.pageLikesRepository.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined, + isLiked: meId ? await this.pageLikesRepository.exist({ where: { pageId: page.id, userId: meId } }) : undefined, }); } @bindThis public packMany( - pages: Page[], - me?: { id: User['id'] } | null | undefined, + pages: MiPage[], + me?: { id: MiUser['id'] } | null | undefined, ) { return Promise.all(pages.map(x => this.pack(x, me))); } diff --git a/packages/backend/src/core/entities/PageLikeEntityService.ts b/packages/backend/src/core/entities/PageLikeEntityService.ts index 3460c1e422..4dc691ab93 100644 --- a/packages/backend/src/core/entities/PageLikeEntityService.ts +++ b/packages/backend/src/core/entities/PageLikeEntityService.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { PageLikesRepository } from '@/models/index.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 { PageEntityService } from './PageEntityService.js'; +import type { PageLikesRepository } from '@/models/_.js'; +import type { } from '@/models/Blocking.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiPageLike } from '@/models/PageLike.js'; import { bindThis } from '@/decorators.js'; +import { PageEntityService } from './PageEntityService.js'; @Injectable() export class PageLikeEntityService { @@ -19,8 +24,8 @@ export class PageLikeEntityService { @bindThis public async pack( - src: PageLike['id'] | PageLike, - me?: { id: User['id'] } | null | undefined, + src: MiPageLike['id'] | MiPageLike, + me?: { id: MiUser['id'] } | null | undefined, ) { const like = typeof src === 'object' ? src : await this.pageLikesRepository.findOneByOrFail({ id: src }); @@ -33,7 +38,7 @@ export class PageLikeEntityService { @bindThis public packMany( likes: any[], - me: { id: User['id'] }, + me: { id: MiUser['id'] }, ) { return Promise.all(likes.map(x => this.pack(x, me))); } diff --git a/packages/backend/src/core/entities/RenoteMutingEntityService.ts b/packages/backend/src/core/entities/RenoteMutingEntityService.ts index f8871e0495..7111fab08a 100644 --- a/packages/backend/src/core/entities/RenoteMutingEntityService.ts +++ b/packages/backend/src/core/entities/RenoteMutingEntityService.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { RenoteMutingsRepository } from '@/models/index.js'; +import type { RenoteMutingsRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { User } from '@/models/entities/User.js'; -import type { RenoteMuting } from '@/models/entities/RenoteMuting.js'; +import type { } from '@/models/Blocking.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiRenoteMuting } from '@/models/RenoteMuting.js'; import { bindThis } from '@/decorators.js'; import { UserEntityService } from './UserEntityService.js'; @@ -21,8 +26,8 @@ export class RenoteMutingEntityService { @bindThis public async pack( - src: RenoteMuting['id'] | RenoteMuting, - me?: { id: User['id'] } | null | undefined, + src: MiRenoteMuting['id'] | MiRenoteMuting, + me?: { id: MiUser['id'] } | null | undefined, ): Promise> { const muting = typeof src === 'object' ? src : await this.renoteMutingsRepository.findOneByOrFail({ id: src }); @@ -39,7 +44,7 @@ export class RenoteMutingEntityService { @bindThis public packMany( mutings: any[], - me: { id: User['id'] }, + me: { id: MiUser['id'] }, ) { return Promise.all(mutings.map(x => this.pack(x, me))); } diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts index 54818782dd..23e82561d6 100644 --- a/packages/backend/src/core/entities/RoleEntityService.ts +++ b/packages/backend/src/core/entities/RoleEntityService.ts @@ -1,13 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + 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 type { RoleAssignmentsRepository, RolesRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { User } from '@/models/entities/User.js'; -import type { Role } from '@/models/entities/Role.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiRole } from '@/models/Role.js'; import { bindThis } from '@/decorators.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; -import { UserEntityService } from './UserEntityService.js'; @Injectable() export class RoleEntityService { @@ -17,15 +21,13 @@ export class RoleEntityService { @Inject(DI.roleAssignmentsRepository) private roleAssignmentsRepository: RoleAssignmentsRepository, - - private userEntityService: UserEntityService, ) { } @bindThis public async pack( - src: Role['id'] | Role, - me?: { id: User['id'] } | null | undefined, + src: MiRole['id'] | MiRole, + me?: { id: MiUser['id'] } | null | undefined, ) { const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src }); @@ -71,7 +73,7 @@ export class RoleEntityService { @bindThis public packMany( roles: any[], - me: { id: User['id'] }, + me: { id: MiUser['id'] }, ) { 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 51fa7543d9..8c88e8560a 100644 --- a/packages/backend/src/core/entities/SigninEntityService.ts +++ b/packages/backend/src/core/entities/SigninEntityService.ts @@ -1,24 +1,22 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { SigninsRepository } from '@/models/index.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { Signin } from '@/models/entities/Signin.js'; -import { UserEntityService } from './UserEntityService.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import type { } from '@/models/Blocking.js'; +import type { MiSignin } from '@/models/Signin.js'; import { bindThis } from '@/decorators.js'; @Injectable() export class SigninEntityService { constructor( - @Inject(DI.signinsRepository) - private signinsRepository: SigninsRepository, - - private userEntityService: UserEntityService, ) { } @bindThis public async pack( - src: Signin, + src: MiSignin, ) { return src; } diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index bfd506ea86..3dd64ce625 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -1,7 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import { In, Not } from 'typeorm'; import * as Redis from 'ioredis'; -import Ajv from 'ajv'; +import _Ajv from 'ajv'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; @@ -9,15 +13,15 @@ import type { Packed } from '@/misc/json-schema.js'; import type { Promiseable } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; -import type { LocalUser, PartialLocalUser, PartialRemoteUser, 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, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/index.js'; +import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; +import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/User.js'; +import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, MiUserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import type { OnModuleInit } from '@nestjs/common'; -import type { AntennaService } from '../AntennaService.js'; +import type { AnnouncementService } from '../AnnouncementService.js'; import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { NoteEntityService } from './NoteEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js'; @@ -31,17 +35,18 @@ type IsMeAndIsUserDetailed : Packed<'UserLite'>; +const Ajv = _Ajv.default; const ajv = new Ajv(); -function isLocalUser(user: User): user is LocalUser; -function isLocalUser(user: T): user is (T & { host: null; }); -function isLocalUser(user: User | { host: User['host'] }): boolean { +function isLocalUser(user: MiUser): user is MiLocalUser; +function isLocalUser(user: T): user is (T & { host: null; }); +function isLocalUser(user: MiUser | { host: MiUser['host'] }): boolean { return user.host == null; } -function isRemoteUser(user: User): user is RemoteUser; -function isRemoteUser(user: T): user is (T & { host: string; }); -function isRemoteUser(user: User | { host: User['host'] }): boolean { +function isRemoteUser(user: MiUser): user is MiRemoteUser; +function isRemoteUser(user: T): user is (T & { host: string; }); +function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean { return !isLocalUser(user); } @@ -52,7 +57,7 @@ export class UserEntityService implements OnModuleInit { private driveFileEntityService: DriveFileEntityService; private pageEntityService: PageEntityService; private customEmojiService: CustomEmojiService; - private antennaService: AntennaService; + private announcementService: AnnouncementService; private roleService: RoleService; private federatedInstanceService: FederatedInstanceService; @@ -92,27 +97,18 @@ export class UserEntityService implements OnModuleInit { @Inject(DI.noteUnreadsRepository) private noteUnreadsRepository: NoteUnreadsRepository, - @Inject(DI.channelFollowingsRepository) - private channelFollowingsRepository: ChannelFollowingsRepository, - @Inject(DI.userNotePiningsRepository) private userNotePiningsRepository: UserNotePiningsRepository, @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - @Inject(DI.instancesRepository) - private instancesRepository: InstancesRepository, - @Inject(DI.announcementReadsRepository) private announcementReadsRepository: AnnouncementReadsRepository, @Inject(DI.announcementsRepository) private announcementsRepository: AnnouncementsRepository, - @Inject(DI.pagesRepository) - private pagesRepository: PagesRepository, - @Inject(DI.userMemosRepository) private userMemosRepository: UserMemoRepository, @@ -131,7 +127,7 @@ export class UserEntityService implements OnModuleInit { this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); this.pageEntityService = this.moduleRef.get('PageEntityService'); this.customEmojiService = this.moduleRef.get('CustomEmojiService'); - this.antennaService = this.moduleRef.get('AntennaService'); + this.announcementService = this.moduleRef.get('AnnouncementService'); this.roleService = this.moduleRef.get('RoleService'); this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); } @@ -149,16 +145,15 @@ export class UserEntityService implements OnModuleInit { public isRemoteUser = isRemoteUser; @bindThis - public async getRelation(me: User['id'], target: User['id']) { + public async getRelation(me: MiUser['id'], target: MiUser['id']) { + const following = await this.followingsRepository.findOneBy({ + followerId: me, + followeeId: target, + }); return awaitAll({ id: target, - isFollowing: this.followingsRepository.count({ - where: { - followerId: me, - followeeId: target, - }, - take: 1, - }).then(n => n > 0), + following, + isFollowing: following != null, isFollowed: this.followingsRepository.count({ where: { followerId: target, @@ -212,35 +207,24 @@ export class UserEntityService implements OnModuleInit { } @bindThis - public async getHasUnreadAnnouncement(userId: User['id']): Promise { - const reads = await this.announcementReadsRepository.findBy({ - userId: userId, - }); - - const count = await this.announcementsRepository.countBy(reads.length > 0 ? { - id: Not(In(reads.map(read => read.announcementId))), - } : {}); - - return count > 0; - } - - @bindThis - public async getHasUnreadAntenna(userId: User['id']): Promise { + public async getHasUnreadAntenna(userId: MiUser['id']): Promise { /* const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId); - const unread = myAntennas.length > 0 ? await this.antennaNotesRepository.findOneBy({ - antennaId: In(myAntennas.map(x => x.id)), - read: false, - }) : null; + const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exist({ + where: { + antennaId: In(myAntennas.map(x => x.id)), + read: false, + }, + }) : false); - return unread != null; + return isUnread; */ return false; // TODO } @bindThis - public async getHasUnreadNotification(userId: User['id']): Promise { + public async getHasUnreadNotification(userId: MiUser['id']): Promise { const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`); const latestNotificationIdsRes = await this.redisClient.xrevrange( @@ -254,7 +238,7 @@ export class UserEntityService implements OnModuleInit { } @bindThis - public async getHasPendingReceivedFollowRequest(userId: User['id']): Promise { + public async getHasPendingReceivedFollowRequest(userId: MiUser['id']): Promise { const count = await this.followRequestsRepository.countBy({ followeeId: userId, }); @@ -263,7 +247,7 @@ export class UserEntityService implements OnModuleInit { } @bindThis - public getOnlineStatus(user: User): 'unknown' | 'online' | 'active' | 'offline' { + public getOnlineStatus(user: MiUser): 'unknown' | 'online' | 'active' | 'offline' { if (user.hideOnlineStatus) return 'unknown'; if (user.lastActiveDate == null) return 'unknown'; const elapsed = Date.now() - user.lastActiveDate.getTime(); @@ -275,12 +259,12 @@ export class UserEntityService implements OnModuleInit { } @bindThis - public getIdenticonUrl(user: User): string { + public getIdenticonUrl(user: MiUser): string { return `${this.config.url}/identicon/${user.username.toLowerCase()}@${user.host ?? this.config.host}`; } @bindThis - public getUserUri(user: LocalUser | PartialLocalUser | RemoteUser | PartialRemoteUser): string { + public getUserUri(user: MiLocalUser | MiPartialLocalUser | MiRemoteUser | MiPartialRemoteUser): string { return this.isRemoteUser(user) ? user.uri : this.genLocalUserUri(user.id); } @@ -291,12 +275,12 @@ export class UserEntityService implements OnModuleInit { } public async pack( - src: User['id'] | User, - me?: { id: User['id']; } | null | undefined, + src: MiUser['id'] | MiUser, + me?: { id: MiUser['id']; } | null | undefined, options?: { detail?: D, includeSecrets?: boolean, - userProfile?: UserProfile, + userProfile?: MiUserProfile, }, ): Promise> { const opts = Object.assign({ @@ -326,7 +310,7 @@ export class UserEntityService implements OnModuleInit { const meId = me ? me.id : null; const isMe = meId === user.id; - const iAmModerator = me ? await this.roleService.isModerator(me as User) : false; + const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; const relation = meId && !isMe && opts.detail ? await this.getRelation(meId, user.id) : null; const pins = opts.detail ? await this.userNotePiningsRepository.createQueryBuilder('pin') @@ -348,6 +332,7 @@ export class UserEntityService implements OnModuleInit { const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null; const isAdmin = isMe && opts.detail ? this.roleService.isAdministrator(user) : null; + const unreadAnnouncements = isMe && opts.detail ? await this.announcementService.getUnreadAnnouncements(user) : null; const falsy = opts.detail ? false : undefined; @@ -398,6 +383,7 @@ export class UserEntityService implements OnModuleInit { birthday: profile!.birthday, lang: profile!.lang, fields: profile!.fields, + verifiedLinks: profile!.verifiedLinks, followersCount: followersCount ?? 0, followingCount: followingCount ?? 0, notesCount: user.notesCount, @@ -448,6 +434,7 @@ export class UserEntityService implements OnModuleInit { preventAiLearning: profile!.preventAiLearning, isExplorable: user.isExplorable, isDeleted: user.isDeleted, + twoFactorBackupCodesStock: profile?.twoFactorBackupSecret?.length === 5 ? 'full' : (profile?.twoFactorBackupSecret?.length ?? 0) > 0 ? 'partial' : 'none', hideOnlineStatus: user.hideOnlineStatus, hasUnreadSpecifiedNotes: this.noteUnreadsRepository.count({ where: { userId: user.id, isSpecified: true }, @@ -457,7 +444,8 @@ export class UserEntityService implements OnModuleInit { where: { userId: user.id, isMentioned: true }, take: 1, }).then(count => count > 0), - hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id), + hasUnreadAnnouncement: unreadAnnouncements!.length > 0, + unreadAnnouncements, hasUnreadAntenna: this.getHasUnreadAntenna(user.id), hasUnreadChannel: false, // 後方互換性のため hasUnreadNotification: this.getHasUnreadNotification(user.id), @@ -497,6 +485,7 @@ export class UserEntityService implements OnModuleInit { isBlocked: relation.isBlocked, isMuted: relation.isMuted, isRenoteMuted: relation.isRenoteMuted, + notify: relation.following?.notify ?? 'none', } : {}), } as Promiseable> as Promiseable>; @@ -504,8 +493,8 @@ export class UserEntityService implements OnModuleInit { } public packMany( - users: (User['id'] | User)[], - me?: { id: User['id'] } | null | undefined, + users: (MiUser['id'] | MiUser)[], + me?: { id: MiUser['id'] } | null | undefined, options?: { detail?: D, includeSecrets?: boolean, diff --git a/packages/backend/src/core/entities/UserListEntityService.ts b/packages/backend/src/core/entities/UserListEntityService.ts index 8628819278..a7f2885194 100644 --- a/packages/backend/src/core/entities/UserListEntityService.ts +++ b/packages/backend/src/core/entities/UserListEntityService.ts @@ -1,11 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; +import type { UserListJoiningsRepository, UserListsRepository } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/entities/Blocking.js'; -import type { UserList } from '@/models/entities/UserList.js'; +import type { } from '@/models/Blocking.js'; +import type { MiUserList } from '@/models/UserList.js'; import { bindThis } from '@/decorators.js'; -import { UserEntityService } from './UserEntityService.js'; @Injectable() export class UserListEntityService { @@ -15,14 +19,12 @@ export class UserListEntityService { @Inject(DI.userListJoiningsRepository) private userListJoiningsRepository: UserListJoiningsRepository, - - private userEntityService: UserEntityService, ) { } @bindThis public async pack( - src: UserList['id'] | UserList, + src: MiUserList['id'] | MiUserList, ): Promise> { const userList = typeof src === 'object' ? src : await this.userListsRepository.findOneByOrFail({ id: src }); diff --git a/packages/backend/src/daemons/DaemonModule.ts b/packages/backend/src/daemons/DaemonModule.ts index 683f9cbfe3..236985076c 100644 --- a/packages/backend/src/daemons/DaemonModule.ts +++ b/packages/backend/src/daemons/DaemonModule.ts @@ -1,7 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Module } from '@nestjs/common'; import { CoreModule } from '@/core/CoreModule.js'; import { GlobalModule } from '@/GlobalModule.js'; -import { JanitorService } from './JanitorService.js'; import { QueueStatsService } from './QueueStatsService.js'; import { ServerStatsService } from './ServerStatsService.js'; @@ -11,12 +15,10 @@ import { ServerStatsService } from './ServerStatsService.js'; CoreModule, ], providers: [ - JanitorService, QueueStatsService, ServerStatsService, ], exports: [ - JanitorService, QueueStatsService, ServerStatsService, ], diff --git a/packages/backend/src/daemons/JanitorService.ts b/packages/backend/src/daemons/JanitorService.ts deleted file mode 100644 index f826d50625..0000000000 --- a/packages/backend/src/daemons/JanitorService.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { LessThan } from 'typeorm'; -import { DI } from '@/di-symbols.js'; -import type { AttestationChallengesRepository } from '@/models/index.js'; -import { bindThis } from '@/decorators.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; - -const interval = 30 * 60 * 1000; - -@Injectable() -export class JanitorService implements OnApplicationShutdown { - private intervalId: NodeJS.Timer; - - constructor( - @Inject(DI.attestationChallengesRepository) - private attestationChallengesRepository: AttestationChallengesRepository, - ) { - } - - /** - * Clean up database occasionally - */ - @bindThis - public start(): void { - const tick = async () => { - await this.attestationChallengesRepository.delete({ - createdAt: LessThan(new Date(new Date().getTime() - 5 * 60 * 1000)), - }); - }; - - tick(); - - this.intervalId = setInterval(tick, interval); - } - - @bindThis - public dispose(): void { - clearInterval(this.intervalId); - } - - @bindThis - public onApplicationShutdown(signal?: string | undefined): void { - this.dispose(); - } -} diff --git a/packages/backend/src/daemons/QueueStatsService.ts b/packages/backend/src/daemons/QueueStatsService.ts index 53a0d14cd7..5edc0f45ab 100644 --- a/packages/backend/src/daemons/QueueStatsService.ts +++ b/packages/backend/src/daemons/QueueStatsService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import Xev from 'xev'; import * as Bull from 'bullmq'; @@ -14,7 +19,7 @@ const interval = 10000; @Injectable() export class QueueStatsService implements OnApplicationShutdown { - private intervalId: NodeJS.Timer; + private intervalId: NodeJS.Timeout; constructor( @Inject(DI.config) @@ -81,7 +86,7 @@ export class QueueStatsService implements OnApplicationShutdown { this.intervalId = setInterval(tick, interval); } - + @bindThis public dispose(): void { clearInterval(this.intervalId); diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts index 6cd71c0e2a..d294628740 100644 --- a/packages/backend/src/daemons/ServerStatsService.ts +++ b/packages/backend/src/daemons/ServerStatsService.ts @@ -1,8 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import si from 'systeminformation'; import Xev from 'xev'; import * as osUtils from 'os-utils'; import { bindThis } from '@/decorators.js'; +import { MetaService } from '@/core/MetaService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; const ev = new Xev(); @@ -14,9 +20,10 @@ const round = (num: number) => Math.round(num * 10) / 10; @Injectable() export class ServerStatsService implements OnApplicationShutdown { - private intervalId: NodeJS.Timer; + private intervalId: NodeJS.Timeout | null = null; constructor( + private metaService: MetaService, ) { } @@ -24,7 +31,9 @@ export class ServerStatsService implements OnApplicationShutdown { * Report server stats regularly */ @bindThis - public start(): void { + public async start(): Promise { + if (!(await this.metaService.fetch(true)).enableServerMachineStats) return; + const log = [] as any[]; ev.on('requestServerStatsLog', x => { @@ -64,7 +73,9 @@ export class ServerStatsService implements OnApplicationShutdown { @bindThis public dispose(): void { - clearInterval(this.intervalId); + if (this.intervalId) { + clearInterval(this.intervalId); + } } @bindThis diff --git a/packages/backend/src/decorators.ts b/packages/backend/src/decorators.ts index db23317eef..6b439978db 100644 --- a/packages/backend/src/decorators.ts +++ b/packages/backend/src/decorators.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + // https://github.com/andreypopp/autobind-decorator /** diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 4a073f102f..72ec98cebe 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const DI = { config: Symbol('config'), db: Symbol('db'), @@ -21,7 +26,6 @@ export const DI = { userProfilesRepository: Symbol('userProfilesRepository'), userKeypairsRepository: Symbol('userKeypairsRepository'), userPendingsRepository: Symbol('userPendingsRepository'), - attestationChallengesRepository: Symbol('attestationChallengesRepository'), userSecurityKeysRepository: Symbol('userSecurityKeysRepository'), userPublickeysRepository: Symbol('userPublickeysRepository'), userListsRepository: Symbol('userListsRepository'), diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts index d7c8304b47..af1c3bdd3c 100644 --- a/packages/backend/src/env.ts +++ b/packages/backend/src/env.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + const envOption = { onlyQueue: false, onlyServer: false, diff --git a/packages/backend/src/global.d.ts b/packages/backend/src/global.d.ts index 7343aa1994..a9e6243cc4 100644 --- a/packages/backend/src/global.d.ts +++ b/packages/backend/src/global.d.ts @@ -1 +1,6 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + type FIXME = any; diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts index 91039098f1..5c10559ec6 100644 --- a/packages/backend/src/logger.ts +++ b/packages/backend/src/logger.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import cluster from 'node:cluster'; import chalk from 'chalk'; import { default as convertColor } from 'color-convert'; import { format as dateFormat } from 'date-fns'; import { bindThis } from '@/decorators.js'; import { envOption } from './env.js'; -import type { KEYWORD } from 'color-convert/conversions'; +import type { KEYWORD } from 'color-convert/conversions.js'; type Context = { name: string; @@ -13,6 +18,7 @@ type Context = { type Level = 'error' | 'success' | 'warning' | 'debug' | 'info'; +// eslint-disable-next-line import/no-default-export export default class Logger { private context: Context; private parentLogger: Logger | null = null; diff --git a/packages/backend/src/misc/acct.ts b/packages/backend/src/misc/acct.ts index d1a6852a95..5db72746c0 100644 --- a/packages/backend/src/misc/acct.ts +++ b/packages/backend/src/misc/acct.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export type Acct = { username: string; host: string | null; }; export function parse(acct: string): Acct { - if (acct.startsWith('@')) acct = acct.substr(1); + if (acct.startsWith('@')) acct = acct.substring(1); const split = acct.split('@', 2); return { username: split[0], host: split[1] ?? null }; } diff --git a/packages/backend/src/misc/api-permissions.ts b/packages/backend/src/misc/api-permissions.ts index 160cdf9fd6..57c9308844 100644 --- a/packages/backend/src/misc/api-permissions.ts +++ b/packages/backend/src/misc/api-permissions.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const kinds = [ 'read:account', 'write:account', diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 5610929648..c235871931 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as Redis from 'ioredis'; import { bindThis } from '@/decorators.js'; @@ -83,6 +88,16 @@ export class RedisKVCache { // TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする } + + @bindThis + public gc() { + this.memoryCache.gc(); + } + + @bindThis + public dispose() { + this.memoryCache.dispose(); + } } export class RedisSingleCache { @@ -171,20 +186,39 @@ export class RedisSingleCache { // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? -export class MemoryKVCache { - public cache: Map; - private lifetime: number; +function nothingToDo(value: T): V { + return value as unknown as V; +} - constructor(lifetime: MemoryKVCache['lifetime']) { +export class MemoryKVCache { + public cache: Map; + private lifetime: number; + private gcIntervalHandle: NodeJS.Timeout; + private toMapConverter: (value: T) => V; + private fromMapConverter: (cached: V) => T | undefined; + + constructor(lifetime: MemoryKVCache['lifetime'], options: { + toMapConverter: (value: T) => V; + fromMapConverter: (cached: V) => T | undefined; + } = { + toMapConverter: nothingToDo, + fromMapConverter: nothingToDo, + }) { this.cache = new Map(); this.lifetime = lifetime; + this.toMapConverter = options.toMapConverter; + this.fromMapConverter = options.fromMapConverter; + + this.gcIntervalHandle = setInterval(() => { + this.gc(); + }, 1000 * 60 * 3); } @bindThis public set(key: string, value: T): void { this.cache.set(key, { date: Date.now(), - value, + value: this.toMapConverter(value), }); } @@ -196,20 +230,21 @@ export class MemoryKVCache { this.cache.delete(key); return undefined; } - return cached.value; + return this.fromMapConverter(cached.value); } @bindThis - public delete(key: string) { + public delete(key: string): void { this.cache.delete(key); } /** * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + * fetcherの引数はcacheに保存されている値があれば渡されます */ @bindThis - public async fetch(key: string, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { + public async fetch(key: string, fetcher: (value: V | undefined) => Promise, validator?: (cachedValue: T) => boolean): Promise { const cachedValue = this.get(key); if (cachedValue !== undefined) { if (validator) { @@ -224,7 +259,7 @@ export class MemoryKVCache { } // Cache MISS - const value = await fetcher(); + const value = await fetcher(this.cache.get(key)?.value); this.set(key, value); return value; } @@ -232,9 +267,10 @@ export class MemoryKVCache { /** * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + * fetcherの引数はcacheに保存されている値があれば渡されます */ @bindThis - public async fetchMaybe(key: string, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { + public async fetchMaybe(key: string, fetcher: (value: V | undefined) => Promise, validator?: (cachedValue: T) => boolean): Promise { const cachedValue = this.get(key); if (cachedValue !== undefined) { if (validator) { @@ -249,12 +285,27 @@ export class MemoryKVCache { } // Cache MISS - const value = await fetcher(); + const value = await fetcher(this.cache.get(key)?.value); if (value !== undefined) { this.set(key, value); } return value; } + + @bindThis + public gc(): void { + const now = Date.now(); + for (const [key, { date }] of this.cache.entries()) { + if ((now - date) > this.lifetime) { + this.cache.delete(key); + } + } + } + + @bindThis + public dispose(): void { + clearInterval(this.gcIntervalHandle); + } } export class MemorySingleCache { diff --git a/packages/backend/src/misc/check-https.ts b/packages/backend/src/misc/check-https.ts index b33f019973..0b13ccabdd 100644 --- a/packages/backend/src/misc/check-https.ts +++ b/packages/backend/src/misc/check-https.ts @@ -1,4 +1,9 @@ -export function checkHttps(url: string) { - return url.startsWith('https://') || - (url.startsWith('http://') && process.env.NODE_ENV !== 'production'); +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function checkHttps(url: string): boolean { + return url.startsWith('https://') || + (url.startsWith('http://') && process.env.NODE_ENV !== 'production'); } diff --git a/packages/backend/src/misc/check-word-mute.ts b/packages/backend/src/misc/check-word-mute.ts index 910bebfcfe..cef5595451 100644 --- a/packages/backend/src/misc/check-word-mute.ts +++ b/packages/backend/src/misc/check-word-mute.ts @@ -1,16 +1,21 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { AhoCorasick } from 'slacc'; import RE2 from 're2'; -import type { Note } from '@/models/entities/Note.js'; -import type { User } from '@/models/entities/User.js'; +import type { MiNote } from '@/models/Note.js'; +import type { MiUser } from '@/models/User.js'; type NoteLike = { - userId: Note['userId']; - text: Note['text']; - cw?: Note['cw']; + userId: MiNote['userId']; + text: MiNote['text']; + cw?: MiNote['cw']; }; type UserLike = { - id: User['id']; + id: MiUser['id']; }; const acCache = new Map(); diff --git a/packages/backend/src/misc/clone.ts b/packages/backend/src/misc/clone.ts index 16fad24129..9d20deac3b 100644 --- a/packages/backend/src/misc/clone.ts +++ b/packages/backend/src/misc/clone.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + // structredCloneが遅いため // SEE: http://var.blog.jp/archives/86038606.html diff --git a/packages/backend/src/misc/content-disposition.ts b/packages/backend/src/misc/content-disposition.ts index b2aec471d5..1ac8c88d21 100644 --- a/packages/backend/src/misc/content-disposition.ts +++ b/packages/backend/src/misc/content-disposition.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import cd from 'content-disposition'; export function contentDisposition(type: 'inline' | 'attachment', filename: string): string { diff --git a/packages/backend/src/misc/correct-filename.ts b/packages/backend/src/misc/correct-filename.ts index 23a0699f39..9130af44c3 100644 --- a/packages/backend/src/misc/correct-filename.ts +++ b/packages/backend/src/misc/correct-filename.ts @@ -1,15 +1,58 @@ -// 与えられた拡張子とファイル名が一致しているかどうかを確認し、 -// 一致していない場合は拡張子を付与して返す +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * Array.includes()よりSet.has()の方が高速 + */ +const targetExtsToSkip = new Set([ + '.gz', + '.tar', + '.tgz', + '.bz2', + '.xz', + '.zip', + '.7z', +]); + +const extRegExp = /\.[0-9a-zA-Z]+$/i; + +/** + * 与えられた拡張子とファイル名が一致しているかどうかを確認し、 + * 一致していない場合は拡張子を付与して返す + * + * extはfile-typeのextを想定 + */ export function correctFilename(filename: string, ext: string | null) { - const dotExt = ext ? ext.startsWith('.') ? ext : `.${ext}` : '.unknown'; - if (filename.endsWith(dotExt)) { - return filename; - } - if (ext === 'jpg' && filename.endsWith('.jpeg')) { - return filename; - } - if (ext === 'tif' && filename.endsWith('.tiff')) { + const dotExt = ext ? ext[0] === '.' ? ext : `.${ext}` : '.unknown'; + + const match = extRegExp.exec(filename); + if (!match || !match[0]) { + // filenameが拡張子を持っていない場合は拡張子をつける + return `${filename}${dotExt}`; + } + + const filenameExt = match[0].toLowerCase(); + if ( + // 未知のファイル形式かつ拡張子がある場合は何もしない + ext === null || + // 拡張子が一致している場合は何もしない + filenameExt === dotExt || + + // jpeg, tiffを同一視 + dotExt === '.jpg' && filenameExt === '.jpeg' || + dotExt === '.tif' && filenameExt === '.tiff' || + // dllもexeもportable executableなので判定が正しく行われない + dotExt === '.exe' && filenameExt === '.dll' || + + // 圧縮形式っぽければ下手に拡張子を変えない + // https://github.com/misskey-dev/misskey/issues/11482 + targetExtsToSkip.has(dotExt) + ) { return filename; } + + // 拡張子があるが一致していないなどの場合は拡張子を付け足す return `${filename}${dotExt}`; } diff --git a/packages/backend/src/misc/create-temp.ts b/packages/backend/src/misc/create-temp.ts index 7b8942e308..2bb0e88489 100644 --- a/packages/backend/src/misc/create-temp.ts +++ b/packages/backend/src/misc/create-temp.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as tmp from 'tmp'; export function createTemp(): Promise<[string, () => void]> { diff --git a/packages/backend/src/misc/dev-null.ts b/packages/backend/src/misc/dev-null.ts index 38b9d82669..f510177c0b 100644 --- a/packages/backend/src/misc/dev-null.ts +++ b/packages/backend/src/misc/dev-null.ts @@ -1,11 +1,16 @@ -import { Writable, WritableOptions } from "node:stream"; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Writable, WritableOptions } from 'node:stream'; export class DevNull extends Writable implements NodeJS.WritableStream { - constructor(opts?: WritableOptions) { - super(opts); - } + constructor(opts?: WritableOptions) { + super(opts); + } - _write (chunk: any, encoding: BufferEncoding, cb: (err?: Error | null) => void) { - setImmediate(cb); - } + _write (chunk: any, encoding: BufferEncoding, cb: (err?: Error | null) => void) { + setImmediate(cb); + } } diff --git a/packages/backend/src/misc/emoji-regex.ts b/packages/backend/src/misc/emoji-regex.ts index 1c6f5776db..24e4092aeb 100644 --- a/packages/backend/src/misc/emoji-regex.ts +++ b/packages/backend/src/misc/emoji-regex.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + // taken from twemoji-parser/dist/lib/regex.js const twemojiRegex = /(?:\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83d\udc68\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc68\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83d\udc69\ud83c[\udffb-\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffc-\udfff]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffd-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffd\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\u2764\ufe0f\u200d\ud83e\uddd1\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83e\udef1\ud83c\udffb\u200d\ud83e\udef2\ud83c[\udffc-\udfff]|\ud83e\udef1\ud83c\udffc\u200d\ud83e\udef2\ud83c[\udffb\udffd-\udfff]|\ud83e\udef1\ud83c\udffd\u200d\ud83e\udef2\ud83c[\udffb\udffc\udffe\udfff]|\ud83e\udef1\ud83c\udffe\u200d\ud83e\udef2\ud83c[\udffb-\udffd\udfff]|\ud83e\udef1\ud83c\udfff\u200d\ud83e\udef2\ud83c[\udffb-\udffe]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d\udc8f\ud83c[\udffb-\udfff]|\ud83d\udc91\ud83c[\udffb-\udfff]|\ud83e\udd1d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d\udc8f\udc91]|\ud83e\udd1d)|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd4\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83d\ude36\u200d\ud83c\udf2b\ufe0f|\u2764\ufe0f\u200d\ud83d\udd25|\u2764\ufe0f\u200d\ud83e\ude79|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83d\ude2e\u200d\ud83d\udca8|\ud83d\ude35\u200d\ud83d\udcab|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd\udec3-\udec5\udef0-\udef6]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udc8e\udc90\udc92-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udedd-\udedf\udeeb\udeec\udef4-\udefc\udfe0-\udfeb\udff0]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78-\uddb4\uddb7\uddba\uddbc-\uddcc\uddd0\uddde-\uddff\ude70-\ude74\ude78-\ude7c\ude80-\ude86\ude90-\udeac\udeb0-\udeba\udec0-\udec2\uded0-\uded9\udee0-\udee7]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f/g; diff --git a/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts index 14c25922ad..0b898d47e8 100644 --- a/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts +++ b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as mfm from 'mfm-js'; import { unique } from '@/misc/prelude/array.js'; diff --git a/packages/backend/src/misc/extract-hashtags.ts b/packages/backend/src/misc/extract-hashtags.ts index d293fd7f52..3bd56e98eb 100644 --- a/packages/backend/src/misc/extract-hashtags.ts +++ b/packages/backend/src/misc/extract-hashtags.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as mfm from 'mfm-js'; import { unique } from '@/misc/prelude/array.js'; diff --git a/packages/backend/src/misc/extract-mentions.ts b/packages/backend/src/misc/extract-mentions.ts index c8762e797b..272eb92192 100644 --- a/packages/backend/src/misc/extract-mentions.ts +++ b/packages/backend/src/misc/extract-mentions.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + // test is located in test/extract-mentions import * as mfm from 'mfm-js'; diff --git a/packages/backend/src/misc/fastify-reply-error.ts b/packages/backend/src/misc/fastify-reply-error.ts index 4e987175e2..7c889bab7a 100644 --- a/packages/backend/src/misc/fastify-reply-error.ts +++ b/packages/backend/src/misc/fastify-reply-error.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + // https://www.fastify.io/docs/latest/Reference/Reply/#async-await-and-promises export class FastifyReplyError extends Error { public message: string; diff --git a/packages/backend/src/misc/gen-identicon.ts b/packages/backend/src/misc/gen-identicon.ts index b40745973e..c36b00af63 100644 --- a/packages/backend/src/misc/gen-identicon.ts +++ b/packages/backend/src/misc/gen-identicon.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + /** * Identicon generator * https://en.wikipedia.org/wiki/Identicon diff --git a/packages/backend/src/misc/gen-key-pair.ts b/packages/backend/src/misc/gen-key-pair.ts index e2ad598501..c0815613e7 100644 --- a/packages/backend/src/misc/gen-key-pair.ts +++ b/packages/backend/src/misc/gen-key-pair.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as crypto from 'node:crypto'; import * as util from 'node:util'; diff --git a/packages/backend/src/misc/generate-invite-code.ts b/packages/backend/src/misc/generate-invite-code.ts new file mode 100644 index 0000000000..7c88561179 --- /dev/null +++ b/packages/backend/src/misc/generate-invite-code.ts @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { secureRndstr } from './secure-rndstr.js'; + +const CHARS = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'; // [0-9A-Z] w/o [01IO] (32 patterns) + +export function generateInviteCode(): string { + const code = secureRndstr(8, { + chars: CHARS, + }); + + const uniqueId = []; + let n = Math.floor(Date.now() / 1000 / 60); + while (true) { + uniqueId.push(CHARS[n % CHARS.length]); + const t = Math.floor(n / CHARS.length); + if (!t) break; + n = t; + } + + return code + uniqueId.reverse().join(''); +} diff --git a/packages/backend/src/misc/generate-native-user-token.ts b/packages/backend/src/misc/generate-native-user-token.ts index 5d8a4c5378..094c625120 100644 --- a/packages/backend/src/misc/generate-native-user-token.ts +++ b/packages/backend/src/misc/generate-native-user-token.ts @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { secureRndstr } from '@/misc/secure-rndstr.js'; -export default () => secureRndstr(16, true); +// eslint-disable-next-line import/no-default-export +export default () => secureRndstr(16); diff --git a/packages/backend/src/misc/get-ip-hash.ts b/packages/backend/src/misc/get-ip-hash.ts index 70e61aef8c..3a01e4f578 100644 --- a/packages/backend/src/misc/get-ip-hash.ts +++ b/packages/backend/src/misc/get-ip-hash.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import IPCIDR from 'ip-cidr'; -export function getIpHash(ip: string) { +export function getIpHash(ip: string): string { try { // because a single person may control many IPv6 addresses, // only a /64 subnet prefix of any IP will be taken into account. diff --git a/packages/backend/src/misc/get-note-summary.ts b/packages/backend/src/misc/get-note-summary.ts index 964f20b25b..1bda5cdcf7 100644 --- a/packages/backend/src/misc/get-note-summary.ts +++ b/packages/backend/src/misc/get-note-summary.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import type { Packed } from './json-schema.js'; /** diff --git a/packages/backend/src/misc/get-reaction-emoji.ts b/packages/backend/src/misc/get-reaction-emoji.ts index c2e0b98582..80ef7ff7bc 100644 --- a/packages/backend/src/misc/get-reaction-emoji.ts +++ b/packages/backend/src/misc/get-reaction-emoji.ts @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// eslint-disable-next-line import/no-default-export export default function(reaction: string): string { switch (reaction) { case 'like': return '👍'; diff --git a/packages/backend/src/misc/i18n.ts b/packages/backend/src/misc/i18n.ts index b1c727827d..4c9d1a08e3 100644 --- a/packages/backend/src/misc/i18n.ts +++ b/packages/backend/src/misc/i18n.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class I18n> { public locale: T; diff --git a/packages/backend/src/misc/id/aid.ts b/packages/backend/src/misc/id/aid.ts index f0cbc9900d..ec8aa849c9 100644 --- a/packages/backend/src/misc/id/aid.ts +++ b/packages/backend/src/misc/id/aid.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + // AID // 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さ2の[ノイズ文字列] diff --git a/packages/backend/src/misc/id/aidx.ts b/packages/backend/src/misc/id/aidx.ts new file mode 100644 index 0000000000..5b031ea4c0 --- /dev/null +++ b/packages/backend/src/misc/id/aidx.ts @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// AIDX +// 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さ4の[個体ID] + 長さ4の[カウンタ] +// (c) mei23 +// https://misskey.m544.net/notes/71899acdcc9859ec5708ac24 + +import { customAlphabet } from 'nanoid'; + +export const aidxRegExp = /^[0-9a-z]{16}$/; + +const TIME2000 = 946684800000; +const TIME_LENGTH = 8; +const NODE_LENGTH = 4; +const NOISE_LENGTH = 4; + +const nodeId = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', NODE_LENGTH)(); +let counter = 0; + +function getTime(time: number): string { + time = time - TIME2000; + if (time < 0) time = 0; + + return time.toString(36).padStart(TIME_LENGTH, '0').slice(-TIME_LENGTH); +} + +function getNoise(): string { + return counter.toString(36).padStart(NOISE_LENGTH, '0').slice(-NOISE_LENGTH); +} + +export function genAidx(date: Date): string { + const t = date.getTime(); + if (isNaN(t)) throw new Error('Failed to create AIDX: Invalid Date'); + counter++; + return getTime(t) + nodeId + getNoise(); +} + +export function parseAidx(id: string): { date: Date; } { + const time = parseInt(id.slice(0, TIME_LENGTH), 36) + TIME2000; + return { date: new Date(time) }; +} diff --git a/packages/backend/src/misc/id/meid.ts b/packages/backend/src/misc/id/meid.ts index 337416b059..82cda37237 100644 --- a/packages/backend/src/misc/id/meid.ts +++ b/packages/backend/src/misc/id/meid.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + const CHARS = '0123456789abcdef'; // same as object-id diff --git a/packages/backend/src/misc/id/meidg.ts b/packages/backend/src/misc/id/meidg.ts index 19d0bc1fd2..fba7156718 100644 --- a/packages/backend/src/misc/id/meidg.ts +++ b/packages/backend/src/misc/id/meidg.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + const CHARS = '0123456789abcdef'; // 4bit Fixed hex value 'g' diff --git a/packages/backend/src/misc/id/object-id.ts b/packages/backend/src/misc/id/object-id.ts index aec3447bd7..e3b6e8e433 100644 --- a/packages/backend/src/misc/id/object-id.ts +++ b/packages/backend/src/misc/id/object-id.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + const CHARS = '0123456789abcdef'; // same as meid diff --git a/packages/backend/src/misc/id/ulid.ts b/packages/backend/src/misc/id/ulid.ts index e8aa752890..00dd67dafe 100644 --- a/packages/backend/src/misc/id/ulid.ts +++ b/packages/backend/src/misc/id/ulid.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + // Crockford's Base32 // https://github.com/ulid/spec#encoding const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; @@ -5,10 +10,10 @@ const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; export const ulidRegExp = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/; export function parseUlid(id: string): { date: Date; } { - const timestamp = id.slice(0, 10); - let time = 0; - for (let i = 0; i < 10; i++) { - time = time * 32 + CHARS.indexOf(timestamp[i]); - } - return { date: new Date(time) }; + const timestamp = id.slice(0, 10); + let time = 0; + for (let i = 0; i < 10; i++) { + time = time * 32 + CHARS.indexOf(timestamp[i]); + } + return { date: new Date(time) }; } diff --git a/packages/backend/src/misc/identifiable-error.ts b/packages/backend/src/misc/identifiable-error.ts index e394123f1b..71a4773fac 100644 --- a/packages/backend/src/misc/identifiable-error.ts +++ b/packages/backend/src/misc/identifiable-error.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + /** * ID付きエラー */ diff --git a/packages/backend/src/misc/is-duplicate-key-value-error.ts b/packages/backend/src/misc/is-duplicate-key-value-error.ts index 04ff191e41..91e0a6b93d 100644 --- a/packages/backend/src/misc/is-duplicate-key-value-error.ts +++ b/packages/backend/src/misc/is-duplicate-key-value-error.ts @@ -1,3 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { QueryFailedError } from 'typeorm'; + export function isDuplicateKeyValueError(e: unknown | Error): boolean { - return (e as any).message && (e as Error).message.startsWith('duplicate key value'); + return e instanceof QueryFailedError && e.driverError.code === '23505'; } diff --git a/packages/backend/src/misc/is-instance-muted.ts b/packages/backend/src/misc/is-instance-muted.ts index 73ad0b3b82..b231058a95 100644 --- a/packages/backend/src/misc/is-instance-muted.ts +++ b/packages/backend/src/misc/is-instance-muted.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import type { Packed } from './json-schema.js'; export function isInstanceMuted(note: Packed<'Note'>, mutedInstances: Set): boolean { diff --git a/packages/backend/src/misc/is-mime-image.ts b/packages/backend/src/misc/is-mime-image.ts index 46a66efc0f..1a5a8cf0f4 100644 --- a/packages/backend/src/misc/is-mime-image.ts +++ b/packages/backend/src/misc/is-mime-image.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; const dictionary = { diff --git a/packages/backend/src/misc/is-native-token.ts b/packages/backend/src/misc/is-native-token.ts index 2833c570c8..618e60b7d8 100644 --- a/packages/backend/src/misc/is-native-token.ts +++ b/packages/backend/src/misc/is-native-token.ts @@ -1 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// eslint-disable-next-line import/no-default-export export default (token: string) => token.length === 16; diff --git a/packages/backend/src/misc/is-not-null.ts b/packages/backend/src/misc/is-not-null.ts index d89a1957be..153a9e51ef 100644 --- a/packages/backend/src/misc/is-not-null.ts +++ b/packages/backend/src/misc/is-not-null.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + // 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 { diff --git a/packages/backend/src/misc/is-quote.ts b/packages/backend/src/misc/is-quote.ts index 248b25a0bf..059f6a4b5f 100644 --- a/packages/backend/src/misc/is-quote.ts +++ b/packages/backend/src/misc/is-quote.ts @@ -1,5 +1,11 @@ -import type { Note } from '@/models/entities/Note.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -export default function(note: Note): boolean { +import type { MiNote } from '@/models/Note.js'; + +// eslint-disable-next-line import/no-default-export +export default function(note: MiNote): boolean { return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0)); } diff --git a/packages/backend/src/misc/is-user-related.ts b/packages/backend/src/misc/is-user-related.ts index e6bbdb5d35..edd65a3c1c 100644 --- a/packages/backend/src/misc/is-user-related.ts +++ b/packages/backend/src/misc/is-user-related.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export function isUserRelated(note: any, userIds: Set): boolean { if (userIds.has(note.userId)) { return true; diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index e748f93a26..80c1041c62 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { packedUserLiteSchema, packedUserDetailedNotMeOnlySchema, @@ -19,6 +24,7 @@ import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js' import { packedBlockingSchema } from '@/models/json-schema/blocking.js'; import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js'; import { packedHashtagSchema } from '@/models/json-schema/hashtag.js'; +import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js'; import { packedPageSchema } from '@/models/json-schema/page.js'; import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js'; import { packedChannelSchema } from '@/models/json-schema/channel.js'; @@ -29,6 +35,7 @@ import { packedQueueCountSchema } from '@/models/json-schema/queue.js'; import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js'; import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js'; import { packedFlashSchema } from '@/models/json-schema/flash.js'; +import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -40,6 +47,7 @@ export const refs = { User: packedUserSchema, UserList: packedUserListSchema, + Announcement: packedAnnouncementSchema, App: packedAppSchema, Note: packedNoteSchema, NoteReaction: packedNoteReactionSchema, @@ -52,6 +60,7 @@ export const refs = { RenoteMuting: packedRenoteMutingSchema, Blocking: packedBlockingSchema, Hashtag: packedHashtagSchema, + InviteCode: packedInviteCodeSchema, Page: packedPageSchema, Channel: packedChannelSchema, QueueCount: packedQueueCountSchema, @@ -131,7 +140,7 @@ type NullOrUndefined

= | T; // https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection -// Get intersection from union +// Get intersection from union type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; type PartialIntersection = Partial>; diff --git a/packages/backend/src/misc/langmap.ts b/packages/backend/src/misc/langmap.ts index 5ee85e6c09..9e287677df 100644 --- a/packages/backend/src/misc/langmap.ts +++ b/packages/backend/src/misc/langmap.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + // TODO: sharedに置いてフロントエンドのと統合したい export const langmap = { 'ach': { diff --git a/packages/backend/src/misc/normalize-for-search.ts b/packages/backend/src/misc/normalize-for-search.ts index 200540566e..9d96f4169d 100644 --- a/packages/backend/src/misc/normalize-for-search.ts +++ b/packages/backend/src/misc/normalize-for-search.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export function normalizeForSearch(tag: string): string { // ref. // - https://analytics-note.xyz/programming/unicode-normalization-forms/ diff --git a/packages/backend/src/misc/nyaize.ts b/packages/backend/src/misc/nyaize.ts index 350f8d2172..0ac77e1006 100644 --- a/packages/backend/src/misc/nyaize.ts +++ b/packages/backend/src/misc/nyaize.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export function nyaize(text: string): string { return text // ja-JP diff --git a/packages/backend/src/misc/prelude/array.ts b/packages/backend/src/misc/prelude/array.ts index 0b2830cb7b..b2f29bcecf 100644 --- a/packages/backend/src/misc/prelude/array.ts +++ b/packages/backend/src/misc/prelude/array.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { EndoRelation, Predicate } from './relation.js'; /** @@ -67,8 +72,9 @@ export function maximum(xs: number[]): number { export function groupBy(f: EndoRelation, xs: T[]): T[][] { const groups = [] as T[][]; for (const x of xs) { - if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) { - groups[groups.length - 1].push(x); + const lastGroup = groups.at(-1); + if (lastGroup !== undefined && f(lastGroup[0], x)) { + lastGroup.push(x); } else { groups.push([x]); } diff --git a/packages/backend/src/misc/prelude/await-all.ts b/packages/backend/src/misc/prelude/await-all.ts index b955c3a5d8..6b8a91f8a5 100644 --- a/packages/backend/src/misc/prelude/await-all.ts +++ b/packages/backend/src/misc/prelude/await-all.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export type Promiseable = { [K in keyof T]: Promise | T[K]; }; @@ -10,7 +15,7 @@ export async function awaitAll(obj: Promiseable): Promise { const resolvedValues = await Promise.all(values.map(value => (!value || !value.constructor || value.constructor.name !== 'Object') ? value - : awaitAll(value) + : awaitAll(value), )); for (let i = 0; i < keys.length; i++) { diff --git a/packages/backend/src/misc/prelude/math.ts b/packages/backend/src/misc/prelude/math.ts index 07b94bec30..87b5017d09 100644 --- a/packages/backend/src/misc/prelude/math.ts +++ b/packages/backend/src/misc/prelude/math.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export function gcd(a: number, b: number): number { return b === 0 ? a : gcd(b, a % b); } diff --git a/packages/backend/src/misc/prelude/maybe.ts b/packages/backend/src/misc/prelude/maybe.ts index df7c4ed52a..17c100b80d 100644 --- a/packages/backend/src/misc/prelude/maybe.ts +++ b/packages/backend/src/misc/prelude/maybe.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export interface IMaybe { isJust(): this is IJust; } diff --git a/packages/backend/src/misc/prelude/relation.ts b/packages/backend/src/misc/prelude/relation.ts index 1f4703f52f..3456c1a0bc 100644 --- a/packages/backend/src/misc/prelude/relation.ts +++ b/packages/backend/src/misc/prelude/relation.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export type Predicate = (a: T) => boolean; export type Relation = (a: T, b: U) => boolean; diff --git a/packages/backend/src/misc/prelude/string.ts b/packages/backend/src/misc/prelude/string.ts index b907e0a2e1..a727ab7f1d 100644 --- a/packages/backend/src/misc/prelude/string.ts +++ b/packages/backend/src/misc/prelude/string.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export function concat(xs: string[]): string { return xs.join(''); } diff --git a/packages/backend/src/misc/prelude/symbol.ts b/packages/backend/src/misc/prelude/symbol.ts index 51e12f7450..91c058a845 100644 --- a/packages/backend/src/misc/prelude/symbol.ts +++ b/packages/backend/src/misc/prelude/symbol.ts @@ -1 +1,6 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const fallback = Symbol('fallback'); diff --git a/packages/backend/src/misc/prelude/time.ts b/packages/backend/src/misc/prelude/time.ts index b21978b186..4479db1081 100644 --- a/packages/backend/src/misc/prelude/time.ts +++ b/packages/backend/src/misc/prelude/time.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + const dateTimeIntervals = { 'day': 86400000, 'hour': 3600000, diff --git a/packages/backend/src/misc/prelude/url.ts b/packages/backend/src/misc/prelude/url.ts index 9b1dabc789..633eb98218 100644 --- a/packages/backend/src/misc/prelude/url.ts +++ b/packages/backend/src/misc/prelude/url.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + /* objを検査して * 1. 配列に何も入っていない時はクエリを付けない * 2. プロパティがundefinedの時はクエリを付けない * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) - */ + */ export function query(obj: Record): string { const params = Object.entries(obj) .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) diff --git a/packages/backend/src/misc/prelude/xml.ts b/packages/backend/src/misc/prelude/xml.ts index b4469a1d8d..bca116a7ec 100644 --- a/packages/backend/src/misc/prelude/xml.ts +++ b/packages/backend/src/misc/prelude/xml.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + const map: Record = { '&': '&', '<': '<', diff --git a/packages/backend/src/misc/reset-db.ts b/packages/backend/src/misc/reset-db.ts index 835cd2ba28..a571460a59 100644 --- a/packages/backend/src/misc/reset-db.ts +++ b/packages/backend/src/misc/reset-db.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import type { DataSource } from 'typeorm'; export async function resetDb(db: DataSource) { diff --git a/packages/backend/src/misc/safe-for-sql.ts b/packages/backend/src/misc/safe-for-sql.ts index 02eb7f0a26..d7bdd0a81c 100644 --- a/packages/backend/src/misc/safe-for-sql.ts +++ b/packages/backend/src/misc/safe-for-sql.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export function safeForSql(text: string): boolean { return !/[\0\x08\x09\x1a\n\r"'\\\%]/g.test(text); } diff --git a/packages/backend/src/misc/secure-rndstr.ts b/packages/backend/src/misc/secure-rndstr.ts index 8d4fcb1ba9..01368d808a 100644 --- a/packages/backend/src/misc/secure-rndstr.ts +++ b/packages/backend/src/misc/secure-rndstr.ts @@ -1,10 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as crypto from 'node:crypto'; -const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'; +export const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'; const LU_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; -export function secureRndstr(length = 32, useLU = true): string { - const chars = useLU ? LU_CHARS : L_CHARS; +export function secureRndstr(length = 32, { chars = LU_CHARS } = {}): string { const chars_len = chars.length; let str = ''; diff --git a/packages/backend/src/misc/show-machine-info.ts b/packages/backend/src/misc/show-machine-info.ts index fa5a53e313..ed0fa651f1 100644 --- a/packages/backend/src/misc/show-machine-info.ts +++ b/packages/backend/src/misc/show-machine-info.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as os from 'node:os'; import sysUtils from 'systeminformation'; import type Logger from '@/logger.js'; diff --git a/packages/backend/src/misc/sql-like-escape.ts b/packages/backend/src/misc/sql-like-escape.ts index 8470dca3de..85cc7405e1 100644 --- a/packages/backend/src/misc/sql-like-escape.ts +++ b/packages/backend/src/misc/sql-like-escape.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export function sqlLikeEscape(s: string) { return s.replace(/([%_])/g, '\\$1'); } diff --git a/packages/backend/src/misc/status-error.ts b/packages/backend/src/misc/status-error.ts index 0a33f8acaf..4285685d24 100644 --- a/packages/backend/src/misc/status-error.ts +++ b/packages/backend/src/misc/status-error.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class StatusError extends Error { public statusCode: number; public statusMessage?: string; diff --git a/packages/backend/src/misc/truncate.ts b/packages/backend/src/misc/truncate.ts index cb120331a1..b65202fbd4 100644 --- a/packages/backend/src/misc/truncate.ts +++ b/packages/backend/src/misc/truncate.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { substring } from 'stringz'; export function truncate(input: string, size: number): string; diff --git a/packages/backend/src/models/entities/AbuseUserReport.ts b/packages/backend/src/models/AbuseUserReport.ts similarity index 65% rename from packages/backend/src/models/entities/AbuseUserReport.ts rename to packages/backend/src/models/AbuseUserReport.ts index 07305cf23a..2551af7cb6 100644 --- a/packages/backend/src/models/entities/AbuseUserReport.ts +++ b/packages/backend/src/models/AbuseUserReport.ts @@ -1,9 +1,14 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class AbuseUserReport { +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('abuse_user_report') +export class MiAbuseUserReport { @PrimaryColumn(id()) public id: string; @@ -15,35 +20,35 @@ export class AbuseUserReport { @Index() @Column(id()) - public targetUserId: User['id']; + public targetUserId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public targetUser: User | null; + public targetUser: MiUser | null; @Index() @Column(id()) - public reporterId: User['id']; + public reporterId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public reporter: User | null; + public reporter: MiUser | null; @Column({ ...id(), nullable: true, }) - public assigneeId: User['id'] | null; + public assigneeId: MiUser['id'] | null; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'SET NULL', }) @JoinColumn() - public assignee: User | null; + public assignee: MiUser | null; @Index() @Column('boolean', { diff --git a/packages/backend/src/models/entities/AccessToken.ts b/packages/backend/src/models/AccessToken.ts similarity index 72% rename from packages/backend/src/models/entities/AccessToken.ts rename to packages/backend/src/models/AccessToken.ts index 8e987ffeef..5a6269a729 100644 --- a/packages/backend/src/models/entities/AccessToken.ts +++ b/packages/backend/src/models/AccessToken.ts @@ -1,10 +1,15 @@ -import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { App } from './App.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class AccessToken { +import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiApp } from './App.js'; + +@Entity('access_token') +export class MiAccessToken { @PrimaryColumn(id()) public id: string; @@ -39,25 +44,25 @@ export class AccessToken { @Index() @Column(id()) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Column({ ...id(), nullable: true, }) - public appId: App['id'] | null; + public appId: MiApp['id'] | null; - @ManyToOne(type => App, { + @ManyToOne(type => MiApp, { onDelete: 'CASCADE', }) @JoinColumn() - public app: App | null; + public app: MiApp | null; @Column('varchar', { length: 128, diff --git a/packages/backend/src/models/entities/Ad.ts b/packages/backend/src/models/Ad.ts similarity index 79% rename from packages/backend/src/models/entities/Ad.ts rename to packages/backend/src/models/Ad.ts index 56baf863ca..6dfc9cb30e 100644 --- a/packages/backend/src/models/entities/Ad.ts +++ b/packages/backend/src/models/Ad.ts @@ -1,8 +1,13 @@ -import { Entity, Index, Column, PrimaryColumn } from 'typeorm'; -import { id } from '../id.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class Ad { +import { Entity, Index, Column, PrimaryColumn } from 'typeorm'; +import { id } from './util/id.js'; + +@Entity('ad') +export class MiAd { @PrimaryColumn(id()) public id: string; @@ -55,8 +60,11 @@ export class Ad { length: 8192, nullable: false, }) public memo: string; - - constructor(data: Partial) { + @Column('integer', { + default: 0, nullable: false, + }) + public dayOfWeek: number; + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { diff --git a/packages/backend/src/models/Announcement.ts b/packages/backend/src/models/Announcement.ts new file mode 100644 index 0000000000..34b092a8d4 --- /dev/null +++ b/packages/backend/src/models/Announcement.ts @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Entity, Index, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('announcement') +export class MiAnnouncement { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Announcement.', + }) + public createdAt: Date; + + @Column('timestamp with time zone', { + comment: 'The updated date of the Announcement.', + nullable: true, + }) + public updatedAt: Date | null; + + @Column('varchar', { + length: 8192, nullable: false, + }) + public text: string; + + @Column('varchar', { + length: 256, nullable: false, + }) + public title: string; + + @Column('varchar', { + length: 1024, nullable: true, + }) + public imageUrl: string | null; + + // info, warning, error, success + @Column('varchar', { + length: 256, nullable: false, + default: 'info', + }) + public icon: string; + + // normal ... お知らせページ掲載 + // banner ... お知らせページ掲載 + バナー表示 + // dialog ... お知らせページ掲載 + ダイアログ表示 + @Column('varchar', { + length: 256, nullable: false, + default: 'normal', + }) + public display: string; + + @Column('boolean', { + default: false, + }) + public needConfirmationToRead: boolean; + + @Index() + @Column('boolean', { + default: true, + }) + public isActive: boolean; + + @Index() + @Column('boolean', { + default: false, + }) + public forExistingUsers: boolean; + + @Index() + @Column({ + ...id(), + nullable: true, + }) + public userId: MiUser['id'] | null; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + 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/AnnouncementRead.ts b/packages/backend/src/models/AnnouncementRead.ts new file mode 100644 index 0000000000..3d6ec5652c --- /dev/null +++ b/packages/backend/src/models/AnnouncementRead.ts @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiAnnouncement } from './Announcement.js'; + +@Entity('announcement_read') +@Index(['userId', 'announcementId'], { unique: true }) +export class MiAnnouncementRead { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the AnnouncementRead.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Index() + @Column(id()) + public announcementId: MiAnnouncement['id']; + + @ManyToOne(type => MiAnnouncement, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public announcement: MiAnnouncement | null; +} diff --git a/packages/backend/src/models/entities/Antenna.ts b/packages/backend/src/models/Antenna.ts similarity index 67% rename from packages/backend/src/models/entities/Antenna.ts rename to packages/backend/src/models/Antenna.ts index e63e7f2c72..dc398b6dd2 100644 --- a/packages/backend/src/models/entities/Antenna.ts +++ b/packages/backend/src/models/Antenna.ts @@ -1,10 +1,15 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { UserList } from './UserList.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class Antenna { +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiUserList } from './UserList.js'; + +@Entity('antenna') +export class MiAntenna { @PrimaryColumn(id()) public id: string; @@ -22,13 +27,13 @@ export class Antenna { ...id(), comment: 'The owner ID.', }) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Column('varchar', { length: 128, @@ -36,20 +41,20 @@ export class Antenna { }) public name: string; - @Column('enum', { enum: ['home', 'all', 'users', 'list'] }) - public src: 'home' | 'all' | 'users' | 'list'; + @Column('enum', { enum: ['home', 'all', 'users', 'list', 'users_blacklist'] }) + public src: 'home' | 'all' | 'users' | 'list' | 'users_blacklist'; @Column({ ...id(), nullable: true, }) - public userListId: UserList['id'] | null; + public userListId: MiUserList['id'] | null; - @ManyToOne(type => UserList, { + @ManyToOne(type => MiUserList, { onDelete: 'CASCADE', }) @JoinColumn() - public userList: UserList | null; + public userList: MiUserList | null; @Column('varchar', { length: 1024, array: true, diff --git a/packages/backend/src/models/entities/App.ts b/packages/backend/src/models/App.ts similarity index 75% rename from packages/backend/src/models/entities/App.ts rename to packages/backend/src/models/App.ts index 3a1ea7732e..c599ef8be0 100644 --- a/packages/backend/src/models/entities/App.ts +++ b/packages/backend/src/models/App.ts @@ -1,9 +1,14 @@ -import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class App { +import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('app') +export class MiApp { @PrimaryColumn(id()) public id: string; @@ -19,13 +24,13 @@ export class App { nullable: true, comment: 'The owner ID.', }) - public userId: User['id'] | null; + public userId: MiUser['id'] | null; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'SET NULL', nullable: true, }) - public user: User | null; + public user: MiUser | null; @Index() @Column('varchar', { diff --git a/packages/backend/src/models/entities/AuthSession.ts b/packages/backend/src/models/AuthSession.ts similarity index 52% rename from packages/backend/src/models/entities/AuthSession.ts rename to packages/backend/src/models/AuthSession.ts index 6b2f50e8d6..d9de6b6979 100644 --- a/packages/backend/src/models/entities/AuthSession.ts +++ b/packages/backend/src/models/AuthSession.ts @@ -1,10 +1,15 @@ -import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { App } from './App.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class AuthSession { +import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiApp } from './App.js'; + +@Entity('auth_session') +export class MiAuthSession { @PrimaryColumn(id()) public id: string; @@ -23,21 +28,21 @@ export class AuthSession { ...id(), nullable: true, }) - public userId: User['id'] | null; + public userId: MiUser['id'] | null; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', nullable: true, }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Column(id()) - public appId: App['id']; + public appId: MiApp['id']; - @ManyToOne(type => App, { + @ManyToOne(type => MiApp, { onDelete: 'CASCADE', }) @JoinColumn() - public app: App | null; + public app: MiApp | null; } diff --git a/packages/backend/src/models/entities/Blocking.ts b/packages/backend/src/models/Blocking.ts similarity index 56% rename from packages/backend/src/models/entities/Blocking.ts rename to packages/backend/src/models/Blocking.ts index 9892ff308e..1e3dd3a644 100644 --- a/packages/backend/src/models/entities/Blocking.ts +++ b/packages/backend/src/models/Blocking.ts @@ -1,10 +1,15 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('blocking') @Index(['blockerId', 'blockeeId'], { unique: true }) -export class Blocking { +export class MiBlocking { @PrimaryColumn(id()) public id: string; @@ -19,24 +24,24 @@ export class Blocking { ...id(), comment: 'The blockee user ID.', }) - public blockeeId: User['id']; + public blockeeId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public blockee: User | null; + public blockee: MiUser | null; @Index() @Column({ ...id(), comment: 'The blocker user ID.', }) - public blockerId: User['id']; + public blockerId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public blocker: User | null; + public blocker: MiUser | null; } diff --git a/packages/backend/src/models/entities/Channel.ts b/packages/backend/src/models/Channel.ts similarity index 70% rename from packages/backend/src/models/entities/Channel.ts rename to packages/backend/src/models/Channel.ts index d7c4583da3..ae3886a657 100644 --- a/packages/backend/src/models/entities/Channel.ts +++ b/packages/backend/src/models/Channel.ts @@ -1,10 +1,15 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { DriveFile } from './DriveFile.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class Channel { +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiDriveFile } from './DriveFile.js'; + +@Entity('channel') +export class MiChannel { @PrimaryColumn(id()) public id: string; @@ -26,13 +31,13 @@ export class Channel { nullable: true, comment: 'The owner ID.', }) - public userId: User['id'] | null; + public userId: MiUser['id'] | null; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'SET NULL', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Column('varchar', { length: 128, @@ -51,13 +56,13 @@ export class Channel { nullable: true, comment: 'The ID of banner Channel.', }) - public bannerId: DriveFile['id'] | null; + public bannerId: MiDriveFile['id'] | null; - @ManyToOne(type => DriveFile, { + @ManyToOne(type => MiDriveFile, { onDelete: 'SET NULL', }) @JoinColumn() - public banner: DriveFile | null; + public banner: MiDriveFile | null; @Column('varchar', { array: true, length: 128, default: '{}', @@ -89,4 +94,9 @@ export class Channel { comment: 'The count of users.', }) public usersCount: number; + + @Column('boolean', { + default: false, + }) + public isSensitive: boolean; } diff --git a/packages/backend/src/models/ChannelFavorite.ts b/packages/backend/src/models/ChannelFavorite.ts new file mode 100644 index 0000000000..ab74aa5530 --- /dev/null +++ b/packages/backend/src/models/ChannelFavorite.ts @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiChannel } from './Channel.js'; + +@Entity('channel_favorite') +@Index(['userId', 'channelId'], { unique: true }) +export class MiChannelFavorite { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the ChannelFavorite.', + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + }) + public channelId: MiChannel['id']; + + @ManyToOne(type => MiChannel, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public channel: MiChannel | null; + + @Index() + @Column({ + ...id(), + }) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; +} diff --git a/packages/backend/src/models/entities/ChannelFollowing.ts b/packages/backend/src/models/ChannelFollowing.ts similarity index 52% rename from packages/backend/src/models/entities/ChannelFollowing.ts rename to packages/backend/src/models/ChannelFollowing.ts index c65c38b67d..c62a95332a 100644 --- a/packages/backend/src/models/entities/ChannelFollowing.ts +++ b/packages/backend/src/models/ChannelFollowing.ts @@ -1,11 +1,16 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { Channel } from './Channel.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiChannel } from './Channel.js'; + +@Entity('channel_following') @Index(['followerId', 'followeeId'], { unique: true }) -export class ChannelFollowing { +export class MiChannelFollowing { @PrimaryColumn(id()) public id: string; @@ -20,24 +25,24 @@ export class ChannelFollowing { ...id(), comment: 'The followee channel ID.', }) - public followeeId: Channel['id']; + public followeeId: MiChannel['id']; - @ManyToOne(type => Channel, { + @ManyToOne(type => MiChannel, { onDelete: 'CASCADE', }) @JoinColumn() - public followee: Channel | null; + public followee: MiChannel | null; @Index() @Column({ ...id(), comment: 'The follower user ID.', }) - public followerId: User['id']; + public followerId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public follower: User | null; + public follower: MiUser | null; } diff --git a/packages/backend/src/models/entities/Clip.ts b/packages/backend/src/models/Clip.ts similarity index 71% rename from packages/backend/src/models/entities/Clip.ts rename to packages/backend/src/models/Clip.ts index 825a32c981..c60b2964e0 100644 --- a/packages/backend/src/models/entities/Clip.ts +++ b/packages/backend/src/models/Clip.ts @@ -1,9 +1,14 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class Clip { +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('clip') +export class MiClip { @PrimaryColumn(id()) public id: string; @@ -23,13 +28,13 @@ export class Clip { ...id(), comment: 'The owner ID.', }) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Column('varchar', { length: 128, diff --git a/packages/backend/src/models/ClipFavorite.ts b/packages/backend/src/models/ClipFavorite.ts new file mode 100644 index 0000000000..054764389b --- /dev/null +++ b/packages/backend/src/models/ClipFavorite.ts @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiClip } from './Clip.js'; + +@Entity('clip_favorite') +@Index(['userId', 'clipId'], { unique: true }) +export class MiClipFavorite { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Column(id()) + public clipId: MiClip['id']; + + @ManyToOne(type => MiClip, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public clip: MiClip | null; +} diff --git a/packages/backend/src/models/ClipNote.ts b/packages/backend/src/models/ClipNote.ts new file mode 100644 index 0000000000..b7cc5ee39b --- /dev/null +++ b/packages/backend/src/models/ClipNote.ts @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; +import { id } from './util/id.js'; +import { MiNote } from './Note.js'; +import { MiClip } from './Clip.js'; + +@Entity('clip_note') +@Index(['noteId', 'clipId'], { unique: true }) +export class MiClipNote { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column({ + ...id(), + comment: 'The note ID.', + }) + public noteId: MiNote['id']; + + @ManyToOne(type => MiNote, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: MiNote | null; + + @Index() + @Column({ + ...id(), + comment: 'The clip ID.', + }) + public clipId: MiClip['id']; + + @ManyToOne(type => MiClip, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public clip: MiClip | null; +} diff --git a/packages/backend/src/models/entities/DriveFile.ts b/packages/backend/src/models/DriveFile.ts similarity index 88% rename from packages/backend/src/models/entities/DriveFile.ts rename to packages/backend/src/models/DriveFile.ts index 7b9670fb92..c12f0e0f02 100644 --- a/packages/backend/src/models/entities/DriveFile.ts +++ b/packages/backend/src/models/DriveFile.ts @@ -1,11 +1,16 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { DriveFolder } from './DriveFolder.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiDriveFolder } from './DriveFolder.js'; + +@Entity('drive_file') @Index(['userId', 'folderId', 'id']) -export class DriveFile { +export class MiDriveFile { @PrimaryColumn(id()) public id: string; @@ -21,13 +26,13 @@ export class DriveFile { nullable: true, comment: 'The owner ID.', }) - public userId: User['id'] | null; + public userId: MiUser['id'] | null; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'SET NULL', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Index() @Column('varchar', { @@ -141,13 +146,13 @@ export class DriveFile { nullable: true, comment: 'The parent folder ID. If null, it means the DriveFile is located in root.', }) - public folderId: DriveFolder['id'] | null; + public folderId: MiDriveFolder['id'] | null; - @ManyToOne(type => DriveFolder, { + @ManyToOne(type => MiDriveFolder, { onDelete: 'SET NULL', }) @JoinColumn() - public folder: DriveFolder | null; + public folder: MiDriveFolder | null; @Index() @Column('boolean', { diff --git a/packages/backend/src/models/entities/DriveFolder.ts b/packages/backend/src/models/DriveFolder.ts similarity index 60% rename from packages/backend/src/models/entities/DriveFolder.ts rename to packages/backend/src/models/DriveFolder.ts index 2a73a0875d..3e049136bd 100644 --- a/packages/backend/src/models/entities/DriveFolder.ts +++ b/packages/backend/src/models/DriveFolder.ts @@ -1,9 +1,14 @@ -import { JoinColumn, ManyToOne, Entity, PrimaryColumn, Index, Column } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class DriveFolder { +import { JoinColumn, ManyToOne, Entity, PrimaryColumn, Index, Column } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('drive_folder') +export class MiDriveFolder { @PrimaryColumn(id()) public id: string; @@ -25,13 +30,13 @@ export class DriveFolder { nullable: true, comment: 'The owner ID.', }) - public userId: User['id'] | null; + public userId: MiUser['id'] | null; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Index() @Column({ @@ -39,11 +44,11 @@ export class DriveFolder { nullable: true, comment: 'The parent folder ID. If null, it means the DriveFolder is located in root.', }) - public parentId: DriveFolder['id'] | null; + public parentId: MiDriveFolder['id'] | null; - @ManyToOne(type => DriveFolder, { + @ManyToOne(type => MiDriveFolder, { onDelete: 'SET NULL', }) @JoinColumn() - public parent: DriveFolder | null; + public parent: MiDriveFolder | null; } diff --git a/packages/backend/src/models/entities/Emoji.ts b/packages/backend/src/models/Emoji.ts similarity index 88% rename from packages/backend/src/models/entities/Emoji.ts rename to packages/backend/src/models/Emoji.ts index 8fd3e65f5e..563ac1d9d3 100644 --- a/packages/backend/src/models/entities/Emoji.ts +++ b/packages/backend/src/models/Emoji.ts @@ -1,9 +1,14 @@ -import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; -import { id } from '../id.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; +import { id } from './util/id.js'; + +@Entity('emoji') @Index(['name', 'host'], { unique: true }) -export class Emoji { +export class MiEmoji { @PrimaryColumn(id()) public id: string; diff --git a/packages/backend/src/models/entities/Flash.ts b/packages/backend/src/models/Flash.ts similarity index 63% rename from packages/backend/src/models/entities/Flash.ts rename to packages/backend/src/models/Flash.ts index 4ccc908a6a..185063029d 100644 --- a/packages/backend/src/models/entities/Flash.ts +++ b/packages/backend/src/models/Flash.ts @@ -1,9 +1,14 @@ -import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class Flash { +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('flash') +export class MiFlash { @PrimaryColumn(id()) public id: string; @@ -34,13 +39,13 @@ export class Flash { ...id(), comment: 'The ID of author.', }) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Column('varchar', { length: 65536, @@ -56,4 +61,13 @@ export class Flash { default: 0, }) public likedCount: number; + + /** + * public ... 公開 + * private ... プロフィールには表示しない + */ + @Column('varchar', { + length: 512, default: 'public', + }) + public visibility: 'public' | 'private'; } diff --git a/packages/backend/src/models/FlashLike.ts b/packages/backend/src/models/FlashLike.ts new file mode 100644 index 0000000000..7c66010ae6 --- /dev/null +++ b/packages/backend/src/models/FlashLike.ts @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiFlash } from './Flash.js'; + +@Entity('flash_like') +@Index(['userId', 'flashId'], { unique: true }) +export class MiFlashLike { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Column(id()) + public flashId: MiFlash['id']; + + @ManyToOne(type => MiFlash, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public flash: MiFlash | null; +} diff --git a/packages/backend/src/models/entities/FollowRequest.ts b/packages/backend/src/models/FollowRequest.ts similarity index 77% rename from packages/backend/src/models/entities/FollowRequest.ts rename to packages/backend/src/models/FollowRequest.ts index 0988e7e504..769b9a6cb5 100644 --- a/packages/backend/src/models/entities/FollowRequest.ts +++ b/packages/backend/src/models/FollowRequest.ts @@ -1,10 +1,15 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('follow_request') @Index(['followerId', 'followeeId'], { unique: true }) -export class FollowRequest { +export class MiFollowRequest { @PrimaryColumn(id()) public id: string; @@ -18,26 +23,26 @@ export class FollowRequest { ...id(), comment: 'The followee user ID.', }) - public followeeId: User['id']; + public followeeId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public followee: User | null; + public followee: MiUser | null; @Index() @Column({ ...id(), comment: 'The follower user ID.', }) - public followerId: User['id']; + public followerId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public follower: User | null; + public follower: MiUser | null; @Column('varchar', { length: 128, nullable: true, diff --git a/packages/backend/src/models/entities/Following.ts b/packages/backend/src/models/Following.ts similarity index 72% rename from packages/backend/src/models/entities/Following.ts rename to packages/backend/src/models/Following.ts index 112afd7e6e..8c9f965fad 100644 --- a/packages/backend/src/models/entities/Following.ts +++ b/packages/backend/src/models/Following.ts @@ -1,10 +1,15 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('following') @Index(['followerId', 'followeeId'], { unique: true }) -export class Following { +export class MiFollowing { @PrimaryColumn(id()) public id: string; @@ -19,26 +24,33 @@ export class Following { ...id(), comment: 'The followee user ID.', }) - public followeeId: User['id']; + public followeeId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public followee: User | null; + public followee: MiUser | null; @Index() @Column({ ...id(), comment: 'The follower user ID.', }) - public followerId: User['id']; + public followerId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public follower: User | null; + public follower: MiUser | null; + + @Index() + @Column('varchar', { + length: 32, + nullable: true, + }) + public notify: 'normal' | null; //#region Denormalized fields @Index() diff --git a/packages/backend/src/models/GalleryLike.ts b/packages/backend/src/models/GalleryLike.ts new file mode 100644 index 0000000000..b5f71764aa --- /dev/null +++ b/packages/backend/src/models/GalleryLike.ts @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiGalleryPost } from './GalleryPost.js'; + +@Entity('gallery_like') +@Index(['userId', 'postId'], { unique: true }) +export class MiGalleryLike { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Column(id()) + public postId: MiGalleryPost['id']; + + @ManyToOne(type => MiGalleryPost, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public post: MiGalleryPost | null; +} diff --git a/packages/backend/src/models/entities/GalleryPost.ts b/packages/backend/src/models/GalleryPost.ts similarity index 71% rename from packages/backend/src/models/entities/GalleryPost.ts rename to packages/backend/src/models/GalleryPost.ts index 36e879afa7..4c6063f32b 100644 --- a/packages/backend/src/models/entities/GalleryPost.ts +++ b/packages/backend/src/models/GalleryPost.ts @@ -1,10 +1,15 @@ -import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import type { DriveFile } from './DriveFile.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class GalleryPost { +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import type { MiDriveFile } from './DriveFile.js'; + +@Entity('gallery_post') +export class MiGalleryPost { @PrimaryColumn(id()) public id: string; @@ -35,20 +40,20 @@ export class GalleryPost { ...id(), comment: 'The ID of author.', }) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Index() @Column({ ...id(), array: true, default: '{}', }) - public fileIds: DriveFile['id'][]; + public fileIds: MiDriveFile['id'][]; @Index() @Column('boolean', { @@ -69,7 +74,7 @@ export class GalleryPost { }) public tags: string[]; - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { diff --git a/packages/backend/src/models/entities/Hashtag.ts b/packages/backend/src/models/Hashtag.ts similarity index 66% rename from packages/backend/src/models/entities/Hashtag.ts rename to packages/backend/src/models/Hashtag.ts index 2d6bfaa045..1493774752 100644 --- a/packages/backend/src/models/entities/Hashtag.ts +++ b/packages/backend/src/models/Hashtag.ts @@ -1,9 +1,14 @@ -import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; -import { id } from '../id.js'; -import type { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class Hashtag { +import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; +import { id } from './util/id.js'; +import type { MiUser } from './User.js'; + +@Entity('hashtag') +export class MiHashtag { @PrimaryColumn(id()) public id: string; @@ -17,7 +22,7 @@ export class Hashtag { ...id(), array: true, }) - public mentionedUserIds: User['id'][]; + public mentionedUserIds: MiUser['id'][]; @Index() @Column('integer', { @@ -29,7 +34,7 @@ export class Hashtag { ...id(), array: true, }) - public mentionedLocalUserIds: User['id'][]; + public mentionedLocalUserIds: MiUser['id'][]; @Index() @Column('integer', { @@ -41,7 +46,7 @@ export class Hashtag { ...id(), array: true, }) - public mentionedRemoteUserIds: User['id'][]; + public mentionedRemoteUserIds: MiUser['id'][]; @Index() @Column('integer', { @@ -53,7 +58,7 @@ export class Hashtag { ...id(), array: true, }) - public attachedUserIds: User['id'][]; + public attachedUserIds: MiUser['id'][]; @Index() @Column('integer', { @@ -65,7 +70,7 @@ export class Hashtag { ...id(), array: true, }) - public attachedLocalUserIds: User['id'][]; + public attachedLocalUserIds: MiUser['id'][]; @Index() @Column('integer', { @@ -77,7 +82,7 @@ export class Hashtag { ...id(), array: true, }) - public attachedRemoteUserIds: User['id'][]; + public attachedRemoteUserIds: MiUser['id'][]; @Index() @Column('integer', { diff --git a/packages/backend/src/models/entities/Instance.ts b/packages/backend/src/models/Instance.ts similarity index 93% rename from packages/backend/src/models/entities/Instance.ts rename to packages/backend/src/models/Instance.ts index 09328b57f8..b225d918d6 100644 --- a/packages/backend/src/models/entities/Instance.ts +++ b/packages/backend/src/models/Instance.ts @@ -1,8 +1,13 @@ -import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; -import { id } from '../id.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class Instance { +import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; +import { id } from './util/id.js'; + +@Entity('instance') +export class MiInstance { @PrimaryColumn(id()) public id: string; diff --git a/packages/backend/src/models/entities/Meta.ts b/packages/backend/src/models/Meta.ts similarity index 86% rename from packages/backend/src/models/entities/Meta.ts rename to packages/backend/src/models/Meta.ts index 6d44e4edc7..e69bef8e98 100644 --- a/packages/backend/src/models/entities/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -1,10 +1,14 @@ -import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import type { Clip } from './Clip.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class Meta { +import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('meta') +export class MiMeta { @PrimaryColumn({ type: 'varchar', length: 32, @@ -16,6 +20,11 @@ export class Meta { }) public name: string | null; + @Column('varchar', { + length: 64, nullable: true, + }) + public shortName: string | null; + @Column('varchar', { length: 1024, nullable: true, }) @@ -101,30 +110,59 @@ export class Meta { length: 1024, nullable: true, }) - public errorImageUrl: string | null; + public iconUrl: string | null; @Column('varchar', { length: 1024, nullable: true, }) - public iconUrl: string | null; + public app192IconUrl: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public app512IconUrl: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public serverErrorImageUrl: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public notFoundImageUrl: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public infoImageUrl: string | null; + + @Column('boolean', { + default: false, + }) + public cacheRemoteFiles: boolean; @Column('boolean', { default: true, }) - public cacheRemoteFiles: boolean; + public cacheRemoteSensitiveFiles: boolean; @Column({ ...id(), nullable: true, }) - public proxyAccountId: User['id'] | null; + public proxyAccountId: MiUser['id'] | null; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'SET NULL', }) @JoinColumn() - public proxyAccount: User | null; + public proxyAccount: MiUser | null; @Column('boolean', { default: false, @@ -401,6 +439,16 @@ export class Meta { }) public enableChartsForFederatedInstances: boolean; + @Column('boolean', { + default: false, + }) + public enableServerMachineStats: boolean; + + @Column('boolean', { + default: true, + }) + public enableIdenticonGeneration: boolean; + @Column('jsonb', { default: { }, }) @@ -413,6 +461,12 @@ export class Meta { }) public serverRules: string[]; + @Column('varchar', { + length: 8192, + default: '{}', + }) + public manifestJsonOverride: string; + @Column('varchar', { length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }', }) diff --git a/packages/backend/src/models/entities/ModerationLog.ts b/packages/backend/src/models/ModerationLog.ts similarity index 57% rename from packages/backend/src/models/entities/ModerationLog.ts rename to packages/backend/src/models/ModerationLog.ts index ab6a226cf7..a12b6ab614 100644 --- a/packages/backend/src/models/entities/ModerationLog.ts +++ b/packages/backend/src/models/ModerationLog.ts @@ -1,9 +1,14 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class ModerationLog { +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('moderation_log') +export class MiModerationLog { @PrimaryColumn(id()) public id: string; @@ -14,13 +19,13 @@ export class ModerationLog { @Index() @Column(id()) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Column('varchar', { length: 128, diff --git a/packages/backend/src/models/entities/MutedNote.ts b/packages/backend/src/models/MutedNote.ts similarity index 54% rename from packages/backend/src/models/entities/MutedNote.ts rename to packages/backend/src/models/MutedNote.ts index 78347d8917..89a678a2a7 100644 --- a/packages/backend/src/models/entities/MutedNote.ts +++ b/packages/backend/src/models/MutedNote.ts @@ -1,12 +1,17 @@ -import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; -import { id } from '../id.js'; -import { mutedNoteReasons } from '../../types.js'; -import { Note } from './Note.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; +import { mutedNoteReasons } from '@/types.js'; +import { id } from './util/id.js'; +import { MiNote } from './Note.js'; +import { MiUser } from './User.js'; + +@Entity('muted_note') @Index(['noteId', 'userId'], { unique: true }) -export class MutedNote { +export class MiMutedNote { @PrimaryColumn(id()) public id: string; @@ -15,26 +20,26 @@ export class MutedNote { ...id(), comment: 'The note ID.', }) - public noteId: Note['id']; + public noteId: MiNote['id']; - @ManyToOne(type => Note, { + @ManyToOne(type => MiNote, { onDelete: 'CASCADE', }) @JoinColumn() - public note: Note | null; + public note: MiNote | null; @Index() @Column({ ...id(), comment: 'The user ID.', }) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; /** * ミュートされた理由。 diff --git a/packages/backend/src/models/entities/Muting.ts b/packages/backend/src/models/Muting.ts similarity index 60% rename from packages/backend/src/models/entities/Muting.ts rename to packages/backend/src/models/Muting.ts index bf5498b96a..2f06ca8e5e 100644 --- a/packages/backend/src/models/entities/Muting.ts +++ b/packages/backend/src/models/Muting.ts @@ -1,10 +1,15 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('muting') @Index(['muterId', 'muteeId'], { unique: true }) -export class Muting { +export class MiMuting { @PrimaryColumn(id()) public id: string; @@ -25,24 +30,24 @@ export class Muting { ...id(), comment: 'The mutee user ID.', }) - public muteeId: User['id']; + public muteeId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public mutee: User | null; + public mutee: MiUser | null; @Index() @Column({ ...id(), comment: 'The muter user ID.', }) - public muterId: User['id']; + public muterId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public muter: User | null; + public muter: MiUser | null; } diff --git a/packages/backend/src/models/entities/Note.ts b/packages/backend/src/models/Note.ts similarity index 79% rename from packages/backend/src/models/entities/Note.ts rename to packages/backend/src/models/Note.ts index 4f49a05950..ed86d4549e 100644 --- a/packages/backend/src/models/entities/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -1,15 +1,20 @@ -import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { noteVisibilities } from '../../types.js'; -import { User } from './User.js'; -import { Channel } from './Channel.js'; -import type { DriveFile } from './DriveFile.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { noteVisibilities } from '@/types.js'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiChannel } from './Channel.js'; +import type { MiDriveFile } from './DriveFile.js'; + +@Entity('note') @Index('IDX_NOTE_TAGS', { synchronize: false }) @Index('IDX_NOTE_MENTIONS', { synchronize: false }) @Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false }) -export class Note { +export class MiNote { @PrimaryColumn(id()) public id: string; @@ -25,13 +30,13 @@ export class Note { nullable: true, comment: 'The ID of reply target.', }) - public replyId: Note['id'] | null; + public replyId: MiNote['id'] | null; - @ManyToOne(type => Note, { + @ManyToOne(type => MiNote, { onDelete: 'CASCADE', }) @JoinColumn() - public reply: Note | null; + public reply: MiNote | null; @Index() @Column({ @@ -39,13 +44,13 @@ export class Note { nullable: true, comment: 'The ID of renote target.', }) - public renoteId: Note['id'] | null; + public renoteId: MiNote['id'] | null; - @ManyToOne(type => Note, { + @ManyToOne(type => MiNote, { onDelete: 'CASCADE', }) @JoinColumn() - public renote: Note | null; + public renote: MiNote | null; @Index() @Column('varchar', { @@ -74,13 +79,13 @@ export class Note { ...id(), comment: 'The ID of author.', }) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Column('boolean', { default: false, @@ -102,6 +107,11 @@ export class Note { }) public repliesCount: number; + @Column('smallint', { + default: 0, + }) + public clippedCount: number; + @Column('jsonb', { default: {}, }) @@ -139,7 +149,7 @@ export class Note { ...id(), array: true, default: '{}', }) - public fileIds: DriveFile['id'][]; + public fileIds: MiDriveFile['id'][]; @Index() @Column('varchar', { @@ -152,14 +162,14 @@ export class Note { ...id(), array: true, default: '{}', }) - public visibleUserIds: User['id'][]; + public visibleUserIds: MiUser['id'][]; @Index() @Column({ ...id(), array: true, default: '{}', }) - public mentions: User['id'][]; + public mentions: MiUser['id'][]; @Column('text', { default: '[]', @@ -188,13 +198,13 @@ export class Note { nullable: true, comment: 'The ID of source channel.', }) - public channelId: Channel['id'] | null; + public channelId: MiChannel['id'] | null; - @ManyToOne(type => Channel, { + @ManyToOne(type => MiChannel, { onDelete: 'CASCADE', }) @JoinColumn() - public channel: Channel | null; + public channel: MiChannel | null; //#region Denormalized fields @Index() @@ -209,7 +219,7 @@ export class Note { nullable: true, comment: '[Denormalized]', }) - public replyUserId: User['id'] | null; + public replyUserId: MiUser['id'] | null; @Column('varchar', { length: 128, nullable: true, @@ -222,7 +232,7 @@ export class Note { nullable: true, comment: '[Denormalized]', }) - public renoteUserId: User['id'] | null; + public renoteUserId: MiUser['id'] | null; @Column('varchar', { length: 128, nullable: true, @@ -231,7 +241,7 @@ export class Note { public renoteUserHost: string | null; //#endregion - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { diff --git a/packages/backend/src/models/NoteFavorite.ts b/packages/backend/src/models/NoteFavorite.ts new file mode 100644 index 0000000000..1171684bcf --- /dev/null +++ b/packages/backend/src/models/NoteFavorite.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiNote } from './Note.js'; +import { MiUser } from './User.js'; + +@Entity('note_favorite') +@Index(['userId', 'noteId'], { unique: true }) +export class MiNoteFavorite { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the NoteFavorite.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Column(id()) + public noteId: MiNote['id']; + + @ManyToOne(type => MiNote, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: MiNote | null; +} diff --git a/packages/backend/src/models/entities/NoteReaction.ts b/packages/backend/src/models/NoteReaction.ts similarity index 59% rename from packages/backend/src/models/entities/NoteReaction.ts rename to packages/backend/src/models/NoteReaction.ts index c3c381af56..7c08d31c6d 100644 --- a/packages/backend/src/models/entities/NoteReaction.ts +++ b/packages/backend/src/models/NoteReaction.ts @@ -1,11 +1,16 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { Note } from './Note.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiNote } from './Note.js'; + +@Entity('note_reaction') @Index(['userId', 'noteId'], { unique: true }) -export class NoteReaction { +export class MiNoteReaction { @PrimaryColumn(id()) public id: string; @@ -17,23 +22,23 @@ export class NoteReaction { @Index() @Column(id()) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user?: User | null; + public user?: MiUser | null; @Index() @Column(id()) - public noteId: Note['id']; + public noteId: MiNote['id']; - @ManyToOne(type => Note, { + @ManyToOne(type => MiNote, { onDelete: 'CASCADE', }) @JoinColumn() - public note?: Note | null; + public note?: MiNote | null; // TODO: 対象noteのuserIdを非正規化したい(「受け取ったリアクション一覧」のようなものを(JOIN無しで)実装したいため) diff --git a/packages/backend/src/models/entities/NoteThreadMuting.ts b/packages/backend/src/models/NoteThreadMuting.ts similarity index 54% rename from packages/backend/src/models/entities/NoteThreadMuting.ts rename to packages/backend/src/models/NoteThreadMuting.ts index 3c884fe615..2d120e4c25 100644 --- a/packages/backend/src/models/entities/NoteThreadMuting.ts +++ b/packages/backend/src/models/NoteThreadMuting.ts @@ -1,10 +1,15 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('note_thread_muting') @Index(['userId', 'threadId'], { unique: true }) -export class NoteThreadMuting { +export class MiNoteThreadMuting { @PrimaryColumn(id()) public id: string; @@ -16,13 +21,13 @@ export class NoteThreadMuting { @Column({ ...id(), }) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Index() @Column('varchar', { diff --git a/packages/backend/src/models/entities/NoteUnread.ts b/packages/backend/src/models/NoteUnread.ts similarity index 55% rename from packages/backend/src/models/entities/NoteUnread.ts rename to packages/backend/src/models/NoteUnread.ts index af91234d0f..d86a474553 100644 --- a/packages/backend/src/models/entities/NoteUnread.ts +++ b/packages/backend/src/models/NoteUnread.ts @@ -1,34 +1,39 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { Note } from './Note.js'; -import type { Channel } from './Channel.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiNote } from './Note.js'; +import type { MiChannel } from './Channel.js'; + +@Entity('note_unread') @Index(['userId', 'noteId'], { unique: true }) -export class NoteUnread { +export class MiNoteUnread { @PrimaryColumn(id()) public id: string; @Index() @Column(id()) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Index() @Column(id()) - public noteId: Note['id']; + public noteId: MiNote['id']; - @ManyToOne(type => Note, { + @ManyToOne(type => MiNote, { onDelete: 'CASCADE', }) @JoinColumn() - public note: Note | null; + public note: MiNote | null; /** * メンションか否か @@ -50,7 +55,7 @@ export class NoteUnread { ...id(), comment: '[Denormalized]', }) - public noteUserId: User['id']; + public noteUserId: MiUser['id']; @Index() @Column({ @@ -58,6 +63,6 @@ export class NoteUnread { nullable: true, comment: '[Denormalized]', }) - public noteChannelId: Channel['id'] | null; + public noteChannelId: MiChannel['id'] | null; //#endregion } diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts new file mode 100644 index 0000000000..c0a9df2e23 --- /dev/null +++ b/packages/backend/src/models/Notification.ts @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { notificationTypes } from '@/types.js'; +import { MiUser } from './User.js'; +import { MiNote } from './Note.js'; +import { MiFollowRequest } from './FollowRequest.js'; +import { MiAccessToken } from './AccessToken.js'; + +export type MiNotification = { + id: string; + + // RedisのためDateではなくstring + createdAt: string; + + /** + * 通知の送信者(initiator) + */ + notifierId: MiUser['id'] | null; + + /** + * 通知の種類。 + */ + type: typeof notificationTypes[number]; + + noteId: MiNote['id'] | null; + + followRequestId: MiFollowRequest['id'] | null; + + reaction: string | null; + + choice: number | null; + + achievement: string | null; + + /** + * アプリ通知のbody + */ + customBody: string | null; + + /** + * アプリ通知のheader + * (省略時はアプリ名で表示されることを期待) + */ + customHeader: string | null; + + /** + * アプリ通知のicon(URL) + * (省略時はアプリアイコンで表示されることを期待) + */ + customIcon: string | null; + + /** + * アプリ通知のアプリ(のトークン) + */ + appAccessTokenId: MiAccessToken['id'] | null; +} diff --git a/packages/backend/src/models/entities/Page.ts b/packages/backend/src/models/Page.ts similarity index 75% rename from packages/backend/src/models/entities/Page.ts rename to packages/backend/src/models/Page.ts index 6078bc1bc7..3cb986f4ee 100644 --- a/packages/backend/src/models/entities/Page.ts +++ b/packages/backend/src/models/Page.ts @@ -1,11 +1,16 @@ -import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { DriveFile } from './DriveFile.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiDriveFile } from './DriveFile.js'; + +@Entity('page') @Index(['userId', 'name'], { unique: true }) -export class Page { +export class MiPage { @PrimaryColumn(id()) public id: string; @@ -55,25 +60,25 @@ export class Page { ...id(), comment: 'The ID of author.', }) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Column({ ...id(), nullable: true, }) - public eyeCatchingImageId: DriveFile['id'] | null; + public eyeCatchingImageId: MiDriveFile['id'] | null; - @ManyToOne(type => DriveFile, { + @ManyToOne(type => MiDriveFile, { onDelete: 'CASCADE', }) @JoinColumn() - public eyeCatchingImage: DriveFile | null; + public eyeCatchingImage: MiDriveFile | null; @Column('jsonb', { default: [], @@ -104,14 +109,14 @@ export class Page { ...id(), array: true, default: '{}', }) - public visibleUserIds: User['id'][]; + public visibleUserIds: MiUser['id'][]; @Column('integer', { default: 0, }) public likedCount: number; - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { diff --git a/packages/backend/src/models/PageLike.ts b/packages/backend/src/models/PageLike.ts new file mode 100644 index 0000000000..92adf9bcc2 --- /dev/null +++ b/packages/backend/src/models/PageLike.ts @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiPage } from './Page.js'; + +@Entity('page_like') +@Index(['userId', 'pageId'], { unique: true }) +export class MiPageLike { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Column(id()) + public pageId: MiPage['id']; + + @ManyToOne(type => MiPage, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public page: MiPage | null; +} diff --git a/packages/backend/src/models/entities/PasswordResetRequest.ts b/packages/backend/src/models/PasswordResetRequest.ts similarity index 51% rename from packages/backend/src/models/entities/PasswordResetRequest.ts rename to packages/backend/src/models/PasswordResetRequest.ts index 939fcc460f..79f2e984b8 100644 --- a/packages/backend/src/models/entities/PasswordResetRequest.ts +++ b/packages/backend/src/models/PasswordResetRequest.ts @@ -1,9 +1,14 @@ -import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class PasswordResetRequest { +import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('password_reset_request') +export class MiPasswordResetRequest { @PrimaryColumn(id()) public id: string; @@ -20,11 +25,11 @@ export class PasswordResetRequest { @Column({ ...id(), }) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; } diff --git a/packages/backend/src/models/entities/Poll.ts b/packages/backend/src/models/Poll.ts similarity index 69% rename from packages/backend/src/models/entities/Poll.ts rename to packages/backend/src/models/Poll.ts index ee1d646020..5ce0b9a2fc 100644 --- a/packages/backend/src/models/entities/Poll.ts +++ b/packages/backend/src/models/Poll.ts @@ -1,19 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; -import { id } from '../id.js'; -import { noteVisibilities } from '../../types.js'; -import { Note } from './Note.js'; -import type { User } from './User.js'; +import { noteVisibilities } from '@/types.js'; +import { id } from './util/id.js'; +import { MiNote } from './Note.js'; +import type { MiUser } from './User.js'; -@Entity() -export class Poll { +@Entity('poll') +export class MiPoll { @PrimaryColumn(id()) - public noteId: Note['id']; + public noteId: MiNote['id']; - @OneToOne(type => Note, { + @OneToOne(type => MiNote, { onDelete: 'CASCADE', }) @JoinColumn() - public note: Note | null; + public note: MiNote | null; @Column('timestamp with time zone', { nullable: true, @@ -45,7 +50,7 @@ export class Poll { ...id(), comment: '[Denormalized]', }) - public userId: User['id']; + public userId: MiUser['id']; @Index() @Column('varchar', { @@ -55,7 +60,7 @@ export class Poll { public userHost: string | null; //#endregion - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { diff --git a/packages/backend/src/models/entities/PollVote.ts b/packages/backend/src/models/PollVote.ts similarity index 52% rename from packages/backend/src/models/entities/PollVote.ts rename to packages/backend/src/models/PollVote.ts index d447a7be8f..37cd55fc18 100644 --- a/packages/backend/src/models/entities/PollVote.ts +++ b/packages/backend/src/models/PollVote.ts @@ -1,11 +1,16 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { Note } from './Note.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiNote } from './Note.js'; + +@Entity('poll_vote') @Index(['userId', 'noteId', 'choice'], { unique: true }) -export class PollVote { +export class MiPollVote { @PrimaryColumn(id()) public id: string; @@ -17,23 +22,23 @@ export class PollVote { @Index() @Column(id()) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Index() @Column(id()) - public noteId: Note['id']; + public noteId: MiNote['id']; - @ManyToOne(type => Note, { + @ManyToOne(type => MiNote, { onDelete: 'CASCADE', }) @JoinColumn() - public note: Note | null; + public note: MiNote | null; @Column('integer') public choice: number; diff --git a/packages/backend/src/models/PromoNote.ts b/packages/backend/src/models/PromoNote.ts new file mode 100644 index 0000000000..f4425fe88b --- /dev/null +++ b/packages/backend/src/models/PromoNote.ts @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiNote } from './Note.js'; +import type { MiUser } from './User.js'; + +@Entity('promo_note') +export class MiPromoNote { + @PrimaryColumn(id()) + public noteId: MiNote['id']; + + @OneToOne(type => MiNote, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: MiNote | null; + + @Column('timestamp with time zone') + public expiresAt: Date; + + //#region Denormalized fields + @Index() + @Column({ + ...id(), + comment: '[Denormalized]', + }) + public userId: MiUser['id']; + //#endregion +} diff --git a/packages/backend/src/models/PromoRead.ts b/packages/backend/src/models/PromoRead.ts new file mode 100644 index 0000000000..09ebfc8346 --- /dev/null +++ b/packages/backend/src/models/PromoRead.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiNote } from './Note.js'; +import { MiUser } from './User.js'; + +@Entity('promo_read') +@Index(['userId', 'noteId'], { unique: true }) +export class MiPromoRead { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the PromoRead.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Column(id()) + public noteId: MiNote['id']; + + @ManyToOne(type => MiNote, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: MiNote | null; +} diff --git a/packages/backend/src/models/RegistrationTicket.ts b/packages/backend/src/models/RegistrationTicket.ts new file mode 100644 index 0000000000..d94f465916 --- /dev/null +++ b/packages/backend/src/models/RegistrationTicket.ts @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn, OneToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('registration_ticket') +export class MiRegistrationTicket { + @PrimaryColumn(id()) + public id: string; + + @Index({ unique: true }) + @Column('varchar', { + length: 64, + }) + public code: string; + + @Column('timestamp with time zone', { + nullable: true, + }) + public expiresAt: Date | null; + + @Column('timestamp with time zone') + public createdAt: Date; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public createdBy: MiUser | null; + + @Index() + @Column({ + ...id(), + nullable: true, + }) + public createdById: MiUser['id'] | null; + + @OneToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public usedBy: MiUser | null; + + @Index() + @Column({ + ...id(), + nullable: true, + }) + public usedById: MiUser['id'] | null; + + @Column('timestamp with time zone', { + nullable: true, + }) + public usedAt: Date | null; + + @Column('varchar', { + length: 32, + nullable: true, + }) + public pendingUserId: string | null; +} diff --git a/packages/backend/src/models/entities/RegistryItem.ts b/packages/backend/src/models/RegistryItem.ts similarity index 77% rename from packages/backend/src/models/entities/RegistryItem.ts rename to packages/backend/src/models/RegistryItem.ts index 670a236ea0..fdce57c467 100644 --- a/packages/backend/src/models/entities/RegistryItem.ts +++ b/packages/backend/src/models/RegistryItem.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; // TODO: 同じdomain、同じscope、同じkeyのレコードは二つ以上存在しないように制約付けたい -@Entity() -export class RegistryItem { +@Entity('registry_item') +export class MiRegistryItem { @PrimaryColumn(id()) public id: string; @@ -23,13 +28,13 @@ export class RegistryItem { ...id(), comment: 'The owner ID.', }) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Column('varchar', { length: 1024, diff --git a/packages/backend/src/models/entities/Relay.ts b/packages/backend/src/models/Relay.ts similarity index 64% rename from packages/backend/src/models/entities/Relay.ts rename to packages/backend/src/models/Relay.ts index 94d1929574..293fccecfc 100644 --- a/packages/backend/src/models/entities/Relay.ts +++ b/packages/backend/src/models/Relay.ts @@ -1,8 +1,13 @@ -import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; -import { id } from '../id.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class Relay { +import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; +import { id } from './util/id.js'; + +@Entity('relay') +export class MiRelay { @PrimaryColumn(id()) public id: string; diff --git a/packages/backend/src/models/entities/RenoteMuting.ts b/packages/backend/src/models/RenoteMuting.ts similarity index 55% rename from packages/backend/src/models/entities/RenoteMuting.ts rename to packages/backend/src/models/RenoteMuting.ts index 2f803a5fa8..d2a36249dc 100644 --- a/packages/backend/src/models/entities/RenoteMuting.ts +++ b/packages/backend/src/models/RenoteMuting.ts @@ -1,10 +1,15 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('renote_muting') @Index(['muterId', 'muteeId'], { unique: true }) -export class RenoteMuting { +export class MiRenoteMuting { @PrimaryColumn(id()) public id: string; @@ -19,24 +24,24 @@ export class RenoteMuting { ...id(), comment: 'The mutee user ID.', }) - public muteeId: User['id']; + public muteeId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public mutee: User | null; + public mutee: MiUser | null; @Index() @Column({ ...id(), comment: 'The muter user ID.', }) - public muterId: User['id']; + public muterId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public muter: User | null; + public muter: MiUser | null; } diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 4231acc046..766e7ce21c 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -1,402 +1,401 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite, UserMemo, UserListFavorite } from './index.js'; +import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMutedNote, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListJoining, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './_.js'; import type { DataSource } from 'typeorm'; import type { Provider } from '@nestjs/common'; const $usersRepository: Provider = { provide: DI.usersRepository, - useFactory: (db: DataSource) => db.getRepository(User), + useFactory: (db: DataSource) => db.getRepository(MiUser), inject: [DI.db], }; const $notesRepository: Provider = { provide: DI.notesRepository, - useFactory: (db: DataSource) => db.getRepository(Note), + useFactory: (db: DataSource) => db.getRepository(MiNote), inject: [DI.db], }; const $announcementsRepository: Provider = { provide: DI.announcementsRepository, - useFactory: (db: DataSource) => db.getRepository(Announcement), + useFactory: (db: DataSource) => db.getRepository(MiAnnouncement), inject: [DI.db], }; const $announcementReadsRepository: Provider = { provide: DI.announcementReadsRepository, - useFactory: (db: DataSource) => db.getRepository(AnnouncementRead), + useFactory: (db: DataSource) => db.getRepository(MiAnnouncementRead), inject: [DI.db], }; const $appsRepository: Provider = { provide: DI.appsRepository, - useFactory: (db: DataSource) => db.getRepository(App), + useFactory: (db: DataSource) => db.getRepository(MiApp), inject: [DI.db], }; const $noteFavoritesRepository: Provider = { provide: DI.noteFavoritesRepository, - useFactory: (db: DataSource) => db.getRepository(NoteFavorite), + useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite), inject: [DI.db], }; const $noteThreadMutingsRepository: Provider = { provide: DI.noteThreadMutingsRepository, - useFactory: (db: DataSource) => db.getRepository(NoteThreadMuting), + useFactory: (db: DataSource) => db.getRepository(MiNoteThreadMuting), inject: [DI.db], }; const $noteReactionsRepository: Provider = { provide: DI.noteReactionsRepository, - useFactory: (db: DataSource) => db.getRepository(NoteReaction), + useFactory: (db: DataSource) => db.getRepository(MiNoteReaction), inject: [DI.db], }; const $noteUnreadsRepository: Provider = { provide: DI.noteUnreadsRepository, - useFactory: (db: DataSource) => db.getRepository(NoteUnread), + useFactory: (db: DataSource) => db.getRepository(MiNoteUnread), inject: [DI.db], }; const $pollsRepository: Provider = { provide: DI.pollsRepository, - useFactory: (db: DataSource) => db.getRepository(Poll), + useFactory: (db: DataSource) => db.getRepository(MiPoll), inject: [DI.db], }; const $pollVotesRepository: Provider = { provide: DI.pollVotesRepository, - useFactory: (db: DataSource) => db.getRepository(PollVote), + useFactory: (db: DataSource) => db.getRepository(MiPollVote), inject: [DI.db], }; const $userProfilesRepository: Provider = { provide: DI.userProfilesRepository, - useFactory: (db: DataSource) => db.getRepository(UserProfile), + useFactory: (db: DataSource) => db.getRepository(MiUserProfile), inject: [DI.db], }; const $userKeypairsRepository: Provider = { provide: DI.userKeypairsRepository, - useFactory: (db: DataSource) => db.getRepository(UserKeypair), + useFactory: (db: DataSource) => db.getRepository(MiUserKeypair), inject: [DI.db], }; const $userPendingsRepository: Provider = { provide: DI.userPendingsRepository, - useFactory: (db: DataSource) => db.getRepository(UserPending), - inject: [DI.db], -}; - -const $attestationChallengesRepository: Provider = { - provide: DI.attestationChallengesRepository, - useFactory: (db: DataSource) => db.getRepository(AttestationChallenge), + useFactory: (db: DataSource) => db.getRepository(MiUserPending), inject: [DI.db], }; const $userSecurityKeysRepository: Provider = { provide: DI.userSecurityKeysRepository, - useFactory: (db: DataSource) => db.getRepository(UserSecurityKey), + useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey), inject: [DI.db], }; const $userPublickeysRepository: Provider = { provide: DI.userPublickeysRepository, - useFactory: (db: DataSource) => db.getRepository(UserPublickey), + useFactory: (db: DataSource) => db.getRepository(MiUserPublickey), inject: [DI.db], }; const $userListsRepository: Provider = { provide: DI.userListsRepository, - useFactory: (db: DataSource) => db.getRepository(UserList), + useFactory: (db: DataSource) => db.getRepository(MiUserList), inject: [DI.db], }; const $userListFavoritesRepository: Provider = { provide: DI.userListFavoritesRepository, - useFactory: (db: DataSource) => db.getRepository(UserListFavorite), + useFactory: (db: DataSource) => db.getRepository(MiUserListFavorite), inject: [DI.db], }; const $userListJoiningsRepository: Provider = { provide: DI.userListJoiningsRepository, - useFactory: (db: DataSource) => db.getRepository(UserListJoining), + useFactory: (db: DataSource) => db.getRepository(MiUserListJoining), inject: [DI.db], }; const $userNotePiningsRepository: Provider = { provide: DI.userNotePiningsRepository, - useFactory: (db: DataSource) => db.getRepository(UserNotePining), + useFactory: (db: DataSource) => db.getRepository(MiUserNotePining), inject: [DI.db], }; const $userIpsRepository: Provider = { provide: DI.userIpsRepository, - useFactory: (db: DataSource) => db.getRepository(UserIp), + useFactory: (db: DataSource) => db.getRepository(MiUserIp), inject: [DI.db], }; const $usedUsernamesRepository: Provider = { provide: DI.usedUsernamesRepository, - useFactory: (db: DataSource) => db.getRepository(UsedUsername), + useFactory: (db: DataSource) => db.getRepository(MiUsedUsername), inject: [DI.db], }; const $followingsRepository: Provider = { provide: DI.followingsRepository, - useFactory: (db: DataSource) => db.getRepository(Following), + useFactory: (db: DataSource) => db.getRepository(MiFollowing), inject: [DI.db], }; const $followRequestsRepository: Provider = { provide: DI.followRequestsRepository, - useFactory: (db: DataSource) => db.getRepository(FollowRequest), + useFactory: (db: DataSource) => db.getRepository(MiFollowRequest), inject: [DI.db], }; const $instancesRepository: Provider = { provide: DI.instancesRepository, - useFactory: (db: DataSource) => db.getRepository(Instance), + useFactory: (db: DataSource) => db.getRepository(MiInstance), inject: [DI.db], }; const $emojisRepository: Provider = { provide: DI.emojisRepository, - useFactory: (db: DataSource) => db.getRepository(Emoji), + useFactory: (db: DataSource) => db.getRepository(MiEmoji), inject: [DI.db], }; const $driveFilesRepository: Provider = { provide: DI.driveFilesRepository, - useFactory: (db: DataSource) => db.getRepository(DriveFile), + useFactory: (db: DataSource) => db.getRepository(MiDriveFile), inject: [DI.db], }; const $driveFoldersRepository: Provider = { provide: DI.driveFoldersRepository, - useFactory: (db: DataSource) => db.getRepository(DriveFolder), + useFactory: (db: DataSource) => db.getRepository(MiDriveFolder), inject: [DI.db], }; const $metasRepository: Provider = { provide: DI.metasRepository, - useFactory: (db: DataSource) => db.getRepository(Meta), + useFactory: (db: DataSource) => db.getRepository(MiMeta), inject: [DI.db], }; const $mutingsRepository: Provider = { provide: DI.mutingsRepository, - useFactory: (db: DataSource) => db.getRepository(Muting), + useFactory: (db: DataSource) => db.getRepository(MiMuting), inject: [DI.db], }; const $renoteMutingsRepository: Provider = { provide: DI.renoteMutingsRepository, - useFactory: (db: DataSource) => db.getRepository(RenoteMuting), + useFactory: (db: DataSource) => db.getRepository(MiRenoteMuting), inject: [DI.db], }; const $blockingsRepository: Provider = { provide: DI.blockingsRepository, - useFactory: (db: DataSource) => db.getRepository(Blocking), + useFactory: (db: DataSource) => db.getRepository(MiBlocking), inject: [DI.db], }; const $swSubscriptionsRepository: Provider = { provide: DI.swSubscriptionsRepository, - useFactory: (db: DataSource) => db.getRepository(SwSubscription), + useFactory: (db: DataSource) => db.getRepository(MiSwSubscription), inject: [DI.db], }; const $hashtagsRepository: Provider = { provide: DI.hashtagsRepository, - useFactory: (db: DataSource) => db.getRepository(Hashtag), + useFactory: (db: DataSource) => db.getRepository(MiHashtag), inject: [DI.db], }; const $abuseUserReportsRepository: Provider = { provide: DI.abuseUserReportsRepository, - useFactory: (db: DataSource) => db.getRepository(AbuseUserReport), + useFactory: (db: DataSource) => db.getRepository(MiAbuseUserReport), inject: [DI.db], }; const $registrationTicketsRepository: Provider = { provide: DI.registrationTicketsRepository, - useFactory: (db: DataSource) => db.getRepository(RegistrationTicket), + useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket), inject: [DI.db], }; const $authSessionsRepository: Provider = { provide: DI.authSessionsRepository, - useFactory: (db: DataSource) => db.getRepository(AuthSession), + useFactory: (db: DataSource) => db.getRepository(MiAuthSession), inject: [DI.db], }; const $accessTokensRepository: Provider = { provide: DI.accessTokensRepository, - useFactory: (db: DataSource) => db.getRepository(AccessToken), + useFactory: (db: DataSource) => db.getRepository(MiAccessToken), inject: [DI.db], }; const $signinsRepository: Provider = { provide: DI.signinsRepository, - useFactory: (db: DataSource) => db.getRepository(Signin), + useFactory: (db: DataSource) => db.getRepository(MiSignin), inject: [DI.db], }; const $pagesRepository: Provider = { provide: DI.pagesRepository, - useFactory: (db: DataSource) => db.getRepository(Page), + useFactory: (db: DataSource) => db.getRepository(MiPage), inject: [DI.db], }; const $pageLikesRepository: Provider = { provide: DI.pageLikesRepository, - useFactory: (db: DataSource) => db.getRepository(PageLike), + useFactory: (db: DataSource) => db.getRepository(MiPageLike), inject: [DI.db], }; const $galleryPostsRepository: Provider = { provide: DI.galleryPostsRepository, - useFactory: (db: DataSource) => db.getRepository(GalleryPost), + useFactory: (db: DataSource) => db.getRepository(MiGalleryPost), inject: [DI.db], }; const $galleryLikesRepository: Provider = { provide: DI.galleryLikesRepository, - useFactory: (db: DataSource) => db.getRepository(GalleryLike), + useFactory: (db: DataSource) => db.getRepository(MiGalleryLike), inject: [DI.db], }; const $moderationLogsRepository: Provider = { provide: DI.moderationLogsRepository, - useFactory: (db: DataSource) => db.getRepository(ModerationLog), + useFactory: (db: DataSource) => db.getRepository(MiModerationLog), inject: [DI.db], }; const $clipsRepository: Provider = { provide: DI.clipsRepository, - useFactory: (db: DataSource) => db.getRepository(Clip), + useFactory: (db: DataSource) => db.getRepository(MiClip), inject: [DI.db], }; const $clipNotesRepository: Provider = { provide: DI.clipNotesRepository, - useFactory: (db: DataSource) => db.getRepository(ClipNote), + useFactory: (db: DataSource) => db.getRepository(MiClipNote), inject: [DI.db], }; const $clipFavoritesRepository: Provider = { provide: DI.clipFavoritesRepository, - useFactory: (db: DataSource) => db.getRepository(ClipFavorite), + useFactory: (db: DataSource) => db.getRepository(MiClipFavorite), inject: [DI.db], }; const $antennasRepository: Provider = { provide: DI.antennasRepository, - useFactory: (db: DataSource) => db.getRepository(Antenna), + useFactory: (db: DataSource) => db.getRepository(MiAntenna), inject: [DI.db], }; const $promoNotesRepository: Provider = { provide: DI.promoNotesRepository, - useFactory: (db: DataSource) => db.getRepository(PromoNote), + useFactory: (db: DataSource) => db.getRepository(MiPromoNote), inject: [DI.db], }; const $promoReadsRepository: Provider = { provide: DI.promoReadsRepository, - useFactory: (db: DataSource) => db.getRepository(PromoRead), + useFactory: (db: DataSource) => db.getRepository(MiPromoRead), inject: [DI.db], }; const $relaysRepository: Provider = { provide: DI.relaysRepository, - useFactory: (db: DataSource) => db.getRepository(Relay), + useFactory: (db: DataSource) => db.getRepository(MiRelay), inject: [DI.db], }; const $mutedNotesRepository: Provider = { provide: DI.mutedNotesRepository, - useFactory: (db: DataSource) => db.getRepository(MutedNote), + useFactory: (db: DataSource) => db.getRepository(MiMutedNote), inject: [DI.db], }; const $channelsRepository: Provider = { provide: DI.channelsRepository, - useFactory: (db: DataSource) => db.getRepository(Channel), + useFactory: (db: DataSource) => db.getRepository(MiChannel), inject: [DI.db], }; const $channelFollowingsRepository: Provider = { provide: DI.channelFollowingsRepository, - useFactory: (db: DataSource) => db.getRepository(ChannelFollowing), + useFactory: (db: DataSource) => db.getRepository(MiChannelFollowing), inject: [DI.db], }; const $channelFavoritesRepository: Provider = { provide: DI.channelFavoritesRepository, - useFactory: (db: DataSource) => db.getRepository(ChannelFavorite), + useFactory: (db: DataSource) => db.getRepository(MiChannelFavorite), inject: [DI.db], }; const $registryItemsRepository: Provider = { provide: DI.registryItemsRepository, - useFactory: (db: DataSource) => db.getRepository(RegistryItem), + useFactory: (db: DataSource) => db.getRepository(MiRegistryItem), inject: [DI.db], }; const $webhooksRepository: Provider = { provide: DI.webhooksRepository, - useFactory: (db: DataSource) => db.getRepository(Webhook), + useFactory: (db: DataSource) => db.getRepository(MiWebhook), inject: [DI.db], }; const $adsRepository: Provider = { provide: DI.adsRepository, - useFactory: (db: DataSource) => db.getRepository(Ad), + useFactory: (db: DataSource) => db.getRepository(MiAd), inject: [DI.db], }; const $passwordResetRequestsRepository: Provider = { provide: DI.passwordResetRequestsRepository, - useFactory: (db: DataSource) => db.getRepository(PasswordResetRequest), + useFactory: (db: DataSource) => db.getRepository(MiPasswordResetRequest), inject: [DI.db], }; const $retentionAggregationsRepository: Provider = { provide: DI.retentionAggregationsRepository, - useFactory: (db: DataSource) => db.getRepository(RetentionAggregation), + useFactory: (db: DataSource) => db.getRepository(MiRetentionAggregation), inject: [DI.db], }; const $flashsRepository: Provider = { provide: DI.flashsRepository, - useFactory: (db: DataSource) => db.getRepository(Flash), + useFactory: (db: DataSource) => db.getRepository(MiFlash), inject: [DI.db], }; const $flashLikesRepository: Provider = { provide: DI.flashLikesRepository, - useFactory: (db: DataSource) => db.getRepository(FlashLike), + useFactory: (db: DataSource) => db.getRepository(MiFlashLike), inject: [DI.db], }; const $rolesRepository: Provider = { provide: DI.rolesRepository, - useFactory: (db: DataSource) => db.getRepository(Role), + useFactory: (db: DataSource) => db.getRepository(MiRole), inject: [DI.db], }; const $roleAssignmentsRepository: Provider = { provide: DI.roleAssignmentsRepository, - useFactory: (db: DataSource) => db.getRepository(RoleAssignment), + useFactory: (db: DataSource) => db.getRepository(MiRoleAssignment), inject: [DI.db], }; const $userMemosRepository: Provider = { provide: DI.userMemosRepository, - useFactory: (db: DataSource) => db.getRepository(UserMemo), + useFactory: (db: DataSource) => db.getRepository(MiUserMemo), inject: [DI.db], }; @@ -418,7 +417,6 @@ const $userMemosRepository: Provider = { $userProfilesRepository, $userKeypairsRepository, $userPendingsRepository, - $attestationChallengesRepository, $userSecurityKeysRepository, $userPublickeysRepository, $userListsRepository, @@ -486,7 +484,6 @@ const $userMemosRepository: Provider = { $userProfilesRepository, $userKeypairsRepository, $userPendingsRepository, - $attestationChallengesRepository, $userSecurityKeysRepository, $userPublickeysRepository, $userListsRepository, diff --git a/packages/backend/src/models/entities/RetentionAggregation.ts b/packages/backend/src/models/RetentionAggregation.ts similarity index 68% rename from packages/backend/src/models/entities/RetentionAggregation.ts rename to packages/backend/src/models/RetentionAggregation.ts index c7bf38b3af..9da401597c 100644 --- a/packages/backend/src/models/entities/RetentionAggregation.ts +++ b/packages/backend/src/models/RetentionAggregation.ts @@ -1,9 +1,14 @@ -import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; -import { id } from '../id.js'; -import type { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class RetentionAggregation { +import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; +import { id } from './util/id.js'; +import type { MiUser } from './User.js'; + +@Entity('retention_aggregation') +export class MiRetentionAggregation { @PrimaryColumn(id()) public id: string; @@ -28,7 +33,7 @@ export class RetentionAggregation { ...id(), array: true, }) - public userIds: User['id'][]; + public userIds: MiUser['id'][]; @Column('integer', { }) diff --git a/packages/backend/src/models/entities/Role.ts b/packages/backend/src/models/Role.ts similarity index 94% rename from packages/backend/src/models/entities/Role.ts rename to packages/backend/src/models/Role.ts index 61f40d59da..df7541db3d 100644 --- a/packages/backend/src/models/entities/Role.ts +++ b/packages/backend/src/models/Role.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Entity, Column, PrimaryColumn } from 'typeorm'; -import { id } from '../id.js'; +import { id } from './util/id.js'; type CondFormulaValueAnd = { type: 'and'; @@ -79,8 +84,8 @@ export type RoleCondFormulaValue = CondFormulaValueNotesLessThanOrEq | CondFormulaValueNotesMoreThanOrEq; -@Entity() -export class Role { +@Entity('role') +export class MiRole { @PrimaryColumn(id()) public id: string; diff --git a/packages/backend/src/models/entities/RoleAssignment.ts b/packages/backend/src/models/RoleAssignment.ts similarity index 57% rename from packages/backend/src/models/entities/RoleAssignment.ts rename to packages/backend/src/models/RoleAssignment.ts index 972810940f..4e5322c60b 100644 --- a/packages/backend/src/models/entities/RoleAssignment.ts +++ b/packages/backend/src/models/RoleAssignment.ts @@ -1,11 +1,16 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { Role } from './Role.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiRole } from './Role.js'; +import { MiUser } from './User.js'; + +@Entity('role_assignment') @Index(['userId', 'roleId'], { unique: true }) -export class RoleAssignment { +export class MiRoleAssignment { @PrimaryColumn(id()) public id: string; @@ -19,26 +24,26 @@ export class RoleAssignment { ...id(), comment: 'The user ID.', }) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Index() @Column({ ...id(), comment: 'The role ID.', }) - public roleId: Role['id']; + public roleId: MiRole['id']; - @ManyToOne(type => Role, { + @ManyToOne(type => MiRole, { onDelete: 'CASCADE', }) @JoinColumn() - public role: Role | null; + public role: MiRole | null; @Index() @Column('timestamp with time zone', { diff --git a/packages/backend/src/models/entities/Signin.ts b/packages/backend/src/models/Signin.ts similarity index 60% rename from packages/backend/src/models/entities/Signin.ts rename to packages/backend/src/models/Signin.ts index 380bf028a6..a8b1a45c53 100644 --- a/packages/backend/src/models/entities/Signin.ts +++ b/packages/backend/src/models/Signin.ts @@ -1,9 +1,14 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class Signin { +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('signin') +export class MiSignin { @PrimaryColumn(id()) public id: string; @@ -14,13 +19,13 @@ export class Signin { @Index() @Column(id()) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Column('varchar', { length: 128, diff --git a/packages/backend/src/models/entities/SwSubscription.ts b/packages/backend/src/models/SwSubscription.ts similarity index 61% rename from packages/backend/src/models/entities/SwSubscription.ts rename to packages/backend/src/models/SwSubscription.ts index 0658294983..be1e4e3687 100644 --- a/packages/backend/src/models/entities/SwSubscription.ts +++ b/packages/backend/src/models/SwSubscription.ts @@ -1,9 +1,14 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class SwSubscription { +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('sw_subscription') +export class MiSwSubscription { @PrimaryColumn(id()) public id: string; @@ -12,13 +17,13 @@ export class SwSubscription { @Index() @Column(id()) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Column('varchar', { length: 512, diff --git a/packages/backend/src/models/entities/UsedUsername.ts b/packages/backend/src/models/UsedUsername.ts similarity index 58% rename from packages/backend/src/models/entities/UsedUsername.ts rename to packages/backend/src/models/UsedUsername.ts index eb90bef6ca..c75bf424c1 100644 --- a/packages/backend/src/models/entities/UsedUsername.ts +++ b/packages/backend/src/models/UsedUsername.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { PrimaryColumn, Entity, Column } from 'typeorm'; -@Entity() -export class UsedUsername { +@Entity('used_username') +export class MiUsedUsername { @PrimaryColumn('varchar', { length: 128, }) @@ -10,7 +15,7 @@ export class UsedUsername { @Column('timestamp with time zone') public createdAt: Date; - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { diff --git a/packages/backend/src/models/entities/User.ts b/packages/backend/src/models/User.ts similarity index 89% rename from packages/backend/src/models/entities/User.ts rename to packages/backend/src/models/User.ts index 6669890cf6..b040d302ce 100644 --- a/packages/backend/src/models/entities/User.ts +++ b/packages/backend/src/models/User.ts @@ -1,10 +1,15 @@ -import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; -import { id } from '../id.js'; -import { DriveFile } from './DriveFile.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; +import { id } from './util/id.js'; +import { MiDriveFile } from './DriveFile.js'; + +@Entity('user') @Index(['usernameLower', 'host'], { unique: true }) -export class User { +export class MiUser { @PrimaryColumn(id()) public id: string; @@ -98,26 +103,26 @@ export class User { nullable: true, comment: 'The ID of avatar DriveFile.', }) - public avatarId: DriveFile['id'] | null; + public avatarId: MiDriveFile['id'] | null; - @OneToOne(type => DriveFile, { + @OneToOne(type => MiDriveFile, { onDelete: 'SET NULL', }) @JoinColumn() - public avatar: DriveFile | null; + public avatar: MiDriveFile | null; @Column({ ...id(), nullable: true, comment: 'The ID of banner DriveFile.', }) - public bannerId: DriveFile['id'] | null; + public bannerId: MiDriveFile['id'] | null; - @OneToOne(type => DriveFile, { + @OneToOne(type => MiDriveFile, { onDelete: 'SET NULL', }) @JoinColumn() - public banner: DriveFile | null; + public banner: MiDriveFile | null; @Column('varchar', { length: 512, nullable: true, @@ -239,7 +244,7 @@ export class User { }) public token: string | null; - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { @@ -248,24 +253,24 @@ export class User { } } -export type LocalUser = User & { +export type MiLocalUser = MiUser & { host: null; uri: null; } -export type PartialLocalUser = Partial & { - id: User['id']; +export type MiPartialLocalUser = Partial & { + id: MiUser['id']; host: null; uri: null; } -export type RemoteUser = User & { +export type MiRemoteUser = MiUser & { host: string; uri: string; } -export type PartialRemoteUser = Partial & { - id: User['id']; +export type MiPartialRemoteUser = Partial & { + id: MiUser['id']; host: string; uri: string; } diff --git a/packages/backend/src/models/entities/UserIp.ts b/packages/backend/src/models/UserIp.ts similarity index 55% rename from packages/backend/src/models/entities/UserIp.ts rename to packages/backend/src/models/UserIp.ts index 628e3d0361..60a7bc8b01 100644 --- a/packages/backend/src/models/entities/UserIp.ts +++ b/packages/backend/src/models/UserIp.ts @@ -1,10 +1,15 @@ -import { Entity, Index, Column, PrimaryGeneratedColumn } from 'typeorm'; -import { id } from '../id.js'; -import type { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { Entity, Index, Column, PrimaryGeneratedColumn } from 'typeorm'; +import { id } from './util/id.js'; +import type { MiUser } from './User.js'; + +@Entity('user_ip') @Index(['userId', 'ip'], { unique: true }) -export class UserIp { +export class MiUserIp { @PrimaryGeneratedColumn() public id: string; @@ -14,7 +19,7 @@ export class UserIp { @Index() @Column(id()) - public userId: User['id']; + public userId: MiUser['id']; @Column('varchar', { length: 128, diff --git a/packages/backend/src/models/entities/UserKeypair.ts b/packages/backend/src/models/UserKeypair.ts similarity index 51% rename from packages/backend/src/models/entities/UserKeypair.ts rename to packages/backend/src/models/UserKeypair.ts index 3cd02d3c4f..a316dbaeb4 100644 --- a/packages/backend/src/models/entities/UserKeypair.ts +++ b/packages/backend/src/models/UserKeypair.ts @@ -1,17 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { PrimaryColumn, Entity, JoinColumn, Column, OneToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; -@Entity() -export class UserKeypair { +@Entity('user_keypair') +export class MiUserKeypair { @PrimaryColumn(id()) - public userId: User['id']; + public userId: MiUser['id']; - @OneToOne(type => User, { + @OneToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Column('varchar', { length: 4096, @@ -23,7 +28,7 @@ export class UserKeypair { }) public privateKey: string; - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { diff --git a/packages/backend/src/models/entities/UserList.ts b/packages/backend/src/models/UserList.ts similarity index 62% rename from packages/backend/src/models/entities/UserList.ts rename to packages/backend/src/models/UserList.ts index 94f3dc3cb3..9af85af97e 100644 --- a/packages/backend/src/models/entities/UserList.ts +++ b/packages/backend/src/models/UserList.ts @@ -1,9 +1,14 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class UserList { +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('user_list') +export class MiUserList { @PrimaryColumn(id()) public id: string; @@ -17,7 +22,7 @@ export class UserList { ...id(), comment: 'The owner ID.', }) - public userId: User['id']; + public userId: MiUser['id']; @Index() @Column('boolean', { @@ -25,11 +30,11 @@ export class UserList { }) public isPublic: boolean; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Column('varchar', { length: 128, diff --git a/packages/backend/src/models/UserListFavorite.ts b/packages/backend/src/models/UserListFavorite.ts new file mode 100644 index 0000000000..d0b054b932 --- /dev/null +++ b/packages/backend/src/models/UserListFavorite.ts @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiUserList } from './UserList.js'; + +@Entity('user_list_favorite') +@Index(['userId', 'userListId'], { unique: true }) +export class MiUserListFavorite { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Column(id()) + public userListId: MiUserList['id']; + + @ManyToOne(type => MiUserList, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public userList: MiUserList | null; +} diff --git a/packages/backend/src/models/entities/UserListJoining.ts b/packages/backend/src/models/UserListJoining.ts similarity index 51% rename from packages/backend/src/models/entities/UserListJoining.ts rename to packages/backend/src/models/UserListJoining.ts index a40793a3e8..4918f2f700 100644 --- a/packages/backend/src/models/entities/UserListJoining.ts +++ b/packages/backend/src/models/UserListJoining.ts @@ -1,11 +1,16 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { UserList } from './UserList.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiUserList } from './UserList.js'; + +@Entity('user_list_joining') @Index(['userId', 'userListId'], { unique: true }) -export class UserListJoining { +export class MiUserListJoining { @PrimaryColumn(id()) public id: string; @@ -19,24 +24,24 @@ export class UserListJoining { ...id(), comment: 'The user ID.', }) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Index() @Column({ ...id(), comment: 'The list ID.', }) - public userListId: UserList['id']; + public userListId: MiUserList['id']; - @ManyToOne(type => UserList, { + @ManyToOne(type => MiUserList, { onDelete: 'CASCADE', }) @JoinColumn() - public userList: UserList | null; + public userList: MiUserList | null; } diff --git a/packages/backend/src/models/entities/UserMemo.ts b/packages/backend/src/models/UserMemo.ts similarity index 54% rename from packages/backend/src/models/entities/UserMemo.ts rename to packages/backend/src/models/UserMemo.ts index 7dc34b4346..ab5e812c44 100644 --- a/packages/backend/src/models/entities/UserMemo.ts +++ b/packages/backend/src/models/UserMemo.ts @@ -1,10 +1,15 @@ -import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('user_memo') @Index(['userId', 'targetUserId'], { unique: true }) -export class UserMemo { +export class MiUserMemo { @PrimaryColumn(id()) public id: string; @@ -13,26 +18,26 @@ export class UserMemo { ...id(), comment: 'The ID of author.', }) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Index() @Column({ ...id(), comment: 'The ID of target user.', }) - public targetUserId: User['id']; + public targetUserId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public targetUser: User | null; + public targetUser: MiUser | null; @Column('varchar', { length: 2048, diff --git a/packages/backend/src/models/UserNotePining.ts b/packages/backend/src/models/UserNotePining.ts new file mode 100644 index 0000000000..1d50a5068e --- /dev/null +++ b/packages/backend/src/models/UserNotePining.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiNote } from './Note.js'; +import { MiUser } from './User.js'; + +@Entity('user_note_pining') +@Index(['userId', 'noteId'], { unique: true }) +export class MiUserNotePining { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserNotePinings.', + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Column(id()) + public noteId: MiNote['id']; + + @ManyToOne(type => MiNote, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: MiNote | null; +} diff --git a/packages/backend/src/models/entities/UserPending.ts b/packages/backend/src/models/UserPending.ts similarity index 69% rename from packages/backend/src/models/entities/UserPending.ts rename to packages/backend/src/models/UserPending.ts index 7637948841..b15ededa14 100644 --- a/packages/backend/src/models/entities/UserPending.ts +++ b/packages/backend/src/models/UserPending.ts @@ -1,8 +1,13 @@ -import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; -import { id } from '../id.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -@Entity() -export class UserPending { +import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; +import { id } from './util/id.js'; + +@Entity('user_pending') +export class MiUserPending { @PrimaryColumn(id()) public id: string; diff --git a/packages/backend/src/models/entities/UserProfile.ts b/packages/backend/src/models/UserProfile.ts similarity index 86% rename from packages/backend/src/models/entities/UserProfile.ts rename to packages/backend/src/models/UserProfile.ts index 236ee8f988..e4405c9da7 100644 --- a/packages/backend/src/models/entities/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -1,21 +1,26 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; import { obsoleteNotificationTypes, ffVisibility, notificationTypes } from '@/types.js'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { Page } from './Page.js'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiPage } from './Page.js'; // TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも // ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン -@Entity() -export class UserProfile { +@Entity('user_profile') +export class MiUserProfile { @PrimaryColumn(id()) - public userId: User['id']; + public userId: MiUser['id']; - @OneToOne(type => User, { + @OneToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Column('varchar', { length: 128, nullable: true, @@ -43,6 +48,12 @@ export class UserProfile { value: string; }[]; + @Column('varchar', { + array: true, + default: '{}', + }) + public verifiedLinks: string[]; + @Column('varchar', { length: 32, nullable: true, }) @@ -96,6 +107,11 @@ export class UserProfile { }) public twoFactorSecret: string | null; + @Column('varchar', { + nullable: true, array: true, + }) + public twoFactorBackupSecret: string[] | null; + @Column('boolean', { default: false, }) @@ -181,13 +197,13 @@ export class UserProfile { ...id(), nullable: true, }) - public pinnedPageId: Page['id'] | null; + public pinnedPageId: MiPage['id'] | null; - @OneToOne(type => Page, { + @OneToOne(type => MiPage, { onDelete: 'SET NULL', }) @JoinColumn() - public pinnedPage: Page | null; + public pinnedPage: MiPage | null; @Index() @Column('boolean', { @@ -207,7 +223,7 @@ export class UserProfile { public mutedInstances: string[]; @Column('enum', { - enum: [ + enum: [ ...notificationTypes, // マイグレーションで削除が困難なので古いenumは残しておく ...obsoleteNotificationTypes, @@ -239,7 +255,7 @@ export class UserProfile { public userHost: string | null; //#endregion - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { diff --git a/packages/backend/src/models/entities/UserPublickey.ts b/packages/backend/src/models/UserPublickey.ts similarity index 52% rename from packages/backend/src/models/entities/UserPublickey.ts rename to packages/backend/src/models/UserPublickey.ts index 7b505e5b4c..33de73c636 100644 --- a/packages/backend/src/models/entities/UserPublickey.ts +++ b/packages/backend/src/models/UserPublickey.ts @@ -1,17 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; -@Entity() -export class UserPublickey { +@Entity('user_publickey') +export class MiUserPublickey { @PrimaryColumn(id()) - public userId: User['id']; + public userId: MiUser['id']; - @OneToOne(type => User, { + @OneToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Index({ unique: true }) @Column('varchar', { @@ -24,7 +29,7 @@ export class UserPublickey { }) public keyPem: string; - constructor(data: Partial) { + constructor(data: Partial) { if (data == null) return; for (const [k, v] of Object.entries(data)) { diff --git a/packages/backend/src/models/UserSecurityKey.ts b/packages/backend/src/models/UserSecurityKey.ts new file mode 100644 index 0000000000..02c29bfbb5 --- /dev/null +++ b/packages/backend/src/models/UserSecurityKey.ts @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('user_security_key') +export class MiUserSecurityKey { + @PrimaryColumn('varchar', { + comment: 'Variable-length id given to navigator.credentials.get()', + }) + public id: string; + + @Index() + @Column(id()) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Column('varchar', { + comment: 'User-defined name for this key', + length: 30, + }) + public name: string; + + @Index() + @Column('varchar', { + comment: 'The public key of the UserSecurityKey, hex-encoded.', + }) + public publicKey: string; + + @Column('bigint', { + comment: 'The number of times the UserSecurityKey was validated.', + default: 0, + }) + public counter: number; + + @Column('timestamp with time zone', { + comment: 'Timestamp of the last time the UserSecurityKey was used.', + default: () => 'now()', + }) + public lastUsed: Date; + + @Column('varchar', { + comment: 'The type of Backup Eligibility in authenticator data', + length: 32, nullable: true, + }) + public credentialDeviceType: string | null; + + @Column('boolean', { + comment: 'Whether or not the credential has been backed up', + nullable: true, + }) + public credentialBackedUp: boolean | null; + + @Column('varchar', { + comment: 'The type of the credential returned by the browser', + length: 32, array: true, nullable: true, + }) + public transports: string[] | null; + + 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/Webhook.ts b/packages/backend/src/models/Webhook.ts similarity index 79% rename from packages/backend/src/models/entities/Webhook.ts rename to packages/backend/src/models/Webhook.ts index eabb604de9..5b009c18a6 100644 --- a/packages/backend/src/models/entities/Webhook.ts +++ b/packages/backend/src/models/Webhook.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const; -@Entity() -export class Webhook { +@Entity('webhook') +export class MiWebhook { @PrimaryColumn(id()) public id: string; @@ -19,13 +24,13 @@ export class Webhook { ...id(), comment: 'The owner ID.', }) - public userId: User['id']; + public userId: MiUser['id']; - @ManyToOne(type => User, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() - public user: User | null; + public user: MiUser | null; @Column('varchar', { length: 128, diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts new file mode 100644 index 0000000000..6be7bd0df6 --- /dev/null +++ b/packages/backend/src/models/_.ts @@ -0,0 +1,205 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; +import { MiAccessToken } from '@/models/AccessToken.js'; +import { MiAd } from '@/models/Ad.js'; +import { MiAnnouncement } from '@/models/Announcement.js'; +import { MiAnnouncementRead } from '@/models/AnnouncementRead.js'; +import { MiAntenna } from '@/models/Antenna.js'; +import { MiApp } from '@/models/App.js'; +import { MiAuthSession } from '@/models/AuthSession.js'; +import { MiBlocking } from '@/models/Blocking.js'; +import { MiChannelFollowing } from '@/models/ChannelFollowing.js'; +import { MiChannelFavorite } from '@/models/ChannelFavorite.js'; +import { MiClip } from '@/models/Clip.js'; +import { MiClipNote } from '@/models/ClipNote.js'; +import { MiClipFavorite } from '@/models/ClipFavorite.js'; +import { MiDriveFile } from '@/models/DriveFile.js'; +import { MiDriveFolder } from '@/models/DriveFolder.js'; +import { MiEmoji } from '@/models/Emoji.js'; +import { MiFollowing } from '@/models/Following.js'; +import { MiFollowRequest } from '@/models/FollowRequest.js'; +import { MiGalleryLike } from '@/models/GalleryLike.js'; +import { MiGalleryPost } from '@/models/GalleryPost.js'; +import { MiHashtag } from '@/models/Hashtag.js'; +import { MiInstance } from '@/models/Instance.js'; +import { MiMeta } from '@/models/Meta.js'; +import { MiModerationLog } from '@/models/ModerationLog.js'; +import { MiMutedNote } from '@/models/MutedNote.js'; +import { MiMuting } from '@/models/Muting.js'; +import { MiRenoteMuting } from '@/models/RenoteMuting.js'; +import { MiNote } from '@/models/Note.js'; +import { MiNoteFavorite } from '@/models/NoteFavorite.js'; +import { MiNoteReaction } from '@/models/NoteReaction.js'; +import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js'; +import { MiNoteUnread } from '@/models/NoteUnread.js'; +import { MiPage } from '@/models/Page.js'; +import { MiPageLike } from '@/models/PageLike.js'; +import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js'; +import { MiPoll } from '@/models/Poll.js'; +import { MiPollVote } from '@/models/PollVote.js'; +import { MiPromoNote } from '@/models/PromoNote.js'; +import { MiPromoRead } from '@/models/PromoRead.js'; +import { MiRegistrationTicket } from '@/models/RegistrationTicket.js'; +import { MiRegistryItem } from '@/models/RegistryItem.js'; +import { MiRelay } from '@/models/Relay.js'; +import { MiSignin } from '@/models/Signin.js'; +import { MiSwSubscription } from '@/models/SwSubscription.js'; +import { MiUsedUsername } from '@/models/UsedUsername.js'; +import { MiUser } from '@/models/User.js'; +import { MiUserIp } from '@/models/UserIp.js'; +import { MiUserKeypair } from '@/models/UserKeypair.js'; +import { MiUserList } from '@/models/UserList.js'; +import { MiUserListJoining } from '@/models/UserListJoining.js'; +import { MiUserNotePining } from '@/models/UserNotePining.js'; +import { MiUserPending } from '@/models/UserPending.js'; +import { MiUserProfile } from '@/models/UserProfile.js'; +import { MiUserPublickey } from '@/models/UserPublickey.js'; +import { MiUserSecurityKey } from '@/models/UserSecurityKey.js'; +import { MiUserMemo } from '@/models/UserMemo.js'; +import { MiWebhook } from '@/models/Webhook.js'; +import { MiChannel } from '@/models/Channel.js'; +import { MiRetentionAggregation } from '@/models/RetentionAggregation.js'; +import { MiRole } from '@/models/Role.js'; +import { MiRoleAssignment } from '@/models/RoleAssignment.js'; +import { MiFlash } from '@/models/Flash.js'; +import { MiFlashLike } from '@/models/FlashLike.js'; +import { MiUserListFavorite } from '@/models/UserListFavorite.js'; +import type { Repository } from 'typeorm'; + +export { + MiAbuseUserReport, + MiAccessToken, + MiAd, + MiAnnouncement, + MiAnnouncementRead, + MiAntenna, + MiApp, + MiAuthSession, + MiBlocking, + MiChannelFollowing, + MiChannelFavorite, + MiClip, + MiClipNote, + MiClipFavorite, + MiDriveFile, + MiDriveFolder, + MiEmoji, + MiFollowing, + MiFollowRequest, + MiGalleryLike, + MiGalleryPost, + MiHashtag, + MiInstance, + MiMeta, + MiModerationLog, + MiMutedNote, + MiMuting, + MiRenoteMuting, + MiNote, + MiNoteFavorite, + MiNoteReaction, + MiNoteThreadMuting, + MiNoteUnread, + MiPage, + MiPageLike, + MiPasswordResetRequest, + MiPoll, + MiPollVote, + MiPromoNote, + MiPromoRead, + MiRegistrationTicket, + MiRegistryItem, + MiRelay, + MiSignin, + MiSwSubscription, + MiUsedUsername, + MiUser, + MiUserIp, + MiUserKeypair, + MiUserList, + MiUserListFavorite, + MiUserListJoining, + MiUserNotePining, + MiUserPending, + MiUserProfile, + MiUserPublickey, + MiUserSecurityKey, + MiWebhook, + MiChannel, + MiRetentionAggregation, + MiRole, + MiRoleAssignment, + MiFlash, + MiFlashLike, + MiUserMemo, +}; + +export type AbuseUserReportsRepository = Repository; +export type AccessTokensRepository = Repository; +export type AdsRepository = Repository; +export type AnnouncementsRepository = Repository; +export type AnnouncementReadsRepository = Repository; +export type AntennasRepository = Repository; +export type AppsRepository = Repository; +export type AuthSessionsRepository = Repository; +export type BlockingsRepository = Repository; +export type ChannelFollowingsRepository = Repository; +export type ChannelFavoritesRepository = Repository; +export type ClipsRepository = Repository; +export type ClipNotesRepository = Repository; +export type ClipFavoritesRepository = Repository; +export type DriveFilesRepository = Repository; +export type DriveFoldersRepository = Repository; +export type EmojisRepository = Repository; +export type FollowingsRepository = Repository; +export type FollowRequestsRepository = Repository; +export type GalleryLikesRepository = Repository; +export type GalleryPostsRepository = Repository; +export type HashtagsRepository = Repository; +export type InstancesRepository = Repository; +export type MetasRepository = Repository; +export type ModerationLogsRepository = Repository; +export type MutedNotesRepository = Repository; +export type MutingsRepository = Repository; +export type RenoteMutingsRepository = Repository; +export type NotesRepository = Repository; +export type NoteFavoritesRepository = Repository; +export type NoteReactionsRepository = Repository; +export type NoteThreadMutingsRepository = Repository; +export type NoteUnreadsRepository = Repository; +export type PagesRepository = Repository; +export type PageLikesRepository = Repository; +export type PasswordResetRequestsRepository = Repository; +export type PollsRepository = Repository; +export type PollVotesRepository = Repository; +export type PromoNotesRepository = Repository; +export type PromoReadsRepository = Repository; +export type RegistrationTicketsRepository = Repository; +export type RegistryItemsRepository = Repository; +export type RelaysRepository = Repository; +export type SigninsRepository = Repository; +export type SwSubscriptionsRepository = Repository; +export type UsedUsernamesRepository = Repository; +export type UsersRepository = Repository; +export type UserIpsRepository = Repository; +export type UserKeypairsRepository = Repository; +export type UserListsRepository = Repository; +export type UserListFavoritesRepository = Repository; +export type UserListJoiningsRepository = Repository; +export type UserNotePiningsRepository = Repository; +export type UserPendingsRepository = Repository; +export type UserProfilesRepository = Repository; +export type UserPublickeysRepository = Repository; +export type UserSecurityKeysRepository = Repository; +export type WebhooksRepository = Repository; +export type ChannelsRepository = Repository; +export type RetentionAggregationsRepository = Repository; +export type RolesRepository = Repository; +export type RoleAssignmentsRepository = Repository; +export type FlashsRepository = Repository; +export type FlashLikesRepository = Repository; +export type UserMemoRepository = Repository; diff --git a/packages/backend/src/models/entities/Announcement.ts b/packages/backend/src/models/entities/Announcement.ts deleted file mode 100644 index beb2f82462..0000000000 --- a/packages/backend/src/models/entities/Announcement.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Entity, Index, Column, PrimaryColumn } from 'typeorm'; -import { id } from '../id.js'; - -@Entity() -export class Announcement { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the Announcement.', - }) - public createdAt: Date; - - @Column('timestamp with time zone', { - comment: 'The updated date of the Announcement.', - nullable: true, - }) - public updatedAt: Date | null; - - @Column('varchar', { - length: 8192, nullable: false, - }) - public text: string; - - @Column('varchar', { - length: 256, nullable: false, - }) - public title: string; - - @Column('varchar', { - length: 1024, nullable: true, - }) - public imageUrl: string | null; - - 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/AnnouncementRead.ts b/packages/backend/src/models/entities/AnnouncementRead.ts deleted file mode 100644 index 72cf688800..0000000000 --- a/packages/backend/src/models/entities/AnnouncementRead.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { Announcement } from './Announcement.js'; - -@Entity() -@Index(['userId', 'announcementId'], { unique: true }) -export class AnnouncementRead { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the AnnouncementRead.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column(id()) - public announcementId: Announcement['id']; - - @ManyToOne(type => Announcement, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public announcement: Announcement | null; -} diff --git a/packages/backend/src/models/entities/AttestationChallenge.ts b/packages/backend/src/models/entities/AttestationChallenge.ts deleted file mode 100644 index 4795642657..0000000000 --- a/packages/backend/src/models/entities/AttestationChallenge.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; - -@Entity() -export class AttestationChallenge { - @PrimaryColumn(id()) - public id: string; - - @Index() - @PrimaryColumn(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column('varchar', { - length: 64, - comment: 'Hex-encoded sha256 hash of the challenge.', - }) - public challenge: string; - - @Column('timestamp with time zone', { - comment: 'The date challenge was created for expiry purposes.', - }) - public createdAt: Date; - - @Column('boolean', { - comment: - 'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.', - default: false, - }) - public registrationChallenge: 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/ChannelFavorite.ts b/packages/backend/src/models/entities/ChannelFavorite.ts deleted file mode 100644 index cfb2c892cf..0000000000 --- a/packages/backend/src/models/entities/ChannelFavorite.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { Channel } from './Channel.js'; - -@Entity() -@Index(['userId', 'channelId'], { unique: true }) -export class ChannelFavorite { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the ChannelFavorite.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - }) - public channelId: Channel['id']; - - @ManyToOne(type => Channel, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public channel: Channel | null; - - @Index() - @Column({ - ...id(), - }) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; -} diff --git a/packages/backend/src/models/entities/ClipFavorite.ts b/packages/backend/src/models/entities/ClipFavorite.ts deleted file mode 100644 index 623471e671..0000000000 --- a/packages/backend/src/models/entities/ClipFavorite.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { Clip } from './Clip.js'; - -@Entity() -@Index(['userId', 'clipId'], { unique: true }) -export class ClipFavorite { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public clipId: Clip['id']; - - @ManyToOne(type => Clip, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public clip: Clip | null; -} diff --git a/packages/backend/src/models/entities/ClipNote.ts b/packages/backend/src/models/entities/ClipNote.ts deleted file mode 100644 index bc9ef4b874..0000000000 --- a/packages/backend/src/models/entities/ClipNote.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; -import { id } from '../id.js'; -import { Note } from './Note.js'; -import { Clip } from './Clip.js'; - -@Entity() -@Index(['noteId', 'clipId'], { unique: true }) -export class ClipNote { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - comment: 'The note ID.', - }) - public noteId: Note['id']; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Index() - @Column({ - ...id(), - comment: 'The clip ID.', - }) - public clipId: Clip['id']; - - @ManyToOne(type => Clip, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public clip: Clip | null; -} diff --git a/packages/backend/src/models/entities/FlashLike.ts b/packages/backend/src/models/entities/FlashLike.ts deleted file mode 100644 index 81d39191ca..0000000000 --- a/packages/backend/src/models/entities/FlashLike.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { Flash } from './Flash.js'; - -@Entity() -@Index(['userId', 'flashId'], { unique: true }) -export class FlashLike { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public flashId: Flash['id']; - - @ManyToOne(type => Flash, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public flash: Flash | null; -} diff --git a/packages/backend/src/models/entities/GalleryLike.ts b/packages/backend/src/models/entities/GalleryLike.ts deleted file mode 100644 index cc54b528e9..0000000000 --- a/packages/backend/src/models/entities/GalleryLike.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { GalleryPost } from './GalleryPost.js'; - -@Entity() -@Index(['userId', 'postId'], { unique: true }) -export class GalleryLike { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public postId: GalleryPost['id']; - - @ManyToOne(type => GalleryPost, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public post: GalleryPost | null; -} diff --git a/packages/backend/src/models/entities/NoteFavorite.ts b/packages/backend/src/models/entities/NoteFavorite.ts deleted file mode 100644 index 80c97cb531..0000000000 --- a/packages/backend/src/models/entities/NoteFavorite.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { Note } from './Note.js'; -import { User } from './User.js'; - -@Entity() -@Index(['userId', 'noteId'], { unique: true }) -export class NoteFavorite { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the NoteFavorite.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; -} diff --git a/packages/backend/src/models/entities/Notification.ts b/packages/backend/src/models/entities/Notification.ts deleted file mode 100644 index aa6f997124..0000000000 --- a/packages/backend/src/models/entities/Notification.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { notificationTypes } from '@/types.js'; -import { User } from './User.js'; -import { Note } from './Note.js'; -import { FollowRequest } from './FollowRequest.js'; -import { AccessToken } from './AccessToken.js'; - -export type Notification = { - id: string; - - // RedisのためDateではなくstring - createdAt: string; - - /** - * 通知の送信者(initiator) - */ - notifierId: User['id'] | null; - - /** - * 通知の種類。 - * follow - フォローされた - * mention - 投稿で自分が言及された - * reply - 投稿に返信された - * renote - 投稿がRenoteされた - * quote - 投稿が引用Renoteされた - * reaction - 投稿にリアクションされた - * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した - * receiveFollowRequest - フォローリクエストされた - * followRequestAccepted - 自分の送ったフォローリクエストが承認された - * achievementEarned - 実績を獲得 - * app - アプリ通知 - */ - type: typeof notificationTypes[number]; - - noteId: Note['id'] | null; - - followRequestId: FollowRequest['id'] | null; - - reaction: string | null; - - choice: number | null; - - achievement: string | null; - - /** - * アプリ通知のbody - */ - customBody: string | null; - - /** - * アプリ通知のheader - * (省略時はアプリ名で表示されることを期待) - */ - customHeader: string | null; - - /** - * アプリ通知のicon(URL) - * (省略時はアプリアイコンで表示されることを期待) - */ - customIcon: string | null; - - /** - * アプリ通知のアプリ(のトークン) - */ - appAccessTokenId: AccessToken['id'] | null; -} diff --git a/packages/backend/src/models/entities/PageLike.ts b/packages/backend/src/models/entities/PageLike.ts deleted file mode 100644 index f8c5943a3e..0000000000 --- a/packages/backend/src/models/entities/PageLike.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { Page } from './Page.js'; - -@Entity() -@Index(['userId', 'pageId'], { unique: true }) -export class PageLike { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public pageId: Page['id']; - - @ManyToOne(type => Page, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public page: Page | null; -} diff --git a/packages/backend/src/models/entities/PromoNote.ts b/packages/backend/src/models/entities/PromoNote.ts deleted file mode 100644 index 958008338a..0000000000 --- a/packages/backend/src/models/entities/PromoNote.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; -import { id } from '../id.js'; -import { Note } from './Note.js'; -import type { User } from './User.js'; - -@Entity() -export class PromoNote { - @PrimaryColumn(id()) - public noteId: Note['id']; - - @OneToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Column('timestamp with time zone') - public expiresAt: Date; - - //#region Denormalized fields - @Index() - @Column({ - ...id(), - comment: '[Denormalized]', - }) - public userId: User['id']; - //#endregion -} diff --git a/packages/backend/src/models/entities/PromoRead.ts b/packages/backend/src/models/entities/PromoRead.ts deleted file mode 100644 index 27f5d0dc11..0000000000 --- a/packages/backend/src/models/entities/PromoRead.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { Note } from './Note.js'; -import { User } from './User.js'; - -@Entity() -@Index(['userId', 'noteId'], { unique: true }) -export class PromoRead { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the PromoRead.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; -} diff --git a/packages/backend/src/models/entities/RegistrationTicket.ts b/packages/backend/src/models/entities/RegistrationTicket.ts deleted file mode 100644 index 139e40f85e..0000000000 --- a/packages/backend/src/models/entities/RegistrationTicket.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; -import { id } from '../id.js'; - -@Entity() -export class RegistrationTicket { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index({ unique: true }) - @Column('varchar', { - length: 64, - }) - public code: string; -} diff --git a/packages/backend/src/models/entities/UserListFavorite.ts b/packages/backend/src/models/entities/UserListFavorite.ts deleted file mode 100644 index e57abb460a..0000000000 --- a/packages/backend/src/models/entities/UserListFavorite.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; -import { UserList } from './UserList.js'; - -@Entity() -@Index(['userId', 'userListId'], { unique: true }) -export class UserListFavorite { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public userListId: UserList['id']; - - @ManyToOne(type => UserList, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userList: UserList | null; -} diff --git a/packages/backend/src/models/entities/UserNotePining.ts b/packages/backend/src/models/entities/UserNotePining.ts deleted file mode 100644 index fee95d4f7d..0000000000 --- a/packages/backend/src/models/entities/UserNotePining.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { Note } from './Note.js'; -import { User } from './User.js'; - -@Entity() -@Index(['userId', 'noteId'], { unique: true }) -export class UserNotePining { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the UserNotePinings.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; -} diff --git a/packages/backend/src/models/entities/UserSecurityKey.ts b/packages/backend/src/models/entities/UserSecurityKey.ts deleted file mode 100644 index 947692a32b..0000000000 --- a/packages/backend/src/models/entities/UserSecurityKey.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './User.js'; - -@Entity() -export class UserSecurityKey { - @PrimaryColumn('varchar', { - comment: 'Variable-length id given to navigator.credentials.get()', - }) - public id: string; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column('varchar', { - comment: - 'Variable-length public key used to verify attestations (hex-encoded).', - }) - public publicKey: string; - - @Column('timestamp with time zone', { - comment: - 'The date of the last time the UserSecurityKey was successfully validated.', - }) - public lastUsed: Date; - - @Column('varchar', { - comment: 'User-defined name for this key', - length: 30, - }) - public name: string; - - 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/id.ts b/packages/backend/src/models/id.ts deleted file mode 100644 index d614fc5048..0000000000 --- a/packages/backend/src/models/id.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const id = () => ({ - type: 'varchar' as const, - length: 32, -}); diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts deleted file mode 100644 index 4b230ab742..0000000000 --- a/packages/backend/src/models/index.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { AbuseUserReport } from '@/models/entities/AbuseUserReport.js'; -import { AccessToken } from '@/models/entities/AccessToken.js'; -import { Ad } from '@/models/entities/Ad.js'; -import { Announcement } from '@/models/entities/Announcement.js'; -import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js'; -import { Antenna } from '@/models/entities/Antenna.js'; -import { App } from '@/models/entities/App.js'; -import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js'; -import { AuthSession } from '@/models/entities/AuthSession.js'; -import { Blocking } from '@/models/entities/Blocking.js'; -import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js'; -import { ChannelFavorite } from '@/models/entities/ChannelFavorite.js'; -import { Clip } from '@/models/entities/Clip.js'; -import { ClipNote } from '@/models/entities/ClipNote.js'; -import { ClipFavorite } from '@/models/entities/ClipFavorite.js'; -import { DriveFile } from '@/models/entities/DriveFile.js'; -import { DriveFolder } from '@/models/entities/DriveFolder.js'; -import { Emoji } from '@/models/entities/Emoji.js'; -import { Following } from '@/models/entities/Following.js'; -import { FollowRequest } from '@/models/entities/FollowRequest.js'; -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 { Meta } from '@/models/entities/Meta.js'; -import { ModerationLog } from '@/models/entities/ModerationLog.js'; -import { MutedNote } from '@/models/entities/MutedNote.js'; -import { Muting } from '@/models/entities/Muting.js'; -import { RenoteMuting } from '@/models/entities/RenoteMuting.js'; -import { Note } from '@/models/entities/Note.js'; -import { NoteFavorite } from '@/models/entities/NoteFavorite.js'; -import { NoteReaction } from '@/models/entities/NoteReaction.js'; -import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js'; -import { NoteUnread } from '@/models/entities/NoteUnread.js'; -import { Page } from '@/models/entities/Page.js'; -import { PageLike } from '@/models/entities/PageLike.js'; -import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js'; -import { Poll } from '@/models/entities/Poll.js'; -import { PollVote } from '@/models/entities/PollVote.js'; -import { PromoNote } from '@/models/entities/PromoNote.js'; -import { PromoRead } from '@/models/entities/PromoRead.js'; -import { RegistrationTicket } from '@/models/entities/RegistrationTicket.js'; -import { RegistryItem } from '@/models/entities/RegistryItem.js'; -import { Relay } from '@/models/entities/Relay.js'; -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 { UserIp } from '@/models/entities/UserIp.js'; -import { UserKeypair } from '@/models/entities/UserKeypair.js'; -import { UserList } from '@/models/entities/UserList.js'; -import { UserListFavorite } from './entities/UserListFavorite.js'; -import { UserListJoining } from '@/models/entities/UserListJoining.js'; -import { UserNotePining } from '@/models/entities/UserNotePining.js'; -import { UserPending } from '@/models/entities/UserPending.js'; -import { UserProfile } from '@/models/entities/UserProfile.js'; -import { UserPublickey } from '@/models/entities/UserPublickey.js'; -import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js'; -import { UserMemo } from '@/models/entities/UserMemo.js'; -import { Webhook } from '@/models/entities/Webhook.js'; -import { Channel } from '@/models/entities/Channel.js'; -import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js'; -import { Role } from '@/models/entities/Role.js'; -import { RoleAssignment } from '@/models/entities/RoleAssignment.js'; -import { Flash } from '@/models/entities/Flash.js'; -import { FlashLike } from '@/models/entities/FlashLike.js'; -import type { Repository } from 'typeorm'; - -export { - AbuseUserReport, - AccessToken, - Ad, - Announcement, - AnnouncementRead, - Antenna, - App, - AttestationChallenge, - AuthSession, - Blocking, - ChannelFollowing, - ChannelFavorite, - Clip, - ClipNote, - ClipFavorite, - DriveFile, - DriveFolder, - Emoji, - Following, - FollowRequest, - GalleryLike, - GalleryPost, - Hashtag, - Instance, - Meta, - ModerationLog, - MutedNote, - Muting, - RenoteMuting, - Note, - NoteFavorite, - NoteReaction, - NoteThreadMuting, - NoteUnread, - Page, - PageLike, - PasswordResetRequest, - Poll, - PollVote, - PromoNote, - PromoRead, - RegistrationTicket, - RegistryItem, - Relay, - Signin, - SwSubscription, - UsedUsername, - User, - UserIp, - UserKeypair, - UserList, - UserListFavorite, - UserListJoining, - UserNotePining, - UserPending, - UserProfile, - UserPublickey, - UserSecurityKey, - Webhook, - Channel, - RetentionAggregation, - Role, - RoleAssignment, - Flash, - FlashLike, - UserMemo, -}; - -export type AbuseUserReportsRepository = Repository; -export type AccessTokensRepository = Repository; -export type AdsRepository = Repository; -export type AnnouncementsRepository = Repository; -export type AnnouncementReadsRepository = Repository; -export type AntennasRepository = Repository; -export type AppsRepository = Repository; -export type AttestationChallengesRepository = Repository; -export type AuthSessionsRepository = Repository; -export type BlockingsRepository = Repository; -export type ChannelFollowingsRepository = Repository; -export type ChannelFavoritesRepository = Repository; -export type ClipsRepository = Repository; -export type ClipNotesRepository = Repository; -export type ClipFavoritesRepository = Repository; -export type DriveFilesRepository = Repository; -export type DriveFoldersRepository = Repository; -export type EmojisRepository = Repository; -export type FollowingsRepository = Repository; -export type FollowRequestsRepository = Repository; -export type GalleryLikesRepository = Repository; -export type GalleryPostsRepository = Repository; -export type HashtagsRepository = Repository; -export type InstancesRepository = Repository; -export type MetasRepository = Repository; -export type ModerationLogsRepository = Repository; -export type MutedNotesRepository = Repository; -export type MutingsRepository = Repository; -export type RenoteMutingsRepository = Repository; -export type NotesRepository = Repository; -export type NoteFavoritesRepository = Repository; -export type NoteReactionsRepository = Repository; -export type NoteThreadMutingsRepository = Repository; -export type NoteUnreadsRepository = Repository; -export type PagesRepository = Repository; -export type PageLikesRepository = Repository; -export type PasswordResetRequestsRepository = Repository; -export type PollsRepository = Repository; -export type PollVotesRepository = Repository; -export type PromoNotesRepository = Repository; -export type PromoReadsRepository = Repository; -export type RegistrationTicketsRepository = Repository; -export type RegistryItemsRepository = Repository; -export type RelaysRepository = Repository; -export type SigninsRepository = Repository; -export type SwSubscriptionsRepository = Repository; -export type UsedUsernamesRepository = Repository; -export type UsersRepository = Repository; -export type UserIpsRepository = Repository; -export type UserKeypairsRepository = Repository; -export type UserListsRepository = Repository; -export type UserListFavoritesRepository = Repository; -export type UserListJoiningsRepository = Repository; -export type UserNotePiningsRepository = Repository; -export type UserPendingsRepository = Repository; -export type UserProfilesRepository = Repository; -export type UserPublickeysRepository = Repository; -export type UserSecurityKeysRepository = Repository; -export type WebhooksRepository = Repository; -export type ChannelsRepository = Repository; -export type RetentionAggregationsRepository = Repository; -export type RolesRepository = Repository; -export type RoleAssignmentsRepository = Repository; -export type FlashsRepository = Repository; -export type FlashLikesRepository = Repository; -export type UserMemoRepository = Repository; diff --git a/packages/backend/src/models/json-schema/announcement.ts b/packages/backend/src/models/json-schema/announcement.ts new file mode 100644 index 0000000000..c7e24c7f29 --- /dev/null +++ b/packages/backend/src/models/json-schema/announcement.ts @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedAnnouncementSchema = { + 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: true, + format: 'date-time', + }, + text: { + type: 'string', + optional: false, nullable: false, + }, + title: { + type: 'string', + optional: false, nullable: false, + }, + imageUrl: { + type: 'string', + optional: false, nullable: true, + }, + icon: { + type: 'string', + optional: false, nullable: false, + }, + display: { + type: 'string', + optional: false, nullable: false, + }, + forYou: { + type: 'boolean', + optional: false, nullable: false, + }, + needConfirmationToRead: { + type: 'boolean', + optional: false, nullable: false, + }, + isRead: { + type: 'boolean', + optional: true, nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/antenna.ts b/packages/backend/src/models/json-schema/antenna.ts index 4483510610..7b6475919c 100644 --- a/packages/backend/src/models/json-schema/antenna.ts +++ b/packages/backend/src/models/json-schema/antenna.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedAntennaSchema = { type: 'object', properties: { @@ -42,7 +47,7 @@ export const packedAntennaSchema = { src: { type: 'string', optional: false, nullable: false, - enum: ['home', 'all', 'users', 'list'], + enum: ['home', 'all', 'users', 'list', 'users_blacklist'], }, userListId: { type: 'string', diff --git a/packages/backend/src/models/json-schema/app.ts b/packages/backend/src/models/json-schema/app.ts index c80dc81c33..9e0916299c 100644 --- a/packages/backend/src/models/json-schema/app.ts +++ b/packages/backend/src/models/json-schema/app.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedAppSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/blocking.ts b/packages/backend/src/models/json-schema/blocking.ts index 5532322420..0b58f1f8d7 100644 --- a/packages/backend/src/models/json-schema/blocking.ts +++ b/packages/backend/src/models/json-schema/blocking.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedBlockingSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/channel.ts b/packages/backend/src/models/json-schema/channel.ts index fd61a70c0e..f1019d1461 100644 --- a/packages/backend/src/models/json-schema/channel.ts +++ b/packages/backend/src/models/json-schema/channel.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedChannelSchema = { type: 'object', properties: { @@ -67,5 +72,9 @@ export const packedChannelSchema = { type: 'string', optional: false, nullable: false, }, + isSensitive: { + type: 'boolean', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/clip.ts b/packages/backend/src/models/json-schema/clip.ts index 7310e59013..64f7a2ad9c 100644 --- a/packages/backend/src/models/json-schema/clip.ts +++ b/packages/backend/src/models/json-schema/clip.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedClipSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/drive-file.ts b/packages/backend/src/models/json-schema/drive-file.ts index 4359076612..87f1340812 100644 --- a/packages/backend/src/models/json-schema/drive-file.ts +++ b/packages/backend/src/models/json-schema/drive-file.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedDriveFileSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/drive-folder.ts b/packages/backend/src/models/json-schema/drive-folder.ts index 88cb8ab4a2..51107d423f 100644 --- a/packages/backend/src/models/json-schema/drive-folder.ts +++ b/packages/backend/src/models/json-schema/drive-folder.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedDriveFolderSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts index 63f56e77cb..99a58f8773 100644 --- a/packages/backend/src/models/json-schema/emoji.ts +++ b/packages/backend/src/models/json-schema/emoji.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedEmojiSimpleSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts index 42d93dfac9..ac07519f16 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedFederationInstanceSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/flash.ts b/packages/backend/src/models/json-schema/flash.ts index 8471a138ec..9453ba1dce 100644 --- a/packages/backend/src/models/json-schema/flash.ts +++ b/packages/backend/src/models/json-schema/flash.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedFlashSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/following.ts b/packages/backend/src/models/json-schema/following.ts index 2bcffbfc4d..3a24ebb619 100644 --- a/packages/backend/src/models/json-schema/following.ts +++ b/packages/backend/src/models/json-schema/following.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedFollowingSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/gallery-post.ts b/packages/backend/src/models/json-schema/gallery-post.ts index fc503d4a64..cf260c0bf5 100644 --- a/packages/backend/src/models/json-schema/gallery-post.ts +++ b/packages/backend/src/models/json-schema/gallery-post.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedGalleryPostSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/hashtag.ts b/packages/backend/src/models/json-schema/hashtag.ts index 98f8827640..a48e972a5d 100644 --- a/packages/backend/src/models/json-schema/hashtag.ts +++ b/packages/backend/src/models/json-schema/hashtag.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedHashtagSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/invite-code.ts b/packages/backend/src/models/json-schema/invite-code.ts new file mode 100644 index 0000000000..cd8bf98d90 --- /dev/null +++ b/packages/backend/src/models/json-schema/invite-code.ts @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedInviteCodeSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + code: { + type: 'string', + optional: false, nullable: false, + example: 'GR6S02ERUA5VR', + }, + expiresAt: { + type: 'string', + optional: false, nullable: true, + format: 'date-time', + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + createdBy: { + type: 'object', + optional: false, nullable: true, + ref: 'UserLite', + }, + usedBy: { + type: 'object', + optional: false, nullable: true, + ref: 'UserLite', + }, + usedAt: { + type: 'string', + optional: false, nullable: true, + format: 'date-time', + }, + used: { + type: 'boolean', + optional: false, nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/muting.ts b/packages/backend/src/models/json-schema/muting.ts index 3ab99e17e7..dde9dc0288 100644 --- a/packages/backend/src/models/json-schema/muting.ts +++ b/packages/backend/src/models/json-schema/muting.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedMutingSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/note-favorite.ts b/packages/backend/src/models/json-schema/note-favorite.ts index d133f7367d..3f0007d917 100644 --- a/packages/backend/src/models/json-schema/note-favorite.ts +++ b/packages/backend/src/models/json-schema/note-favorite.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedNoteFavoriteSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/note-reaction.ts b/packages/backend/src/models/json-schema/note-reaction.ts index 0d8fc5449b..e3335f426e 100644 --- a/packages/backend/src/models/json-schema/note-reaction.ts +++ b/packages/backend/src/models/json-schema/note-reaction.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedNoteReactionSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index 58ef425dcd..eb744aa109 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedNoteSchema = { type: 'object', properties: { @@ -134,6 +139,10 @@ export const packedNoteSchema = { type: 'string', optional: false, nullable: true, }, + isSensitive: { + type: 'boolean', + optional: true, nullable: false, + } }, }, }, diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index e88ca61ba0..2c434913da 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { notificationTypes } from '@/types.js'; export const packedNotificationSchema = { diff --git a/packages/backend/src/models/json-schema/page.ts b/packages/backend/src/models/json-schema/page.ts index 55ba3ce7f7..3f20a4b802 100644 --- a/packages/backend/src/models/json-schema/page.ts +++ b/packages/backend/src/models/json-schema/page.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedPageSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/queue.ts b/packages/backend/src/models/json-schema/queue.ts index 7ceeda26af..43da6e605d 100644 --- a/packages/backend/src/models/json-schema/queue.ts +++ b/packages/backend/src/models/json-schema/queue.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedQueueCountSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/renote-muting.ts b/packages/backend/src/models/json-schema/renote-muting.ts index 69ed8510da..feed1ceb09 100644 --- a/packages/backend/src/models/json-schema/renote-muting.ts +++ b/packages/backend/src/models/json-schema/renote-muting.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedRenoteMutingSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/user-list.ts b/packages/backend/src/models/json-schema/user-list.ts index 1e620516e4..e257d9984c 100644 --- a/packages/backend/src/models/json-schema/user-list.ts +++ b/packages/backend/src/models/json-schema/user-list.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedUserListSchema = { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index f9a20ac398..f15b225a30 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + export const packedUserLiteSchema = { type: 'object', properties: { @@ -164,6 +169,15 @@ export const packedUserDetailedNotMeOnlySchema = { }, }, }, + verifiedLinks: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'string', + nullable: false, optional: false, + format: 'url', + }, + }, followersCount: { type: 'number', nullable: false, optional: false, @@ -259,6 +273,10 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'string', nullable: false, optional: true, }, + notify: { + type: 'string', + nullable: false, optional: true, + }, //#endregion }, } as const; @@ -316,6 +334,11 @@ export const packedMeDetailedOnlySchema = { type: 'boolean', nullable: false, optional: false, }, + twoFactorBackupCodesStock: { + type: 'string', + enum: ['full', 'partial', 'none'], + nullable: false, optional: false, + }, hideOnlineStatus: { type: 'boolean', nullable: false, optional: false, diff --git a/packages/backend/src/models/util/id.ts b/packages/backend/src/models/util/id.ts new file mode 100644 index 0000000000..81e83b8db9 --- /dev/null +++ b/packages/backend/src/models/util/id.ts @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const id = () => ({ + type: 'varchar' as const, + length: 32, +}); diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 488979c409..10126eab2b 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + // https://github.com/typeorm/typeorm/issues/2400 import pg from 'pg'; pg.types.setTypeParser(20, Number); @@ -6,72 +11,71 @@ import { DataSource, Logger } from 'typeorm'; import * as highlight from 'cli-highlight'; import { entities as charts } from '@/core/chart/entities.js'; -import { AbuseUserReport } from '@/models/entities/AbuseUserReport.js'; -import { AccessToken } from '@/models/entities/AccessToken.js'; -import { Ad } from '@/models/entities/Ad.js'; -import { Announcement } from '@/models/entities/Announcement.js'; -import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js'; -import { Antenna } from '@/models/entities/Antenna.js'; -import { App } from '@/models/entities/App.js'; -import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js'; -import { AuthSession } from '@/models/entities/AuthSession.js'; -import { Blocking } from '@/models/entities/Blocking.js'; -import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js'; -import { ChannelFavorite } from '@/models/entities/ChannelFavorite.js'; -import { Clip } from '@/models/entities/Clip.js'; -import { ClipNote } from '@/models/entities/ClipNote.js'; -import { ClipFavorite } from '@/models/entities/ClipFavorite.js'; -import { DriveFile } from '@/models/entities/DriveFile.js'; -import { DriveFolder } from '@/models/entities/DriveFolder.js'; -import { Emoji } from '@/models/entities/Emoji.js'; -import { Following } from '@/models/entities/Following.js'; -import { FollowRequest } from '@/models/entities/FollowRequest.js'; -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 { Meta } from '@/models/entities/Meta.js'; -import { ModerationLog } from '@/models/entities/ModerationLog.js'; -import { MutedNote } from '@/models/entities/MutedNote.js'; -import { Muting } from '@/models/entities/Muting.js'; -import { RenoteMuting } from '@/models/entities/RenoteMuting.js'; -import { Note } from '@/models/entities/Note.js'; -import { NoteFavorite } from '@/models/entities/NoteFavorite.js'; -import { NoteReaction } from '@/models/entities/NoteReaction.js'; -import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js'; -import { NoteUnread } from '@/models/entities/NoteUnread.js'; -import { Page } from '@/models/entities/Page.js'; -import { PageLike } from '@/models/entities/PageLike.js'; -import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js'; -import { Poll } from '@/models/entities/Poll.js'; -import { PollVote } from '@/models/entities/PollVote.js'; -import { PromoNote } from '@/models/entities/PromoNote.js'; -import { PromoRead } from '@/models/entities/PromoRead.js'; -import { RegistrationTicket } from '@/models/entities/RegistrationTicket.js'; -import { RegistryItem } from '@/models/entities/RegistryItem.js'; -import { Relay } from '@/models/entities/Relay.js'; -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 { UserIp } from '@/models/entities/UserIp.js'; -import { UserKeypair } from '@/models/entities/UserKeypair.js'; -import { UserList } from '@/models/entities/UserList.js'; -import { UserListFavorite } from '@/models/entities/UserListFavorite.js'; -import { UserListJoining } from '@/models/entities/UserListJoining.js'; -import { UserNotePining } from '@/models/entities/UserNotePining.js'; -import { UserPending } from '@/models/entities/UserPending.js'; -import { UserProfile } from '@/models/entities/UserProfile.js'; -import { UserPublickey } from '@/models/entities/UserPublickey.js'; -import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js'; -import { Webhook } from '@/models/entities/Webhook.js'; -import { Channel } from '@/models/entities/Channel.js'; -import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js'; -import { Role } from '@/models/entities/Role.js'; -import { RoleAssignment } from '@/models/entities/RoleAssignment.js'; -import { Flash } from '@/models/entities/Flash.js'; -import { FlashLike } from '@/models/entities/FlashLike.js'; -import { UserMemo } from '@/models/entities/UserMemo.js'; +import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; +import { MiAccessToken } from '@/models/AccessToken.js'; +import { MiAd } from '@/models/Ad.js'; +import { MiAnnouncement } from '@/models/Announcement.js'; +import { MiAnnouncementRead } from '@/models/AnnouncementRead.js'; +import { MiAntenna } from '@/models/Antenna.js'; +import { MiApp } from '@/models/App.js'; +import { MiAuthSession } from '@/models/AuthSession.js'; +import { MiBlocking } from '@/models/Blocking.js'; +import { MiChannelFollowing } from '@/models/ChannelFollowing.js'; +import { MiChannelFavorite } from '@/models/ChannelFavorite.js'; +import { MiClip } from '@/models/Clip.js'; +import { MiClipNote } from '@/models/ClipNote.js'; +import { MiClipFavorite } from '@/models/ClipFavorite.js'; +import { MiDriveFile } from '@/models/DriveFile.js'; +import { MiDriveFolder } from '@/models/DriveFolder.js'; +import { MiEmoji } from '@/models/Emoji.js'; +import { MiFollowing } from '@/models/Following.js'; +import { MiFollowRequest } from '@/models/FollowRequest.js'; +import { MiGalleryLike } from '@/models/GalleryLike.js'; +import { MiGalleryPost } from '@/models/GalleryPost.js'; +import { MiHashtag } from '@/models/Hashtag.js'; +import { MiInstance } from '@/models/Instance.js'; +import { MiMeta } from '@/models/Meta.js'; +import { MiModerationLog } from '@/models/ModerationLog.js'; +import { MiMutedNote } from '@/models/MutedNote.js'; +import { MiMuting } from '@/models/Muting.js'; +import { MiRenoteMuting } from '@/models/RenoteMuting.js'; +import { MiNote } from '@/models/Note.js'; +import { MiNoteFavorite } from '@/models/NoteFavorite.js'; +import { MiNoteReaction } from '@/models/NoteReaction.js'; +import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js'; +import { MiNoteUnread } from '@/models/NoteUnread.js'; +import { MiPage } from '@/models/Page.js'; +import { MiPageLike } from '@/models/PageLike.js'; +import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js'; +import { MiPoll } from '@/models/Poll.js'; +import { MiPollVote } from '@/models/PollVote.js'; +import { MiPromoNote } from '@/models/PromoNote.js'; +import { MiPromoRead } from '@/models/PromoRead.js'; +import { MiRegistrationTicket } from '@/models/RegistrationTicket.js'; +import { MiRegistryItem } from '@/models/RegistryItem.js'; +import { MiRelay } from '@/models/Relay.js'; +import { MiSignin } from '@/models/Signin.js'; +import { MiSwSubscription } from '@/models/SwSubscription.js'; +import { MiUsedUsername } from '@/models/UsedUsername.js'; +import { MiUser } from '@/models/User.js'; +import { MiUserIp } from '@/models/UserIp.js'; +import { MiUserKeypair } from '@/models/UserKeypair.js'; +import { MiUserList } from '@/models/UserList.js'; +import { MiUserListFavorite } from '@/models/UserListFavorite.js'; +import { MiUserListJoining } from '@/models/UserListJoining.js'; +import { MiUserNotePining } from '@/models/UserNotePining.js'; +import { MiUserPending } from '@/models/UserPending.js'; +import { MiUserProfile } from '@/models/UserProfile.js'; +import { MiUserPublickey } from '@/models/UserPublickey.js'; +import { MiUserSecurityKey } from '@/models/UserSecurityKey.js'; +import { MiWebhook } from '@/models/Webhook.js'; +import { MiChannel } from '@/models/Channel.js'; +import { MiRetentionAggregation } from '@/models/RetentionAggregation.js'; +import { MiRole } from '@/models/Role.js'; +import { MiRoleAssignment } from '@/models/RoleAssignment.js'; +import { MiFlash } from '@/models/Flash.js'; +import { MiFlashLike } from '@/models/FlashLike.js'; +import { MiUserMemo } from '@/models/UserMemo.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; @@ -121,72 +125,71 @@ class MyCustomLogger implements Logger { } export const entities = [ - Announcement, - AnnouncementRead, - Meta, - Instance, - App, - AuthSession, - AccessToken, - User, - UserProfile, - UserKeypair, - UserPublickey, - UserList, - UserListFavorite, - UserListJoining, - UserNotePining, - UserSecurityKey, - UsedUsername, - AttestationChallenge, - Following, - FollowRequest, - Muting, - RenoteMuting, - Blocking, - Note, - NoteFavorite, - NoteReaction, - NoteThreadMuting, - NoteUnread, - Page, - PageLike, - GalleryPost, - GalleryLike, - DriveFile, - DriveFolder, - Poll, - PollVote, - Emoji, - Hashtag, - SwSubscription, - AbuseUserReport, - RegistrationTicket, - Signin, - ModerationLog, - Clip, - ClipNote, - ClipFavorite, - Antenna, - PromoNote, - PromoRead, - Relay, - MutedNote, - Channel, - ChannelFollowing, - ChannelFavorite, - RegistryItem, - Ad, - PasswordResetRequest, - UserPending, - Webhook, - UserIp, - RetentionAggregation, - Role, - RoleAssignment, - Flash, - FlashLike, - UserMemo, + MiAnnouncement, + MiAnnouncementRead, + MiMeta, + MiInstance, + MiApp, + MiAuthSession, + MiAccessToken, + MiUser, + MiUserProfile, + MiUserKeypair, + MiUserPublickey, + MiUserList, + MiUserListFavorite, + MiUserListJoining, + MiUserNotePining, + MiUserSecurityKey, + MiUsedUsername, + MiFollowing, + MiFollowRequest, + MiMuting, + MiRenoteMuting, + MiBlocking, + MiNote, + MiNoteFavorite, + MiNoteReaction, + MiNoteThreadMuting, + MiNoteUnread, + MiPage, + MiPageLike, + MiGalleryPost, + MiGalleryLike, + MiDriveFile, + MiDriveFolder, + MiPoll, + MiPollVote, + MiEmoji, + MiHashtag, + MiSwSubscription, + MiAbuseUserReport, + MiRegistrationTicket, + MiSignin, + MiModerationLog, + MiClip, + MiClipNote, + MiClipFavorite, + MiAntenna, + MiPromoNote, + MiPromoRead, + MiRelay, + MiMutedNote, + MiChannel, + MiChannelFollowing, + MiChannelFavorite, + MiRegistryItem, + MiAd, + MiPasswordResetRequest, + MiUserPending, + MiWebhook, + MiUserIp, + MiRetentionAggregation, + MiRole, + MiRoleAssignment, + MiFlash, + MiFlashLike, + MiUserMemo, ...charts, ]; @@ -227,7 +230,7 @@ export function createPostgresDataSource(config: Config) { options: { host: config.redis.host, port: config.redis.port, - family: config.redis.family == null ? 0 : config.redis.family, + family: config.redis.family ?? 0, password: config.redis.pass, keyPrefix: `${config.redis.prefix}:query:`, db: config.redis.db ?? 0, diff --git a/packages/backend/src/queue/QueueLoggerService.ts b/packages/backend/src/queue/QueueLoggerService.ts index 648af893c2..618d1d5c2f 100644 --- a/packages/backend/src/queue/QueueLoggerService.ts +++ b/packages/backend/src/queue/QueueLoggerService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index e1c6b93d9b..e6327002c5 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Module } from '@nestjs/common'; import { CoreModule } from '@/core/CoreModule.js'; import { GlobalModule } from '@/GlobalModule.js'; diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 42f9c1af7d..5201bfed8e 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Bull from 'bullmq'; import type { Config } from '@/config.js'; @@ -283,7 +288,7 @@ export class QueueProcessorService implements OnApplicationShutdown { }); const relationshipLogger = this.logger.createSubLogger('relationship'); - + this.relationshipQueueWorker .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`)) diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts index d240fe70e0..87d075304d 100644 --- a/packages/backend/src/queue/const.ts +++ b/packages/backend/src/queue/const.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Config } from '@/config.js'; import type * as Bull from 'bullmq'; @@ -15,11 +20,8 @@ export const QUEUE = { export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions { return { connection: { - port: config.redisForJobQueue.port, - host: config.redisForJobQueue.host, - family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family, - password: config.redisForJobQueue.pass, - db: config.redisForJobQueue.db ?? 0, + ...config.redisForJobQueue, + keyPrefix: undefined, }, prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue:${queueName}` : `queue:${queueName}`, }; diff --git a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts index 600ce0828f..5aac3f19e8 100644 --- a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts +++ b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts @@ -1,10 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { IsNull, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import type { RetentionAggregationsRepository, UsersRepository } from '@/models/index.js'; +import type { RetentionAggregationsRepository, UsersRepository } from '@/models/_.js'; import { deepClone } from '@/misc/clone.js'; import { IdService } from '@/core/IdService.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; @@ -16,9 +20,6 @@ export class AggregateRetentionProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts index c4ee212bab..9b07389dc3 100644 --- a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts @@ -1,8 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { MutingsRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { MutingsRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { UserMutingService } from '@/core/UserMutingService.js'; @@ -14,9 +18,6 @@ export class CheckExpiredMutingsProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, diff --git a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts index 22d7c1b4fb..55c444eee6 100644 --- a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts @@ -1,6 +1,9 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import FederationChart from '@/core/chart/charts/federation.js'; import NotesChart from '@/core/chart/charts/notes.js'; @@ -23,9 +26,6 @@ export class CleanChartsProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - private federationChart: FederationChart, private notesChart: NotesChart, private usersChart: UsersChart, diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index cefa6da5e9..f0453f7054 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { In, LessThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { AntennasRepository, MutedNotesRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { AntennasRepository, MutedNotesRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; +import type { Config } from '@/config.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -53,12 +58,14 @@ export class CleanProcessorService { reason: 'word', }); - // 7日以上使われてないアンテナを停止 - this.antennasRepository.update({ - lastUsedAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 7))), - }, { - isActive: false, - }); + // 使われてないアンテナを停止 + if (this.config.deactivateAntennaThreshold > 0) { + this.antennasRepository.update({ + lastUsedAt: LessThan(new Date(Date.now() - this.config.deactivateAntennaThreshold)), + }, { + isActive: false, + }); + } const expiredRoleAssignments = await this.roleAssignmentsRepository.createQueryBuilder('assign') .where('assign.expiresAt IS NOT NULL') diff --git a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts index c54bf59ae4..b62cc8a8fd 100644 --- a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts @@ -1,8 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { IsNull, MoreThan, Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { bindThis } from '@/decorators.js'; @@ -14,9 +18,6 @@ export class CleanRemoteFilesProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -31,7 +32,7 @@ export class CleanRemoteFilesProcessorService { this.logger.info('Deleting cached remote files...'); let deletedCount = 0; - let cursor: any = null; + let cursor: MiDriveFile['id'] | null = null; while (true) { const files = await this.driveFilesRepository.find({ @@ -51,7 +52,7 @@ export class CleanRemoteFilesProcessorService { break; } - cursor = files[files.length - 1].id; + cursor = files.at(-1)?.id ?? null; await Promise.all(files.map(file => this.driveService.deleteFileSync(file, true))); diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts index 39dd801af0..39967165d4 100644 --- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -1,14 +1,19 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; -import type { DriveFile } from '@/models/entities/DriveFile.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { MiNote } from '@/models/Note.js'; import { EmailService } from '@/core/EmailService.js'; import { bindThis } from '@/decorators.js'; +import { SearchService } from '@/core/SearchService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbUserDeleteJobData } from '../types.js'; @@ -18,9 +23,6 @@ export class DeleteAccountProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -36,6 +38,7 @@ export class DeleteAccountProcessorService { private driveService: DriveService, private emailService: EmailService, private queueLoggerService: QueueLoggerService, + private searchService: SearchService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('delete-account'); } @@ -50,7 +53,7 @@ export class DeleteAccountProcessorService { } { // Delete notes - let cursor: Note['id'] | null = null; + let cursor: MiNote['id'] | null = null; while (true) { const notes = await this.notesRepository.find({ @@ -62,22 +65,26 @@ export class DeleteAccountProcessorService { order: { id: 1, }, - }) as Note[]; + }) as MiNote[]; if (notes.length === 0) { break; } - cursor = notes[notes.length - 1].id; + cursor = notes.at(-1)?.id ?? null; await this.notesRepository.delete(notes.map(note => note.id)); + + for (const note of notes) { + await this.searchService.unindexNote(note); + } } this.logger.succ('All of notes deleted'); } { // Delete files - let cursor: DriveFile['id'] | null = null; + let cursor: MiDriveFile['id'] | null = null; while (true) { const files = await this.driveFilesRepository.find({ @@ -89,13 +96,13 @@ export class DeleteAccountProcessorService { order: { id: 1, }, - }) as DriveFile[]; + }) as MiDriveFile[]; if (files.length === 0) { break; } - cursor = files[files.length - 1].id; + cursor = files.at(-1)?.id ?? null; for (const file of files) { await this.driveService.deleteFileSync(file); diff --git a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts index 6772c5dc76..6d0a45bcc0 100644 --- a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts @@ -1,8 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, DriveFilesRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { UsersRepository, DriveFilesRepository, MiDriveFile } from '@/models/_.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { bindThis } from '@/decorators.js'; @@ -15,9 +19,6 @@ export class DeleteDriveFilesProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -40,7 +41,7 @@ export class DeleteDriveFilesProcessorService { } let deletedCount = 0; - let cursor: any = null; + let cursor: MiDriveFile['id'] | null = null; while (true) { const files = await this.driveFilesRepository.find({ @@ -59,7 +60,7 @@ export class DeleteDriveFilesProcessorService { break; } - cursor = files[files.length - 1].id; + cursor = files.at(-1)?.id ?? null; for (const file of files) { await this.driveService.deleteFileSync(file); diff --git a/packages/backend/src/queue/processors/DeleteFileProcessorService.ts b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts index edf87bd921..a4638bfaaf 100644 --- a/packages/backend/src/queue/processors/DeleteFileProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts @@ -1,6 +1,9 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { bindThis } from '@/decorators.js'; @@ -13,9 +16,6 @@ export class DeleteFileProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - private driveService: DriveService, private queueLoggerService: QueueLoggerService, ) { diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 406e9df850..4a1d9f28b4 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -1,15 +1,19 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import * as Bull from 'bullmq'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, InstancesRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { InstancesRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; 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 { MemorySingleCache } from '@/misc/cache.js'; -import type { Instance } from '@/models/entities/Instance.js'; +import type { MiInstance } from '@/models/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'; @@ -22,19 +26,13 @@ import type { DeliverJobData } from '../types.js'; @Injectable() export class DeliverProcessorService { private logger: Logger; - private suspendedHostsCache: MemorySingleCache; + private suspendedHostsCache: MemorySingleCache; private latest: string | null; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, - private metaService: MetaService, private utilityService: UtilityService, private federatedInstanceService: FederatedInstanceService, @@ -46,7 +44,7 @@ export class DeliverProcessorService { private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('deliver'); - this.suspendedHostsCache = new MemorySingleCache(1000 * 60 * 60); + this.suspendedHostsCache = new MemorySingleCache(1000 * 60 * 60); } @bindThis diff --git a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts index 21501592f2..4a48084436 100644 --- a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts +++ b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts @@ -1,7 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { PollVotesRepository, NotesRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { PollVotesRepository, NotesRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; @@ -14,9 +18,6 @@ export class EndedPollNotificationProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts index ac52325c8d..f941fb6e85 100644 --- a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts @@ -1,10 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import { format as DateFormat } from 'date-fns'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { AntennasRepository, UsersRepository, UserListJoiningsRepository, User } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { AntennasRepository, UsersRepository, UserListJoiningsRepository, MiUser } from '@/models/_.js'; import Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { bindThis } from '@/decorators.js'; @@ -19,9 +23,6 @@ export class ExportAntennasProcessorService { private logger: Logger; constructor ( - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -30,7 +31,7 @@ export class ExportAntennasProcessorService { @Inject(DI.userListJoiningsRepository) private userListJoiningsRepository: UserListJoiningsRepository, - + private driveService: DriveService, private utilityService: UtilityService, private queueLoggerService: QueueLoggerService, @@ -62,7 +63,7 @@ export class ExportAntennasProcessorService { const antennas = await this.antennsRepository.findBy({ userId: job.data.user.id }); write('['); for (const [index, antenna] of antennas.entries()) { - let users: User[] | undefined; + let users: MiUser[] | undefined; if (antenna.userListId !== null) { const joinings = await this.userListJoiningsRepository.findBy({ userListId: antenna.userListId }); users = await this.usersRepository.findBy({ diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts index eb758e162d..0a37e3ca1e 100644 --- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts @@ -1,10 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as fs from 'node:fs'; 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 } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { UsersRepository, BlockingsRepository, MiBlocking } from '@/models/_.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; @@ -19,9 +23,6 @@ export class ExportBlockingProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -53,7 +54,7 @@ export class ExportBlockingProcessorService { const stream = fs.createWriteStream(path, { flags: 'a' }); let exportedCount = 0; - let cursor: any = null; + let cursor: MiBlocking['id'] | null = null; while (true) { const blockings = await this.blockingsRepository.find({ @@ -72,7 +73,7 @@ export class ExportBlockingProcessorService { break; } - cursor = blockings[blockings.length - 1].id; + cursor = blockings.at(-1)?.id ?? null; for (const block of blockings) { const u = await this.usersRepository.findOneBy({ id: block.blockeeId }); diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts index 3203d9f3e5..d5387fe42e 100644 --- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; @@ -5,7 +10,7 @@ import { format as dateFormat } from 'date-fns'; import mime from 'mime-types'; import archiver from 'archiver'; import { DI } from '@/di-symbols.js'; -import type { EmojisRepository, UsersRepository } from '@/models/index.js'; +import type { EmojisRepository, UsersRepository } from '@/models/_.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/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts index 76c38a6b86..7248c7a649 100644 --- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts @@ -1,15 +1,19 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as fs from 'node:fs'; 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 { NoteFavorite, NoteFavoritesRepository, NotesRepository, PollsRepository, User, UsersRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { MiNoteFavorite, NoteFavoritesRepository, PollsRepository, MiUser, UsersRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; -import type { Poll } from '@/models/entities/Poll.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { MiPoll } from '@/models/Poll.js'; +import type { MiNote } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -20,18 +24,12 @@ export class ExportFavoritesProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.pollsRepository) private pollsRepository: PollsRepository, - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - @Inject(DI.noteFavoritesRepository) private noteFavoritesRepository: NoteFavoritesRepository, @@ -74,7 +72,7 @@ export class ExportFavoritesProcessorService { await write('['); let exportedFavoritesCount = 0; - let cursor: NoteFavorite['id'] | null = null; + let cursor: MiNoteFavorite['id'] | null = null; while (true) { const favorites = await this.noteFavoritesRepository.find({ @@ -87,17 +85,17 @@ export class ExportFavoritesProcessorService { id: 1, }, relations: ['note', 'note.user'], - }) as (NoteFavorite & { note: Note & { user: User } })[]; + }) as (MiNoteFavorite & { note: MiNote & { user: MiUser } })[]; if (favorites.length === 0) { job.updateProgress(100); break; } - cursor = favorites[favorites.length - 1].id; + cursor = favorites.at(-1)?.id ?? null; for (const favorite of favorites) { - let poll: Poll | undefined; + let poll: MiPoll | undefined; if (favorite.note.hasPoll) { poll = await this.pollsRepository.findOneByOrFail({ noteId: favorite.note.id }); } @@ -129,7 +127,7 @@ export class ExportFavoritesProcessorService { } } -function serialize(favorite: NoteFavorite & { note: Note & { user: User } }, poll: Poll | null = null): Record { +function serialize(favorite: MiNoteFavorite & { note: MiNote & { user: MiUser } }, poll: MiPoll | null = null): Record { return { id: favorite.id, createdAt: favorite.createdAt, diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts index 8726cb1402..c9739eb1cb 100644 --- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts @@ -1,14 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import { In, MoreThan, Not } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, FollowingsRepository, MutingsRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { UsersRepository, FollowingsRepository, MutingsRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; -import type { Following } from '@/models/entities/Following.js'; +import type { MiFollowing } from '@/models/Following.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; @@ -20,9 +24,6 @@ export class ExportFollowingProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -56,7 +57,7 @@ export class ExportFollowingProcessorService { try { const stream = fs.createWriteStream(path, { flags: 'a' }); - let cursor: Following['id'] | null = null; + let cursor: MiFollowing['id'] | null = null; const mutings = job.data.excludeMuting ? await this.mutingsRepository.findBy({ muterId: user.id, @@ -73,13 +74,13 @@ export class ExportFollowingProcessorService { order: { id: 1, }, - }) as Following[]; + }) as MiFollowing[]; if (followings.length === 0) { break; } - cursor = followings[followings.length - 1].id; + cursor = followings.at(-1)?.id ?? null; for (const following of followings) { const u = await this.usersRepository.findOneBy({ id: following.followeeId }); diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts index 0f11a9e843..c8425c1f2d 100644 --- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts @@ -1,10 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import { IsNull, MoreThan } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import type { MutingsRepository, UsersRepository, BlockingsRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { MutingsRepository, UsersRepository, MiMuting } from '@/models/_.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; @@ -19,15 +23,9 @@ export class ExportMutingProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.blockingsRepository) - private blockingsRepository: BlockingsRepository, - @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, @@ -56,7 +54,7 @@ export class ExportMutingProcessorService { const stream = fs.createWriteStream(path, { flags: 'a' }); let exportedCount = 0; - let cursor: any = null; + let cursor: MiMuting['id'] | null = null; while (true) { const mutes = await this.mutingsRepository.find({ @@ -76,7 +74,7 @@ export class ExportMutingProcessorService { break; } - cursor = mutes[mutes.length - 1].id; + cursor = mutes.at(-1)?.id ?? null; for (const mute of mutes) { const u = await this.usersRepository.findOneBy({ id: mute.muteeId }); diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts index 24fb331883..e0bc80e190 100644 --- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts @@ -1,16 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as fs from 'node:fs'; 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 { NotesRepository, PollsRepository, UsersRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; -import type { Poll } from '@/models/entities/Poll.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { MiPoll } from '@/models/Poll.js'; +import type { MiNote } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { Packed } from '@/misc/json-schema.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbJobDataWithUser } from '../types.js'; @@ -20,9 +26,6 @@ export class ExportNotesProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -34,6 +37,8 @@ export class ExportNotesProcessorService { private driveService: DriveService, private queueLoggerService: QueueLoggerService, + + private driveFileEntityService: DriveFileEntityService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('export-notes'); } @@ -71,7 +76,7 @@ export class ExportNotesProcessorService { await write('['); let exportedNotesCount = 0; - let cursor: Note['id'] | null = null; + let cursor: MiNote['id'] | null = null; while (true) { const notes = await this.notesRepository.find({ @@ -83,21 +88,22 @@ export class ExportNotesProcessorService { order: { id: 1, }, - }) as Note[]; + }) as MiNote[]; if (notes.length === 0) { job.updateProgress(100); break; } - cursor = notes[notes.length - 1].id; + cursor = notes.at(-1)?.id ?? null; for (const note of notes) { - let poll: Poll | undefined; + let poll: MiPoll | undefined; if (note.hasPoll) { poll = await this.pollsRepository.findOneByOrFail({ noteId: note.id }); } - const content = JSON.stringify(serialize(note, poll)); + const files = await this.driveFileEntityService.packManyByIds(note.fileIds); + const content = JSON.stringify(serialize(note, poll, files)); const isFirst = exportedNotesCount === 0; await write(isFirst ? content : ',\n' + content); exportedNotesCount++; @@ -125,12 +131,13 @@ export class ExportNotesProcessorService { } } -function serialize(note: Note, poll: Poll | null = null): Record { +function serialize(note: MiNote, poll: MiPoll | null = null, files: Packed<'DriveFile'>[]): Record { return { id: note.id, text: note.text, createdAt: note.createdAt, fileIds: note.fileIds, + files: files, replyId: note.replyId, renoteId: note.renoteId, poll: poll, diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts index ec63358053..7baaa7081a 100644 --- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts @@ -1,10 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; 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'; -import type { Config } from '@/config.js'; +import type { UserListJoiningsRepository, UserListsRepository, UsersRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; @@ -19,9 +23,6 @@ export class ExportUserListsProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts index 575cad69d5..7c95bccaff 100644 --- a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts @@ -1,25 +1,32 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable, Inject } from '@nestjs/common'; -import Ajv from 'ajv'; +import _Ajv from 'ajv'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import Logger from '@/logger.js'; -import type { AntennasRepository } from '@/models/index.js'; +import type { AntennasRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import { DBAntennaImportJobData } from '../types.js'; import type * as Bull from 'bullmq'; +const Ajv = _Ajv.default; + const validate = new Ajv().compile({ type: 'object', properties: { name: { type: 'string', minLength: 1, maxLength: 100 }, src: { type: 'string', enum: ['home', 'all', 'users', 'list'] }, - userListAccts: { - type: 'array', + userListAccts: { + type: 'array', items: { type: 'string', - }, + }, nullable: true, }, keywords: { type: 'array', items: { diff --git a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts index 2f1a9e5b03..64520b770b 100644 --- a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, DriveFilesRepository } from '@/models/index.js'; +import type { UsersRepository, DriveFilesRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index d862567871..a52af54a39 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -1,10 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; -import unzipper from 'unzipper'; +import { ZipReader } from 'slacc'; import { DI } from '@/di-symbols.js'; -import type { EmojisRepository, DriveFilesRepository, UsersRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { EmojisRepository, DriveFilesRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { createTempDir } from '@/misc/create-temp.js'; @@ -21,15 +24,6 @@ export class ImportCustomEmojisProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.db) - private db: DataSource, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -72,9 +66,9 @@ export class ImportCustomEmojisProcessorService { } const outputPath = path + '/emojis'; - const unzipStream = fs.createReadStream(destPath); - const extractor = unzipper.Extract({ path: outputPath }); - extractor.on('close', async () => { + try { + this.logger.succ(`Unzipping to ${outputPath}`); + ZipReader.withDestinationPath(outputPath).viaBuffer(await fs.promises.readFile(destPath)); const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8'); const meta = JSON.parse(metaRaw); @@ -113,10 +107,14 @@ export class ImportCustomEmojisProcessorService { } cleanup(); - + this.logger.succ('Imported'); - }); - unzipStream.pipe(extractor); - this.logger.succ(`Unzipping to ${outputPath}`); + } catch (e) { + if (e instanceof Error || typeof e === 'string') { + this.logger.error(e); + } + cleanup(); + throw e; + } } } diff --git a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts index 15bee9672e..2b5e41a12d 100644 --- a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, DriveFilesRepository } from '@/models/index.js'; +import type { UsersRepository, DriveFilesRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; diff --git a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts index 723935cd31..9db4e5d8e0 100644 --- a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts @@ -1,8 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, DriveFilesRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { UsersRepository, DriveFilesRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; @@ -19,9 +23,6 @@ export class ImportMutingProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts index 824ee8157a..54ca1a86df 100644 --- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts @@ -1,8 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; 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'; +import type { UsersRepository, DriveFilesRepository, UserListJoiningsRepository, UserListsRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; @@ -20,9 +24,6 @@ export class ImportUserListsProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index ce1d7aaa1b..99e823f9fa 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -1,20 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { URL } from 'node:url'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import httpSignature from '@peertube/http-signature'; import * as Bull from 'bullmq'; -import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; 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 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 { RemoteUser } from '@/models/entities/User.js'; -import type { UserPublickey } from '@/models/entities/UserPublickey.js'; +import type { MiRemoteUser } from '@/models/User.js'; +import type { MiUserPublickey } from '@/models/UserPublickey.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -30,16 +32,12 @@ export class InboxProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - private utilityService: UtilityService, private metaService: MetaService, private apInboxService: ApInboxService, private federatedInstanceService: FederatedInstanceService, private fetchInstanceMetadataService: FetchInstanceMetadataService, private ldSignatureService: LdSignatureService, - private apRequestService: ApRequestService, private apPersonService: ApPersonService, private apDbResolverService: ApDbResolverService, private instanceChart: InstanceChart, @@ -76,8 +74,8 @@ export class InboxProcessorService { // HTTP-Signature keyIdを元にDBから取得 let authUser: { - user: RemoteUser; - key: UserPublickey | null; + user: MiRemoteUser; + key: MiUserPublickey | null; } | null = await this.apDbResolverService.getAuthUserFromKeyId(signature.keyId); // keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得 diff --git a/packages/backend/src/queue/processors/RelationshipProcessorService.ts b/packages/backend/src/queue/processors/RelationshipProcessorService.ts index 722260d948..5b2d2ef313 100644 --- a/packages/backend/src/queue/processors/RelationshipProcessorService.ts +++ b/packages/backend/src/queue/processors/RelationshipProcessorService.ts @@ -1,16 +1,21 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type * as Bull from 'bullmq'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { bindThis } from '@/decorators.js'; import type Logger from '@/logger.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import { RelationshipJobData } from '../types.js'; -import type { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; -import { LocalUser, RemoteUser } from '@/models/entities/User.js'; +import { MiLocalUser, MiRemoteUser } from '@/models/User.js'; +import { RelationshipJobData } from '../types.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; @Injectable() export class RelationshipProcessorService { @@ -40,7 +45,7 @@ export class RelationshipProcessorService { const [follower, followee] = await Promise.all([ this.usersRepository.findOneByOrFail({ id: job.data.from.id }), this.usersRepository.findOneByOrFail({ id: job.data.to.id }), - ]) as [LocalUser | RemoteUser, LocalUser | RemoteUser]; + ]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser]; await this.userFollowingService.unfollow(follower, followee, job.data.silent); return 'ok'; } diff --git a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts index eab8e1e68d..b3b055ef8c 100644 --- a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts @@ -1,18 +1,13 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; -import FederationChart from '@/core/chart/charts/federation.js'; import NotesChart from '@/core/chart/charts/notes.js'; import UsersChart from '@/core/chart/charts/users.js'; -import ActiveUsersChart from '@/core/chart/charts/active-users.js'; -import InstanceChart from '@/core/chart/charts/instance.js'; -import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; import DriveChart from '@/core/chart/charts/drive.js'; -import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; -import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; -import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; -import ApRequestChart from '@/core/chart/charts/ap-request.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -22,21 +17,9 @@ export class ResyncChartsProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - - private federationChart: FederationChart, private notesChart: NotesChart, private usersChart: UsersChart, - private activeUsersChart: ActiveUsersChart, - private instanceChart: InstanceChart, - private perUserNotesChart: PerUserNotesChart, private driveChart: DriveChart, - private perUserReactionsChart: PerUserReactionsChart, - private perUserFollowingChart: PerUserFollowingChart, - private perUserDriveChart: PerUserDriveChart, - private apRequestChart: ApRequestChart, - private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('resync-charts'); diff --git a/packages/backend/src/queue/processors/TickChartsProcessorService.ts b/packages/backend/src/queue/processors/TickChartsProcessorService.ts index f1696bf567..7b1efb71e0 100644 --- a/packages/backend/src/queue/processors/TickChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/TickChartsProcessorService.ts @@ -1,6 +1,9 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import FederationChart from '@/core/chart/charts/federation.js'; import NotesChart from '@/core/chart/charts/notes.js'; @@ -23,9 +26,6 @@ export class TickChartsProcessorService { private logger: Logger; constructor( - @Inject(DI.config) - private config: Config, - private federationChart: FederationChart, private notesChart: NotesChart, private usersChart: UsersChart, diff --git a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts index 8b40c16749..a41f5565c8 100644 --- a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import * as Bull from 'bullmq'; import { DI } from '@/di-symbols.js'; -import type { WebhooksRepository } from '@/models/index.js'; +import type { WebhooksRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; @@ -31,7 +36,7 @@ export class WebhookDeliverProcessorService { public async process(job: Bull.Job): Promise { try { this.logger.debug(`delivering ${job.data.webhookId}`); - + const res = await this.httpRequestService.send(job.data.to, { method: 'POST', headers: { @@ -42,6 +47,7 @@ export class WebhookDeliverProcessorService { 'Content-Type': 'application/json', }, body: JSON.stringify({ + server: this.config.url, hookId: job.data.webhookId, userId: job.data.userId, eventId: job.data.eventId, @@ -50,25 +56,25 @@ export class WebhookDeliverProcessorService { body: job.data.content, }), }); - + this.webhooksRepository.update({ id: job.data.webhookId }, { latestSentAt: new Date(), latestStatus: res.status, }); - + return 'Success'; } catch (res) { this.webhooksRepository.update({ id: job.data.webhookId }, { latestSentAt: new Date(), latestStatus: res instanceof StatusError ? res.statusCode : 1, }); - + if (res instanceof StatusError) { // 4xx if (res.isClientError) { throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); } - + // 5xx etc. throw new Error(`${res.statusCode} ${res.statusMessage}`); } else { diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 776dd3aa12..c9122f5ca2 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; -import type { DriveFile } from '@/models/entities/DriveFile.js'; -import type { Note } from '@/models/entities/Note.js'; -import type { User } from '@/models/entities/User.js'; -import type { Webhook } from '@/models/entities/Webhook.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { MiNote } from '@/models/Note.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiWebhook } from '@/models/Webhook.js'; import type { IActivity } from '@/core/activitypub/type.js'; import type httpSignature from '@peertube/http-signature'; @@ -73,7 +78,7 @@ export type DbUserDeleteJobData = { export type DbUserImportJobData = { user: ThinUser; - fileId: DriveFile['id']; + fileId: MiDriveFile['id']; }; export type DBAntennaImportJobData = { @@ -93,14 +98,14 @@ export type ObjectStorageFileJobData = { }; export type EndedPollNotificationJobData = { - noteId: Note['id']; + noteId: MiNote['id']; }; export type WebhookDeliverJobData = { type: string; content: unknown; - webhookId: Webhook['id']; - userId: User['id']; + webhookId: MiWebhook['id']; + userId: MiUser['id']; to: string; secret: string; createdAt: number; @@ -108,5 +113,5 @@ export type WebhookDeliverJobData = { }; export type ThinUser = { - id: User['id']; + id: MiUser['id']; }; diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 455acd1e47..2428fa2792 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { IncomingMessage } from 'node:http'; import { Inject, Injectable } from '@nestjs/common'; import fastifyAccepts from '@fastify/accepts'; @@ -6,16 +11,16 @@ import { Brackets, In, IsNull, LessThan, Not } from 'typeorm'; import accepts from 'accepts'; import vary from 'vary'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository } from '@/models/index.js'; +import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js'; 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 { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; +import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; import { UserKeypairService } from '@/core/UserKeypairService.js'; -import type { Following } from '@/models/entities/Following.js'; +import type { MiFollowing } from '@/models/Following.js'; import { countIf } from '@/misc/prelude/array.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { MiNote } from '@/models/Note.js'; import { QueryService } from '@/core/QueryService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -82,7 +87,7 @@ export class ActivityPubServerService { * @param note Note */ @bindThis - private async packActivity(note: Note): Promise { + private async packActivity(note: MiNote): Promise { if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) { const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId }); return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note); @@ -153,7 +158,7 @@ export class ActivityPubServerService { if (page) { const query = { followeeId: user.id, - } as FindOptionsWhere; + } as FindOptionsWhere; // カーソルが指定されている場合 if (cursor) { @@ -181,7 +186,7 @@ export class ActivityPubServerService { undefined, inStock ? `${partOf}?${url.query({ page: 'true', - cursor: followings[followings.length - 1].id, + cursor: followings.at(-1)!.id, })}` : undefined, ); @@ -189,7 +194,11 @@ export class ActivityPubServerService { return (this.apRendererService.addContext(rendered)); } else { // index page - const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`); + 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.addContext(rendered)); @@ -241,7 +250,7 @@ export class ActivityPubServerService { if (page) { const query = { followerId: user.id, - } as FindOptionsWhere; + } as FindOptionsWhere; // カーソルが指定されている場合 if (cursor) { @@ -269,7 +278,7 @@ export class ActivityPubServerService { undefined, inStock ? `${partOf}?${url.query({ page: 'true', - cursor: followings[followings.length - 1].id, + cursor: followings.at(-1)!.id, })}` : undefined, ); @@ -277,7 +286,11 @@ export class ActivityPubServerService { return (this.apRendererService.addContext(rendered)); } else { // index page - const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`); + 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.addContext(rendered)); @@ -310,7 +323,10 @@ export class ActivityPubServerService { const rendered = this.apRendererService.renderOrderedCollection( `${this.config.url}/users/${userId}/collections/featured`, - renderedNotes.length, undefined, undefined, renderedNotes, + renderedNotes.length, + undefined, + undefined, + renderedNotes, ); reply.header('Cache-Control', 'public, max-age=180'); @@ -369,7 +385,7 @@ export class ActivityPubServerService { })) .andWhere('note.localOnly = FALSE'); - const notes = await query.take(limit).getMany(); + const notes = await query.limit(limit).getMany(); if (sinceId) notes.reverse(); @@ -387,7 +403,7 @@ export class ActivityPubServerService { })}` : undefined, notes.length ? `${partOf}?${url.query({ page: 'true', - until_id: notes[notes.length - 1].id, + until_id: notes.at(-1)!.id, })}` : undefined, ); @@ -395,7 +411,9 @@ export class ActivityPubServerService { return (this.apRendererService.addContext(rendered)); } else { // index page - const rendered = this.apRendererService.renderOrderedCollection(partOf, user.notesCount, + const rendered = this.apRendererService.renderOrderedCollection( + partOf, + user.notesCount, `${partOf}?page=true`, `${partOf}?page=true&since_id=000000000000000000000000`, ); @@ -406,7 +424,7 @@ export class ActivityPubServerService { } @bindThis - private async userInfo(request: FastifyRequest, reply: FastifyReply, user: User | null) { + private async userInfo(request: FastifyRequest, reply: FastifyReply, user: MiUser | null) { if (user == null) { reply.code(404); return; @@ -414,7 +432,7 @@ export class ActivityPubServerService { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as LocalUser))); + return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser))); } @bindThis @@ -630,7 +648,7 @@ export class ActivityPubServerService { id: request.params.followee, host: Not(IsNull()), }), - ]) as [LocalUser | RemoteUser | null, LocalUser | RemoteUser | null]; + ]) as [MiLocalUser | MiRemoteUser | null, MiLocalUser | MiRemoteUser | null]; if (follower == null || followee == null) { reply.code(404); @@ -665,7 +683,7 @@ export class ActivityPubServerService { id: followRequest.followeeId, host: Not(IsNull()), }), - ]) as [LocalUser | RemoteUser | null, LocalUser | RemoteUser | null]; + ]) as [MiLocalUser | MiRemoteUser | null, MiLocalUser | MiRemoteUser | null]; if (follower == null || followee == null) { reply.code(404); diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 98329ddffa..11721263d3 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -1,10 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; import { Inject, Injectable } from '@nestjs/common'; import rename from 'rename'; +import sharp from 'sharp'; +import { sharpBmp } from 'sharp-read-bmp'; import type { Config } from '@/config.js'; -import type { DriveFile, DriveFilesRepository } from '@/models/index.js'; +import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { createTemp } from '@/misc/create-temp.js'; import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; @@ -18,11 +25,9 @@ import { contentDisposition } from '@/misc/content-disposition.js'; import { FileInfoService } from '@/core/FileInfoService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; -import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; import { isMimeImage } from '@/misc/is-mime-image.js'; -import sharp from 'sharp'; -import { sharpBmp } from 'sharp-read-bmp'; import { correctFilename } from '@/misc/correct-filename.js'; +import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -180,8 +185,8 @@ export class FileServerService { reply.header('Content-Disposition', contentDisposition( 'inline', - correctFilename(file.filename, image.ext) - ) + correctFilename(file.filename, image.ext), + ), ); return image.data; } @@ -278,11 +283,11 @@ export class FileServerService { }; } else { const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) })) - .resize({ - height: 'emoji' in request.query ? 128 : 320, - withoutEnlargement: true, - }) - .webp(webpDefault); + .resize({ + height: 'emoji' in request.query ? 128 : 320, + withoutEnlargement: true, + }) + .webp(webpDefault); image = { data, @@ -355,8 +360,8 @@ export class FileServerService { reply.header('Content-Disposition', contentDisposition( 'inline', - correctFilename(file.filename, image.ext) - ) + correctFilename(file.filename, image.ext), + ), ); return image.data; } catch (e) { @@ -367,8 +372,8 @@ export class FileServerService { @bindThis private async getStreamAndTypeFromUrl(url: string): Promise< - { state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; } - | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; mime: string; ext: string | null; path: string; } + { state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: MiDriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; } + | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; } | '404' | '204' > { @@ -406,8 +411,8 @@ export class FileServerService { @bindThis private async getFileFromKey(key: string): Promise< - { state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; } - | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; mime: string; ext: string | null; path: string; } + { state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; } + | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; } | '404' | '204' > { diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 666a91fcee..79f130dabe 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -1,6 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; @@ -14,6 +18,7 @@ import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; const nodeinfo2_1path = '/nodeinfo/2.1'; const nodeinfo2_0path = '/nodeinfo/2.0'; +const nodeinfo_homepage = 'https://misskey-hub.net'; @Injectable() export class NodeinfoServerService { @@ -21,12 +26,6 @@ export class NodeinfoServerService { @Inject(DI.config) private config: Config, - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - private userEntityService: UserEntityService, private metaService: MetaService, private notesChart: NotesChart, @@ -37,10 +36,10 @@ export class NodeinfoServerService { @bindThis public getLinks() { - return [/* (awaiting release) { - rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', - href: config.url + nodeinfo2_1path - }, */{ + return [{ + rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', + href: this.config.url + nodeinfo2_1path + }, { rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', href: this.config.url + nodeinfo2_0path, }]; @@ -48,7 +47,7 @@ export class NodeinfoServerService { @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { - const nodeinfo2 = async () => { + const nodeinfo2 = async (version: number) => { const now = Date.now(); const notesChart = await this.notesChart.getChart('hour', 1, null); @@ -75,10 +74,12 @@ export class NodeinfoServerService { const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies }; - return { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const document: any = { software: { name: 'misskey', version: this.config.version, + homepage: nodeinfo_homepage, repository: meta.repositoryUrl, }, protocols: ['activitypub'], @@ -116,23 +117,36 @@ export class NodeinfoServerService { themeColor: meta.themeColor ?? '#86b300', }, }; + if (version >= 21) { + document.software.repository = meta.repositoryUrl; + document.software.homepage = meta.repositoryUrl; + } + return document; }; const cache = new MemorySingleCache>>(1000 * 60 * 10); fastify.get(nodeinfo2_1path, async (request, reply) => { - const base = await cache.fetch(() => nodeinfo2()); + const base = await cache.fetch(() => nodeinfo2(21)); - reply.header('Cache-Control', 'public, max-age=600'); + reply + .type( + 'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.1#"', + ) + .header('Cache-Control', 'public, max-age=600'); return { version: '2.1', ...base }; }); fastify.get(nodeinfo2_0path, async (request, reply) => { - const base = await cache.fetch(() => nodeinfo2()); + const base = await cache.fetch(() => nodeinfo2(20)); delete (base as any).software.repository; - reply.header('Cache-Control', 'public, max-age=600'); + reply + .type( + 'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"', + ) + .header('Cache-Control', 'public, max-age=600'); return { version: '2.0', ...base }; }); diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index da86b2c1d3..fa81380f01 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Module } from '@nestjs/common'; import { EndpointsModule } from '@/server/api/EndpointsModule.js'; import { CoreModule } from '@/core/CoreModule.js'; @@ -36,6 +41,7 @@ import { UserListChannelService } from './api/stream/channels/user-list.js'; import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; import { ClientLoggerService } from './web/ClientLoggerService.js'; import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js'; +import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; @Module({ imports: [ @@ -78,6 +84,7 @@ import { RoleTimelineChannelService } from './api/stream/channels/role-timeline. ServerStatsChannelService, UserListChannelService, OpenApiServerService, + OAuth2ProviderService, ], exports: [ ServerService, diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index c3d45e4ad6..0e4a5ece3e 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import cluster from 'node:cluster'; import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; @@ -7,7 +12,7 @@ import fastifyStatic from '@fastify/static'; 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 type { EmojisRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; @@ -16,6 +21,7 @@ import { createTemp } from '@/misc/create-temp.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; +import { MetaService } from '@/core/MetaService.js'; import { ActivityPubServerService } from './ActivityPubServerService.js'; import { NodeinfoServerService } from './NodeinfoServerService.js'; import { ApiServerService } from './api/ApiServerService.js'; @@ -24,6 +30,7 @@ import { WellKnownServerService } from './WellKnownServerService.js'; import { FileServerService } from './FileServerService.js'; import { ClientServerService } from './web/ClientServerService.js'; import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; +import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; const _dirname = fileURLToPath(new URL('.', import.meta.url)); @@ -45,6 +52,7 @@ export class ServerService implements OnApplicationShutdown { @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, + private metaService: MetaService, private userEntityService: UserEntityService, private apiServerService: ApiServerService, private openApiServerService: OpenApiServerService, @@ -56,12 +64,13 @@ export class ServerService implements OnApplicationShutdown { private clientServerService: ClientServerService, private globalEventService: GlobalEventService, private loggerService: LoggerService, + private oauth2ProviderService: OAuth2ProviderService, ) { this.logger = this.loggerService.getLogger('server', 'gray', false); } @bindThis - public async launch() { + public async launch(): Promise { const fastify = Fastify({ trustProxy: true, logger: !['production', 'test'].includes(process.env.NODE_ENV ?? ''), @@ -90,6 +99,7 @@ export class ServerService implements OnApplicationShutdown { fastify.register(this.activityPubServerService.createServer); fastify.register(this.nodeinfoServerService.createServer); fastify.register(this.wellKnownServerService.createServer); + fastify.register(this.oauth2ProviderService.createServer); fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { const path = request.params.path; @@ -161,11 +171,16 @@ export class ServerService implements OnApplicationShutdown { }); fastify.get<{ Params: { x: string } }>('/identicon/:x', async (request, reply) => { - const [temp, cleanup] = await createTemp(); - await genIdenticon(request.params.x, fs.createWriteStream(temp)); reply.header('Content-Type', 'image/png'); reply.header('Cache-Control', 'public, max-age=86400'); - return fs.createReadStream(temp).on('close', () => cleanup()); + + if ((await this.metaService.fetch()).enableIdenticonGeneration) { + const [temp, cleanup] = await createTemp(); + await genIdenticon(request.params.x, fs.createWriteStream(temp)); + return fs.createReadStream(temp).on('close', () => cleanup()); + } else { + return reply.redirect('/static-assets/avatar.png'); + } }); fastify.get<{ Params: { code: string } }>('/verify-email/:code', async (request, reply) => { @@ -217,14 +232,25 @@ export class ServerService implements OnApplicationShutdown { } }); - fastify.listen({ port: this.config.port, host: '0.0.0.0' }); + if (this.config.socket) { + if (fs.existsSync(this.config.socket)) { + fs.unlinkSync(this.config.socket); + } + fastify.listen({ path: this.config.socket }, (err, address) => { + if (this.config.chmodSocket) { + fs.chmodSync(this.config.socket!, this.config.chmodSocket); + } + }); + } else { + fastify.listen({ port: this.config.port, host: '0.0.0.0' }); + } await fastify.ready(); } @bindThis public async dispose(): Promise { - await this.streamingApiServerService.detach(); + await this.streamingApiServerService.detach(); await this.#fastify.close(); } diff --git a/packages/backend/src/server/WellKnownServerService.ts b/packages/backend/src/server/WellKnownServerService.ts index 9bf8deb221..8fc3c96de6 100644 --- a/packages/backend/src/server/WellKnownServerService.ts +++ b/packages/backend/src/server/WellKnownServerService.ts @@ -1,18 +1,23 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import vary from 'vary'; +import fastifyAccepts from '@fastify/accepts'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import { escapeAttribute, escapeValue } from '@/misc/prelude/xml.js'; -import type { User } from '@/models/entities/User.js'; +import type { MiUser } from '@/models/User.js'; import * as Acct from '@/misc/acct.js'; -import { NodeinfoServerService } from './NodeinfoServerService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import type { FindOptionsWhere } from 'typeorm'; import { bindThis } from '@/decorators.js'; +import { NodeinfoServerService } from './NodeinfoServerService.js'; +import type { FindOptionsWhere } from 'typeorm'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; -import fastifyAccepts from '@fastify/accepts'; @Injectable() export class WellKnownServerService { @@ -68,7 +73,7 @@ export class WellKnownServerService { }); fastify.get('/.well-known/host-meta.json', async (request, reply) => { - reply.header('Content-Type', jrd); + reply.header('Content-Type', 'application/json'); return { links: [{ rel: 'lrdd', @@ -88,13 +93,13 @@ fastify.get('/.well-known/change-password', async (request, reply) => { */ fastify.get<{ Querystring: { resource: string } }>(webFingerPath, async (request, reply) => { - const fromId = (id: User['id']): FindOptionsWhere => ({ + const fromId = (id: MiUser['id']): FindOptionsWhere => ({ id, host: IsNull(), isSuspended: false, }); - const generateQuery = (resource: string): FindOptionsWhere | number => + const generateQuery = (resource: string): FindOptionsWhere | number => resource.startsWith(`${this.config.url.toLowerCase()}/users/`) ? fromId(resource.split('/').pop()!) : fromAcct(Acct.parse( @@ -102,7 +107,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => { resource.startsWith('acct:') ? resource.slice('acct:'.length) : resource)); - const fromAcct = (acct: Acct.Acct): FindOptionsWhere | number => + const fromAcct = (acct: Acct.Acct): FindOptionsWhere | number => !acct.host || acct.host === this.config.host.toLowerCase() ? { usernameLower: acct.username, host: IsNull(), diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index dad1a4132a..085a0fd58a 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -1,14 +1,18 @@ -import { pipeline } from 'node:stream'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { randomUUID } from 'node:crypto'; import * as fs from 'node:fs'; -import { promisify } from 'node:util'; +import * as stream from 'node:stream/promises'; 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 { LocalUser, User } from '@/models/entities/User.js'; -import type { AccessToken } from '@/models/entities/AccessToken.js'; +import type { MiLocalUser, MiUser } from '@/models/User.js'; +import type { MiAccessToken } from '@/models/AccessToken.js'; import type Logger from '@/logger.js'; -import type { UserIpsRepository } from '@/models/index.js'; +import type { UserIpsRepository } from '@/models/_.js'; import { MetaService } from '@/core/MetaService.js'; import { createTemp } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; @@ -21,8 +25,6 @@ import type { FastifyRequest, FastifyReply } from 'fastify'; import type { OnApplicationShutdown } from '@nestjs/common'; import type { IEndpointMeta, IEndpoint } from './endpoints.js'; -const pump = promisify(pipeline); - const accessDenied = { message: 'Access denied.', code: 'ACCESS_DENIED', @@ -32,8 +34,8 @@ const accessDenied = { @Injectable() export class ApiCallService implements OnApplicationShutdown { private logger: Logger; - private userIpHistories: Map>; - private userIpHistoriesClearIntervalId: NodeJS.Timer; + private userIpHistories: Map>; + private userIpHistoriesClearIntervalId: NodeJS.Timeout; constructor( @Inject(DI.userIpsRepository) @@ -46,51 +48,79 @@ export class ApiCallService implements OnApplicationShutdown { private apiLoggerService: ApiLoggerService, ) { this.logger = this.apiLoggerService.logger; - this.userIpHistories = new Map>(); + this.userIpHistories = new Map>(); this.userIpHistoriesClearIntervalId = setInterval(() => { this.userIpHistories.clear(); }, 1000 * 60 * 60); } + #sendApiError(reply: FastifyReply, err: ApiError): void { + let statusCode = err.httpStatusCode; + if (err.httpStatusCode === 401) { + reply.header('WWW-Authenticate', 'Bearer realm="Misskey"'); + } else if (err.kind === 'client') { + reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_request", error_description="${err.message}"`); + statusCode = statusCode ?? 400; + } else if (err.kind === 'permission') { + // (ROLE_PERMISSION_DENIEDは関係ない) + if (err.code === 'PERMISSION_DENIED') { + reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`); + } + statusCode = statusCode ?? 403; + } else if (!statusCode) { + statusCode = 500; + } + this.send(reply, statusCode, err); + } + + #sendAuthenticationError(reply: FastifyReply, err: unknown): void { + if (err instanceof AuthenticationError) { + const message = 'Authentication failed. Please ensure your token is correct.'; + reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_token", error_description="${message}"`); + this.send(reply, 401, new ApiError({ + message: 'Authentication failed. Please ensure your token is correct.', + code: 'AUTHENTICATION_FAILED', + id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', + })); + } else { + this.send(reply, 500, new ApiError()); + } + } + @bindThis public handleRequest( endpoint: IEndpoint & { exec: any }, request: FastifyRequest<{ Body: Record | undefined, Querystring: Record }>, reply: FastifyReply, - ) { + ): void { const body = request.method === 'GET' ? request.query : request.body; - const token = body?.['i']; + // https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive) + const token = request.headers.authorization?.startsWith('Bearer ') + ? request.headers.authorization.slice(7) + : body?.['i']; if (token != null && typeof token !== 'string') { reply.code(400); return; } this.authenticateService.authenticate(token).then(([user, app]) => { this.call(endpoint, user, app, body, null, request).then((res) => { - if (request.method === 'GET' && endpoint.meta.cacheSec && !body?.['i'] && !user) { + if (request.method === 'GET' && endpoint.meta.cacheSec && !token && !user) { reply.header('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`); } this.send(reply, res); }).catch((err: ApiError) => { - this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : err.kind === 'permission' ? 403 : 500, err); + this.#sendApiError(reply, err); }); if (user) { this.logIp(request, user); } }).catch(err => { - if (err instanceof AuthenticationError) { - this.send(reply, 403, new ApiError({ - message: 'Authentication failed. Please ensure your token is correct.', - code: 'AUTHENTICATION_FAILED', - id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', - })); - } else { - this.send(reply, 500, new ApiError()); - } + this.#sendAuthenticationError(reply, err); }); } @@ -99,7 +129,7 @@ export class ApiCallService implements OnApplicationShutdown { endpoint: IEndpoint & { exec: any }, request: FastifyRequest<{ Body: Record, Querystring: Record }>, reply: FastifyReply, - ) { + ): Promise { const multipartData = await request.file().catch(() => { /* Fastify throws if the remote didn't send multipart data. Return 400 below. */ }); @@ -110,14 +140,17 @@ export class ApiCallService implements OnApplicationShutdown { } const [path] = await createTemp(); - await pump(multipartData.file, fs.createWriteStream(path)); + await stream.pipeline(multipartData.file, fs.createWriteStream(path)); const fields = {} as Record; for (const [k, v] of Object.entries(multipartData.fields)) { fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined; } - const token = fields['i']; + // https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive) + const token = request.headers.authorization?.startsWith('Bearer ') + ? request.headers.authorization.slice(7) + : fields['i']; if (token != null && typeof token !== 'string') { reply.code(400); return; @@ -129,22 +162,14 @@ export class ApiCallService implements OnApplicationShutdown { }, request).then((res) => { this.send(reply, res); }).catch((err: ApiError) => { - this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : err.kind === 'permission' ? 403 : 500, err); + this.#sendApiError(reply, err); }); if (user) { this.logIp(request, user); } }).catch(err => { - if (err instanceof AuthenticationError) { - this.send(reply, 403, new ApiError({ - message: 'Authentication failed. Please ensure your token is correct.', - code: 'AUTHENTICATION_FAILED', - id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', - })); - } else { - this.send(reply, 500, new ApiError()); - } + this.#sendAuthenticationError(reply, err); }); } @@ -171,7 +196,7 @@ export class ApiCallService implements OnApplicationShutdown { } @bindThis - private async logIp(request: FastifyRequest, user: LocalUser) { + private async logIp(request: FastifyRequest, user: MiLocalUser) { const meta = await this.metaService.fetch(); if (!meta.enableIpLogging) return; const ip = request.ip; @@ -197,8 +222,8 @@ export class ApiCallService implements OnApplicationShutdown { @bindThis private async call( ep: IEndpoint & { exec: any }, - user: LocalUser | null | undefined, - token: AccessToken | null | undefined, + user: MiLocalUser | null | undefined, + token: MiAccessToken | null | undefined, data: any, file: { name: string; @@ -213,7 +238,7 @@ export class ApiCallService implements OnApplicationShutdown { } if (ep.meta.limit) { - // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app. + // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app. let limitActor: string; if (user) { limitActor = user.id; @@ -255,8 +280,8 @@ export class ApiCallService implements OnApplicationShutdown { throw new ApiError({ message: 'Your account has been suspended.', code: 'YOUR_ACCOUNT_SUSPENDED', + kind: 'permission', id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370', - httpStatusCode: 403, }); } } @@ -266,8 +291,8 @@ export class ApiCallService implements OnApplicationShutdown { throw new ApiError({ message: 'You have moved your account.', code: 'YOUR_ACCOUNT_MOVED', + kind: 'permission', id: '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31', - httpStatusCode: 403, }); } } @@ -278,6 +303,7 @@ export class ApiCallService implements OnApplicationShutdown { throw new ApiError({ message: 'You are not assigned to a moderator role.', code: 'ROLE_PERMISSION_DENIED', + kind: 'permission', id: 'd33d5333-db36-423d-a8f9-1a2b9549da41', }); } @@ -285,6 +311,7 @@ export class ApiCallService implements OnApplicationShutdown { throw new ApiError({ message: 'You are not assigned to an administrator role.', code: 'ROLE_PERMISSION_DENIED', + kind: 'permission', id: 'c3d38592-54c0-429d-be96-5636b0431a61', }); } @@ -296,6 +323,7 @@ export class ApiCallService implements OnApplicationShutdown { throw new ApiError({ message: 'You are not assigned to a required role.', code: 'ROLE_PERMISSION_DENIED', + kind: 'permission', id: '7f86f06f-7e15-4057-8561-f4b6d4ac755a', }); } @@ -305,6 +333,7 @@ export class ApiCallService implements OnApplicationShutdown { throw new ApiError({ message: 'Your app does not have the necessary permissions to use this endpoint.', code: 'PERMISSION_DENIED', + kind: 'permission', id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838', }); } @@ -317,7 +346,7 @@ export class ApiCallService implements OnApplicationShutdown { try { data[k] = JSON.parse(data[k]); } catch (e) { - throw new ApiError({ + throw new ApiError({ message: 'Invalid param.', code: 'INVALID_PARAM', id: '0b5f1631-7c1a-41a6-b399-cce335f34d85', @@ -335,7 +364,7 @@ export class ApiCallService implements OnApplicationShutdown { if (err instanceof ApiError || err instanceof AuthenticationError) { throw err; } else { - const errId = uuid(); + const errId = randomUUID(); this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, { ep: ep.name, ps: data, diff --git a/packages/backend/src/server/api/ApiLoggerService.ts b/packages/backend/src/server/api/ApiLoggerService.ts index 7f534b1efd..2339366a5d 100644 --- a/packages/backend/src/server/api/ApiLoggerService.ts +++ b/packages/backend/src/server/api/ApiLoggerService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index d3b1c7786d..1758c03aca 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import cors from '@fastify/cors'; import multipart from '@fastify/multipart'; import fastifyCookie from '@fastify/cookie'; import { ModuleRef } from '@nestjs/core'; import type { Config } from '@/config.js'; -import type { UsersRepository, InstancesRepository, AccessTokensRepository } from '@/models/index.js'; +import type { InstancesRepository, AccessTokensRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -22,9 +27,6 @@ export class ApiServerService { @Inject(DI.config) private config: Config, - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index e23591d876..f075688194 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -1,10 +1,15 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js'; -import type { LocalUser } from '@/models/entities/User.js'; -import type { AccessToken } from '@/models/entities/AccessToken.js'; +import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/_.js'; +import type { MiLocalUser } from '@/models/User.js'; +import type { MiAccessToken } from '@/models/AccessToken.js'; import { MemoryKVCache } from '@/misc/cache.js'; -import type { App } from '@/models/entities/App.js'; +import type { MiApp } from '@/models/App.js'; import { CacheService } from '@/core/CacheService.js'; import isNativeToken from '@/misc/is-native-token.js'; import { bindThis } from '@/decorators.js'; @@ -17,8 +22,8 @@ export class AuthenticationError extends Error { } @Injectable() -export class AuthenticateService { - private appCache: MemoryKVCache; +export class AuthenticateService implements OnApplicationShutdown { + private appCache: MemoryKVCache; constructor( @Inject(DI.usersRepository) @@ -32,23 +37,23 @@ export class AuthenticateService { private cacheService: CacheService, ) { - this.appCache = new MemoryKVCache(Infinity); + this.appCache = new MemoryKVCache(Infinity); } @bindThis - public async authenticate(token: string | null | undefined): Promise<[LocalUser | null, AccessToken | null]> { + public async authenticate(token: string | null | undefined): Promise<[MiLocalUser | null, MiAccessToken | null]> { if (token == null) { return [null, null]; } - + if (isNativeToken(token)) { const user = await this.cacheService.localUserByNativeTokenCache.fetch(token, - () => this.usersRepository.findOneBy({ token }) as Promise); - + () => this.usersRepository.findOneBy({ token }) as Promise); + if (user == null) { throw new AuthenticationError('user not found'); } - + return [user, null]; } else { const accessToken = await this.accessTokensRepository.findOne({ @@ -58,31 +63,41 @@ export class AuthenticateService { token: token, // miauth }], }); - + if (accessToken == null) { throw new AuthenticationError('invalid signature'); } - + this.accessTokensRepository.update(accessToken.id, { lastUsedAt: new Date(), }); - + const user = await this.cacheService.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, () => this.appsRepository.findOneByOrFail({ id: accessToken.appId! })); - + return [user, { id: accessToken.id, permission: app.permission, - } as AccessToken]; + } as MiAccessToken]; } else { return [user, accessToken]; } } } + + @bindThis + public dispose(): void { + this.appCache.dispose(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } } diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 1e32e9988d..41a11bfb19 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Module } from '@nestjs/common'; import { CoreModule } from '@/core/CoreModule.js'; @@ -38,7 +43,8 @@ import * as ep___admin_federation_updateInstance from './endpoints/admin/federat import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'; import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'; import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js'; -import * as ep___invite from './endpoints/invite.js'; +import * as ep___admin_invite_create from './endpoints/admin/invite/create.js'; +import * as ep___admin_invite_list from './endpoints/admin/invite/list.js'; import * as ep___admin_promo_create from './endpoints/admin/promo/create.js'; import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js'; import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js'; @@ -154,6 +160,7 @@ import * as ep___federation_users from './endpoints/federation/users.js'; import * as ep___federation_stats from './endpoints/federation/stats.js'; import * as ep___following_create from './endpoints/following/create.js'; import * as ep___following_delete from './endpoints/following/delete.js'; +import * as ep___following_update from './endpoints/following/update.js'; import * as ep___following_invalidate from './endpoints/following/invalidate.js'; import * as ep___following_requests_accept from './endpoints/following/requests/accept.js'; import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js'; @@ -230,6 +237,10 @@ 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___invite_create from './endpoints/invite/create.js'; +import * as ep___invite_delete from './endpoints/invite/delete.js'; +import * as ep___invite_list from './endpoints/invite/list.js'; +import * as ep___invite_limit from './endpoints/invite/limit.js'; import * as ep___meta from './endpoints/meta.js'; import * as ep___emojis from './endpoints/emojis.js'; import * as ep___emoji from './endpoints/emoji.js'; @@ -273,6 +284,7 @@ import * as ep___notes_unrenote from './endpoints/notes/unrenote.js'; import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js'; import * as ep___notifications_create from './endpoints/notifications/create.js'; import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js'; +import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js'; import * as ep___pagePush from './endpoints/page-push.js'; import * as ep___pages_create from './endpoints/pages/create.js'; import * as ep___pages_delete from './endpoints/pages/delete.js'; @@ -326,6 +338,7 @@ import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js'; import * as ep___users_notes from './endpoints/users/notes.js'; import * as ep___users_pages from './endpoints/users/pages.js'; +import * as ep___users_flashs from './endpoints/users/flashs.js'; import * as ep___users_reactions from './endpoints/users/reactions.js'; import * as ep___users_recommendation from './endpoints/users/recommendation.js'; import * as ep___users_relation from './endpoints/users/relation.js'; @@ -333,7 +346,6 @@ import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js'; import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js'; import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_show from './endpoints/users/show.js'; -import * as ep___users_stats from './endpoints/users/stats.js'; import * as ep___users_achievements from './endpoints/users/achievements.js'; import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js'; @@ -379,7 +391,8 @@ const $admin_federation_updateInstance: Provider = { provide: 'ep:admin/federati const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', useClass: ep___admin_getIndexStats.default }; const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', useClass: ep___admin_getTableStats.default }; const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default }; -const $invite: Provider = { provide: 'ep:invite', useClass: ep___invite.default }; +const $admin_invite_create: Provider = { provide: 'ep:admin/invite/create', useClass: ep___admin_invite_create.default }; +const $admin_invite_list: Provider = { provide: 'ep:admin/invite/list', useClass: ep___admin_invite_list.default }; const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useClass: ep___admin_promo_create.default }; const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default }; const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default }; @@ -495,6 +508,7 @@ const $federation_users: Provider = { provide: 'ep:federation/users', useClass: const $federation_stats: Provider = { provide: 'ep:federation/stats', useClass: ep___federation_stats.default }; const $following_create: Provider = { provide: 'ep:following/create', useClass: ep___following_create.default }; const $following_delete: Provider = { provide: 'ep:following/delete', useClass: ep___following_delete.default }; +const $following_update: Provider = { provide: 'ep:following/update', useClass: ep___following_update.default }; const $following_invalidate: Provider = { provide: 'ep:following/invalidate', useClass: ep___following_invalidate.default }; const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default }; const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default }; @@ -571,6 +585,10 @@ const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep 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 $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default }; +const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default }; +const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default }; +const $invite_limit: Provider = { provide: 'ep:invite/limit', useClass: ep___invite_limit.default }; const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default }; const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default }; const $emoji: Provider = { provide: 'ep:emoji', useClass: ep___emoji.default }; @@ -614,6 +632,7 @@ const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep__ const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default }; const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default }; const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default }; +const $notifications_testNotification: Provider = { provide: 'ep:notifications/test-notification', useClass: ep___notifications_testNotification.default }; const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default }; const $pages_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default }; const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default }; @@ -667,6 +686,7 @@ const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite' const $users_lists_create_from_public: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_create_from_public.default }; const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default }; const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default }; +const $users_flashs: Provider = { provide: 'ep:users/flashs', useClass: ep___users_flashs.default }; const $users_reactions: Provider = { provide: 'ep:users/reactions', useClass: ep___users_reactions.default }; const $users_recommendation: Provider = { provide: 'ep:users/recommendation', useClass: ep___users_recommendation.default }; const $users_relation: Provider = { provide: 'ep:users/relation', useClass: ep___users_relation.default }; @@ -674,7 +694,6 @@ const $users_reportAbuse: Provider = { provide: 'ep:users/report-abuse', useClas const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-username-and-host', useClass: ep___users_searchByUsernameAndHost.default }; const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default }; const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default }; -const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default }; const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default }; const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default }; const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default }; @@ -724,7 +743,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_getIndexStats, $admin_getTableStats, $admin_getUserIps, - $invite, + $admin_invite_create, + $admin_invite_list, $admin_promo_create, $admin_queue_clear, $admin_queue_deliverDelayed, @@ -840,6 +860,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $federation_stats, $following_create, $following_delete, + $following_update, $following_invalidate, $following_requests_accept, $following_requests_cancel, @@ -916,6 +937,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_webhooks_show, $i_webhooks_update, $i_webhooks_delete, + $invite_create, + $invite_delete, + $invite_list, + $invite_limit, $meta, $emojis, $emoji, @@ -959,6 +984,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $notes_userListTimeline, $notifications_create, $notifications_markAllAsRead, + $notifications_testNotification, $pagePush, $pages_create, $pages_delete, @@ -1012,6 +1038,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_lists_create_from_public, $users_notes, $users_pages, + $users_flashs, $users_reactions, $users_recommendation, $users_relation, @@ -1019,7 +1046,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_searchByUsernameAndHost, $users_search, $users_show, - $users_stats, $users_achievements, $users_updateMemo, $fetchRss, @@ -1063,7 +1089,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_getIndexStats, $admin_getTableStats, $admin_getUserIps, - $invite, + $admin_invite_create, + $admin_invite_list, $admin_promo_create, $admin_queue_clear, $admin_queue_deliverDelayed, @@ -1179,6 +1206,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $federation_stats, $following_create, $following_delete, + $following_update, $following_invalidate, $following_requests_accept, $following_requests_cancel, @@ -1255,6 +1283,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_webhooks_show, $i_webhooks_update, $i_webhooks_delete, + $invite_create, + $invite_delete, + $invite_list, + $invite_limit, $meta, $emojis, $emoji, @@ -1349,6 +1381,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_lists_create_from_public, $users_notes, $users_pages, + $users_flashs, $users_reactions, $users_recommendation, $users_relation, @@ -1356,7 +1389,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_searchByUsernameAndHost, $users_search, $users_show, - $users_stats, $users_achievements, $users_updateMemo, $fetchRss, diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts index c94884a78c..e2b98c34e7 100644 --- a/packages/backend/src/server/api/GetterService.ts +++ b/packages/backend/src/server/api/GetterService.ts @@ -1,9 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UsersRepository } from '@/models/index.js'; +import type { NotesRepository, UsersRepository } from '@/models/_.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; +import type { MiNote } from '@/models/Note.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -24,7 +29,7 @@ export class GetterService { * Get note for API processing */ @bindThis - public async getNote(noteId: Note['id']) { + public async getNote(noteId: MiNote['id']) { const note = await this.notesRepository.findOneBy({ id: noteId }); if (note == null) { @@ -38,21 +43,21 @@ export class GetterService { * Get user for API processing */ @bindThis - public async getUser(userId: User['id']) { + public async getUser(userId: MiUser['id']) { const user = await this.usersRepository.findOneBy({ id: userId }); if (user == null) { throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.'); } - return user as LocalUser | RemoteUser; + return user as MiLocalUser | MiRemoteUser; } /** * Get remote user for API processing */ @bindThis - public async getRemoteUser(userId: User['id']) { + public async getRemoteUser(userId: MiUser['id']) { const user = await this.getUser(userId); if (!this.userEntityService.isRemoteUser(user)) { @@ -66,7 +71,7 @@ export class GetterService { * Get local user for API processing */ @bindThis - public async getLocalUser(userId: User['id']) { + public async getLocalUser(userId: MiUser['id']) { const user = await this.getUser(userId); if (!this.userEntityService.isLocalUser(user)) { diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts index fe2db1d66a..0e644aa091 100644 --- a/packages/backend/src/server/api/RateLimiterService.ts +++ b/packages/backend/src/server/api/RateLimiterService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import Limiter from 'ratelimiter'; import * as Redis from 'ioredis'; @@ -38,14 +43,14 @@ export class RateLimiterService { max: 1, db: this.redisClient, }); - + minIntervalLimiter.get((err, info) => { if (err) { return reject('ERR'); } - + this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); - + if (info.remaining === 0) { reject('BRIEF_REQUEST_INTERVAL'); } else { @@ -57,7 +62,7 @@ export class RateLimiterService { } }); }; - + // Long term limit const max = (): void => { const limiter = new Limiter({ @@ -66,14 +71,14 @@ export class RateLimiterService { max: limitation.max! / factor, db: this.redisClient, }); - + limiter.get((err, info) => { if (err) { return reject('ERR'); } - + this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); - + if (info.remaining === 0) { reject('RATE_LIMIT_EXCEEDED'); } else { @@ -81,13 +86,13 @@ export class RateLimiterService { } }); }; - + const hasShortTermLimit = typeof limitation.minInterval === 'number'; - + const hasLongTermLimit = typeof limitation.duration === 'number' && typeof limitation.max === 'number'; - + if (hasShortTermLimit) { min(); } else if (hasLongTermLimit) { diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index bd3d8a28da..150f3f24d4 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -1,19 +1,29 @@ -import { randomBytes } from 'node:crypto'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; 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 { + SigninsRepository, + UserProfilesRepository, + UsersRepository, +} from '@/models/_.js'; import type { Config } from '@/config.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; -import type { LocalUser } from '@/models/entities/User.js'; +import type { MiLocalUser } from '@/models/User.js'; import { IdService } from '@/core/IdService.js'; -import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; import { bindThis } from '@/decorators.js'; +import { WebAuthnService } from '@/core/WebAuthnService.js'; +import { UserAuthService } from '@/core/UserAuthService.js'; import { RateLimiterService } from './RateLimiterService.js'; import { SigninService } from './SigninService.js'; -import type { FastifyRequest, FastifyReply } from 'fastify'; +import type { AuthenticationResponseJSON } from '@simplewebauthn/typescript-types'; +import type { FastifyReply, FastifyRequest } from 'fastify'; @Injectable() export class SigninApiService { @@ -24,22 +34,17 @@ export class SigninApiService { @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.userSecurityKeysRepository) - private userSecurityKeysRepository: UserSecurityKeysRepository, - @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - @Inject(DI.attestationChallengesRepository) - private attestationChallengesRepository: AttestationChallengesRepository, - @Inject(DI.signinsRepository) private signinsRepository: SigninsRepository, private idService: IdService, private rateLimiterService: RateLimiterService, private signinService: SigninService, - private twoFactorAuthenticationService: TwoFactorAuthenticationService, + private userAuthService: UserAuthService, + private webAuthnService: WebAuthnService, ) { } @@ -50,11 +55,7 @@ export class SigninApiService { username: string; password: string; token?: string; - signature?: string; - authenticatorData?: string; - clientDataJSON?: string; - credentialId?: string; - challengeId?: string; + credential?: AuthenticationResponseJSON; }; }>, reply: FastifyReply, @@ -105,7 +106,7 @@ export class SigninApiService { const user = await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull(), - }) as LocalUser; + }) as MiLocalUser; if (user == null) { return error(404, { @@ -125,7 +126,7 @@ export class SigninApiService { const same = await bcrypt.compare(password, profile.password!); const fail = async (status?: number, failure?: { id: string }) => { - // Append signin history + // Append signin history await this.signinsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), @@ -155,78 +156,25 @@ export class SigninApiService { }); } - const delta = OTPAuth.TOTP.validate({ - secret: OTPAuth.Secret.fromBase32(profile.twoFactorSecret!), - digits: 6, - token, - window: 1, - }); - - if (delta === null) { + try { + await this.userAuthService.twoFactorAuthenticate(profile, token); + } catch (e) { 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) { + + return this.signinService.signin(request, reply, user); + } else if (body.credential) { if (!same && !profile.usePasswordLessLogin) { return await fail(403, { id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', }); } - const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex'); - const clientData = JSON.parse(clientDataJSON.toString('utf-8')); - const challenge = await this.attestationChallengesRepository.findOneBy({ - userId: user.id, - id: body.challengeId, - registrationChallenge: false, - challenge: this.twoFactorAuthenticationService.hash(clientData.challenge).toString('hex'), - }); + const authorized = await this.webAuthnService.verifyAuthentication(user.id, body.credential); - if (!challenge) { - return await fail(403, { - id: '2715a88a-2125-4013-932f-aa6fe72792da', - }); - } - - await this.attestationChallengesRepository.delete({ - userId: user.id, - id: body.challengeId, - }); - - if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) { - return await fail(403, { - id: '2715a88a-2125-4013-932f-aa6fe72792da', - }); - } - - const securityKey = await this.userSecurityKeysRepository.findOneBy({ - id: Buffer.from( - body.credentialId - .replace(/-/g, '+') - .replace(/_/g, '/'), - 'base64', - ).toString('hex'), - }); - - if (!securityKey) { - return await fail(403, { - id: '66269679-aeaf-4474-862b-eb761197e046', - }); - } - - const isValid = this.twoFactorAuthenticationService.verifySignin({ - publicKey: Buffer.from(securityKey.publicKey, 'hex'), - authenticatorData: Buffer.from(body.authenticatorData, 'hex'), - clientDataJSON, - clientData, - signature: Buffer.from(body.signature, 'hex'), - challenge: challenge.challenge, - }); - - if (isValid) { + if (authorized) { return this.signinService.signin(request, reply, user); } else { return await fail(403, { @@ -240,42 +188,11 @@ export class SigninApiService { }); } - const keys = await this.userSecurityKeysRepository.findBy({ - userId: user.id, - }); - - if (keys.length === 0) { - return await fail(403, { - id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4', - }); - } - - // 32 byte challenge - const challenge = randomBytes(32).toString('base64') - .replace(/=/g, '') - .replace(/\+/g, '-') - .replace(/\//g, '_'); - - const challengeId = this.idService.genId(); - - await this.attestationChallengesRepository.insert({ - userId: user.id, - id: challengeId, - challenge: this.twoFactorAuthenticationService.hash(Buffer.from(challenge, 'utf-8')).toString('hex'), - createdAt: new Date(), - registrationChallenge: false, - }); + const authRequest = await this.webAuthnService.initiateAuthentication(user.id); reply.code(200); - return { - challenge, - challengeId, - securityKeys: keys.map(key => ({ - id: key.id, - })), - }; + return authRequest; } - // never get here + // never get here } } - diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts index aaf1d10b42..cebba8c8ee 100644 --- a/packages/backend/src/server/api/SigninService.ts +++ b/packages/backend/src/server/api/SigninService.ts @@ -1,9 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { SigninsRepository } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { SigninsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; -import type { LocalUser } from '@/models/entities/User.js'; +import type { MiLocalUser } from '@/models/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { SigninEntityService } from '@/core/entities/SigninEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -12,9 +16,6 @@ import type { FastifyRequest, FastifyReply } from 'fastify'; @Injectable() export class SigninService { constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.signinsRepository) private signinsRepository: SigninsRepository, @@ -25,7 +26,7 @@ export class SigninService { } @bindThis - public signin(request: FastifyRequest, reply: FastifyReply, user: LocalUser) { + public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) { setImmediate(async () => { // Append signin history const record = await this.signinsRepository.insert({ @@ -36,7 +37,7 @@ export class SigninService { headers: request.headers as any, success: true, }).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0])); - + // Publish signin event this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record)); }); diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index b2bd7d82e7..431df581b5 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -1,9 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import rndstr from 'rndstr'; import bcrypt from 'bcryptjs'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket } from '@/models/_.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; import { CaptchaService } from '@/core/CaptchaService.js'; @@ -11,9 +15,10 @@ 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 { LocalUser } from '@/models/entities/User.js'; +import { MiLocalUser } from '@/models/User.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { bindThis } from '@/decorators.js'; +import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { SigninService } from './SigninService.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; @@ -67,7 +72,7 @@ export class SignupApiService { const body = request.body; const instance = await this.metaService.fetch(true); - + // Verify *Captcha // ただしテスト時はこの機構は障害となるため無効にする if (process.env.NODE_ENV !== 'test') { @@ -76,7 +81,7 @@ export class SignupApiService { throw new FastifyReplyError(400, err); }); } - + if (instance.enableRecaptcha && instance.recaptchaSecretKey) { await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { throw new FastifyReplyError(400, err); @@ -89,51 +94,61 @@ export class SignupApiService { }); } } - + const username = body['username']; const password = body['password']; const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] ?? null) : null; const invitationCode = body['invitationCode']; const emailAddress = body['emailAddress']; - + if (instance.emailRequiredForSignup) { if (emailAddress == null || typeof emailAddress !== 'string') { reply.code(400); return; } - + const res = await this.emailService.validateEmailForAccount(emailAddress); if (!res.available) { reply.code(400); return; } } - + + let ticket: MiRegistrationTicket | null = null; + if (instance.disableRegistration) { if (invitationCode == null || typeof invitationCode !== 'string') { reply.code(400); return; } - - const ticket = await this.registrationTicketsRepository.findOneBy({ + + ticket = await this.registrationTicketsRepository.findOneBy({ code: invitationCode, }); - + if (ticket == null) { reply.code(400); return; } - - this.registrationTicketsRepository.delete(ticket.id); + + if (ticket.expiresAt && ticket.expiresAt < new Date()) { + reply.code(400); + return; + } + + if (ticket.usedAt) { + reply.code(400); + return; + } } - + if (instance.emailRequiredForSignup) { - if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) { + if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { throw new FastifyReplyError(400, 'DUPLICATED_USERNAME'); } // Check deleted username duplication - if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) { + if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) { throw new FastifyReplyError(400, 'USED_USERNAME'); } @@ -142,20 +157,20 @@ export class SignupApiService { throw new FastifyReplyError(400, 'DENIED_USERNAME'); } - const code = rndstr('a-z0-9', 16); + const code = secureRndstr(16, { chars: L_CHARS }); // Generate hash of password const salt = await bcrypt.genSalt(8); const hash = await bcrypt.hash(password, salt); - await this.userPendingsRepository.insert({ + const pendingUser = await this.userPendingsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), code, email: emailAddress!, username: username, password: hash, - }); + }).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0])); const link = `${this.config.url}/signup-complete/${code}`; @@ -163,6 +178,13 @@ export class SignupApiService { `To complete signup, please click this link:
${link}`, `To complete signup, please click this link: ${link}`); + if (ticket) { + await this.registrationTicketsRepository.update(ticket.id, { + usedAt: new Date(), + pendingUserId: pendingUser.id, + }); + } + reply.code(204); return; } else { @@ -170,12 +192,20 @@ export class SignupApiService { const { account, secret } = await this.signupService.signup({ username, password, host, }); - + const res = await this.userEntityService.pack(account, account, { detail: true, includeSecrets: true, }); - + + if (ticket) { + await this.registrationTicketsRepository.update(ticket.id, { + usedAt: new Date(), + usedBy: account, + usedById: account.id, + }); + } + return { ...res, token: secret, @@ -212,7 +242,16 @@ export class SignupApiService { emailVerifyCode: null, }); - return this.signinService.signin(request, reply, account as LocalUser); + const ticket = await this.registrationTicketsRepository.findOneBy({ pendingUserId: pendingUser.id }); + if (ticket) { + await this.registrationTicketsRepository.update(ticket.id, { + usedBy: account, + usedById: account.id, + pendingUserId: null, + }); + } + + return this.signinService.signin(request, reply, account as MiLocalUser); } catch (err) { throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString()); } diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index 893dfe956e..9acaa688c5 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -1,18 +1,21 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { EventEmitter } from 'events'; import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import * as WebSocket from 'ws'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, AccessToken } from '@/models/index.js'; -import type { Config } from '@/config.js'; +import type { UsersRepository, MiAccessToken } from '@/models/_.js'; import { NoteReadService } from '@/core/NoteReadService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; -import { LocalUser } from '@/models/entities/User'; +import { MiLocalUser } from '@/models/User.js'; import { AuthenticateService, AuthenticationError } from './AuthenticateService.js'; -import MainStreamConnection from './stream/index.js'; +import MainStreamConnection from './stream/Connection.js'; import { ChannelsService } from './stream/ChannelsService.js'; import type * as http from 'node:http'; @@ -23,9 +26,6 @@ export class StreamingApiServerService { #cleanConnectionsIntervalId: NodeJS.Timeout | null = null; constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.redisForSub) private redisForSub: Redis.Redis, @@ -55,14 +55,24 @@ export class StreamingApiServerService { const q = new URL(request.url, `http://${request.headers.host}`).searchParams; - let user: LocalUser | null = null; - let app: AccessToken | null = null; + let user: MiLocalUser | null = null; + let app: MiAccessToken | null = null; + + // https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 + // Note that the standard WHATWG WebSocket API does not support setting any headers, + // but non-browser apps may still be able to set it. + const token = request.headers.authorization?.startsWith('Bearer ') + ? request.headers.authorization.slice(7) + : q.get('i'); try { - [user, app] = await this.authenticateService.authenticate(q.get('i')); + [user, app] = await this.authenticateService.authenticate(token); } catch (e) { if (e instanceof AuthenticationError) { - socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); + socket.write([ + 'HTTP/1.1 401 Unauthorized', + 'WWW-Authenticate: Bearer realm="Misskey", error="invalid_token", error_description="Failed to authenticate"', + ].join('\r\n') + '\r\n\r\n'); } else { socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n'); } @@ -93,21 +103,27 @@ export class StreamingApiServerService { }); }); + const globalEv = new EventEmitter(); + + this.redisForSub.on('message', (_: string, data: string) => { + const parsed = JSON.parse(data); + globalEv.emit('message', parsed); + }); + this.#wss.on('connection', async (connection: WebSocket.WebSocket, request: http.IncomingMessage, ctx: { stream: MainStreamConnection, - user: LocalUser | null; - app: AccessToken | null + user: MiLocalUser | null; + app: MiAccessToken | null }) => { const { stream, user, app } = ctx; const ev = new EventEmitter(); - async function onRedisMessage(_: string, data: string): Promise { - const parsed = JSON.parse(data); - ev.emit(parsed.channel, parsed.message); + function onRedisMessage(data: any): void { + ev.emit(data.channel, data.message); } - this.redisForSub.on('message', onRedisMessage); + globalEv.on('message', onRedisMessage); await stream.listen(ev, connection); @@ -127,27 +143,28 @@ export class StreamingApiServerService { connection.once('close', () => { ev.removeAllListeners(); stream.dispose(); - this.redisForSub.off('message', onRedisMessage); + globalEv.off('message', onRedisMessage); + this.#connections.delete(connection); if (userUpdateIntervalId) clearInterval(userUpdateIntervalId); }); - connection.on('message', async (data) => { + connection.on('pong', () => { this.#connections.set(connection, Date.now()); - if (data.toString() === 'ping') { - connection.send('pong'); - } }); }); + // 一定期間通信が無いコネクションは実際には切断されている可能性があるため定期的にterminateする this.#cleanConnectionsIntervalId = setInterval(() => { const now = Date.now(); for (const [connection, lastActive] of this.#connections.entries()) { - if (now - lastActive > 1000 * 60 * 5) { + if (now - lastActive > 1000 * 60 * 2) { connection.terminate(); this.#connections.delete(connection); + } else { + connection.ping(); } } - }, 1000 * 60 * 5); + }, 1000 * 60); } @bindThis diff --git a/packages/backend/src/server/api/endpoint-base.ts b/packages/backend/src/server/api/endpoint-base.ts index 1555a3ca46..d5279faa1c 100644 --- a/packages/backend/src/server/api/endpoint-base.ts +++ b/packages/backend/src/server/api/endpoint-base.ts @@ -1,11 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as fs from 'node:fs'; -import Ajv from 'ajv'; +import _Ajv from 'ajv'; import type { Schema, SchemaType } from '@/misc/json-schema.js'; -import type { LocalUser } from '@/models/entities/User.js'; -import type { AccessToken } from '@/models/entities/AccessToken.js'; +import type { MiLocalUser } from '@/models/User.js'; +import type { MiAccessToken } from '@/models/AccessToken.js'; import { ApiError } from './error.js'; import type { IEndpointMeta } from './endpoints.js'; +const Ajv = _Ajv.default; + const ajv = new Ajv({ useDefaults: true, }); @@ -21,34 +28,34 @@ type File = { // TODO: paramsの型をT['params']のスキーマ定義から推論する 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) => + (params: SchemaType, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | 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 ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => Promise; + public exec: (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => Promise; constructor(meta: T, paramDef: Ps, cb: Executor) { const validate = ajv.compile(paramDef); - this.exec = (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => { + this.exec = (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => { let cleanup: undefined | (() => void) = undefined; - + if (meta.requireFile) { cleanup = () => { if (file) fs.unlink(file.path, () => {}); }; - + if (file == null) return Promise.reject(new ApiError({ message: 'File required.', code: 'FILE_REQUIRED', id: '4267801e-70d1-416a-b011-4ee502885d8b', })); } - + const valid = validate(params); if (!valid) { if (file) cleanup!(); - + const errors = validate.errors!; const err = new ApiError({ message: 'Invalid param.', @@ -60,7 +67,7 @@ export abstract class Endpoint { }); return Promise.reject(err); } - + return cb(params as SchemaType, user, token, file, cleanup, ip, headers); }; } diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 7e678a6404..ab20a708ef 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import type { Schema } from '@/misc/json-schema.js'; import { RolePolicies } from '@/core/RoleService.js'; @@ -38,7 +43,8 @@ import * as ep___admin_federation_updateInstance from './endpoints/admin/federat import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'; import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'; import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js'; -import * as ep___invite from './endpoints/invite.js'; +import * as ep___admin_invite_create from './endpoints/admin/invite/create.js'; +import * as ep___admin_invite_list from './endpoints/admin/invite/list.js'; import * as ep___admin_promo_create from './endpoints/admin/promo/create.js'; import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js'; import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js'; @@ -154,6 +160,7 @@ import * as ep___federation_users from './endpoints/federation/users.js'; import * as ep___federation_stats from './endpoints/federation/stats.js'; import * as ep___following_create from './endpoints/following/create.js'; import * as ep___following_delete from './endpoints/following/delete.js'; +import * as ep___following_update from './endpoints/following/update.js'; import * as ep___following_invalidate from './endpoints/following/invalidate.js'; import * as ep___following_requests_accept from './endpoints/following/requests/accept.js'; import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js'; @@ -230,6 +237,10 @@ 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___invite_create from './endpoints/invite/create.js'; +import * as ep___invite_delete from './endpoints/invite/delete.js'; +import * as ep___invite_list from './endpoints/invite/list.js'; +import * as ep___invite_limit from './endpoints/invite/limit.js'; import * as ep___meta from './endpoints/meta.js'; import * as ep___emojis from './endpoints/emojis.js'; import * as ep___emoji from './endpoints/emoji.js'; @@ -273,6 +284,7 @@ import * as ep___notes_unrenote from './endpoints/notes/unrenote.js'; import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js'; import * as ep___notifications_create from './endpoints/notifications/create.js'; import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js'; +import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js'; import * as ep___pagePush from './endpoints/page-push.js'; import * as ep___pages_create from './endpoints/pages/create.js'; import * as ep___pages_delete from './endpoints/pages/delete.js'; @@ -326,6 +338,7 @@ import * as ep___users_lists_create_from_public from './endpoints/users/lists/cr import * as ep___users_lists_update from './endpoints/users/lists/update.js'; import * as ep___users_notes from './endpoints/users/notes.js'; import * as ep___users_pages from './endpoints/users/pages.js'; +import * as ep___users_flashs from './endpoints/users/flashs.js'; import * as ep___users_reactions from './endpoints/users/reactions.js'; import * as ep___users_recommendation from './endpoints/users/recommendation.js'; import * as ep___users_relation from './endpoints/users/relation.js'; @@ -333,7 +346,6 @@ import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js'; import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js'; import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_show from './endpoints/users/show.js'; -import * as ep___users_stats from './endpoints/users/stats.js'; import * as ep___users_achievements from './endpoints/users/achievements.js'; import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js'; @@ -377,7 +389,8 @@ const eps = [ ['admin/get-index-stats', ep___admin_getIndexStats], ['admin/get-table-stats', ep___admin_getTableStats], ['admin/get-user-ips', ep___admin_getUserIps], - ['invite', ep___invite], + ['admin/invite/create', ep___admin_invite_create], + ['admin/invite/list', ep___admin_invite_list], ['admin/promo/create', ep___admin_promo_create], ['admin/queue/clear', ep___admin_queue_clear], ['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed], @@ -493,6 +506,7 @@ const eps = [ ['federation/stats', ep___federation_stats], ['following/create', ep___following_create], ['following/delete', ep___following_delete], + ['following/update', ep___following_update], ['following/invalidate', ep___following_invalidate], ['following/requests/accept', ep___following_requests_accept], ['following/requests/cancel', ep___following_requests_cancel], @@ -569,6 +583,10 @@ const eps = [ ['i/webhooks/show', ep___i_webhooks_show], ['i/webhooks/update', ep___i_webhooks_update], ['i/webhooks/delete', ep___i_webhooks_delete], + ['invite/create', ep___invite_create], + ['invite/delete', ep___invite_delete], + ['invite/list', ep___invite_list], + ['invite/limit', ep___invite_limit], ['meta', ep___meta], ['emojis', ep___emojis], ['emoji', ep___emoji], @@ -612,6 +630,7 @@ const eps = [ ['notes/user-list-timeline', ep___notes_userListTimeline], ['notifications/create', ep___notifications_create], ['notifications/mark-all-as-read', ep___notifications_markAllAsRead], + ['notifications/test-notification', ep___notifications_testNotification], ['page-push', ep___pagePush], ['pages/create', ep___pages_create], ['pages/delete', ep___pages_delete], @@ -665,6 +684,7 @@ const eps = [ ['users/lists/create-from-public', ep___users_lists_create_from_public], ['users/notes', ep___users_notes], ['users/pages', ep___users_pages], + ['users/flashs', ep___users_flashs], ['users/reactions', ep___users_reactions], ['users/recommendation', ep___users_recommendation], ['users/relation', ep___users_relation], @@ -672,7 +692,6 @@ const eps = [ ['users/search-by-username-and-host', ep___users_searchByUsernameAndHost], ['users/search', ep___users_search], ['users/show', ep___users_show], - ['users/stats', ep___users_stats], ['users/achievements', ep___users_achievements], ['users/update-memo', ep___users_updateMemo], ['fetch-rss', ep___fetchRss], @@ -792,4 +811,5 @@ const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => { }; }); +// eslint-disable-next-line import/no-default-export export default endpoints; diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts index 9bba16166f..be4fc82f0c 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AbuseUserReportsRepository } from '@/models/index.js'; +import type { AbuseUserReportsRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { AbuseUserReportEntityService } from '@/core/entities/AbuseUserReportEntityService.js'; @@ -87,9 +92,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.abuseUserReportsRepository) private abuseUserReportsRepository: AbuseUserReportsRepository, @@ -115,7 +119,7 @@ export default class extends Endpoint { case 'remote': query.andWhere('report.targetUserHost IS NOT NULL'); break; } - const reports = await query.take(ps.limit).getMany(); + const reports = await query.limit(ps.limit).getMany(); return await this.abuseUserReportEntityService.packMany(reports); }); diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index 8a3541dffe..070e88f6f3 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/_.js'; import { SignupService } from '@/core/SignupService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { localUsernameSchema, passwordSchema } from '@/models/entities/User.js'; +import { localUsernameSchema, passwordSchema } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -32,9 +37,8 @@ export const paramDef = { required: ['username', 'password'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts index 16232813a8..60e928ccbe 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts @@ -1,8 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/_.js'; import { QueueService } from '@/core/QueueService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -22,16 +26,14 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, private userEntityService: UserEntityService, private queueService: QueueService, - private globalEventService: GlobalEventService, private userSuspendService: UserSuspendService, ) { super(meta, paramDef, async (ps, me) => { 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 917242db3f..a13d08fd3a 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AdsRepository } from '@/models/index.js'; +import type { AdsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; @@ -22,13 +27,13 @@ export const paramDef = { expiresAt: { type: 'integer' }, startsAt: { type: 'integer' }, imageUrl: { type: 'string', minLength: 1 }, + dayOfWeek: { type: 'integer' }, }, - required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl'], + required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.adsRepository) private adsRepository: AdsRepository, @@ -41,6 +46,7 @@ export default class extends Endpoint { createdAt: new Date(), expiresAt: new Date(ps.expiresAt), startsAt: new Date(ps.startsAt), + dayOfWeek: ps.dayOfWeek, url: ps.url, imageUrl: ps.imageUrl, priority: ps.priority, diff --git a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts index f4c9885408..d3c53d4f67 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AdsRepository } from '@/models/index.js'; +import type { AdsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -27,9 +32,8 @@ export const paramDef = { required: ['id'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.adsRepository) private adsRepository: AdsRepository, 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 0b6d006052..adff3ed0ae 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/list.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AdsRepository } from '@/models/index.js'; +import type { AdsRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; @@ -21,9 +26,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.adsRepository) private adsRepository: AdsRepository, @@ -32,7 +36,7 @@ export default class extends Endpoint { ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId); - const ads = await query.take(ps.limit).getMany(); + const ads = await query.limit(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 dbab7e9d4f..5b77f67e10 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AdsRepository } from '@/models/index.js'; +import type { AdsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -31,13 +36,13 @@ export const paramDef = { ratio: { type: 'integer' }, expiresAt: { type: 'integer' }, startsAt: { type: 'integer' }, + dayOfWeek: { type: 'integer' }, }, - required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt'], + required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'dayOfWeek'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.adsRepository) private adsRepository: AdsRepository, @@ -56,6 +61,7 @@ export default class extends Endpoint { imageUrl: ps.imageUrl, expiresAt: new Date(ps.expiresAt), startsAt: new Date(ps.startsAt), + dayOfWeek: ps.dayOfWeek, }); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts index 751b6be7f4..c2f69bb159 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts @@ -1,8 +1,11 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AnnouncementsRepository } from '@/models/index.js'; -import { IdService } from '@/core/IdService.js'; -import { DI } from '@/di-symbols.js'; +import { AnnouncementService } from '@/core/AnnouncementService.js'; export const meta = { tags: ['admin'], @@ -52,30 +55,35 @@ export const paramDef = { title: { type: 'string', minLength: 1 }, text: { type: 'string', minLength: 1 }, imageUrl: { type: 'string', nullable: true, minLength: 1 }, + icon: { type: 'string', enum: ['info', 'warning', 'error', 'success'], default: 'info' }, + display: { type: 'string', enum: ['normal', 'banner', 'dialog'], default: 'normal' }, + forExistingUsers: { type: 'boolean', default: false }, + needConfirmationToRead: { type: 'boolean', default: false }, + userId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, }, required: ['title', 'text', 'imageUrl'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.announcementsRepository) - private announcementsRepository: AnnouncementsRepository, - - private idService: IdService, + private announcementService: AnnouncementService, ) { super(meta, paramDef, async (ps, me) => { - const announcement = await this.announcementsRepository.insert({ - id: this.idService.genId(), + const { raw, packed } = await this.announcementService.create({ createdAt: new Date(), updatedAt: null, title: ps.title, text: ps.text, imageUrl: ps.imageUrl, - }).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0])); + icon: ps.icon, + display: ps.display, + forExistingUsers: ps.forExistingUsers, + needConfirmationToRead: ps.needConfirmationToRead, + userId: ps.userId, + }); - return Object.assign({}, announcement, { createdAt: announcement.createdAt.toISOString(), updatedAt: null }); + return packed; }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts index 18d50b8b2a..80eb6d7a80 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AnnouncementsRepository } from '@/models/index.js'; +import type { AnnouncementsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -27,9 +32,8 @@ export const paramDef = { required: ['id'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.announcementsRepository) private announcementsRepository: AnnouncementsRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index 9b20494129..c82e702eef 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/index.js'; -import type { Announcement } from '@/models/entities/Announcement.js'; +import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/_.js'; +import type { MiAnnouncement } from '@/models/Announcement.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; @@ -61,13 +66,13 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + userId: { type: 'string', format: 'misskey:id', nullable: true }, }, required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.announcementsRepository) private announcementsRepository: AnnouncementsRepository, @@ -79,10 +84,15 @@ export default class extends Endpoint { ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); + if (ps.userId) { + query.andWhere('announcement.userId = :userId', { userId: ps.userId }); + } else { + query.andWhere('announcement.userId IS NULL'); + } - const announcements = await query.take(ps.limit).getMany(); + const announcements = await query.limit(ps.limit).getMany(); - const reads = new Map(); + const reads = new Map(); for (const announcement of announcements) { reads.set(announcement, await this.announcementReadsRepository.countBy({ @@ -97,6 +107,12 @@ export default class extends Endpoint { title: announcement.title, text: announcement.text, imageUrl: announcement.imageUrl, + icon: announcement.icon, + display: announcement.display, + isActive: announcement.isActive, + forExistingUsers: announcement.forExistingUsers, + needConfirmationToRead: announcement.needConfirmationToRead, + userId: announcement.userId, reads: reads.get(announcement)!, })); }); diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts index 12db1f78fb..782928048b 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AnnouncementsRepository } from '@/models/index.js'; +import type { AnnouncementsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -26,13 +31,17 @@ export const paramDef = { title: { type: 'string', minLength: 1 }, text: { type: 'string', minLength: 1 }, imageUrl: { type: 'string', nullable: true, minLength: 0 }, + icon: { type: 'string', enum: ['info', 'warning', 'error', 'success'] }, + display: { type: 'string', enum: ['normal', 'banner', 'dialog'] }, + forExistingUsers: { type: 'boolean' }, + needConfirmationToRead: { type: 'boolean' }, + isActive: { type: 'boolean' }, }, - required: ['id', 'title', 'text', 'imageUrl'], + required: ['id'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.announcementsRepository) private announcementsRepository: AnnouncementsRepository, @@ -47,7 +56,12 @@ export default class extends Endpoint { title: ps.title, text: ps.text, /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */ - imageUrl: ps.imageUrl || null, + imageUrl: ps.imageUrl || null, + display: ps.display, + icon: ps.icon, + forExistingUsers: ps.forExistingUsers, + needConfirmationToRead: ps.needConfirmationToRead, + isActive: ps.isActive, }); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/delete-account.ts b/packages/backend/src/server/api/endpoints/admin/delete-account.ts index d0485fddd8..9ef09b172e 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-account.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DeleteAccountService } from '@/core/DeleteAccountService.js'; import { DI } from '@/di-symbols.js'; @@ -22,9 +27,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts index c193ed3fb3..e47ecd81cf 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { DriveService } from '@/core/DriveService.js'; import { DI } from '@/di-symbols.js'; @@ -19,9 +24,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, 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 a8964af449..8af44029c5 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,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; @@ -15,9 +20,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private queueService: QueueService, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts index 4f7e02fe92..75d689966f 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { DriveService } from '@/core/DriveService.js'; import { DI } from '@/di-symbols.js'; @@ -18,9 +23,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/drive/files.ts b/packages/backend/src/server/api/endpoints/admin/drive/files.ts index 8a4498d5fa..ac8a70e3da 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/files.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; @@ -41,9 +46,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -76,7 +80,7 @@ export default class extends Endpoint { } } - const files = await query.take(ps.limit).getMany(); + const files = await query.limit(ps.limit).getMany(); return await this.driveFileEntityService.packMany(files, { detail: true, withUser: true, self: true }); }); 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 1d27ac2137..7fb5342f8d 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,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { DriveFilesRepository, UsersRepository } from '@/models/index.js'; +import type { DriveFilesRepository, UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; @@ -148,9 +153,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, 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 6e604ed885..66ee4cab3b 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 @@ -1,4 +1,9 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; @@ -22,9 +27,8 @@ export const paramDef = { required: ['ids', 'aliases'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private customEmojiService: CustomEmojiService, ) { 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 ba88ae857d..022808fe91 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import rndstr from 'rndstr'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -47,15 +52,15 @@ export const paramDef = { // TODO: ロジックをサービスに切り出す -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, private customEmojiService: CustomEmojiService, + private emojiEntityService: EmojiEntityService, private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { @@ -78,9 +83,7 @@ export default class extends Endpoint { emojiId: emoji.id, }); - return { - id: emoji.id, - }; + return this.emojiEntityService.packDetailed(emoji); }); } } 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 82dca9cc70..c5f986ff02 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -1,9 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; -import type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; import { DI } from '@/di-symbols.js'; import { DriveService } from '@/core/DriveService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -47,13 +51,9 @@ export const paramDef = { // TODO: ロジックをサービスに切り出す -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.db) - private db: DataSource, - @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, @@ -69,7 +69,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchEmoji); } - let driveFile: DriveFile; + let driveFile: MiDriveFile; try { // Create file 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 9f8263629b..4221913049 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 @@ -1,4 +1,9 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; @@ -19,9 +24,8 @@ export const paramDef = { required: ['ids'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private customEmojiService: CustomEmojiService, ) { 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 429c819fe0..f020e22182 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts @@ -1,4 +1,9 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; @@ -25,9 +30,8 @@ export const paramDef = { required: ['id'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private customEmojiService: CustomEmojiService, ) { 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 e26f0506ce..208616c0ac 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,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; @@ -16,9 +21,8 @@ export const paramDef = { required: ['fileId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private queueService: QueueService, ) { 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 df3c28deff..855ab8cd24 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 @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; @@ -72,9 +77,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, @@ -98,7 +102,7 @@ export default class extends Endpoint { const emojis = await q .orderBy('emoji.id', 'DESC') - .take(ps.limit) + .limit(ps.limit) .getMany(); 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 4aa4ad82b4..ab16d86a3d 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { EmojisRepository } from '@/models/index.js'; -import type { Emoji } from '@/models/entities/Emoji.js'; +import type { EmojisRepository } from '@/models/_.js'; +import type { MiEmoji } from '@/models/Emoji.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; @@ -66,9 +71,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, @@ -80,18 +84,18 @@ export default class extends Endpoint { const q = this.queryService.makePaginationQuery(this.emojisRepository.createQueryBuilder('emoji'), ps.sinceId, ps.untilId) .andWhere('emoji.host IS NULL'); - let emojis: Emoji[]; + let emojis: MiEmoji[]; if (ps.query) { //q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); - //const emojis = await q.take(ps.limit).getMany(); + //const emojis = await q.limit(ps.limit).getMany(); emojis = await q.getMany(); const queryarry = ps.query.match(/\:([a-z0-9_]*)\:/g); if (queryarry) { - emojis = emojis.filter(emoji => - queryarry.includes(`:${emoji.name}:`) + emojis = emojis.filter(emoji => + queryarry.includes(`:${emoji.name}:`), ); } else { emojis = emojis.filter(emoji => @@ -101,7 +105,7 @@ export default class extends Endpoint { } emojis.splice(ps.limit + 1); } else { - emojis = await q.take(ps.limit).getMany(); + emojis = await q.limit(ps.limit).getMany(); } 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 83f882cac5..a5dd6d5e3a 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 @@ -1,4 +1,9 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; @@ -22,9 +27,8 @@ export const paramDef = { required: ['ids', 'aliases'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private customEmojiService: CustomEmojiService, ) { 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 1d3a432bb7..515053f57b 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 @@ -1,4 +1,9 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; @@ -22,9 +27,8 @@ export const paramDef = { required: ['ids', 'aliases'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private customEmojiService: CustomEmojiService, ) { 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 453968c7a9..8e834ad1dd 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 @@ -1,4 +1,9 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; @@ -24,9 +29,8 @@ export const paramDef = { required: ['ids'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private customEmojiService: CustomEmojiService, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts index b90b9757be..2dc9595a7e 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts @@ -1,4 +1,9 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; @@ -24,9 +29,8 @@ export const paramDef = { required: ['ids'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private customEmojiService: CustomEmojiService, ) { 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 fb22bdc477..f01be9e27a 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -54,9 +59,8 @@ export const paramDef = { required: ['id', 'name', 'aliases'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -70,7 +74,7 @@ export default class extends Endpoint { driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); } - + await this.customEmojiService.update(ps.id, { driveFile, name: ps.name, diff --git a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts index 38fe99b222..b63f01bec3 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { DriveService } from '@/core/DriveService.js'; import { DI } from '@/di-symbols.js'; @@ -19,9 +24,8 @@ export const paramDef = { required: ['host'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts index b7f2858a77..6dbfe3c4f5 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { InstancesRepository } from '@/models/index.js'; +import type { InstancesRepository } from '@/models/_.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; @@ -20,9 +25,8 @@ export const paramDef = { required: ['host'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts index 83f729953a..36ea390e45 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + 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, UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { QueueService } from '@/core/QueueService.js'; @@ -19,9 +24,8 @@ export const paramDef = { required: ['host'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts index 4fd74e591d..fbb91837f2 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { InstancesRepository } from '@/models/index.js'; +import type { InstancesRepository } from '@/models/_.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; @@ -21,9 +26,8 @@ export const paramDef = { required: ['host', 'isSuspended'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts index 8ffd2b01e7..4bd9e7de7f 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -16,9 +21,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, diff --git a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts index 09d61bd741..f953b889a3 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -27,9 +32,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, diff --git a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts index bfcc8a700b..cf94c998fa 100644 --- a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts +++ b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UserIpsRepository } from '@/models/index.js'; +import type { UserIpsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -18,9 +23,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.userIpsRepository) private userIpsRepository: UserIpsRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/invite/create.ts b/packages/backend/src/server/api/endpoints/admin/invite/create.ts new file mode 100644 index 0000000000..7112e06bdc --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/invite/create.ts @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RegistrationTicketsRepository } from '@/models/_.js'; +import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js'; +import { IdService } from '@/core/IdService.js'; +import { DI } from '@/di-symbols.js'; +import { generateInviteCode } from '@/misc/generate-invite-code.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + errors: { + invalidDateTime: { + message: 'Invalid date-time format', + code: 'INVALID_DATE_TIME', + id: 'f1380b15-3760-4c6c-a1db-5c3aaf1cbd49', + }, + }, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + code: { + type: 'string', + optional: false, nullable: false, + example: 'GR6S02ERUA5VR', + }, + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + count: { type: 'integer', minimum: 1, maximum: 100, default: 1 }, + expiresAt: { type: 'string', nullable: true }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.registrationTicketsRepository) + private registrationTicketsRepository: RegistrationTicketsRepository, + + private inviteCodeEntityService: InviteCodeEntityService, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + if (ps.expiresAt && isNaN(Date.parse(ps.expiresAt))) { + throw new ApiError(meta.errors.invalidDateTime); + } + + const ticketsPromises = []; + + for (let i = 0; i < ps.count; i++) { + ticketsPromises.push(this.registrationTicketsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null, + code: generateInviteCode(), + }).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0]))); + } + + const tickets = await Promise.all(ticketsPromises); + return await this.inviteCodeEntityService.packMany(tickets, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/invite/list.ts b/packages/backend/src/server/api/endpoints/admin/invite/list.ts new file mode 100644 index 0000000000..a20a51121a --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/invite/list.ts @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RegistrationTicketsRepository } from '@/models/_.js'; +import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, + offset: { type: 'integer', default: 0 }, + type: { type: 'string', enum: ['unused', 'used', 'expired', 'all'], default: 'all' }, + sort: { type: 'string', enum: ['+createdAt', '-createdAt', '+usedAt', '-usedAt'] }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.registrationTicketsRepository) + private registrationTicketsRepository: RegistrationTicketsRepository, + + private inviteCodeEntityService: InviteCodeEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.registrationTicketsRepository.createQueryBuilder('ticket') + .leftJoinAndSelect('ticket.createdBy', 'createdBy') + .leftJoinAndSelect('ticket.usedBy', 'usedBy'); + + switch (ps.type) { + case 'unused': query.andWhere('ticket.usedBy IS NULL'); break; + case 'used': query.andWhere('ticket.usedBy IS NOT NULL'); break; + case 'expired': query.andWhere('ticket.expiresAt < :now', { now: new Date() }); break; + } + + switch (ps.sort) { + case '+createdAt': query.orderBy('ticket.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('ticket.createdAt', 'ASC'); break; + case '+usedAt': query.orderBy('ticket.usedAt', 'DESC', 'NULLS LAST'); break; + case '-usedAt': query.orderBy('ticket.usedAt', 'ASC', 'NULLS FIRST'); break; + default: query.orderBy('ticket.id', 'DESC'); break; + } + + query.limit(ps.limit); + query.offset(ps.offset); + + const tickets = await query.getMany(); + + return await this.inviteCodeEntityService.packMany(tickets, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 87a2d22ac2..c3ba07cdd0 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -1,5 +1,9 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { MetaService } from '@/core/MetaService.js'; import type { Config } from '@/config.js'; @@ -20,6 +24,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + cacheRemoteSensitiveFiles: { + type: 'boolean', + optional: false, nullable: false, + }, emailRequiredForSignup: { type: 'boolean', optional: false, nullable: false, @@ -61,15 +69,30 @@ export const meta = { type: 'string', optional: false, nullable: true, }, - errorImageUrl: { + serverErrorImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + infoImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + notFoundImageUrl: { type: 'string', optional: false, nullable: true, - default: 'https://xn--931a.moe/aiart/yubitun.png', }, iconUrl: { type: 'string', optional: false, nullable: true, }, + app192IconUrl: { + type: 'string', + optional: false, nullable: true, + }, + app512IconUrl: { + type: 'string', + optional: false, nullable: true, + }, enableEmail: { type: 'boolean', optional: false, nullable: false, @@ -255,6 +278,18 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + enableServerMachineStats: { + type: 'boolean', + optional: false, nullable: false, + }, + enableIdenticonGeneration: { + type: 'boolean', + optional: false, nullable: false, + }, + manifestJsonOverride: { + type: 'string', + optional: true, nullable: false, + }, policies: { type: 'object', optional: false, nullable: false, @@ -270,9 +305,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.config) private config: Config, @@ -287,6 +321,7 @@ export default class extends Endpoint { maintainerEmail: instance.maintainerEmail, version: this.config.version, name: instance.name, + shortName: instance.shortName, uri: this.config.url, description: instance.description, langs: instance.langs, @@ -305,8 +340,12 @@ export default class extends Endpoint { themeColor: instance.themeColor, mascotImageUrl: instance.mascotImageUrl, bannerUrl: instance.bannerUrl, - errorImageUrl: instance.errorImageUrl, + serverErrorImageUrl: instance.serverErrorImageUrl, + notFoundImageUrl: instance.notFoundImageUrl, + infoImageUrl: instance.infoImageUrl, iconUrl: instance.iconUrl, + app192IconUrl: instance.app192IconUrl, + app512IconUrl: instance.app512IconUrl, backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, defaultLightTheme: instance.defaultLightTheme, @@ -315,6 +354,7 @@ export default class extends Endpoint { enableServiceWorker: instance.enableServiceWorker, translatorAvailable: instance.deeplAuthKey != null, cacheRemoteFiles: instance.cacheRemoteFiles, + cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles, pinnedUsers: instance.pinnedUsers, hiddenTags: instance.hiddenTags, blockedHosts: instance.blockedHosts, @@ -355,7 +395,10 @@ export default class extends Endpoint { enableActiveEmailValidation: instance.enableActiveEmailValidation, enableChartsForRemoteUser: instance.enableChartsForRemoteUser, enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances, + enableServerMachineStats: instance.enableServerMachineStats, + enableIdenticonGeneration: instance.enableIdenticonGeneration, policies: { ...DEFAULT_POLICIES, ...instance.policies }, + manifestJsonOverride: instance.manifestJsonOverride, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts index bee1ffbaee..4061e1b5df 100644 --- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { PromoNotesRepository } from '@/models/index.js'; +import type { PromoNotesRepository } from '@/models/_.js'; import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -35,9 +40,8 @@ export const paramDef = { required: ['noteId', 'expiresAt'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.promoNotesRepository) private promoNotesRepository: PromoNotesRepository, @@ -50,9 +54,9 @@ export default class extends Endpoint { throw e; }); - const exist = await this.promoNotesRepository.findOneBy({ noteId: note.id }); + const exist = await this.promoNotesRepository.exist({ where: { noteId: note.id } }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyPromoted); } 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 6e7ae4be2c..c9142e9885 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; @@ -16,9 +21,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private moderationLogService: ModerationLogService, private queueService: QueueService, diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts index 9442bda5eb..1515ae4c74 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -39,9 +44,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject('queue:deliver') public deliverQueue: DeliverQueue, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts index 55a3410d49..febe0d07c6 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -39,9 +44,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject('queue:inbox') public inboxQueue: InboxQueue, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts index e5acd84f15..0cba5b4e25 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; @@ -18,9 +23,8 @@ export const paramDef = { required: ['type'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private moderationLogService: ModerationLogService, private queueService: QueueService, @@ -33,15 +37,35 @@ export default class extends Endpoint { delayedQueues = await this.queueService.deliverQueue.getDelayed(); for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) { const queue = delayedQueues[queueIndex]; - await queue.promote(); + try { + await queue.promote(); + } catch (e) { + if (e instanceof Error) { + if (e.message.indexOf('not in a delayed state') !== -1) { + throw e; + } + } else { + throw e; + } + } } break; - + case 'inbox': delayedQueues = await this.queueService.inboxQueue.getDelayed(); for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) { const queue = delayedQueues[queueIndex]; - await queue.promote(); + try { + await queue.promote(); + } catch (e) { + if (e instanceof Error) { + if (e.message.indexOf('not in a delayed state') !== -1) { + throw e; + } + } else { + throw e; + } + } } break; } diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts index 7f3732c970..901195e9a5 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js'; @@ -38,9 +43,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, 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 f2d4aa8996..b675db2b89 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { URL } from 'node:url'; import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -54,9 +59,8 @@ export const paramDef = { required: ['inbox'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private relayService: RelayService, ) { 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 910c90e78e..0633c57ed5 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/list.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { RelayService } from '@/core/RelayService.js'; @@ -46,9 +51,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private relayService: RelayService, ) { 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 5e26f61fa7..661b4243c4 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { RelayService } from '@/core/RelayService.js'; @@ -17,9 +22,8 @@ export const paramDef = { required: ['inbox'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private relayService: RelayService, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts index d263f99f6e..0dd4fb4126 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -1,9 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; -import rndstr from 'rndstr'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; export const meta = { tags: ['admin'], @@ -33,9 +38,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -54,7 +58,7 @@ export default class extends Endpoint { throw new Error('cannot reset password of root'); } - const passwd = rndstr('a-zA-Z0-9', 8); + const passwd = secureRndstr(8); // Generate hash of password const hash = bcrypt.hashSync(passwd); 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 aead894611..8667640a67 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 @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, AbuseUserReportsRepository } from '@/models/index.js'; +import type { UsersRepository, AbuseUserReportsRepository } from '@/models/_.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; import { QueueService } from '@/core/QueueService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; @@ -24,9 +29,8 @@ export const paramDef = { // TODO: ロジックをサービスに切り出す -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, 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 b80aaba122..9a005982d4 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RolesRepository, UsersRepository } from '@/models/index.js'; +import type { RolesRepository, UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; import { RoleService } from '@/core/RoleService.js'; @@ -48,9 +53,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/roles/create.ts b/packages/backend/src/server/api/endpoints/admin/roles/create.ts index 916172f54a..f567b0d387 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/create.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RolesRepository } from '@/models/index.js'; +import type { RolesRepository } from '@/models/_.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; @@ -50,9 +55,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/roles/delete.ts b/packages/backend/src/server/api/endpoints/admin/roles/delete.ts index b56ebdb3ee..6e012f6428 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/delete.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RolesRepository } from '@/models/index.js'; +import type { RolesRepository } from '@/models/_.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; @@ -30,9 +35,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, 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 edaf638ea9..3ed4b324dc 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/list.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RolesRepository } from '@/models/index.js'; +import type { RolesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; @@ -19,9 +24,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, 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 01028a086f..5f0accab6f 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/show.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/show.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RolesRepository } from '@/models/index.js'; +import type { RolesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; @@ -30,9 +35,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, 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 45c4f76943..0a79296c05 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RolesRepository, UsersRepository } from '@/models/index.js'; +import type { RolesRepository, UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; import { RoleService } from '@/core/RoleService.js'; @@ -50,9 +55,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, 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 5a34eee96c..b4e7e29e90 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,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -22,9 +27,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private metaService: MetaService, private globalEventService: GlobalEventService, diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts index a6b1372fa9..e4e59e487c 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts @@ -1,6 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RolesRepository } from '@/models/index.js'; +import type { RolesRepository } from '@/models/_.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; import { RoleService } from '@/core/RoleService.js'; @@ -59,9 +65,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts index 35edca5460..b1772be777 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; -import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; +import type { RoleAssignmentsRepository, RolesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; @@ -33,9 +38,8 @@ export const paramDef = { required: ['roleId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, @@ -64,7 +68,7 @@ export default class extends Endpoint { .innerJoinAndSelect('assign.user', 'user'); const assigns = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await Promise.all(assigns.map(async assign => ({ 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 5ddc62f476..b9f2c6a6f1 100644 --- a/packages/backend/src/server/api/endpoints/admin/send-email.ts +++ b/packages/backend/src/server/api/endpoints/admin/send-email.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { EmailService } from '@/core/EmailService.js'; @@ -19,9 +24,8 @@ export const paramDef = { required: ['to', 'subject', 'text'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private emailService: EmailService, ) { diff --git a/packages/backend/src/server/api/endpoints/admin/server-info.ts b/packages/backend/src/server/api/endpoints/admin/server-info.ts index 4ef4fdc665..3169373b0e 100644 --- a/packages/backend/src/server/api/endpoints/admin/server-info.ts +++ b/packages/backend/src/server/api/endpoints/admin/server-info.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as os from 'node:os'; import si from 'systeminformation'; import { Inject, Injectable } from '@nestjs/common'; @@ -95,9 +100,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, diff --git a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts index 24335a21cc..d5f97ab149 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ModerationLogsRepository } from '@/models/index.js'; +import type { ModerationLogsRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { ModerationLogEntityService } from '@/core/entities/ModerationLogEntityService.js'; @@ -61,9 +66,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.moderationLogsRepository) private moderationLogsRepository: ModerationLogsRepository, @@ -74,7 +78,7 @@ export default class extends Endpoint { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.moderationLogsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId); - const reports = await query.take(ps.limit).getMany(); + const reports = await query.limit(ps.limit).getMany(); return await this.moderationLogEntityService.packMany(reports); }); 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 f49d2a0966..e065b99e93 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, SigninsRepository, UserProfilesRepository } from '@/models/index.js'; +import type { UsersRepository, SigninsRepository, UserProfilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; @@ -25,9 +30,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -61,6 +65,7 @@ export default class extends Endpoint { const signins = await this.signinsRepository.findBy({ userId: user.id }); + const roleAssigns = await this.roleService.getUserAssigns(user.id); const roles = await this.roleService.getUserRoles(user.id); return { @@ -85,6 +90,11 @@ export default class extends Endpoint { signins, policies: await this.roleService.getUserPolicies(user.id), roles: await this.roleEntityService.packMany(roles, me), + roleAssigns: roleAssigns.map(a => ({ + createdAt: a.createdAt.toISOString(), + expiresAt: a.expiresAt ? a.expiresAt.toISOString() : null, + roleId: a.roleId, + })), }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 426973f282..e89e1a1490 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -42,9 +47,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -104,8 +108,8 @@ export default class extends Endpoint { default: query.orderBy('user.id', 'ASC'); break; } - query.take(ps.limit); - query.skip(ps.offset); + query.limit(ps.limit); + query.offset(ps.offset); const users = await query.getMany(); diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts index 578224c378..89199f8bff 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { IsNull, Not } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; -import type { User } from '@/models/entities/User.js'; +import type { UsersRepository, FollowingsRepository } from '@/models/_.js'; +import type { MiUser } from '@/models/User.js'; import type { RelationshipJobData } from '@/queue/types.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js'; @@ -26,9 +31,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -68,7 +72,7 @@ export default class extends Endpoint { } @bindThis - private async unFollowAll(follower: User) { + private async unFollowAll(follower: MiUser) { const followings = await this.followingsRepository.find({ where: { followerId: follower.id, diff --git a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts index 93b03d8d44..a2779148ed 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/_.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js'; import { DI } from '@/di-symbols.js'; @@ -20,9 +25,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, 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 68aa838bab..a337be27e1 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -1,9 +1,12 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; -import type { Meta } from '@/models/entities/Meta.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import type { MiMeta } from '@/models/Meta.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; export const meta = { @@ -32,15 +35,21 @@ export const paramDef = { themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' }, mascotImageUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true }, - errorImageUrl: { type: 'string', nullable: true }, + serverErrorImageUrl: { type: 'string', nullable: true }, + infoImageUrl: { type: 'string', nullable: true }, + notFoundImageUrl: { type: 'string', nullable: true }, iconUrl: { type: 'string', nullable: true }, + app192IconUrl: { type: 'string', nullable: true }, + app512IconUrl: { type: 'string', nullable: true }, backgroundImageUrl: { type: 'string', nullable: true }, logoImageUrl: { type: 'string', nullable: true }, name: { type: 'string', nullable: true }, + shortName: { type: 'string', nullable: true }, description: { type: 'string', nullable: true }, defaultLightTheme: { type: 'string', nullable: true }, defaultDarkTheme: { type: 'string', nullable: true }, cacheRemoteFiles: { type: 'boolean' }, + cacheRemoteSensitiveFiles: { type: 'boolean' }, emailRequiredForSignup: { type: 'boolean' }, enableHcaptcha: { type: 'boolean' }, hcaptchaSiteKey: { type: 'string', nullable: true }, @@ -94,24 +103,23 @@ export const paramDef = { enableActiveEmailValidation: { type: 'boolean' }, enableChartsForRemoteUser: { type: 'boolean' }, enableChartsForFederatedInstances: { type: 'boolean' }, + enableServerMachineStats: { type: 'boolean' }, + enableIdenticonGeneration: { type: 'boolean' }, serverRules: { type: 'array', items: { type: 'string' } }, preservedUsernames: { type: 'array', items: { type: 'string' } }, + manifestJsonOverride: { type: 'string' }, }, required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.db) - private db: DataSource, - private metaService: MetaService, private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { - const set = {} as Partial; + const set = {} as Partial; if (typeof ps.disableRegistration === 'boolean') { set.disableRegistration = ps.disableRegistration; @@ -132,7 +140,7 @@ export default class extends Endpoint { if (Array.isArray(ps.sensitiveWords)) { set.sensitiveWords = ps.sensitiveWords.filter(Boolean); } - + if (ps.themeColor !== undefined) { set.themeColor = ps.themeColor; } @@ -149,6 +157,26 @@ export default class extends Endpoint { set.iconUrl = ps.iconUrl; } + if (ps.app192IconUrl !== undefined) { + set.app192IconUrl = ps.app192IconUrl; + } + + if (ps.app512IconUrl !== undefined) { + set.app512IconUrl = ps.app512IconUrl; + } + + if (ps.serverErrorImageUrl !== undefined) { + set.serverErrorImageUrl = ps.serverErrorImageUrl; + } + + if (ps.infoImageUrl !== undefined) { + set.infoImageUrl = ps.infoImageUrl; + } + + if (ps.notFoundImageUrl !== undefined) { + set.notFoundImageUrl = ps.notFoundImageUrl; + } + if (ps.backgroundImageUrl !== undefined) { set.backgroundImageUrl = ps.backgroundImageUrl; } @@ -161,6 +189,10 @@ export default class extends Endpoint { set.name = ps.name; } + if (ps.shortName !== undefined) { + set.shortName = ps.shortName; + } + if (ps.description !== undefined) { set.description = ps.description; } @@ -177,6 +209,10 @@ export default class extends Endpoint { set.cacheRemoteFiles = ps.cacheRemoteFiles; } + if (ps.cacheRemoteSensitiveFiles !== undefined) { + set.cacheRemoteSensitiveFiles = ps.cacheRemoteSensitiveFiles; + } + if (ps.emailRequiredForSignup !== undefined) { set.emailRequiredForSignup = ps.emailRequiredForSignup; } @@ -281,10 +317,6 @@ export default class extends Endpoint { set.smtpPass = ps.smtpPass; } - if (ps.errorImageUrl !== undefined) { - set.errorImageUrl = ps.errorImageUrl; - } - if (ps.enableServiceWorker !== undefined) { set.enableServiceWorker = ps.enableServiceWorker; } @@ -389,6 +421,14 @@ export default class extends Endpoint { set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances; } + if (ps.enableServerMachineStats !== undefined) { + set.enableServerMachineStats = ps.enableServerMachineStats; + } + + if (ps.enableIdenticonGeneration !== undefined) { + set.enableIdenticonGeneration = ps.enableIdenticonGeneration; + } + if (ps.serverRules !== undefined) { set.serverRules = ps.serverRules; } @@ -397,6 +437,10 @@ export default class extends Endpoint { set.preservedUsernames = ps.preservedUsernames; } + if (ps.manifestJsonOverride !== undefined) { + set.manifestJsonOverride = ps.manifestJsonOverride; + } + await this.metaService.update(set); this.moderationLogService.log(me, 'updateMeta'); }); diff --git a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts index 33808ee70f..c86a43977e 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -19,9 +24,8 @@ export const paramDef = { required: ['userId', 'text'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts index 79788be4e2..498afe3448 100644 --- a/packages/backend/src/server/api/endpoints/announcements.ts +++ b/packages/backend/src/server/api/endpoints/announcements.ts @@ -1,8 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; +import { AnnouncementService } from '@/core/AnnouncementService.js'; import { DI } from '@/di-symbols.js'; -import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/index.js'; +import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/_.js'; export const meta = { tags: ['meta'], @@ -15,40 +22,7 @@ export const meta = { items: { type: 'object', optional: false, nullable: false, - 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: true, - format: 'date-time', - }, - text: { - type: 'string', - optional: false, nullable: false, - }, - title: { - type: 'string', - optional: false, nullable: false, - }, - imageUrl: { - type: 'string', - optional: false, nullable: true, - }, - isRead: { - type: 'boolean', - optional: true, nullable: false, - }, - }, + ref: 'Announcement', }, }, } as const; @@ -57,16 +31,15 @@ export const paramDef = { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - withUnreads: { type: 'boolean', default: false }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + isActive: { type: 'boolean', default: true }, }, required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.announcementsRepository) private announcementsRepository: AnnouncementsRepository, @@ -75,27 +48,19 @@ export default class extends Endpoint { private announcementReadsRepository: AnnouncementReadsRepository, private queryService: QueryService, + private announcementService: AnnouncementService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); + const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId) + .where('announcement.isActive = :isActive', { isActive: ps.isActive }) + .andWhere(new Brackets(qb => { + if (me) qb.orWhere('announcement.userId = :meId', { meId: me.id }); + qb.orWhere('announcement.userId IS NULL'); + })); - const announcements = await query.take(ps.limit).getMany(); + const announcements = await query.limit(ps.limit).getMany(); - if (me) { - const reads = (await this.announcementReadsRepository.findBy({ - userId: me.id, - })).map(x => x.announcementId); - - for (const announcement of announcements) { - (announcement as any).isRead = reads.includes(announcement.id); - } - } - - return (ps.withUnreads ? announcements.filter((a: any) => !a.isRead) : announcements).map((a) => ({ - ...a, - createdAt: a.createdAt.toISOString(), - updatedAt: a.updatedAt?.toISOString() ?? null, - })); + return this.announcementService.packMany(announcements, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index 5754a9f12a..15fca4904d 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; -import type { UserListsRepository, AntennasRepository } from '@/models/index.js'; +import type { UserListsRepository, AntennasRepository } from '@/models/_.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -42,7 +47,7 @@ export const paramDef = { type: 'object', properties: { name: { type: 'string', minLength: 1, maxLength: 100 }, - src: { type: 'string', enum: ['home', 'all', 'users', 'list'] }, + src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'users_blacklist'] }, userListId: { type: 'string', format: 'misskey:id', nullable: true }, keywords: { type: 'array', items: { type: 'array', items: { @@ -65,9 +70,8 @@ export const paramDef = { required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, @@ -81,8 +85,8 @@ export default class extends Endpoint { private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { - if ((ps.keywords.length === 0) || ps.keywords[0].every(x => x === '')) { - throw new Error('invalid param'); + if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { + throw new Error('either keywords or excludeKeywords is required.'); } const currentAntennasCount = await this.antennasRepository.countBy({ diff --git a/packages/backend/src/server/api/endpoints/antennas/delete.ts b/packages/backend/src/server/api/endpoints/antennas/delete.ts index 5da7a2cb66..e6240aec65 100644 --- a/packages/backend/src/server/api/endpoints/antennas/delete.ts +++ b/packages/backend/src/server/api/endpoints/antennas/delete.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AntennasRepository } from '@/models/index.js'; +import type { AntennasRepository } from '@/models/_.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -29,9 +34,8 @@ export const paramDef = { required: ['antennaId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, diff --git a/packages/backend/src/server/api/endpoints/antennas/list.ts b/packages/backend/src/server/api/endpoints/antennas/list.ts index a0f8979574..3a9f969d24 100644 --- a/packages/backend/src/server/api/endpoints/antennas/list.ts +++ b/packages/backend/src/server/api/endpoints/antennas/list.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AntennasRepository } from '@/models/index.js'; +import type { AntennasRepository } from '@/models/_.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -28,9 +33,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index dca0f443b7..eaae7bff62 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { NotesRepository, AntennasRepository } from '@/models/index.js'; +import type { NotesRepository, AntennasRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteReadService } from '@/core/NoteReadService.js'; import { DI } from '@/di-symbols.js'; @@ -48,9 +53,8 @@ export const paramDef = { required: ['antennaId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.redis) private redisClient: Redis.Redis, @@ -76,6 +80,11 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchAntenna); } + this.antennasRepository.update(antenna.id, { + isActive: true, + lastUsedAt: new Date(), + }); + const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 const noteIdsRes = await this.redisClient.xrevrange( `antennaTimeline:${antenna.id}`, @@ -112,10 +121,6 @@ export default class extends Endpoint { this.noteReadService.read(me.id, notes); } - this.antennasRepository.update(antenna.id, { - lastUsedAt: new Date(), - }); - return await this.noteEntityService.packMany(notes, me); }); } diff --git a/packages/backend/src/server/api/endpoints/antennas/show.ts b/packages/backend/src/server/api/endpoints/antennas/show.ts index ef7ed5b72c..77c9b31763 100644 --- a/packages/backend/src/server/api/endpoints/antennas/show.ts +++ b/packages/backend/src/server/api/endpoints/antennas/show.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AntennasRepository } from '@/models/index.js'; +import type { AntennasRepository } from '@/models/_.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -35,9 +40,8 @@ export const paramDef = { required: ['antennaId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index 5f980bdbeb..0e98746881 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AntennasRepository, UserListsRepository } from '@/models/index.js'; +import type { AntennasRepository, UserListsRepository } from '@/models/_.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -41,7 +46,7 @@ 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'] }, + src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'users_blacklist'] }, userListId: { type: 'string', format: 'misskey:id', nullable: true }, keywords: { type: 'array', items: { type: 'array', items: { @@ -64,9 +69,8 @@ export const paramDef = { required: ['antennaId', 'name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, @@ -78,6 +82,9 @@ export default class extends Endpoint { private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { + if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { + throw new Error('either keywords or excludeKeywords is required.'); + } // Fetch the antenna const antenna = await this.antennasRepository.findOneBy({ id: ps.antennaId, @@ -112,6 +119,8 @@ export default class extends Endpoint { withReplies: ps.withReplies, withFile: ps.withFile, notify: ps.notify, + isActive: true, + lastUsedAt: new Date(), }); this.globalEventService.publishInternalEvent('antennaUpdated', await this.antennasRepository.findOneByOrFail({ id: antenna.id })); diff --git a/packages/backend/src/server/api/endpoints/ap/get.ts b/packages/backend/src/server/api/endpoints/ap/get.ts index c45a86761c..a4a7fd2037 100644 --- a/packages/backend/src/server/api/endpoints/ap/get.ts +++ b/packages/backend/src/server/api/endpoints/ap/get.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -30,9 +35,8 @@ export const paramDef = { required: ['uri'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private apResolverService: ApResolverService, ) { diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index a103d4196a..f442fbdd2f 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -1,9 +1,13 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; 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 { LocalUser, User } from '@/models/entities/User.js'; +import type { MiNote } from '@/models/Note.js'; +import type { MiLocalUser, MiUser } from '@/models/User.js'; import { isActor, isPost, getApId } from '@/core/activitypub/type.js'; import type { SchemaType } from '@/misc/json-schema.js'; import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; @@ -14,7 +18,6 @@ import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { ApiError } from '../../error.js'; @@ -81,16 +84,9 @@ export const paramDef = { required: ['uri'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - private utilityService: UtilityService, private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, @@ -114,7 +110,7 @@ export default class extends Endpoint { * URIからUserかNoteを解決する */ @bindThis - private async fetchAny(uri: string, me: LocalUser | null | undefined): Promise | null> { + private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise | null> { // ブロックしてたら中断 const fetchedMeta = await this.metaService.fetch(); if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, this.utilityService.extractDbHost(uri))) return null; @@ -147,7 +143,7 @@ export default class extends Endpoint { } @bindThis - private async mergePack(me: LocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise | null> { + private async mergePack(me: MiLocalUser | null | undefined, user: MiUser | null | undefined, note: MiNote | null | undefined): Promise | null> { if (user != null) { return { type: 'User', diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts index c1d0a9dd74..cb00221506 100644 --- a/packages/backend/src/server/api/endpoints/app/create.ts +++ b/packages/backend/src/server/api/endpoints/app/create.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AppsRepository } from '@/models/index.js'; +import type { AppsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { unique } from '@/misc/prelude/array.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; @@ -32,9 +37,8 @@ export const paramDef = { required: ['name', 'description', 'permission'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.appsRepository) private appsRepository: AppsRepository, @@ -44,7 +48,7 @@ export default class extends Endpoint { ) { super(meta, paramDef, async (ps, me) => { // Generate secret - const secret = secureRndstr(32, true); + const secret = secureRndstr(32); // for backward compatibility const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1'))); diff --git a/packages/backend/src/server/api/endpoints/app/show.ts b/packages/backend/src/server/api/endpoints/app/show.ts index eaafa8dc1b..cb968a1c65 100644 --- a/packages/backend/src/server/api/endpoints/app/show.ts +++ b/packages/backend/src/server/api/endpoints/app/show.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AppsRepository } from '@/models/index.js'; +import type { AppsRepository } from '@/models/_.js'; import { AppEntityService } from '@/core/entities/AppEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -31,9 +36,8 @@ export const paramDef = { required: ['appId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.appsRepository) private appsRepository: AppsRepository, diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts index 05842460cf..1b1893fd94 100644 --- a/packages/backend/src/server/api/endpoints/auth/accept.ts +++ b/packages/backend/src/server/api/endpoints/auth/accept.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as crypto from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AuthSessionsRepository, AppsRepository, AccessTokensRepository } from '@/models/index.js'; +import type { AuthSessionsRepository, AppsRepository, AccessTokensRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { DI } from '@/di-symbols.js'; @@ -31,9 +36,8 @@ export const paramDef = { required: ['token'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.appsRepository) private appsRepository: AppsRepository, @@ -55,15 +59,17 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchSession); } - const accessToken = secureRndstr(32, true); + const accessToken = secureRndstr(32); // Fetch exist access token - const exist = await this.accessTokensRepository.findOneBy({ - appId: session.appId, - userId: me.id, + const exist = await this.accessTokensRepository.exist({ + where: { + appId: session.appId, + userId: me.id, + }, }); - if (exist == null) { + if (!exist) { const app = await this.appsRepository.findOneByOrFail({ id: session.appId }); // Generate Hash diff --git a/packages/backend/src/server/api/endpoints/auth/session/generate.ts b/packages/backend/src/server/api/endpoints/auth/session/generate.ts index 6108d8202d..8b6a2c213d 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/generate.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/generate.ts @@ -1,7 +1,12 @@ -import { v4 as uuid } from 'uuid'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AppsRepository, AuthSessionsRepository } from '@/models/index.js'; +import type { AppsRepository, AuthSessionsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; @@ -45,9 +50,8 @@ export const paramDef = { required: ['appSecret'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.config) private config: Config, @@ -71,7 +75,7 @@ export default class extends Endpoint { } // Generate token - const token = uuid(); + const token = randomUUID(); // Create session token document const doc = await this.authSessionsRepository.insert({ diff --git a/packages/backend/src/server/api/endpoints/auth/session/show.ts b/packages/backend/src/server/api/endpoints/auth/session/show.ts index db3bf7aa63..0f5da0f252 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/show.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/show.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AuthSessionsRepository } from '@/models/index.js'; +import type { AuthSessionsRepository } from '@/models/_.js'; import { AuthSessionEntityService } from '@/core/entities/AuthSessionEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -48,9 +53,8 @@ export const paramDef = { required: ['token'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.authSessionsRepository) private authSessionsRepository: AuthSessionsRepository, diff --git a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts index b1e7bbfded..ffddda090b 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, AppsRepository, AccessTokensRepository, AuthSessionsRepository } from '@/models/index.js'; +import type { AppsRepository, AccessTokensRepository, AuthSessionsRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -57,13 +62,9 @@ export const paramDef = { required: ['appSecret', 'token'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.appsRepository) private appsRepository: AppsRepository, diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts index d9ba99f209..3c7d7ac8cd 100644 --- a/packages/backend/src/server/api/endpoints/blocking/create.ts +++ b/packages/backend/src/server/api/endpoints/blocking/create.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, BlockingsRepository } from '@/models/index.js'; +import type { UsersRepository, BlockingsRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { DI } from '@/di-symbols.js'; @@ -55,9 +60,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -84,12 +88,14 @@ export default class extends Endpoint { }); // Check if already blocking - const exist = await this.blockingsRepository.findOneBy({ - blockerId: blocker.id, - blockeeId: blockee.id, + const exist = await this.blockingsRepository.exist({ + where: { + blockerId: blocker.id, + blockeeId: blockee.id, + }, }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyBlocking); } diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts index 46dd26a45a..0ce334d559 100644 --- a/packages/backend/src/server/api/endpoints/blocking/delete.ts +++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts @@ -1,12 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, BlockingsRepository } from '@/models/index.js'; +import type { UsersRepository, BlockingsRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['account'], @@ -55,9 +60,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -84,12 +88,14 @@ export default class extends Endpoint { }); // Check not blocking - const exist = await this.blockingsRepository.findOneBy({ - blockerId: blocker.id, - blockeeId: blockee.id, + const exist = await this.blockingsRepository.exist({ + where: { + blockerId: blocker.id, + blockeeId: blockee.id, + }, }); - if (exist == null) { + if (!exist) { throw new ApiError(meta.errors.notBlocking); } diff --git a/packages/backend/src/server/api/endpoints/blocking/list.ts b/packages/backend/src/server/api/endpoints/blocking/list.ts index 969aae06f9..58d24540d1 100644 --- a/packages/backend/src/server/api/endpoints/blocking/list.ts +++ b/packages/backend/src/server/api/endpoints/blocking/list.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { BlockingsRepository } from '@/models/index.js'; +import type { BlockingsRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { BlockingEntityService } from '@/core/entities/BlockingEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -33,9 +38,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, @@ -48,7 +52,7 @@ export default class extends Endpoint { .andWhere('blocking.blockerId = :meId', { meId: me.id }); const blockings = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await this.blockingEntityService.packMany(blockings, me); diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts index 69e2f2504c..e72120e156 100644 --- a/packages/backend/src/server/api/endpoints/channels/create.ts +++ b/packages/backend/src/server/api/endpoints/channels/create.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository, DriveFilesRepository } from '@/models/index.js'; -import type { Channel } from '@/models/entities/Channel.js'; +import type { ChannelsRepository, DriveFilesRepository } from '@/models/_.js'; +import type { MiChannel } from '@/models/Channel.js'; import { IdService } from '@/core/IdService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -44,13 +49,13 @@ export const paramDef = { description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, bannerId: { type: 'string', format: 'misskey:id', nullable: true }, color: { type: 'string', minLength: 1, maxLength: 16 }, + isSensitive: { type: 'boolean', nullable: true }, }, required: ['name'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -81,8 +86,9 @@ export default class extends Endpoint { name: ps.name, description: ps.description ?? null, bannerId: banner ? banner.id : null, + isSensitive: ps.isSensitive ?? false, ...(ps.color !== undefined ? { color: ps.color } : {}), - } as Channel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0])); + } as MiChannel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0])); return await this.channelEntityService.pack(channel, me); }); diff --git a/packages/backend/src/server/api/endpoints/channels/favorite.ts b/packages/backend/src/server/api/endpoints/channels/favorite.ts index c8544273a1..1f78a86dd4 100644 --- a/packages/backend/src/server/api/endpoints/channels/favorite.ts +++ b/packages/backend/src/server/api/endpoints/channels/favorite.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelFavoritesRepository, ChannelsRepository } from '@/models/index.js'; +import type { ChannelFavoritesRepository, ChannelsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -31,9 +36,8 @@ export const paramDef = { required: ['channelId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, diff --git a/packages/backend/src/server/api/endpoints/channels/featured.ts b/packages/backend/src/server/api/endpoints/channels/featured.ts index 1a8d1164c7..412ea1bb16 100644 --- a/packages/backend/src/server/api/endpoints/channels/featured.ts +++ b/packages/backend/src/server/api/endpoints/channels/featured.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository } from '@/models/index.js'; +import type { ChannelsRepository } from '@/models/_.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -26,9 +31,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, @@ -41,7 +45,7 @@ export default class extends Endpoint { .andWhere('channel.isArchived = FALSE') .orderBy('channel.lastNotedAt', 'DESC'); - const channels = await query.take(10).getMany(); + const channels = await query.limit(10).getMany(); return await Promise.all(channels.map(x => this.channelEntityService.pack(x, me))); }); diff --git a/packages/backend/src/server/api/endpoints/channels/follow.ts b/packages/backend/src/server/api/endpoints/channels/follow.ts index f3ca66cfd2..5a43e8be1b 100644 --- a/packages/backend/src/server/api/endpoints/channels/follow.ts +++ b/packages/backend/src/server/api/endpoints/channels/follow.ts @@ -1,8 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelFollowingsRepository, ChannelsRepository } from '@/models/index.js'; +import type { ChannelFollowingsRepository, ChannelsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -32,9 +36,8 @@ export const paramDef = { required: ['channelId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, diff --git a/packages/backend/src/server/api/endpoints/channels/followed.ts b/packages/backend/src/server/api/endpoints/channels/followed.ts index f49f3105d5..6514f1ea3c 100644 --- a/packages/backend/src/server/api/endpoints/channels/followed.ts +++ b/packages/backend/src/server/api/endpoints/channels/followed.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelFollowingsRepository } from '@/models/index.js'; +import type { ChannelFollowingsRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -33,9 +38,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.channelFollowingsRepository) private channelFollowingsRepository: ChannelFollowingsRepository, @@ -48,7 +52,7 @@ export default class extends Endpoint { .andWhere({ followerId: me.id }); const followings = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await Promise.all(followings.map(x => this.channelEntityService.pack(x.followeeId, me))); diff --git a/packages/backend/src/server/api/endpoints/channels/my-favorites.ts b/packages/backend/src/server/api/endpoints/channels/my-favorites.ts index 60525ed060..057a438ac9 100644 --- a/packages/backend/src/server/api/endpoints/channels/my-favorites.ts +++ b/packages/backend/src/server/api/endpoints/channels/my-favorites.ts @@ -1,7 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelFavoritesRepository } from '@/models/index.js'; -import { QueryService } from '@/core/QueryService.js'; +import type { ChannelFavoritesRepository } from '@/models/_.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -30,15 +34,13 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.channelFavoritesRepository) private channelFavoritesRepository: ChannelFavoritesRepository, private channelEntityService: ChannelEntityService, - private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { const query = this.channelFavoritesRepository.createQueryBuilder('favorite') diff --git a/packages/backend/src/server/api/endpoints/channels/owned.ts b/packages/backend/src/server/api/endpoints/channels/owned.ts index 8fae972cb1..b1dd693537 100644 --- a/packages/backend/src/server/api/endpoints/channels/owned.ts +++ b/packages/backend/src/server/api/endpoints/channels/owned.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository } from '@/models/index.js'; +import type { ChannelsRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -33,9 +38,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, @@ -49,7 +53,7 @@ export default class extends Endpoint { .andWhere({ userId: me.id }); const channels = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await Promise.all(channels.map(x => this.channelEntityService.pack(x, me))); diff --git a/packages/backend/src/server/api/endpoints/channels/search.ts b/packages/backend/src/server/api/endpoints/channels/search.ts index a3b40b0bbd..65df45706b 100644 --- a/packages/backend/src/server/api/endpoints/channels/search.ts +++ b/packages/backend/src/server/api/endpoints/channels/search.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; -import type { ChannelsRepository } from '@/models/index.js'; +import type { ChannelsRepository } from '@/models/_.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; @@ -35,9 +40,8 @@ export const paramDef = { required: ['query'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, @@ -61,7 +65,7 @@ export default class extends Endpoint { } const channels = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await Promise.all(channels.map(x => this.channelEntityService.pack(x, me))); diff --git a/packages/backend/src/server/api/endpoints/channels/show.ts b/packages/backend/src/server/api/endpoints/channels/show.ts index 070d14631e..3eaa83c7e8 100644 --- a/packages/backend/src/server/api/endpoints/channels/show.ts +++ b/packages/backend/src/server/api/endpoints/channels/show.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository } from '@/models/index.js'; +import type { ChannelsRepository } from '@/models/_.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -33,9 +38,8 @@ export const paramDef = { required: ['channelId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index c881074bab..026b649537 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository, Note, NotesRepository } from '@/models/index.js'; +import type { ChannelsRepository, MiNote, NotesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; @@ -46,9 +51,8 @@ export const paramDef = { required: ['channelId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.redis) private redisClient: Redis.Redis, @@ -73,11 +77,11 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchChannel); } - let timeline: Note[] = []; + let timeline: MiNote[] = []; const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 let noteIdsRes: [string, string[]][] = []; - + if (!ps.sinceId && !ps.sinceDate) { noteIdsRes = await this.redisClient.xrevrange( `channelTimeline:${channel.id}`, @@ -105,7 +109,7 @@ export default class extends Endpoint { } //#endregion - timeline = await query.take(ps.limit).getMany(); + timeline = await query.limit(ps.limit).getMany(); } else { const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); diff --git a/packages/backend/src/server/api/endpoints/channels/unfavorite.ts b/packages/backend/src/server/api/endpoints/channels/unfavorite.ts index 67fb1ea03e..b4c7af8154 100644 --- a/packages/backend/src/server/api/endpoints/channels/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/channels/unfavorite.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelFavoritesRepository, ChannelsRepository } from '@/models/index.js'; +import type { ChannelFavoritesRepository, ChannelsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -30,9 +35,8 @@ export const paramDef = { required: ['channelId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, diff --git a/packages/backend/src/server/api/endpoints/channels/unfollow.ts b/packages/backend/src/server/api/endpoints/channels/unfollow.ts index f46ff9f286..46883dd548 100644 --- a/packages/backend/src/server/api/endpoints/channels/unfollow.ts +++ b/packages/backend/src/server/api/endpoints/channels/unfollow.ts @@ -1,7 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelFollowingsRepository, ChannelsRepository } from '@/models/index.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; +import type { ChannelFollowingsRepository, ChannelsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -31,9 +35,8 @@ export const paramDef = { required: ['channelId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts index 30d7f8b244..ab69f62a7b 100644 --- a/packages/backend/src/server/api/endpoints/channels/update.ts +++ b/packages/backend/src/server/api/endpoints/channels/update.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository, ChannelsRepository } from '@/models/index.js'; +import type { DriveFilesRepository, ChannelsRepository } from '@/models/_.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; @@ -55,13 +60,13 @@ export const paramDef = { }, }, color: { type: 'string', minLength: 1, maxLength: 16 }, + isSensitive: { type: 'boolean', nullable: true }, }, required: ['channelId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, @@ -109,6 +114,7 @@ export default class extends Endpoint { ...(ps.color !== undefined ? { color: ps.color } : {}), ...(typeof ps.isArchived === 'boolean' ? { isArchived: ps.isArchived } : {}), ...(banner ? { bannerId: banner.id } : {}), + ...(typeof ps.isSensitive === 'boolean' ? { isSensitive: ps.isSensitive } : {}), }); return await this.channelEntityService.pack(channel.id, me); 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 2ab58e4309..e768923ce1 100644 --- a/packages/backend/src/server/api/endpoints/charts/active-users.ts +++ b/packages/backend/src/server/api/endpoints/charts/active-users.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -23,9 +28,8 @@ export const paramDef = { required: ['span'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private activeUsersChart: ActiveUsersChart, ) { 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 e40a53d82e..f518ae41ca 100644 --- a/packages/backend/src/server/api/endpoints/charts/ap-request.ts +++ b/packages/backend/src/server/api/endpoints/charts/ap-request.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -23,9 +28,8 @@ export const paramDef = { required: ['span'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private apRequestChart: ApRequestChart, ) { diff --git a/packages/backend/src/server/api/endpoints/charts/drive.ts b/packages/backend/src/server/api/endpoints/charts/drive.ts index 9a5aff4af9..94afab113e 100644 --- a/packages/backend/src/server/api/endpoints/charts/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/drive.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -23,9 +28,8 @@ export const paramDef = { required: ['span'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private driveChart: DriveChart, ) { diff --git a/packages/backend/src/server/api/endpoints/charts/federation.ts b/packages/backend/src/server/api/endpoints/charts/federation.ts index ed3a968681..bc33930ca4 100644 --- a/packages/backend/src/server/api/endpoints/charts/federation.ts +++ b/packages/backend/src/server/api/endpoints/charts/federation.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -23,9 +28,8 @@ export const paramDef = { required: ['span'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private federationChart: FederationChart, ) { diff --git a/packages/backend/src/server/api/endpoints/charts/instance.ts b/packages/backend/src/server/api/endpoints/charts/instance.ts index c992d525c9..a432845b38 100644 --- a/packages/backend/src/server/api/endpoints/charts/instance.ts +++ b/packages/backend/src/server/api/endpoints/charts/instance.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -24,9 +29,8 @@ export const paramDef = { required: ['span', 'host'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private instanceChart: InstanceChart, ) { diff --git a/packages/backend/src/server/api/endpoints/charts/notes.ts b/packages/backend/src/server/api/endpoints/charts/notes.ts index 5750cd5b78..e1e9d06311 100644 --- a/packages/backend/src/server/api/endpoints/charts/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/notes.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -23,9 +28,8 @@ export const paramDef = { required: ['span'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private notesChart: NotesChart, ) { 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 5e372294b7..b4a58c9872 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/drive.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -24,9 +29,8 @@ export const paramDef = { required: ['span', 'userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private perUserDriveChart: PerUserDriveChart, ) { 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 3f50918fa7..c609c5a7fe 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/following.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/following.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { getJsonSchema } from '@/core/chart/core.js'; @@ -24,9 +29,8 @@ export const paramDef = { required: ['span', 'userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private perUserFollowingChart: PerUserFollowingChart, ) { 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 0517b3283f..ad6a342fb7 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/notes.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -24,9 +29,8 @@ export const paramDef = { required: ['span', 'userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private perUserNotesChart: PerUserNotesChart, ) { 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 8d1a9aee10..635a403d12 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/pv.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/pv.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -24,9 +29,8 @@ export const paramDef = { required: ['span', 'userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private perUserPvChart: PerUserPvChart, ) { 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 f2ff413195..92bc7028ad 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -24,9 +29,8 @@ export const paramDef = { required: ['span', 'userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private perUserReactionsChart: PerUserReactionsChart, ) { diff --git a/packages/backend/src/server/api/endpoints/charts/users.ts b/packages/backend/src/server/api/endpoints/charts/users.ts index 1374f02046..3be3721e3a 100644 --- a/packages/backend/src/server/api/endpoints/charts/users.ts +++ b/packages/backend/src/server/api/endpoints/charts/users.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -23,9 +28,8 @@ export const paramDef = { required: ['span'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private usersChart: UsersChart, ) { diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts index c3561e2a71..749593aa65 100644 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts @@ -1,11 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { IdService } from '@/core/IdService.js'; -import { DI } from '@/di-symbols.js'; -import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; -import { GetterService } from '@/server/api/GetterService.js'; -import { RoleService } from '@/core/RoleService.js'; +import { ClipService } from '@/core/ClipService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -58,60 +59,27 @@ export const paramDef = { required: ['clipId', 'noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.clipsRepository) - private clipsRepository: ClipsRepository, - - @Inject(DI.clipNotesRepository) - private clipNotesRepository: ClipNotesRepository, - - private idService: IdService, - private roleService: RoleService, - private getterService: GetterService, + private clipService: ClipService, ) { 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); + try { + await this.clipService.addNote(me, ps.clipId, ps.noteId); + } catch (e) { + if (e instanceof ClipService.NoSuchClipError) { + throw new ApiError(meta.errors.noSuchClip); + } else if (e instanceof ClipService.NoSuchNoteError) { + throw new ApiError(meta.errors.noSuchNote); + } else if (e instanceof ClipService.AlreadyAddedError) { + throw new ApiError(meta.errors.alreadyClipped); + } else if (e instanceof ClipService.TooManyClipNotesError) { + throw new ApiError(meta.errors.tooManyClipNotes); + } else { + throw e; + } } - - const note = await this.getterService.getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - const exist = await this.clipNotesRepository.findOneBy({ - noteId: note.id, - clipId: clip.id, - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyClipped); - } - - const currentCount = await this.clipNotesRepository.countBy({ - clipId: clip.id, - }); - if (currentCount > (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) { - throw new ApiError(meta.errors.tooManyClipNotes); - } - - await this.clipNotesRepository.insert({ - id: this.idService.genId(), - noteId: note.id, - clipId: clip.id, - }); - - await this.clipsRepository.update(clip.id, { - lastClippedAt: new Date(), - }); }); } } diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts index 5395a5c373..b4c7b52e72 100644 --- a/packages/backend/src/server/api/endpoints/clips/create.ts +++ b/packages/backend/src/server/api/endpoints/clips/create.ts @@ -1,11 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { IdService } from '@/core/IdService.js'; -import type { ClipsRepository } from '@/models/index.js'; +import type { MiClip } from '@/models/_.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; -import { DI } from '@/di-symbols.js'; -import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '@/server/api/error.js'; +import { ClipService } from '@/core/ClipService.js'; export const meta = { tags: ['clips'], @@ -41,34 +44,22 @@ export const paramDef = { required: ['name'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.clipsRepository) - private clipsRepository: ClipsRepository, - private clipEntityService: ClipEntityService, - private roleService: RoleService, - private idService: IdService, + private clipService: ClipService, ) { super(meta, paramDef, async (ps, me) => { - const currentCount = await this.clipsRepository.countBy({ - userId: me.id, - }); - if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) { - throw new ApiError(meta.errors.tooManyClips); + let clip: MiClip; + try { + clip = await this.clipService.create(me, ps.name, ps.isPublic, ps.description ?? null); + } catch (e) { + if (e instanceof ClipService.TooManyClipsError) { + throw new ApiError(meta.errors.tooManyClips); + } + throw e; } - - const clip = await this.clipsRepository.insert({ - id: this.idService.genId(), - createdAt: new Date(), - userId: me.id, - name: ps.name, - isPublic: ps.isPublic, - description: ps.description, - }).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0])); - return await this.clipEntityService.pack(clip, me); }); } diff --git a/packages/backend/src/server/api/endpoints/clips/delete.ts b/packages/backend/src/server/api/endpoints/clips/delete.ts index 077a9ec40f..239945e8a4 100644 --- a/packages/backend/src/server/api/endpoints/clips/delete.ts +++ b/packages/backend/src/server/api/endpoints/clips/delete.ts @@ -1,7 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ClipsRepository } from '@/models/index.js'; -import { DI } from '@/di-symbols.js'; +import { ClipService } from '@/core/ClipService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -28,24 +32,20 @@ export const paramDef = { required: ['clipId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.clipsRepository) - private clipsRepository: ClipsRepository, + private clipService: ClipService, ) { 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); + try { + await this.clipService.delete(me, ps.clipId); + } catch (e) { + if (e instanceof ClipService.NoSuchClipError) { + throw new ApiError(meta.errors.noSuchClip); + } + throw e; } - - await this.clipsRepository.delete(clip.id); }); } } diff --git a/packages/backend/src/server/api/endpoints/clips/favorite.ts b/packages/backend/src/server/api/endpoints/clips/favorite.ts index f08caaf8d7..6cd34f0a54 100644 --- a/packages/backend/src/server/api/endpoints/clips/favorite.ts +++ b/packages/backend/src/server/api/endpoints/clips/favorite.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { ClipsRepository, ClipFavoritesRepository } from '@/models/index.js'; +import type { ClipsRepository, ClipFavoritesRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -37,9 +42,8 @@ export const paramDef = { required: ['clipId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, @@ -58,12 +62,14 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchClip); } - const exist = await this.clipFavoritesRepository.findOneBy({ - clipId: clip.id, - userId: me.id, + const exist = await this.clipFavoritesRepository.exist({ + where: { + clipId: clip.id, + userId: me.id, + }, }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyFavorited); } diff --git a/packages/backend/src/server/api/endpoints/clips/list.ts b/packages/backend/src/server/api/endpoints/clips/list.ts index 3b8deab709..c124762e33 100644 --- a/packages/backend/src/server/api/endpoints/clips/list.ts +++ b/packages/backend/src/server/api/endpoints/clips/list.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ClipsRepository } from '@/models/index.js'; +import type { ClipsRepository } from '@/models/_.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -28,9 +33,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, diff --git a/packages/backend/src/server/api/endpoints/clips/my-favorites.ts b/packages/backend/src/server/api/endpoints/clips/my-favorites.ts index fc727e93bd..c58c16e25f 100644 --- a/packages/backend/src/server/api/endpoints/clips/my-favorites.ts +++ b/packages/backend/src/server/api/endpoints/clips/my-favorites.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ClipFavoritesRepository } from '@/models/index.js'; +import type { ClipFavoritesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; @@ -29,9 +34,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.clipFavoritesRepository) private clipFavoritesRepository: ClipFavoritesRepository, diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts index dcb415b752..1427d8d0a7 100644 --- a/packages/backend/src/server/api/endpoints/clips/notes.ts +++ b/packages/backend/src/server/api/endpoints/clips/notes.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { NotesRepository, ClipsRepository, ClipNotesRepository } from '@/models/index.js'; +import type { NotesRepository, ClipsRepository, ClipNotesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -43,9 +48,8 @@ export const paramDef = { required: ['clipId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, @@ -88,7 +92,7 @@ export default class extends Endpoint { } const notes = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await this.noteEntityService.packMany(notes, me); 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 50c5d758bd..7b153cb555 100644 --- a/packages/backend/src/server/api/endpoints/clips/remove-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/remove-note.ts @@ -1,9 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + 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 { ClipService } from '@/core/ClipService.js'; import { ApiError } from '../../error.js'; -import { GetterService } from '@/server/api/GetterService.js'; export const meta = { tags: ['account', 'notes', 'clips'], @@ -38,37 +41,22 @@ export const paramDef = { required: ['clipId', 'noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.clipsRepository) - private clipsRepository: ClipsRepository, - - @Inject(DI.clipNotesRepository) - private clipNotesRepository: ClipNotesRepository, - - private getterService: GetterService, + private clipService: ClipService, ) { 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); + try { + await this.clipService.removeNote(me, ps.clipId, ps.noteId); + } catch (e) { + if (e instanceof ClipService.NoSuchClipError) { + throw new ApiError(meta.errors.noSuchClip); + } else if (e instanceof ClipService.NoSuchNoteError) { + throw new ApiError(meta.errors.noSuchNote); + } + throw e; } - - 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/clips/show.ts b/packages/backend/src/server/api/endpoints/clips/show.ts index 99d630a9b5..03b1e09dfb 100644 --- a/packages/backend/src/server/api/endpoints/clips/show.ts +++ b/packages/backend/src/server/api/endpoints/clips/show.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ClipsRepository } from '@/models/index.js'; +import type { ClipsRepository } from '@/models/_.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -35,9 +40,8 @@ export const paramDef = { required: ['clipId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, diff --git a/packages/backend/src/server/api/endpoints/clips/unfavorite.ts b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts index 3da252a226..d1007f7a19 100644 --- a/packages/backend/src/server/api/endpoints/clips/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { ClipsRepository, ClipFavoritesRepository } from '@/models/index.js'; +import type { ClipsRepository, ClipFavoritesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -36,9 +41,8 @@ export const paramDef = { required: ['clipId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts index 70f1959353..0b9878578c 100644 --- a/packages/backend/src/server/api/endpoints/clips/update.ts +++ b/packages/backend/src/server/api/endpoints/clips/update.ts @@ -1,8 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ClipsRepository } from '@/models/index.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; -import { DI } from '@/di-symbols.js'; +import { ClipService } from '@/core/ClipService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -40,33 +44,24 @@ export const paramDef = { required: ['clipId', 'name'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.clipsRepository) - private clipsRepository: ClipsRepository, + private clipService: ClipService, private clipEntityService: ClipEntityService, ) { super(meta, paramDef, async (ps, me) => { - // Fetch the clip - const clip = await this.clipsRepository.findOneBy({ - id: ps.clipId, - userId: me.id, - }); - - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); + try { + await this.clipService.update(me, ps.clipId, ps.name, ps.isPublic, ps.description); + } catch (e) { + if (e instanceof ClipService.NoSuchClipError) { + throw new ApiError(meta.errors.noSuchClip); + } + throw e; } - await this.clipsRepository.update(clip.id, { - name: ps.name, - description: ps.description, - isPublic: ps.isPublic, - }); - - return await this.clipEntityService.pack(clip.id, me); + return await this.clipEntityService.pack(ps.clipId, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts index a6ece0311b..71d3ca5f14 100644 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ b/packages/backend/src/server/api/endpoints/drive.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { MetaService } from '@/core/MetaService.js'; @@ -33,9 +38,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private metaService: MetaService, private driveFileEntityService: DriveFileEntityService, diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts index 4609307774..6f3a62977f 100644 --- a/packages/backend/src/server/api/endpoints/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/drive/files.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -36,9 +41,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -73,7 +77,7 @@ export default class extends Endpoint { case '-size': query.orderBy('file.size', 'ASC'); break; } - const files = await query.take(ps.limit).getMany(); + const files = await query.limit(ps.limit).getMany(); return await this.driveFileEntityService.packMany(files, { detail: false, self: true }); }); diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts index 328d0e4643..779231a856 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { NotesRepository, DriveFilesRepository } from '@/models/index.js'; +import type { NotesRepository, DriveFilesRepository } from '@/models/_.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -41,9 +46,8 @@ export const paramDef = { required: ['fileId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts index 290cd4d2ce..85e6312b6a 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -26,20 +31,21 @@ export const paramDef = { required: ['md5'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, ) { super(meta, paramDef, async (ps, me) => { - const file = await this.driveFilesRepository.findOneBy({ - md5: ps.md5, - userId: me.id, + const exist = await this.driveFilesRepository.exist({ + where: { + md5: ps.md5, + userId: me.id, + }, }); - return file != null; + return exist; }); } } diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index a1c1f9325e..5e97588c99 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -1,13 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; -import { Inject, Injectable } from '@nestjs/common'; -import type { DriveFilesRepository } from '@/models/index.js'; +import { Injectable } from '@nestjs/common'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import { DriveService } from '@/core/DriveService.js'; -import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -67,13 +70,9 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, - private driveFileEntityService: DriveFileEntityService, private metaService: MetaService, private driveService: DriveService, diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts index 2ced97ee02..d7fdc81cdb 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { DriveService } from '@/core/DriveService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; @@ -39,9 +44,8 @@ export const paramDef = { required: ['fileId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, diff --git a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts index d6d85f4e77..7b784f253e 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -32,9 +37,8 @@ export const paramDef = { required: ['md5'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, diff --git a/packages/backend/src/server/api/endpoints/drive/files/find.ts b/packages/backend/src/server/api/endpoints/drive/files/find.ts index 858063eb4b..0ceb31e58d 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -34,9 +39,8 @@ export const paramDef = { required: ['name'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, 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 271b33ef4b..474c7f02d3 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { DriveFile } from '@/models/entities/DriveFile.js'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -49,9 +54,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -60,7 +64,7 @@ export default class extends Endpoint { private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - let file: DriveFile | null = null; + let file: MiDriveFile | null = null; if (ps.fileId) { file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); 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 3ecbba22b5..d26ed63474 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/index.js'; +import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -40,7 +45,7 @@ export const meta = { code: 'NO_SUCH_FOLDER', id: 'ea8fb7a5-af77-4a08-b608-c0218176cd73', }, - + restrictedByRole: { message: 'This feature is restricted by your role.', code: 'RESTRICTED_BY_ROLE', @@ -66,9 +71,8 @@ export const paramDef = { required: ['fileId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, 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 c835587c4a..bbe62063cf 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,11 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; -import { Inject, Injectable } from '@nestjs/common'; -import type { DriveFilesRepository } from '@/models/index.js'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveService } from '@/core/DriveService.js'; -import { DI } from '@/di-symbols.js'; export const meta = { tags: ['drive'], @@ -37,13 +40,9 @@ export const paramDef = { required: ['url'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, - private driveFileEntityService: DriveFileEntityService, private driveService: DriveService, private globalEventService: GlobalEventService, diff --git a/packages/backend/src/server/api/endpoints/drive/folders.ts b/packages/backend/src/server/api/endpoints/drive/folders.ts index b41eaf4463..3a09266591 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFoldersRepository } from '@/models/index.js'; +import type { DriveFoldersRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -34,9 +39,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFoldersRepository) private driveFoldersRepository: DriveFoldersRepository, @@ -54,7 +58,7 @@ export default class extends Endpoint { query.andWhere('folder.parentId IS NULL'); } - const folders = await query.take(ps.limit).getMany(); + const folders = await query.limit(ps.limit).getMany(); return await Promise.all(folders.map(folder => this.driveFolderEntityService.pack(folder))); }); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/create.ts b/packages/backend/src/server/api/endpoints/drive/folders/create.ts index 39c9c6bc58..bc3a9bbe21 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/create.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFoldersRepository } from '@/models/index.js'; +import type { DriveFoldersRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -44,9 +49,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFoldersRepository) private driveFoldersRepository: DriveFoldersRepository, diff --git a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts index d921bc1b17..46a00ca3dc 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFoldersRepository, DriveFilesRepository } from '@/models/index.js'; +import type { DriveFoldersRepository, DriveFilesRepository } from '@/models/_.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -35,9 +40,8 @@ export const paramDef = { required: ['folderId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, diff --git a/packages/backend/src/server/api/endpoints/drive/folders/find.ts b/packages/backend/src/server/api/endpoints/drive/folders/find.ts index ee24db11f2..2f5cdcc648 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/find.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/find.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFoldersRepository } from '@/models/index.js'; +import type { DriveFoldersRepository } from '@/models/_.js'; import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -32,9 +37,8 @@ export const paramDef = { required: ['name'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFoldersRepository) private driveFoldersRepository: DriveFoldersRepository, diff --git a/packages/backend/src/server/api/endpoints/drive/folders/show.ts b/packages/backend/src/server/api/endpoints/drive/folders/show.ts index c06263b902..dd44fc46c9 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/show.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFoldersRepository } from '@/models/index.js'; +import type { DriveFoldersRepository } from '@/models/_.js'; import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -35,9 +40,8 @@ export const paramDef = { required: ['folderId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFoldersRepository) private driveFoldersRepository: DriveFoldersRepository, diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts index ff0a78b929..f8683132b2 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFoldersRepository } from '@/models/index.js'; +import type { DriveFoldersRepository } from '@/models/_.js'; import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; @@ -50,9 +55,8 @@ export const paramDef = { required: ['folderId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFoldersRepository) private driveFoldersRepository: DriveFoldersRepository, diff --git a/packages/backend/src/server/api/endpoints/drive/stream.ts b/packages/backend/src/server/api/endpoints/drive/stream.ts index 61bcfea0c3..27e1656f82 100644 --- a/packages/backend/src/server/api/endpoints/drive/stream.ts +++ b/packages/backend/src/server/api/endpoints/drive/stream.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -34,9 +39,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -56,7 +60,7 @@ export default class extends Endpoint { } } - const files = await query.take(ps.limit).getMany(); + const files = await query.limit(ps.limit).getMany(); return await this.driveFileEntityService.packMany(files, { detail: false, self: true }); }); 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 0f13b14d01..787009f13c 100644 --- a/packages/backend/src/server/api/endpoints/email-address/available.ts +++ b/packages/backend/src/server/api/endpoints/email-address/available.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { EmailService } from '@/core/EmailService.js'; @@ -31,9 +36,8 @@ export const paramDef = { required: ['emailAddress'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private emailService: EmailService, ) { diff --git a/packages/backend/src/server/api/endpoints/emoji.ts b/packages/backend/src/server/api/endpoints/emoji.ts index 681d3e649e..ead8c9979e 100644 --- a/packages/backend/src/server/api/endpoints/emoji.ts +++ b/packages/backend/src/server/api/endpoints/emoji.ts @@ -1,9 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; -import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -30,13 +34,9 @@ export const paramDef = { required: ['name'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts index 13cc709d31..2adf0a21b3 100644 --- a/packages/backend/src/server/api/endpoints/emojis.ts +++ b/packages/backend/src/server/api/endpoints/emojis.ts @@ -1,9 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; -import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -37,13 +41,9 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts index b38c97f60a..cecaded20a 100644 --- a/packages/backend/src/server/api/endpoints/endpoint.ts +++ b/packages/backend/src/server/api/endpoints/endpoint.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import endpoints from '../endpoints.js'; @@ -16,9 +21,8 @@ export const paramDef = { required: ['endpoint'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( ) { super(meta, paramDef, async (ps) => { diff --git a/packages/backend/src/server/api/endpoints/endpoints.ts b/packages/backend/src/server/api/endpoints/endpoints.ts index 9e706db747..86def04aca 100644 --- a/packages/backend/src/server/api/endpoints/endpoints.ts +++ b/packages/backend/src/server/api/endpoints/endpoints.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import endpoints from '../endpoints.js'; @@ -29,9 +34,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( ) { super(meta, paramDef, async () => { 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 6b6079ad51..7380c593e3 100644 --- a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts +++ b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -18,9 +23,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private queueService: QueueService, ) { diff --git a/packages/backend/src/server/api/endpoints/federation/followers.ts b/packages/backend/src/server/api/endpoints/federation/followers.ts index be1d6c8e58..a92cf6a9d8 100644 --- a/packages/backend/src/server/api/endpoints/federation/followers.ts +++ b/packages/backend/src/server/api/endpoints/federation/followers.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FollowingsRepository } from '@/models/index.js'; +import type { FollowingsRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -32,9 +37,8 @@ export const paramDef = { required: ['host'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @@ -47,7 +51,7 @@ export default class extends Endpoint { .andWhere('following.followeeHost = :host', { host: ps.host }); const followings = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await this.followingEntityService.packMany(followings, me, { populateFollowee: true }); diff --git a/packages/backend/src/server/api/endpoints/federation/following.ts b/packages/backend/src/server/api/endpoints/federation/following.ts index 74656ce863..d72ceeeea2 100644 --- a/packages/backend/src/server/api/endpoints/federation/following.ts +++ b/packages/backend/src/server/api/endpoints/federation/following.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FollowingsRepository } from '@/models/index.js'; +import type { FollowingsRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -32,9 +37,8 @@ export const paramDef = { required: ['host'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @@ -47,7 +51,7 @@ export default class extends Endpoint { .andWhere('following.followerHost = :host', { host: ps.host }); const followings = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await this.followingEntityService.packMany(followings, me, { populateFollowee: true }); diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 061c6eb5be..be73e5dbb8 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { InstancesRepository } from '@/models/index.js'; +import type { InstancesRepository } from '@/models/_.js'; import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; @@ -41,9 +46,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, @@ -126,7 +130,7 @@ export default class extends Endpoint { query.andWhere('instance.host like :host', { host: '%' + sqlLikeEscape(ps.host.toLowerCase()) + '%' }); } - const instances = await query.take(ps.limit).skip(ps.offset).getMany(); + const instances = await query.limit(ps.limit).offset(ps.offset).getMany(); return await this.instanceEntityService.packMany(instances); }); diff --git a/packages/backend/src/server/api/endpoints/federation/show-instance.ts b/packages/backend/src/server/api/endpoints/federation/show-instance.ts index 66502748b3..71eec11235 100644 --- a/packages/backend/src/server/api/endpoints/federation/show-instance.ts +++ b/packages/backend/src/server/api/endpoints/federation/show-instance.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { InstancesRepository } from '@/models/index.js'; +import type { InstancesRepository } from '@/models/_.js'; import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; @@ -28,9 +33,8 @@ export const paramDef = { required: ['host'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts index 19418e698c..e3ffea7b7e 100644 --- a/packages/backend/src/server/api/endpoints/federation/stats.ts +++ b/packages/backend/src/server/api/endpoints/federation/stats.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { IsNull, MoreThan, Not } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { FollowingsRepository, InstancesRepository } from '@/models/index.js'; +import type { FollowingsRepository, InstancesRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js'; @@ -23,9 +28,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, 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 4596e0c0b5..c0aa882088 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,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; @@ -17,9 +22,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private getterService: GetterService, private apPersonService: ApPersonService, diff --git a/packages/backend/src/server/api/endpoints/federation/users.ts b/packages/backend/src/server/api/endpoints/federation/users.ts index a028930f21..d97171865a 100644 --- a/packages/backend/src/server/api/endpoints/federation/users.ts +++ b/packages/backend/src/server/api/endpoints/federation/users.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -32,9 +37,8 @@ export const paramDef = { required: ['host'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -47,7 +51,7 @@ export default class extends Endpoint { .andWhere('user.host = :host', { host: ps.host }); const users = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await this.userEntityService.packMany(users, me, { detail: true }); diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts index 5849d3111f..37859d8330 100644 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -1,8 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Parser from 'rss-parser'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { Config } from '@/config.js'; -import { DI } from '@/di-symbols.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; const rssParser = new Parser(); @@ -23,13 +26,9 @@ export const paramDef = { required: ['url'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.config) - private config: Config, - private httpRequestService: HttpRequestService, ) { super(meta, paramDef, async (ps, me) => { diff --git a/packages/backend/src/server/api/endpoints/flash/create.ts b/packages/backend/src/server/api/endpoints/flash/create.ts index 3172bdbfda..b46660d218 100644 --- a/packages/backend/src/server/api/endpoints/flash/create.ts +++ b/packages/backend/src/server/api/endpoints/flash/create.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import type { FlashsRepository } from '@/models/index.js'; +import type { FlashsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -37,9 +42,8 @@ export const paramDef = { required: ['title', 'summary', 'script', 'permissions'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, diff --git a/packages/backend/src/server/api/endpoints/flash/delete.ts b/packages/backend/src/server/api/endpoints/flash/delete.ts index e94ede9f68..e5448c816a 100644 --- a/packages/backend/src/server/api/endpoints/flash/delete.ts +++ b/packages/backend/src/server/api/endpoints/flash/delete.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { FlashsRepository } from '@/models/index.js'; +import type { FlashsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -34,9 +39,8 @@ export const paramDef = { required: ['flashId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, diff --git a/packages/backend/src/server/api/endpoints/flash/featured.ts b/packages/backend/src/server/api/endpoints/flash/featured.ts index 570aef96d2..1fa5612ac4 100644 --- a/packages/backend/src/server/api/endpoints/flash/featured.ts +++ b/packages/backend/src/server/api/endpoints/flash/featured.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { FlashsRepository } from '@/models/index.js'; +import type { FlashsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -26,9 +31,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, @@ -40,7 +44,7 @@ export default class extends Endpoint { .andWhere('flash.likedCount > 0') .orderBy('flash.likedCount', 'DESC'); - const flashs = await query.take(10).getMany(); + const flashs = await query.limit(10).getMany(); return await this.flashEntityService.packMany(flashs, me); }); diff --git a/packages/backend/src/server/api/endpoints/flash/like.ts b/packages/backend/src/server/api/endpoints/flash/like.ts index 23de2f3970..a90e5f653a 100644 --- a/packages/backend/src/server/api/endpoints/flash/like.ts +++ b/packages/backend/src/server/api/endpoints/flash/like.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { FlashsRepository, FlashLikesRepository } from '@/models/index.js'; +import type { FlashsRepository, FlashLikesRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -43,9 +48,8 @@ export const paramDef = { required: ['flashId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, @@ -66,12 +70,14 @@ export default class extends Endpoint { } // if already liked - const exist = await this.flashLikesRepository.findOneBy({ - flashId: flash.id, - userId: me.id, + const exist = await this.flashLikesRepository.exist({ + where: { + flashId: flash.id, + userId: me.id, + }, }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyLiked); } diff --git a/packages/backend/src/server/api/endpoints/flash/my-likes.ts b/packages/backend/src/server/api/endpoints/flash/my-likes.ts index f7716ea74a..e328bdbee5 100644 --- a/packages/backend/src/server/api/endpoints/flash/my-likes.ts +++ b/packages/backend/src/server/api/endpoints/flash/my-likes.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FlashLikesRepository } from '@/models/index.js'; +import type { FlashLikesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { FlashLikeEntityService } from '@/core/entities/FlashLikeEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -43,9 +48,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.flashLikesRepository) private flashLikesRepository: FlashLikesRepository, @@ -59,7 +63,7 @@ export default class extends Endpoint { .leftJoinAndSelect('like.flash', 'flash'); const likes = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return this.flashLikeEntityService.packMany(likes, me); diff --git a/packages/backend/src/server/api/endpoints/flash/my.ts b/packages/backend/src/server/api/endpoints/flash/my.ts index baed7f000f..442d8dcd75 100644 --- a/packages/backend/src/server/api/endpoints/flash/my.ts +++ b/packages/backend/src/server/api/endpoints/flash/my.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FlashsRepository } from '@/models/index.js'; +import type { FlashsRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -33,9 +38,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, @@ -48,7 +52,7 @@ export default class extends Endpoint { .andWhere('flash.userId = :meId', { meId: me.id }); const flashs = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await this.flashEntityService.packMany(flashs); diff --git a/packages/backend/src/server/api/endpoints/flash/show.ts b/packages/backend/src/server/api/endpoints/flash/show.ts index 14720a8c8d..c41a27c925 100644 --- a/packages/backend/src/server/api/endpoints/flash/show.ts +++ b/packages/backend/src/server/api/endpoints/flash/show.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, FlashsRepository } from '@/models/index.js'; +import type { FlashsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -33,13 +38,9 @@ export const paramDef = { required: ['flashId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, diff --git a/packages/backend/src/server/api/endpoints/flash/unlike.ts b/packages/backend/src/server/api/endpoints/flash/unlike.ts index 696512b06c..d5c20a1167 100644 --- a/packages/backend/src/server/api/endpoints/flash/unlike.ts +++ b/packages/backend/src/server/api/endpoints/flash/unlike.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { FlashsRepository, FlashLikesRepository } from '@/models/index.js'; +import type { FlashsRepository, FlashLikesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -36,9 +41,8 @@ export const paramDef = { required: ['flashId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, diff --git a/packages/backend/src/server/api/endpoints/flash/update.ts b/packages/backend/src/server/api/endpoints/flash/update.ts index 78dfd4a06a..cc2c926749 100644 --- a/packages/backend/src/server/api/endpoints/flash/update.ts +++ b/packages/backend/src/server/api/endpoints/flash/update.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import type { FlashsRepository, DriveFilesRepository } from '@/models/index.js'; +import type { FlashsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -44,19 +49,16 @@ export const paramDef = { permissions: { type: 'array', items: { type: 'string', } }, + visibility: { type: 'string', enum: ['public', 'private'] }, }, required: ['flashId', 'title', 'summary', 'script', 'permissions'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, - - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, ) { super(meta, paramDef, async (ps, me) => { const flash = await this.flashsRepository.findOneBy({ id: ps.flashId }); diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts index 4ad16de911..e0e7fed87a 100644 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ b/packages/backend/src/server/api/endpoints/following/create.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; +import type { FollowingsRepository } from '@/models/_.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; @@ -14,7 +19,7 @@ export const meta = { limit: { duration: ms('1hour'), - max: 50, + max: 100, }, requireCredential: true, @@ -70,13 +75,9 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @@ -99,12 +100,14 @@ export default class extends Endpoint { }); // Check if already following - const exist = await this.followingsRepository.findOneBy({ - followerId: follower.id, - followeeId: followee.id, + const exist = await this.followingsRepository.exist({ + where: { + followerId: follower.id, + followeeId: followee.id, + }, }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyFollowing); } diff --git a/packages/backend/src/server/api/endpoints/following/delete.ts b/packages/backend/src/server/api/endpoints/following/delete.ts index 4f12db1273..f44692ba6d 100644 --- a/packages/backend/src/server/api/endpoints/following/delete.ts +++ b/packages/backend/src/server/api/endpoints/following/delete.ts @@ -1,12 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; +import type { FollowingsRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['following', 'users'], @@ -55,13 +60,9 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @@ -84,12 +85,14 @@ export default class extends Endpoint { }); // Check not following - const exist = await this.followingsRepository.findOneBy({ - followerId: follower.id, - followeeId: followee.id, + const exist = await this.followingsRepository.exist({ + where: { + followerId: follower.id, + followeeId: followee.id, + }, }); - if (exist == null) { + if (!exist) { throw new ApiError(meta.errors.notFollowing); } diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts index 22304cacda..53ef925b2f 100644 --- a/packages/backend/src/server/api/endpoints/following/invalidate.ts +++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts @@ -1,12 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; +import type { FollowingsRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['following', 'users'], @@ -24,7 +29,7 @@ export const meta = { noSuchUser: { message: 'No such user.', code: 'NO_SUCH_USER', - id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8', + id: 'b77e6ae6-a3e5-40da-9cc8-c240115479cc', }, followerIsYourself: { @@ -36,7 +41,7 @@ export const meta = { notFollowing: { message: 'The other use is not following you.', code: 'NOT_FOLLOWING', - id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09', + id: '918faac3-074f-41ae-9c43-ed5d2946770d', }, }, @@ -55,13 +60,9 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, 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 cca3e60614..91fe922200 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/accept.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/accept.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; @@ -33,9 +38,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private getterService: GetterService, private userFollowingService: UserFollowingService, 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 7325e73cac..d9d5c7041b 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts @@ -1,11 +1,14 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.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'; import { UserFollowingService } from '@/core/UserFollowingService.js'; -import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -44,13 +47,9 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - private userEntityService: UserEntityService, private getterService: GetterService, private userFollowingService: UserFollowingService, diff --git a/packages/backend/src/server/api/endpoints/following/requests/list.ts b/packages/backend/src/server/api/endpoints/following/requests/list.ts index d68248fab9..c4faa88f65 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/list.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/list.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; -import type { FollowRequestsRepository } from '@/models/index.js'; +import type { FollowRequestsRepository } from '@/models/_.js'; import { FollowRequestEntityService } from '@/core/entities/FollowRequestEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -49,9 +54,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, @@ -64,7 +68,7 @@ export default class extends Endpoint { .andWhere('request.followeeId = :meId', { meId: me.id }); const requests = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await Promise.all(requests.map(req => this.followRequestEntityService.pack(req))); 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 a8fdc44876..35f047bcef 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/reject.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/reject.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; @@ -28,9 +33,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private getterService: GetterService, private userFollowingService: UserFollowingService, diff --git a/packages/backend/src/server/api/endpoints/following/update.ts b/packages/backend/src/server/api/endpoints/following/update.ts new file mode 100644 index 0000000000..25f393e517 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/following/update.ts @@ -0,0 +1,107 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ms from 'ms'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { FollowingsRepository } from '@/models/_.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { DI } from '@/di-symbols.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['following', 'users'], + + limit: { + duration: ms('1hour'), + max: 100, + }, + + requireCredential: true, + + kind: 'write:following', + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '14318698-f67e-492a-99da-5353a5ac52be', + }, + + followeeIsYourself: { + message: 'Followee is yourself.', + code: 'FOLLOWEE_IS_YOURSELF', + id: '4c4cbaf9-962a-463b-8418-a5e365dbf2eb', + }, + + notFollowing: { + message: 'You are not following that user.', + code: 'NOT_FOLLOWING', + id: 'b8dc75cf-1cb5-46c9-b14b-5f1ffbd782c9', + }, + }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'UserLite', + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + notify: { type: 'string', enum: ['normal', 'none'] }, + }, + required: ['userId', 'notify'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, + private getterService: GetterService, + private userFollowingService: UserFollowingService, + ) { + super(meta, paramDef, async (ps, me) => { + const follower = me; + + // Check if the follower is yourself + if (me.id === ps.userId) { + throw new ApiError(meta.errors.followeeIsYourself); + } + + // Get followee + const followee = 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 not following + const exist = await this.followingsRepository.findOneBy({ + followerId: follower.id, + followeeId: followee.id, + }); + + if (exist == null) { + throw new ApiError(meta.errors.notFollowing); + } + + await this.followingsRepository.update({ + id: exist.id, + }, { + notify: ps.notify === 'none' ? null : ps.notify, + }); + + return await this.userEntityService.pack(follower.id, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/gallery/featured.ts b/packages/backend/src/server/api/endpoints/gallery/featured.ts index 9994ce90d7..cbab3a83a4 100644 --- a/packages/backend/src/server/api/endpoints/gallery/featured.ts +++ b/packages/backend/src/server/api/endpoints/gallery/featured.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryPostsRepository } from '@/models/_.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -26,9 +31,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, @@ -41,7 +45,7 @@ export default class extends Endpoint { .andWhere('post.likedCount > 0') .orderBy('post.likedCount', 'DESC'); - const posts = await query.take(10).getMany(); + const posts = await query.limit(10).getMany(); return await this.galleryPostEntityService.packMany(posts, me); }); diff --git a/packages/backend/src/server/api/endpoints/gallery/popular.ts b/packages/backend/src/server/api/endpoints/gallery/popular.ts index 55d3dabfb0..c5d06f67dd 100644 --- a/packages/backend/src/server/api/endpoints/gallery/popular.ts +++ b/packages/backend/src/server/api/endpoints/gallery/popular.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryPostsRepository } from '@/models/_.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -26,9 +31,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, @@ -40,7 +44,7 @@ export default class extends Endpoint { .andWhere('post.likedCount > 0') .orderBy('post.likedCount', 'DESC'); - const posts = await query.take(10).getMany(); + const posts = await query.limit(10).getMany(); return await this.galleryPostEntityService.packMany(posts, me); }); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts.ts b/packages/backend/src/server/api/endpoints/gallery/posts.ts index e94003eb79..3ca5f4989a 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryPostsRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -29,9 +34,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, @@ -43,7 +47,7 @@ export default class extends Endpoint { const query = this.queryService.makePaginationQuery(this.galleryPostsRepository.createQueryBuilder('post'), ps.sinceId, ps.untilId) .innerJoinAndSelect('post.user', 'user'); - const posts = await query.take(ps.limit).getMany(); + const posts = await query.limit(ps.limit).getMany(); return await this.galleryPostEntityService.packMany(posts, me); }); 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 ca6bfa7e0f..94701712de 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts @@ -1,9 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + 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 type { DriveFilesRepository, GalleryPostsRepository } from '@/models/_.js'; +import { MiGalleryPost } from '@/models/GalleryPost.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; import { IdService } from '@/core/IdService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -46,9 +51,8 @@ export const paramDef = { required: ['title', 'fileIds'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, @@ -65,13 +69,13 @@ export default class extends Endpoint { id: fileId, userId: me.id, }), - ))).filter((file): file is DriveFile => file != null); + ))).filter((file): file is MiDriveFile => file != null); if (files.length === 0) { throw new Error(); } - const post = await this.galleryPostsRepository.insert(new GalleryPost({ + const post = await this.galleryPostsRepository.insert(new MiGalleryPost({ id: this.idService.genId(), createdAt: new Date(), updatedAt: new Date(), diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts index 6cdcc17b39..deef2912bb 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryPostsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -28,9 +33,8 @@ export const paramDef = { required: ['postId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts index 6ac5fa8606..c557054066 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -43,9 +48,8 @@ export const paramDef = { required: ['postId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, @@ -66,12 +70,14 @@ export default class extends Endpoint { } // if already liked - const exist = await this.galleryLikesRepository.findOneBy({ - postId: post.id, - userId: me.id, + const exist = await this.galleryLikesRepository.exist({ + where: { + postId: post.id, + userId: me.id, + }, }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyLiked); } diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts index f7e828142b..b3eda1be52 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryPostsRepository } from '@/models/_.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -33,9 +38,8 @@ export const paramDef = { required: ['postId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts index 513089217d..832b62282f 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryPostsRepository, GalleryLikesRepository } from '@/models/index.js'; +import type { GalleryPostsRepository, GalleryLikesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -36,9 +41,8 @@ export const paramDef = { required: ['postId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, 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 a2a10d8400..632214a0c2 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + 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 type { DriveFile } from '@/models/entities/DriveFile.js'; +import type { DriveFilesRepository, GalleryPostsRepository } from '@/models/_.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -45,9 +50,8 @@ export const paramDef = { required: ['postId', 'title', 'fileIds'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, @@ -63,7 +67,7 @@ export default class extends Endpoint { id: fileId, userId: me.id, }), - ))).filter((file): file is DriveFile => file != null); + ))).filter((file): file is MiDriveFile => file != null); if (files.length === 0) { throw new Error(); diff --git a/packages/backend/src/server/api/endpoints/get-online-users-count.ts b/packages/backend/src/server/api/endpoints/get-online-users-count.ts index dea0f4799c..8a61168f25 100644 --- a/packages/backend/src/server/api/endpoints/get-online-users-count.ts +++ b/packages/backend/src/server/api/endpoints/get-online-users-count.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { MoreThan } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { USER_ONLINE_THRESHOLD } from '@/const.js'; -import type { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -9,6 +14,8 @@ export const meta = { tags: ['meta'], requireCredential: false, + allowGet: true, + cacheSec: 60 * 1, } as const; export const paramDef = { @@ -17,9 +24,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/server/api/endpoints/hashtags/list.ts b/packages/backend/src/server/api/endpoints/hashtags/list.ts index 226a11de0b..21d863107d 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/list.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/list.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { HashtagsRepository } from '@/models/index.js'; +import type { HashtagsRepository } from '@/models/_.js'; import { HashtagEntityService } from '@/core/entities/HashtagEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -32,9 +37,8 @@ export const paramDef = { required: ['sort'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.hashtagsRepository) private hashtagsRepository: HashtagsRepository, @@ -73,7 +77,7 @@ export default class extends Endpoint { 'tag.attachedRemoteUsersCount', ]); - const tags = await query.take(ps.limit).getMany(); + const tags = await query.limit(ps.limit).getMany(); return this.hashtagEntityService.packMany(tags); }); diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts index 4f5f979767..acfef16b11 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/search.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { HashtagsRepository } from '@/models/index.js'; +import type { HashtagsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; @@ -29,9 +34,8 @@ export const paramDef = { required: ['query'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.hashtagsRepository) private hashtagsRepository: HashtagsRepository, @@ -41,8 +45,8 @@ export default class extends Endpoint { .where('tag.name like :q', { q: sqlLikeEscape(ps.query.toLowerCase()) + '%' }) .orderBy('tag.count', 'DESC') .groupBy('tag.id') - .take(ps.limit) - .skip(ps.offset) + .limit(ps.limit) + .offset(ps.offset) .getMany(); return hashtags.map(tag => tag.name); diff --git a/packages/backend/src/server/api/endpoints/hashtags/show.ts b/packages/backend/src/server/api/endpoints/hashtags/show.ts index 06b0d6e9b2..3ba16fdc85 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/show.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/show.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { HashtagsRepository } from '@/models/index.js'; +import type { HashtagsRepository } from '@/models/_.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { HashtagEntityService } from '@/core/entities/HashtagEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -34,9 +39,8 @@ export const paramDef = { required: ['tag'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.hashtagsRepository) private hashtagsRepository: HashtagsRepository, diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts index cf45cc6c24..75d4fe3819 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { NotesRepository } from '@/models/index.js'; -import type { Note } from '@/models/entities/Note.js'; +import type { NotesRepository } from '@/models/_.js'; +import type { MiNote } from '@/models/Note.js'; import { safeForSql } from '@/misc/safe-for-sql.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { MetaService } from '@/core/MetaService.js'; @@ -26,6 +31,8 @@ export const meta = { tags: ['hashtags'], requireCredential: false, + allowGet: true, + cacheSec: 60 * 1, res: { type: 'array', @@ -61,9 +68,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -94,7 +100,7 @@ export default class extends Endpoint { const tags: { name: string; - users: Note['userId'][]; + users: MiNote['userId'][]; }[] = []; for (const note of tagNotes) { diff --git a/packages/backend/src/server/api/endpoints/hashtags/users.ts b/packages/backend/src/server/api/endpoints/hashtags/users.ts index dd3549020e..1cef76d3d2 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/users.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/users.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/_.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -33,13 +38,12 @@ export const paramDef = { required: ['tag', 'sort'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, - + private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, me) => { @@ -68,7 +72,7 @@ export default class extends Endpoint { case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break; } - const users = await query.take(ps.limit).getMany(); + const users = await query.limit(ps.limit).getMany(); return await this.userEntityService.packMany(users, me, { detail: true }); }); diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts index a3e3e02a12..c0530bf392 100644 --- a/packages/backend/src/server/api/endpoints/i.ts +++ b/packages/backend/src/server/api/endpoints/i.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { UserProfilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -23,7 +28,7 @@ export const meta = { id: 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a', kind: 'permission', }, - } + }, } as const; export const paramDef = { @@ -32,13 +37,9 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, @@ -68,7 +69,7 @@ export default class extends Endpoint { }); userProfile.loggedInDates = [...userProfile.loggedInDates, today]; } - + return await this.userEntityService.pack(userProfile.user!, userProfile.user!, { detail: true, includeSecrets: isSecure, 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 6c31075e05..9f8e2894b8 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts @@ -1,10 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + 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 type { UserProfilesRepository } from '@/models/_.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -21,13 +25,9 @@ export const paramDef = { required: ['token'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, @@ -47,15 +47,18 @@ export default class extends Endpoint { secret: OTPAuth.Secret.fromBase32(profile.twoFactorTempSecret), digits: 6, token, - window: 1, + window: 5, }); if (delta === null) { throw new Error('not verified'); } + const backupCodes = Array.from({ length: 5 }, () => new OTPAuth.Secret().base32); + await this.userProfilesRepository.update(me.id, { twoFactorSecret: profile.twoFactorTempSecret, + twoFactorBackupSecret: backupCodes, twoFactorEnabled: true, }); @@ -64,6 +67,10 @@ export default class extends Endpoint { detail: true, includeSecrets: true, })); + + return { + backupCodes: backupCodes, + }; }); } } 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 e8985a9cd8..6d530aba3b 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 @@ -1,153 +1,102 @@ -import { promisify } from 'node:util'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import bcrypt from 'bcryptjs'; -import cbor from 'cbor'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; -import type { AttestationChallengesRepository, UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; - -const cborDecodeFirst = promisify(cbor.decodeFirst) as any; +import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js'; +import { WebAuthnService } from '@/core/WebAuthnService.js'; +import { ApiError } from '@/server/api/error.js'; +import { UserAuthService } from '@/core/UserAuthService.js'; export const meta = { requireCredential: true, secure: true, + + errors: { + incorrectPassword: { + message: 'Incorrect password.', + code: 'INCORRECT_PASSWORD', + id: '0d7ec6d2-e652-443e-a7bf-9ee9a0cd77b0', + }, + + twoFactorNotEnabled: { + message: '2fa not enabled.', + code: 'TWO_FACTOR_NOT_ENABLED', + id: '798d6847-b1ed-4f9c-b1f9-163c42655995', + }, + }, } as const; export const paramDef = { type: 'object', properties: { - clientDataJSON: { type: 'string' }, - attestationObject: { type: 'string' }, password: { type: 'string' }, - challengeId: { type: 'string' }, + token: { type: 'string', nullable: true }, name: { type: 'string', minLength: 1, maxLength: 30 }, + credential: { type: 'object' }, }, - required: ['clientDataJSON', 'attestationObject', 'password', 'challengeId', 'name'], + required: ['password', 'name', 'credential'], } as const; // eslint-disable-next-line import/no-default-export @Injectable() export default class extends Endpoint { constructor( - @Inject(DI.config) - private config: Config, - @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, @Inject(DI.userSecurityKeysRepository) private userSecurityKeysRepository: UserSecurityKeysRepository, - @Inject(DI.attestationChallengesRepository) - private attestationChallengesRepository: AttestationChallengesRepository, - + private webAuthnService: WebAuthnService, + private userAuthService: UserAuthService, private userEntityService: UserEntityService, private globalEventService: GlobalEventService, - private twoFactorAuthenticationService: TwoFactorAuthenticationService, ) { super(meta, paramDef, async (ps, me) => { - const rpIdHashReal = this.twoFactorAuthenticationService.hash(Buffer.from(this.config.hostname, 'utf-8')); - + const token = ps.token; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + if (profile.twoFactorEnabled) { + if (token == null) { + throw new Error('authentication failed'); + } - if (!same) { - throw new Error('incorrect password'); + try { + await this.userAuthService.twoFactorAuthenticate(profile, token); + } catch (e) { + throw new Error('authentication failed'); + } + } + + const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); + if (!passwordMatched) { + throw new ApiError(meta.errors.incorrectPassword); } if (!profile.twoFactorEnabled) { - throw new Error('2fa not enabled'); + throw new ApiError(meta.errors.twoFactorNotEnabled); } - const clientData = JSON.parse(ps.clientDataJSON); - - if (clientData.type !== 'webauthn.create') { - throw new Error('not a creation attestation'); - } - if (clientData.origin !== this.config.scheme + '://' + this.config.host) { - throw new Error('origin mismatch'); - } - - const clientDataJSONHash = this.twoFactorAuthenticationService.hash(Buffer.from(ps.clientDataJSON, 'utf-8')); - - const attestation = await cborDecodeFirst(ps.attestationObject); - - const rpIdHash = attestation.authData.slice(0, 32); - if (!rpIdHashReal.equals(rpIdHash)) { - throw new Error('rpIdHash mismatch'); - } - - const flags = attestation.authData[32]; - - // eslint:disable-next-line:no-bitwise - if (!(flags & 1)) { - throw new Error('user not present'); - } - - const authData = Buffer.from(attestation.authData); - const credentialIdLength = authData.readUInt16BE(53); - const credentialId = authData.slice(55, 55 + credentialIdLength); - const publicKeyData = authData.slice(55 + credentialIdLength); - const publicKey: Map = await cborDecodeFirst(publicKeyData); - if (publicKey.get(3) !== -7) { - throw new Error('alg mismatch'); - } - - const procedures = this.twoFactorAuthenticationService.getProcedures(); - - if (!(procedures as any)[attestation.fmt]) { - throw new Error('unsupported fmt'); - } - - const verificationData = (procedures as any)[attestation.fmt].verify({ - attStmt: attestation.attStmt, - authenticatorData: authData, - clientDataHash: clientDataJSONHash, - credentialId, - publicKey, - rpIdHash, - }); - if (!verificationData.valid) throw new Error('signature invalid'); - - const attestationChallenge = await this.attestationChallengesRepository.findOneBy({ - userId: me.id, - id: ps.challengeId, - registrationChallenge: true, - challenge: this.twoFactorAuthenticationService.hash(clientData.challenge).toString('hex'), - }); - - if (!attestationChallenge) { - throw new Error('non-existent challenge'); - } - - await this.attestationChallengesRepository.delete({ - userId: me.id, - id: ps.challengeId, - }); - - // Expired challenge (> 5min old) - if ( - new Date().getTime() - attestationChallenge.createdAt.getTime() >= - 5 * 60 * 1000 - ) { - throw new Error('expired challenge'); - } - - const credentialIdString = credentialId.toString('hex'); + const keyInfo = await this.webAuthnService.verifyRegistration(me.id, ps.credential); + const credentialId = Buffer.from(keyInfo.credentialID).toString('base64url'); await this.userSecurityKeysRepository.insert({ + id: credentialId, userId: me.id, - id: credentialIdString, - lastUsed: new Date(), name: ps.name, - publicKey: verificationData.publicKey.toString('hex'), + publicKey: Buffer.from(keyInfo.credentialPublicKey).toString('base64url'), + counter: keyInfo.counter, + credentialDeviceType: keyInfo.credentialDeviceType, + credentialBackedUp: keyInfo.credentialBackedUp, + transports: keyInfo.transports, }); // Publish meUpdated event @@ -157,7 +106,7 @@ export default class extends Endpoint { })); return { - id: credentialIdString, + id: credentialId, name: ps.name, }; }); 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 0ee9f556a8..2ed701014d 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,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; +import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -28,9 +33,8 @@ export const paramDef = { required: ['value'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts index 19c77365c6..c39005f2dd 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts @@ -1,25 +1,48 @@ -import { promisify } from 'node:util'; -import * as crypto from 'node:crypto'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserProfilesRepository, AttestationChallengesRepository } from '@/models/index.js'; -import { IdService } from '@/core/IdService.js'; -import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; +import type { UserProfilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; - -const randomBytes = promisify(crypto.randomBytes); +import { WebAuthnService } from '@/core/WebAuthnService.js'; +import { ApiError } from '@/server/api/error.js'; +import { UserAuthService } from '@/core/UserAuthService.js'; export const meta = { requireCredential: true, secure: true, + + errors: { + userNotFound: { + message: 'User not found.', + code: 'USER_NOT_FOUND', + id: '652f899f-66d4-490e-993e-6606c8ec04c3', + }, + + incorrectPassword: { + message: 'Incorrect password.', + code: 'INCORRECT_PASSWORD', + id: '38769596-efe2-4faf-9bec-abbb3f2cd9ba', + }, + + twoFactorNotEnabled: { + message: '2fa not enabled.', + code: 'TWO_FACTOR_NOT_ENABLED', + id: 'bf32b864-449b-47b8-974e-f9a5468546f1', + }, + }, } as const; export const paramDef = { type: 'object', properties: { password: { type: 'string' }, + token: { type: 'string', nullable: true }, }, required: ['password'], } as const; @@ -31,47 +54,48 @@ export default class extends Endpoint { @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - @Inject(DI.attestationChallengesRepository) - private attestationChallengesRepository: AttestationChallengesRepository, - - private idService: IdService, - private twoFactorAuthenticationService: TwoFactorAuthenticationService, + private webAuthnService: WebAuthnService, + private userAuthService: UserAuthService, ) { super(meta, paramDef, async (ps, me) => { - const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); + const token = ps.token; + const profile = await this.userProfilesRepository.findOne({ + where: { + userId: me.id, + }, + relations: ['user'], + }); - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + if (profile == null) { + throw new ApiError(meta.errors.userNotFound); + } - if (!same) { - throw new Error('incorrect password'); + if (profile.twoFactorEnabled) { + if (token == null) { + throw new Error('authentication failed'); + } + + try { + await this.userAuthService.twoFactorAuthenticate(profile, token); + } catch (e) { + throw new Error('authentication failed'); + } + } + + const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); + if (!passwordMatched) { + throw new ApiError(meta.errors.incorrectPassword); } if (!profile.twoFactorEnabled) { - throw new Error('2fa not enabled'); + throw new ApiError(meta.errors.twoFactorNotEnabled); } - // 32 byte challenge - const entropy = await randomBytes(32); - const challenge = entropy.toString('base64') - .replace(/=/g, '') - .replace(/\+/g, '-') - .replace(/\//g, '_'); - - const challengeId = this.idService.genId(); - - await this.attestationChallengesRepository.insert({ - userId: me.id, - id: challengeId, - challenge: this.twoFactorAuthenticationService.hash(Buffer.from(challenge, 'utf-8')).toString('hex'), - createdAt: new Date(), - registrationChallenge: true, - }); - - return { - challengeId, - challenge, - }; + return await this.webAuthnService.initiateRegistration( + me.id, + profile.user?.username ?? me.id, + profile.user?.name ?? undefined, + ); }); } } 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 eb4d7f9c14..b358c812ee 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -1,44 +1,72 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import bcrypt from 'bcryptjs'; import * as OTPAuth from 'otpauth'; import * as QRCode from 'qrcode'; import { Inject, Injectable } from '@nestjs/common'; -import type { UserProfilesRepository } from '@/models/index.js'; +import type { UserProfilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; +import { ApiError } from '@/server/api/error.js'; +import { UserAuthService } from '@/core/UserAuthService.js'; export const meta = { requireCredential: true, secure: true, + + errors: { + incorrectPassword: { + message: 'Incorrect password.', + code: 'INCORRECT_PASSWORD', + id: '78d6c839-20c9-4c66-b90a-fc0542168b48', + }, + }, } as const; export const paramDef = { type: 'object', properties: { password: { type: 'string' }, + token: { type: 'string', nullable: true }, }, required: ['password'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.config) private config: Config, @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + + private userAuthService: UserAuthService, ) { super(meta, paramDef, async (ps, me) => { + const token = ps.token; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + if (profile.twoFactorEnabled) { + if (token == null) { + throw new Error('authentication failed'); + } - if (!same) { - throw new Error('incorrect password'); + try { + await this.userAuthService.twoFactorAuthenticate(profile, token); + } catch (e) { + throw new Error('authentication failed'); + } + } + + const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); + if (!passwordMatched) { + throw new ApiError(meta.errors.incorrectPassword); } // Generate user's secret key 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 4b726aed80..da8ac98556 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 @@ -1,29 +1,44 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + 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 { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { UserAuthService } from '@/core/UserAuthService.js'; export const meta = { requireCredential: true, secure: true, + + errors: { + incorrectPassword: { + message: 'Incorrect password.', + code: 'INCORRECT_PASSWORD', + id: '141c598d-a825-44c8-9173-cfb9d92be493', + }, + }, } as const; export const paramDef = { type: 'object', properties: { password: { type: 'string' }, + token: { type: 'string', nullable: true }, credentialId: { type: 'string' }, }, required: ['password', 'credentialId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.userSecurityKeysRepository) private userSecurityKeysRepository: UserSecurityKeysRepository, @@ -32,16 +47,28 @@ export default class extends Endpoint { private userProfilesRepository: UserProfilesRepository, private userEntityService: UserEntityService, + private userAuthService: UserAuthService, private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { + const token = ps.token; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + if (profile.twoFactorEnabled) { + if (token == null) { + throw new Error('authentication failed'); + } - if (!same) { - throw new Error('incorrect password'); + try { + await this.userAuthService.twoFactorAuthenticate(profile, token); + } catch (e) { + throw new Error('authentication failed'); + } + } + + const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); + if (!passwordMatched) { + throw new ApiError(meta.errors.incorrectPassword); } // Make sure we only delete the user's own creds 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 e0e7ba6658..338f12c5cd 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts @@ -1,47 +1,75 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + 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 type { UserProfilesRepository } from '@/models/_.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { UserAuthService } from '@/core/UserAuthService.js'; export const meta = { requireCredential: true, secure: true, + + errors: { + incorrectPassword: { + message: 'Incorrect password.', + code: 'INCORRECT_PASSWORD', + id: '7add0395-9901-4098-82f9-4f67af65f775', + }, + }, } as const; export const paramDef = { type: 'object', properties: { password: { type: 'string' }, + token: { type: 'string', nullable: true }, }, required: ['password'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, private userEntityService: UserEntityService, + private userAuthService: UserAuthService, private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { + const token = ps.token; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + if (profile.twoFactorEnabled) { + if (token == null) { + throw new Error('authentication failed'); + } - if (!same) { - throw new Error('incorrect password'); + try { + await this.userAuthService.twoFactorAuthenticate(profile, token); + } catch (e) { + throw new Error('authentication failed'); + } + } + + const passwordMatched = await bcrypt.compare(ps.password, profile.password ?? ''); + if (!passwordMatched) { + throw new ApiError(meta.errors.incorrectPassword); } await this.userProfilesRepository.update(me.id, { twoFactorSecret: null, + twoFactorBackupSecret: null, twoFactorEnabled: false, usePasswordLessLogin: false, }); 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 index d98f60fa5f..1a140c1d05 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + 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 { UserSecurityKeysRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; @@ -20,7 +25,7 @@ export const meta = { }, accessDenied: { - message: 'You do not have edit privilege of the channel.', + message: 'You do not have edit privilege of this key.', code: 'ACCESS_DENIED', id: '1fb7cb09-d46a-4fff-b8df-057708cce513', }, @@ -36,16 +41,12 @@ export const paramDef = { required: ['name', 'credentialId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.userSecurityKeysRepository) private userSecurityKeysRepository: UserSecurityKeysRepository, - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - private userEntityService: UserEntityService, private globalEventService: GlobalEventService, ) { @@ -61,7 +62,7 @@ export default class extends Endpoint { if (key.userId !== me.id) { throw new ApiError(meta.errors.accessDenied); } - + await this.userSecurityKeysRepository.update(key.id, { name: ps.name, }); diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts index 48fb03a8af..daa3e536a4 100644 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ b/packages/backend/src/server/api/endpoints/i/apps.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AccessTokensRepository } from '@/models/index.js'; +import type { AccessTokensRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -17,9 +22,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.accessTokensRepository) private accessTokensRepository: AccessTokensRepository, diff --git a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts index f5a946eb91..32061c2aa4 100644 --- a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts +++ b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { IsNull, Not } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AccessTokensRepository } from '@/models/index.js'; +import type { AccessTokensRepository } from '@/models/_.js'; import { AppEntityService } from '@/core/entities/AppEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -21,9 +26,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.accessTokensRepository) private accessTokensRepository: AccessTokensRepository, diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts index 873835a36c..a3c37ffdb7 100644 --- a/packages/backend/src/server/api/endpoints/i/change-password.ts +++ b/packages/backend/src/server/api/endpoints/i/change-password.ts @@ -1,8 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserProfilesRepository } from '@/models/index.js'; +import type { UserProfilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; +import { UserAuthService } from '@/core/UserAuthService.js'; export const meta = { requireCredential: true, @@ -15,24 +21,38 @@ export const paramDef = { properties: { currentPassword: { type: 'string' }, newPassword: { type: 'string', minLength: 1 }, + token: { type: 'string', nullable: true }, }, required: ['currentPassword', 'newPassword'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + + private userAuthService: UserAuthService, ) { super(meta, paramDef, async (ps, me) => { + const token = ps.token; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - // Compare password - const same = await bcrypt.compare(ps.currentPassword, profile.password!); + if (profile.twoFactorEnabled) { + if (token == null) { + throw new Error('authentication failed'); + } - if (!same) { + try { + await this.userAuthService.twoFactorAuthenticate(profile, token); + } catch (e) { + throw new Error('authentication failed'); + } + } + + const passwordMatched = await bcrypt.compare(ps.currentPassword, profile.password!); + + if (!passwordMatched) { throw new Error('incorrect password'); } 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 4eef496385..b24b3438dc 100644 --- a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts +++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService.js'; @@ -15,9 +20,8 @@ export const paramDef = { required: ['name'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private achievementService: AchievementService, ) { diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts index 77a03d9811..fbac845fda 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -1,9 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DeleteAccountService } from '@/core/DeleteAccountService.js'; import { DI } from '@/di-symbols.js'; +import { UserAuthService } from '@/core/UserAuthService.js'; export const meta = { requireCredential: true, @@ -15,13 +21,13 @@ export const paramDef = { type: 'object', properties: { password: { type: 'string' }, + token: { type: 'string', nullable: true }, }, required: ['password'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -29,19 +35,32 @@ export default class extends Endpoint { @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + private userAuthService: UserAuthService, private deleteAccountService: DeleteAccountService, ) { super(meta, paramDef, async (ps, me) => { + const token = ps.token; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); + + if (profile.twoFactorEnabled) { + if (token == null) { + throw new Error('authentication failed'); + } + + try { + await this.userAuthService.twoFactorAuthenticate(profile, token); + } catch (e) { + throw new Error('authentication failed'); + } + } + const userDetailed = await this.usersRepository.findOneByOrFail({ id: me.id }); if (userDetailed.isDeleted) { return; } - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); - - if (!same) { + const passwordMatched = await bcrypt.compare(ps.password, profile.password!); + if (!passwordMatched) { throw new Error('incorrect password'); } diff --git a/packages/backend/src/server/api/endpoints/i/export-antennas.ts b/packages/backend/src/server/api/endpoints/i/export-antennas.ts index 4182c1b247..23b2f6b4ce 100644 --- a/packages/backend/src/server/api/endpoints/i/export-antennas.ts +++ b/packages/backend/src/server/api/endpoints/i/export-antennas.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; 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 4be88cbc2b..8068a3b305 100644 --- a/packages/backend/src/server/api/endpoints/i/export-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/export-blocking.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -18,9 +23,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private queueService: QueueService, ) { 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 f522d4c409..c22905bc67 100644 --- a/packages/backend/src/server/api/endpoints/i/export-favorites.ts +++ b/packages/backend/src/server/api/endpoints/i/export-favorites.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -18,9 +23,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private queueService: QueueService, ) { 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 1741781c0f..880833ab76 100644 --- a/packages/backend/src/server/api/endpoints/i/export-following.ts +++ b/packages/backend/src/server/api/endpoints/i/export-following.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -21,9 +26,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private queueService: QueueService, ) { 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 8e8042b1f9..8eb70a387a 100644 --- a/packages/backend/src/server/api/endpoints/i/export-mute.ts +++ b/packages/backend/src/server/api/endpoints/i/export-mute.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -18,9 +23,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private queueService: QueueService, ) { 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 ed54c9991c..791f637790 100644 --- a/packages/backend/src/server/api/endpoints/i/export-notes.ts +++ b/packages/backend/src/server/api/endpoints/i/export-notes.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -18,9 +23,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private queueService: QueueService, ) { 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 5c2be38b71..f387f6d016 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,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -18,9 +23,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private queueService: QueueService, ) { diff --git a/packages/backend/src/server/api/endpoints/i/favorites.ts b/packages/backend/src/server/api/endpoints/i/favorites.ts index ce8ab4962a..d6f13c535a 100644 --- a/packages/backend/src/server/api/endpoints/i/favorites.ts +++ b/packages/backend/src/server/api/endpoints/i/favorites.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { NoteFavoritesRepository } from '@/models/index.js'; +import type { NoteFavoritesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteFavoriteEntityService } from '@/core/entities/NoteFavoriteEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -33,9 +38,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.noteFavoritesRepository) private noteFavoritesRepository: NoteFavoritesRepository, @@ -49,7 +53,7 @@ export default class extends Endpoint { .leftJoinAndSelect('favorite.note', 'note'); const favorites = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await this.noteFavoriteEntityService.packMany(favorites, me); diff --git a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts index d1b04cb655..7e37adc4ac 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryLikesRepository } from '@/models/index.js'; +import type { GalleryLikesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { GalleryLikeEntityService } from '@/core/entities/GalleryLikeEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -44,9 +49,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.galleryLikesRepository) private galleryLikesRepository: GalleryLikesRepository, @@ -60,7 +64,7 @@ export default class extends Endpoint { .leftJoinAndSelect('like.post', 'post'); const likes = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await this.galleryLikeEntityService.packMany(likes, me); diff --git a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts index 32d14293f7..148d38aa54 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryPostsRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -33,9 +38,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, @@ -48,7 +52,7 @@ export default class extends Endpoint { .andWhere('post.userId = :meId', { meId: me.id }); const posts = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await this.galleryPostEntityService.packMany(posts, me); diff --git a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts index 3179457817..d62bfbb3ed 100644 --- a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts +++ b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { MutedNotesRepository } from '@/models/index.js'; +import type { MutedNotesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -28,9 +33,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.mutedNotesRepository) private mutedNotesRepository: MutedNotesRepository, diff --git a/packages/backend/src/server/api/endpoints/i/import-antennas.ts b/packages/backend/src/server/api/endpoints/i/import-antennas.ts index efb5ce4223..71db8710af 100644 --- a/packages/backend/src/server/api/endpoints/i/import-antennas.ts +++ b/packages/backend/src/server/api/endpoints/i/import-antennas.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; -import type { AntennasRepository, DriveFilesRepository, UsersRepository, Antenna as _Antenna } from '@/models/index.js'; +import type { AntennasRepository, DriveFilesRepository, UsersRepository, MiAntenna as _Antenna } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { DownloadService } from '@/core/DownloadService.js'; @@ -54,7 +59,7 @@ export default class extends Endpoint { constructor ( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - + @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, @@ -66,8 +71,8 @@ export default class extends Endpoint { private downloadService: DownloadService, ) { super(meta, paramDef, async (ps, me) => { - const users = await this.usersRepository.findOneBy({ id: me.id }); - if (users === null) throw new ApiError(meta.errors.noSuchUser); + const userExist = await this.usersRepository.exist({ where: { id: me.id } }); + if (!userExist) throw new ApiError(meta.errors.noSuchUser); const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (file === null) throw new ApiError(meta.errors.noSuchFile); if (file.size === 0) throw new ApiError(meta.errors.emptyFile); @@ -79,6 +84,6 @@ export default class extends Endpoint { this.queueService.createImportAntennasJob(me, antennas); }); } -} +} export type Antenna = (_Antenna & { userListAccts: string[] | null })[]; diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts index 811971591a..965ad30547 100644 --- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts @@ -1,9 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -52,9 +57,8 @@ export const paramDef = { required: ['fileId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -72,7 +76,7 @@ export default class extends Endpoint { const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), - true + true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts index 8af278c883..38c9283043 100644 --- a/packages/backend/src/server/api/endpoints/i/import-following.ts +++ b/packages/backend/src/server/api/endpoints/i/import-following.ts @@ -1,9 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -51,9 +56,8 @@ export const paramDef = { required: ['fileId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -71,7 +75,7 @@ export default class extends Endpoint { const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), - true + true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts index eb0f9ba474..926cf13d7f 100644 --- a/packages/backend/src/server/api/endpoints/i/import-muting.ts +++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts @@ -1,9 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -52,9 +57,8 @@ export const paramDef = { required: ['fileId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -72,7 +76,7 @@ export default class extends Endpoint { const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), - true + true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts index 4568e93901..2167996435 100644 --- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts +++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts @@ -1,9 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueueService } from '@/core/QueueService.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; -import type { DriveFilesRepository } from '@/models/index.js'; +import type { DriveFilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -51,9 +56,8 @@ export const paramDef = { required: ['fileId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -71,7 +75,7 @@ export default class extends Endpoint { const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), - true + true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts index 261dd527c0..86b726e054 100644 --- a/packages/backend/src/server/api/endpoints/i/move.ts +++ b/packages/backend/src/server/api/endpoints/i/move.ts @@ -1,13 +1,15 @@ -import { Inject, Injectable } from '@nestjs/common'; -import ms from 'ms'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -import type { Config } from '@/config.js'; -import { DI } from '@/di-symbols.js'; +import { Injectable } from '@nestjs/common'; +import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { ApiError } from '@/server/api/error.js'; -import { LocalUser, RemoteUser } from '@/models/entities/User.js'; +import { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; @@ -72,13 +74,9 @@ export const paramDef = { required: ['moveToAccount'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.config) - private config: Config, - private remoteUserResolveService: RemoteUserResolveService, private apiLoggerService: ApiLoggerService, private accountMoveService: AccountMoveService, @@ -101,7 +99,7 @@ export default class extends Endpoint { this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`); throw new ApiError(meta.errors.noSuchUser); }); - const destination = await this.getterService.getUser(moveTo.id) as LocalUser | RemoteUser; + const destination = await this.getterService.getUser(moveTo.id) as MiLocalUser | MiRemoteUser; const newUri = this.userEntityService.getUserUri(destination); // update local db diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index f5662f4a0e..91dd72e805 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -1,16 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Brackets, In } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/_.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'; import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; -import { Notification } from '@/models/entities/Notification.js'; +import { MiNotification } from '@/models/Notification.js'; export const meta = { tags: ['account', 'notifications'], @@ -53,29 +57,18 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.redis) private redisClient: Redis.Redis, - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.mutingsRepository) - private mutingsRepository: MutingsRepository, - - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, private idService: IdService, private notificationEntityService: NotificationEntityService, private notificationService: NotificationService, - private queryService: QueryService, private noteReadService: NoteReadService, ) { super(meta, paramDef, async (ps, me) => { @@ -102,7 +95,7 @@ export default class extends Endpoint { return []; } - let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as Notification[]; + let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as MiNotification[]; if (includeTypes && includeTypes.length > 0) { notifications = notifications.filter(notification => includeTypes.includes(notification.type)); diff --git a/packages/backend/src/server/api/endpoints/i/page-likes.ts b/packages/backend/src/server/api/endpoints/i/page-likes.ts index 70e6e0a6a8..6bf7e6aa9b 100644 --- a/packages/backend/src/server/api/endpoints/i/page-likes.ts +++ b/packages/backend/src/server/api/endpoints/i/page-likes.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { PageLikesRepository } from '@/models/index.js'; +import type { PageLikesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { PageLikeEntityService } from '@/core/entities/PageLikeEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -43,9 +48,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.pageLikesRepository) private pageLikesRepository: PageLikesRepository, @@ -59,7 +63,7 @@ export default class extends Endpoint { .leftJoinAndSelect('like.page', 'page'); const likes = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return this.pageLikeEntityService.packMany(likes, me); diff --git a/packages/backend/src/server/api/endpoints/i/pages.ts b/packages/backend/src/server/api/endpoints/i/pages.ts index 285aa34e91..b8082c018f 100644 --- a/packages/backend/src/server/api/endpoints/i/pages.ts +++ b/packages/backend/src/server/api/endpoints/i/pages.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { PagesRepository } from '@/models/index.js'; +import type { PagesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -33,9 +38,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, @@ -48,7 +52,7 @@ export default class extends Endpoint { .andWhere('page.userId = :meId', { meId: me.id }); const pages = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await this.pageEntityService.packMany(pages); diff --git a/packages/backend/src/server/api/endpoints/i/pin.ts b/packages/backend/src/server/api/endpoints/i/pin.ts index 2293500945..c89cdfa3a4 100644 --- a/packages/backend/src/server/api/endpoints/i/pin.ts +++ b/packages/backend/src/server/api/endpoints/i/pin.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -47,9 +52,8 @@ export const paramDef = { required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private userEntityService: UserEntityService, private notePiningService: NotePiningService, diff --git a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts index b92de4b739..e43ab7c15e 100644 --- a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts +++ b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { NoteUnreadsRepository } from '@/models/index.js'; +import type { NoteUnreadsRepository } from '@/models/_.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; @@ -18,9 +23,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.noteUnreadsRepository) private noteUnreadsRepository: NoteUnreadsRepository, 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 b8922b91e5..ba7859d0d4 100644 --- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts +++ b/packages/backend/src/server/api/endpoints/i/read-announcement.ts @@ -1,11 +1,11 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { 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 { GlobalEventService } from '@/core/GlobalEventService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; +import { AnnouncementService } from '@/core/AnnouncementService.js'; export const meta = { tags: ['account'], @@ -15,11 +15,6 @@ export const meta = { kind: 'write:account', errors: { - noSuchAnnouncement: { - message: 'No such announcement.', - code: 'NO_SUCH_ANNOUNCEMENT', - id: '184663db-df88-4bc2-8b52-fb85f0681939', - }, }, } as const; @@ -31,49 +26,13 @@ export const paramDef = { required: ['announcementId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.announcementsRepository) - private announcementsRepository: AnnouncementsRepository, - - @Inject(DI.announcementReadsRepository) - private announcementReadsRepository: AnnouncementReadsRepository, - - private userEntityService: UserEntityService, - private idService: IdService, - private globalEventService: GlobalEventService, + private announcementService: AnnouncementService, ) { super(meta, paramDef, async (ps, me) => { - // Check if announcement exists - const announcement = await this.announcementsRepository.findOneBy({ id: ps.announcementId }); - - if (announcement == null) { - throw new ApiError(meta.errors.noSuchAnnouncement); - } - - // Check if already read - const read = await this.announcementReadsRepository.findOneBy({ - announcementId: ps.announcementId, - userId: me.id, - }); - - if (read != null) { - return; - } - - // Create read - await this.announcementReadsRepository.insert({ - id: this.idService.genId(), - createdAt: new Date(), - announcementId: ps.announcementId, - userId: me.id, - }); - - if (!await this.userEntityService.getHasUnreadAnnouncement(me.id)) { - this.globalEventService.publishMainStream(me.id, 'readAllAnnouncements'); - } + await this.announcementService.read(me, ps.announcementId); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts index 23ff63f5e9..b70dcfbace 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import generateUserToken from '@/misc/generate-native-user-token.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; @@ -20,9 +25,8 @@ export const paramDef = { required: ['password'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts index 17154c1f76..211e6637dc 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/index.js'; +import type { RegistryItemsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -19,9 +24,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.registryItemsRepository) private registryItemsRepository: RegistryItemsRepository, diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts index 233686dbe1..9c6f2d6781 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/index.js'; +import type { RegistryItemsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -29,9 +34,8 @@ export const paramDef = { required: ['key'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.registryItemsRepository) private registryItemsRepository: RegistryItemsRepository, diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts index 99cdf95bad..729e729b8c 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/index.js'; +import type { RegistryItemsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -29,9 +34,8 @@ export const paramDef = { required: ['key'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.registryItemsRepository) private registryItemsRepository: RegistryItemsRepository, diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts index 362a5e89f4..ffd2860fde 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/index.js'; +import type { RegistryItemsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -19,9 +24,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.registryItemsRepository) private registryItemsRepository: RegistryItemsRepository, diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys.ts b/packages/backend/src/server/api/endpoints/i/registry/keys.ts index 99f69d8bed..7239bb66e1 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/index.js'; +import type { RegistryItemsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -19,9 +24,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.registryItemsRepository) private registryItemsRepository: RegistryItemsRepository, diff --git a/packages/backend/src/server/api/endpoints/i/registry/remove.ts b/packages/backend/src/server/api/endpoints/i/registry/remove.ts index 78a641f5e2..ae687fefe9 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/remove.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/index.js'; +import type { RegistryItemsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -29,9 +34,8 @@ export const paramDef = { required: ['key'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.registryItemsRepository) private registryItemsRepository: RegistryItemsRepository, diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts index 0a4ecb9c51..7637cdcf73 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/index.js'; +import type { RegistryItemsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -15,9 +20,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.registryItemsRepository) private registryItemsRepository: RegistryItemsRepository, diff --git a/packages/backend/src/server/api/endpoints/i/registry/set.ts b/packages/backend/src/server/api/endpoints/i/registry/set.ts index c8e72203c4..c074b152df 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/set.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/set.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/index.js'; +import type { RegistryItemsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; @@ -23,9 +28,8 @@ export const paramDef = { required: ['key', 'value'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.registryItemsRepository) private registryItemsRepository: RegistryItemsRepository, diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts index 93daeb0cd7..8e2f271005 100644 --- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts +++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts @@ -1,7 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AccessTokensRepository } from '@/models/index.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; +import type { AccessTokensRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -18,19 +22,16 @@ export const paramDef = { required: ['tokenId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.accessTokensRepository) private accessTokensRepository: AccessTokensRepository, - - private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { - const token = await this.accessTokensRepository.findOneBy({ id: ps.tokenId }); + const tokenExist = await this.accessTokensRepository.exist({ where: { id: ps.tokenId } }); - if (token) { + if (tokenExist) { await this.accessTokensRepository.delete({ id: ps.tokenId, userId: me.id, diff --git a/packages/backend/src/server/api/endpoints/i/signin-history.ts b/packages/backend/src/server/api/endpoints/i/signin-history.ts index 9b30a24336..139bede7bc 100644 --- a/packages/backend/src/server/api/endpoints/i/signin-history.ts +++ b/packages/backend/src/server/api/endpoints/i/signin-history.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { SigninsRepository } from '@/models/index.js'; +import type { SigninsRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { SigninEntityService } from '@/core/entities/SigninEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -21,9 +26,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.signinsRepository) private signinsRepository: SigninsRepository, @@ -35,7 +39,7 @@ export default class extends Endpoint { const query = this.queryService.makePaginationQuery(this.signinsRepository.createQueryBuilder('signin'), ps.sinceId, ps.untilId) .andWhere('signin.userId = :meId', { meId: me.id }); - const history = await query.take(ps.limit).getMany(); + const history = await query.limit(ps.limit).getMany(); return await Promise.all(history.map(record => this.signinEntityService.pack(record))); }); diff --git a/packages/backend/src/server/api/endpoints/i/unpin.ts b/packages/backend/src/server/api/endpoints/i/unpin.ts index db239dc284..b59c0e954f 100644 --- a/packages/backend/src/server/api/endpoints/i/unpin.ts +++ b/packages/backend/src/server/api/endpoints/i/unpin.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -34,9 +39,8 @@ export const paramDef = { required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private userEntityService: UserEntityService, private notePiningService: NotePiningService, 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 4f543a6472..a36b3a732b 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -1,14 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import rndstr from 'rndstr'; import ms from 'ms'; import bcrypt from 'bcryptjs'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, UserProfilesRepository } from '@/models/index.js'; +import type { UserProfilesRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { EmailService } from '@/core/EmailService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; +import { UserAuthService } from '@/core/UserAuthService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -41,34 +47,43 @@ export const paramDef = { properties: { password: { type: 'string' }, email: { type: 'string', nullable: true }, + token: { type: 'string', nullable: true }, }, required: ['password'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.config) private config: Config, - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, private userEntityService: UserEntityService, private emailService: EmailService, + private userAuthService: UserAuthService, private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { + const token = ps.token; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); - // Compare password - const same = await bcrypt.compare(ps.password, profile.password!); + if (profile.twoFactorEnabled) { + if (token == null) { + throw new Error('authentication failed'); + } - if (!same) { + try { + await this.userAuthService.twoFactorAuthenticate(profile, token); + } catch (e) { + throw new Error('authentication failed'); + } + } + + const passwordMatched = await bcrypt.compare(ps.password, profile.password!); + if (!passwordMatched) { throw new ApiError(meta.errors.incorrectPassword); } @@ -94,7 +109,7 @@ export default class extends Endpoint { this.globalEventService.publishMainStream(me.id, 'meUpdated', iObj); if (ps.email != null) { - const code = rndstr('a-z0-9', 16); + const code = secureRndstr(16, { chars: L_CHARS }); await this.userProfilesRepository.update(me.id, { emailVerifyCode: code, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index d10f690a32..b11e091957 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -1,13 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import RE2 from 're2'; import * as mfm from 'mfm-js'; import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; +import { JSDOM } from 'jsdom'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; import * as Acct from '@/misc/acct.js'; -import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/index.js'; -import type { User } from '@/models/entities/User.js'; -import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/entities/User.js'; -import type { UserProfile } from '@/models/entities/UserProfile.js'; +import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js'; +import type { MiLocalUser, MiUser } from '@/models/User.js'; +import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js'; +import type { MiUserProfile } from '@/models/UserProfile.js'; import { notificationTypes } from '@/types.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { langmap } from '@/misc/langmap.js'; @@ -20,9 +27,11 @@ import { HashtagService } from '@/core/HashtagService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { CacheService } from '@/core/CacheService.js'; -import { AccountMoveService } from '@/core/AccountMoveService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; +import type { Config } from '@/config.js'; +import { safeForSql } from '@/misc/safe-for-sql.js'; import { ApiLoggerService } from '../../ApiLoggerService.js'; import { ApiError } from '../../error.js'; @@ -33,6 +42,11 @@ export const meta = { kind: 'write:account', + limit: { + duration: ms('1hour'), + max: 10, + }, + errors: { noSuchAvatar: { message: 'No such avatar file.', @@ -146,7 +160,7 @@ export const paramDef = { alwaysMarkNsfw: { type: 'boolean' }, autoSensitive: { type: 'boolean' }, ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, - pinnedPageId: { type: 'string', format: 'misskey:id' }, + pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true }, mutedWords: { type: 'array' }, mutedInstances: { type: 'array', items: { type: 'string', @@ -166,10 +180,12 @@ export const paramDef = { }, } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -187,19 +203,19 @@ export default class extends Endpoint { private globalEventService: GlobalEventService, private userFollowingService: UserFollowingService, private accountUpdateService: AccountUpdateService, - private accountMoveService: AccountMoveService, private remoteUserResolveService: RemoteUserResolveService, private apiLoggerService: ApiLoggerService, private hashtagService: HashtagService, private roleService: RoleService, private cacheService: CacheService, + private httpRequestService: HttpRequestService, ) { super(meta, paramDef, async (ps, _user, token) => { - const user = await this.usersRepository.findOneByOrFail({ id: _user.id }); + const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser; const isSecure = token == null; - const updates = {} as Partial; - const profileUpdates = {} as Partial; + const updates = {} as Partial; + const profileUpdates = {} as Partial; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); @@ -294,9 +310,9 @@ export default class extends Endpoint { if (ps.fields) { profileUpdates.fields = ps.fields - .filter(x => typeof x.name === 'string' && x.name !== '' && typeof x.value === 'string' && x.value !== '') + .filter(x => typeof x.name === 'string' && x.name.trim() !== '' && typeof x.value === 'string' && x.value.trim() !== '') .map(x => { - return { name: x.name, value: x.value }; + return { name: x.name.trim(), value: x.value.trim() }; }); } @@ -362,7 +378,11 @@ export default class extends Endpoint { if (Object.keys(updates).includes('alsoKnownAs')) { this.cacheService.uriPersonCache.set(this.userEntityService.genLocalUserUri(user.id), { ...user, ...updates }); } - if (Object.keys(profileUpdates).length > 0) await this.userProfilesRepository.update(user.id, profileUpdates); + + await this.userProfilesRepository.update(user.id, { + ...profileUpdates, + verifiedLinks: [], + }); const iObj = await this.userEntityService.pack(user.id, user, { detail: true, @@ -384,7 +404,34 @@ export default class extends Endpoint { // フォロワーにUpdateを配信 this.accountUpdateService.publishToFollowers(user.id); + const urls = updatedProfile.fields.filter(x => x.value.startsWith('https://')); + for (const url of urls) { + this.verifyLink(url.value, user); + } + return iObj; }); } + + private async verifyLink(url: string, user: MiLocalUser) { + if (!safeForSql(url)) return; + + const html = await this.httpRequestService.getHtml(url); + + const { window } = new JSDOM(html); + const doc = window.document; + + const myLink = `${this.config.url}/@${user.username}`; + + const includesMyLink = Array.from(doc.getElementsByTagName('a')).some(a => a.href === myLink); + + if (includesMyLink) { + await this.userProfilesRepository.createQueryBuilder('profile').update() + .where('userId = :userId', { userId: user.id }) + .set({ + verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている + }) + .execute(); + } + } } diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts index 51fcce6cf0..48eaeff406 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; -import type { WebhooksRepository } from '@/models/index.js'; -import { webhookEventTypes } from '@/models/entities/Webhook.js'; +import type { WebhooksRepository } from '@/models/_.js'; +import { webhookEventTypes } from '@/models/Webhook.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; @@ -29,19 +34,18 @@ export const paramDef = { properties: { name: { type: 'string', minLength: 1, maxLength: 100 }, url: { type: 'string', minLength: 1, maxLength: 1024 }, - secret: { type: 'string', minLength: 1, maxLength: 1024 }, + secret: { type: 'string', maxLength: 1024, default: '' }, on: { type: 'array', items: { type: 'string', enum: webhookEventTypes, } }, }, - required: ['name', 'url', 'secret', 'on'], + required: ['name', 'url', 'on'], } as const; // TODO: ロジックをサービスに切り出す -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.webhooksRepository) private webhooksRepository: WebhooksRepository, diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts index 7bdad136aa..db7d0db13c 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { WebhooksRepository } from '@/models/index.js'; +import type { WebhooksRepository } from '@/models/_.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -31,9 +36,8 @@ export const paramDef = { // TODO: ロジックをサービスに切り出す -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.webhooksRepository) private webhooksRepository: WebhooksRepository, diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts index 58c84938cc..aa8921fe24 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { WebhooksRepository } from '@/models/index.js'; +import type { WebhooksRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -17,9 +22,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.webhooksRepository) private webhooksRepository: WebhooksRepository, diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts index d15ca0050d..f1294bb5c8 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { WebhooksRepository } from '@/models/index.js'; +import type { WebhooksRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -28,9 +33,8 @@ export const paramDef = { required: ['webhookId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.webhooksRepository) private webhooksRepository: WebhooksRepository, diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts index 8ec308eda7..b3e000524d 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { WebhooksRepository } from '@/models/index.js'; -import { webhookEventTypes } from '@/models/entities/Webhook.js'; +import type { WebhooksRepository } from '@/models/_.js'; +import { webhookEventTypes } from '@/models/Webhook.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -29,20 +34,19 @@ export const paramDef = { webhookId: { type: 'string', format: 'misskey:id' }, name: { type: 'string', minLength: 1, maxLength: 100 }, url: { type: 'string', minLength: 1, maxLength: 1024 }, - secret: { type: 'string', minLength: 1, maxLength: 1024 }, + secret: { type: 'string', maxLength: 1024, default: '' }, on: { type: 'array', items: { type: 'string', enum: webhookEventTypes, } }, active: { type: 'boolean' }, }, - required: ['webhookId', 'name', 'url', 'secret', 'on', 'active'], + required: ['webhookId', 'name', 'url', 'on', 'active'], } as const; // TODO: ロジックをサービスに切り出す -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.webhooksRepository) private webhooksRepository: WebhooksRepository, diff --git a/packages/backend/src/server/api/endpoints/invite.ts b/packages/backend/src/server/api/endpoints/invite.ts deleted file mode 100644 index 5d2c479e79..0000000000 --- a/packages/backend/src/server/api/endpoints/invite.ts +++ /dev/null @@ -1,61 +0,0 @@ -import rndstr from 'rndstr'; -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistrationTicketsRepository } from '@/models/index.js'; -import { IdService } from '@/core/IdService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['meta'], - - requireCredential: true, - requireRolePolicy: 'canInvite', - - res: { - type: 'object', - optional: false, nullable: false, - properties: { - code: { - type: 'string', - optional: false, nullable: false, - example: '2ERUA5VR', - maxLength: 8, - minLength: 8, - }, - }, - }, -} 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.registrationTicketsRepository) - private registrationTicketsRepository: RegistrationTicketsRepository, - - private idService: IdService, - ) { - super(meta, paramDef, async (ps, me) => { - const code = rndstr({ - length: 8, - chars: '2-9A-HJ-NP-Z', // [0-9A-Z] w/o [01IO] (32 patterns) - }); - - await this.registrationTicketsRepository.insert({ - id: this.idService.genId(), - createdAt: new Date(), - code, - }); - - return { - code, - }; - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/invite/create.ts b/packages/backend/src/server/api/endpoints/invite/create.ts new file mode 100644 index 0000000000..7361ab616c --- /dev/null +++ b/packages/backend/src/server/api/endpoints/invite/create.ts @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MoreThan } from 'typeorm'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RegistrationTicketsRepository } from '@/models/_.js'; +import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js'; +import { IdService } from '@/core/IdService.js'; +import { RoleService } from '@/core/RoleService.js'; +import { DI } from '@/di-symbols.js'; +import { generateInviteCode } from '@/misc/generate-invite-code.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['meta'], + + requireCredential: true, + requireRolePolicy: 'canInvite', + + errors: { + exceededCreateLimit: { + message: 'You have exceeded the limit for creating an invitation code.', + code: 'EXCEEDED_LIMIT_OF_CREATE_INVITE_CODE', + id: '8b165dd3-6f37-4557-8db1-73175d63c641', + }, + }, + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + code: { + type: 'string', + optional: false, nullable: false, + example: 'GR6S02ERUA5VR', + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.registrationTicketsRepository) + private registrationTicketsRepository: RegistrationTicketsRepository, + + private inviteCodeEntityService: InviteCodeEntityService, + private idService: IdService, + private roleService: RoleService, + ) { + super(meta, paramDef, async (ps, me) => { + const policies = await this.roleService.getUserPolicies(me.id); + + if (policies.inviteLimit) { + const count = await this.registrationTicketsRepository.countBy({ + createdAt: MoreThan(new Date(Date.now() - (policies.inviteLimitCycle * 1000 * 60))), + createdById: me.id, + }); + + if (count >= policies.inviteLimit) { + throw new ApiError(meta.errors.exceededCreateLimit); + } + } + + const ticket = await this.registrationTicketsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + createdBy: me, + createdById: me.id, + expiresAt: policies.inviteExpirationTime ? new Date(Date.now() + (policies.inviteExpirationTime * 1000 * 60)) : null, + code: generateInviteCode(), + }).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0])); + + return await this.inviteCodeEntityService.pack(ticket, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/invite/delete.ts b/packages/backend/src/server/api/endpoints/invite/delete.ts new file mode 100644 index 0000000000..3b57775739 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/invite/delete.ts @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RegistrationTicketsRepository } from '@/models/_.js'; +import { RoleService } from '@/core/RoleService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['meta'], + + requireCredential: true, + requireRolePolicy: 'canInvite', + + errors: { + noSuchCode: { + message: 'No such invite code.', + code: 'NO_SUCH_INVITE_CODE', + id: 'cd4f9ae4-7854-4e3e-8df9-c296f051e634', + }, + + cantDelete: { + message: 'You can\'t delete this invite code.', + code: 'CAN_NOT_DELETE_INVITE_CODE', + id: 'ff17af39-000c-4d4e-abdf-848fa30fc1ce', + }, + + accessDenied: { + message: 'Access denied.', + code: 'ACCESS_DENIED', + id: '5eb8d909-2540-4970-90b8-dd6f86088121', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + inviteId: { type: 'string', format: 'misskey:id' }, + }, + required: ['inviteId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.registrationTicketsRepository) + private registrationTicketsRepository: RegistrationTicketsRepository, + + private roleService: RoleService, + ) { + super(meta, paramDef, async (ps, me) => { + const ticket = await this.registrationTicketsRepository.findOneBy({ id: ps.inviteId }); + const isModerator = await this.roleService.isModerator(me); + + if (ticket == null) { + throw new ApiError(meta.errors.noSuchCode); + } + + if (ticket.createdById !== me.id && !isModerator) { + throw new ApiError(meta.errors.accessDenied); + } + + if (ticket.usedAt && !isModerator) { + throw new ApiError(meta.errors.cantDelete); + } + + await this.registrationTicketsRepository.delete(ticket.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/invite/limit.ts b/packages/backend/src/server/api/endpoints/invite/limit.ts new file mode 100644 index 0000000000..43b94e4f06 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/invite/limit.ts @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { MoreThan } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RegistrationTicketsRepository } from '@/models/_.js'; +import { RoleService } from '@/core/RoleService.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['meta'], + + requireCredential: true, + requireRolePolicy: 'canInvite', + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + remaining: { + type: 'integer', + optional: false, nullable: true, + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.registrationTicketsRepository) + private registrationTicketsRepository: RegistrationTicketsRepository, + + private roleService: RoleService, + ) { + super(meta, paramDef, async (ps, me) => { + const policies = await this.roleService.getUserPolicies(me.id); + + const count = policies.inviteLimit ? await this.registrationTicketsRepository.countBy({ + createdAt: MoreThan(new Date(Date.now() - (policies.inviteExpirationTime * 60 * 1000))), + createdById: me.id, + }) : null; + + return { + remaining: count !== null ? Math.max(0, policies.inviteLimit - count) : null, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/invite/list.ts b/packages/backend/src/server/api/endpoints/invite/list.ts new file mode 100644 index 0000000000..06139b6806 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/invite/list.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RegistrationTicketsRepository } from '@/models/_.js'; +import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['meta'], + + requireCredential: true, + requireRolePolicy: 'canInvite', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.registrationTicketsRepository) + private registrationTicketsRepository: RegistrationTicketsRepository, + + private inviteCodeEntityService: InviteCodeEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.registrationTicketsRepository.createQueryBuilder('ticket'), ps.sinceId, ps.untilId) + .andWhere('ticket.createdById = :meId', { meId: me.id }) + .leftJoinAndSelect('ticket.createdBy', 'createdBy') + .leftJoinAndSelect('ticket.usedBy', 'usedBy'); + + const tickets = await query + .limit(ps.limit) + .getMany(); + + return await this.inviteCodeEntityService.packMany(tickets, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 53d724a9dd..c0cbfa3f48 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -1,7 +1,12 @@ -import { IsNull, LessThanOrEqual, MoreThan } from 'typeorm'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { IsNull, LessThanOrEqual, MoreThan, Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import * as JSON5 from 'json5'; -import type { AdsRepository, UsersRepository } from '@/models/index.js'; +import JSON5 from 'json5'; +import type { AdsRepository, UsersRepository } from '@/models/_.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -83,6 +88,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + cacheRemoteSensitiveFiles: { + type: 'boolean', + optional: false, nullable: false, + }, emailRequiredForSignup: { type: 'boolean', optional: false, nullable: false, @@ -124,10 +133,17 @@ export const meta = { type: 'string', optional: false, nullable: false, }, - errorImageUrl: { + serverErrorImageUrl: { type: 'string', - optional: false, nullable: false, - default: 'https://xn--931a.moe/aiart/yubitun.png', + optional: false, nullable: true, + }, + infoImageUrl: { + type: 'string', + optional: false, nullable: true, + }, + notFoundImageUrl: { + type: 'string', + optional: false, nullable: true, }, iconUrl: { type: 'string', @@ -237,13 +253,12 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.config) private config: Config, - + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -256,12 +271,15 @@ export default class extends Endpoint { super(meta, paramDef, async (ps, me) => { const instance = await this.metaService.fetch(true); - const ads = await this.adsRepository.find({ - where: { - expiresAt: MoreThan(new Date()), - startsAt: LessThanOrEqual(new Date()), - }, - }); + const ads = await this.adsRepository.createQueryBuilder('ads') + .where('ads.expiresAt > :now', { now: new Date() }) + .andWhere('ads.startsAt <= :now', { now: new Date() }) + .andWhere(new Brackets(qb => { + // 曜日のビットフラグを確認する + qb.where('ads.dayOfWeek & :dayOfWeek > 0', { dayOfWeek: 1 << new Date().getDay() }) + .orWhere('ads.dayOfWeek = 0'); + })) + .getMany(); const response: any = { maintainerName: instance.maintainerName, @@ -288,7 +306,9 @@ export default class extends Endpoint { themeColor: instance.themeColor, mascotImageUrl: instance.mascotImageUrl, bannerUrl: instance.bannerUrl, - errorImageUrl: instance.errorImageUrl, + infoImageUrl: instance.infoImageUrl, + serverErrorImageUrl: instance.serverErrorImageUrl, + notFoundImageUrl: instance.notFoundImageUrl, iconUrl: instance.iconUrl, backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, @@ -302,6 +322,7 @@ export default class extends Endpoint { place: ad.place, ratio: ad.ratio, imageUrl: ad.imageUrl, + dayOfWeek: ad.dayOfWeek, })), enableEmail: instance.enableEmail, enableServiceWorker: instance.enableServiceWorker, @@ -316,6 +337,7 @@ export default class extends Endpoint { ...(ps.detail ? { cacheRemoteFiles: instance.cacheRemoteFiles, + cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles, requireSetup: (await this.usersRepository.countBy({ host: IsNull(), })) === 0, diff --git a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts index 97def86262..e40656cb6d 100644 --- a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts +++ b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AccessTokensRepository } from '@/models/index.js'; +import type { AccessTokensRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { DI } from '@/di-symbols.js'; @@ -38,9 +43,8 @@ export const paramDef = { required: ['session', 'permission'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.accessTokensRepository) private accessTokensRepository: AccessTokensRepository, @@ -49,7 +53,7 @@ export default class extends Endpoint { ) { super(meta, paramDef, async (ps, me) => { // Generate access token - const accessToken = secureRndstr(32, true); + const accessToken = secureRndstr(32); const now = new Date(); diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts index ee358d5c6c..49c2b5707d 100644 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ b/packages/backend/src/server/api/endpoints/mute/create.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { MutingsRepository } from '@/models/index.js'; +import type { MutingsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { UserMutingService } from '@/core/UserMutingService.js'; @@ -54,9 +59,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, @@ -79,12 +83,14 @@ export default class extends Endpoint { }); // Check if already muting - const exist = await this.mutingsRepository.findOneBy({ - muterId: muter.id, - muteeId: mutee.id, + const exist = await this.mutingsRepository.exist({ + where: { + muterId: muter.id, + muteeId: mutee.id, + }, }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyMuting); } diff --git a/packages/backend/src/server/api/endpoints/mute/delete.ts b/packages/backend/src/server/api/endpoints/mute/delete.ts index 90b74590be..a3fd2dd82f 100644 --- a/packages/backend/src/server/api/endpoints/mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/mute/delete.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { MutingsRepository } from '@/models/index.js'; +import type { MutingsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { UserMutingService } from '@/core/UserMutingService.js'; @@ -42,9 +47,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, diff --git a/packages/backend/src/server/api/endpoints/mute/list.ts b/packages/backend/src/server/api/endpoints/mute/list.ts index 9ec6d17273..2a41182ebc 100644 --- a/packages/backend/src/server/api/endpoints/mute/list.ts +++ b/packages/backend/src/server/api/endpoints/mute/list.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { MutingsRepository } from '@/models/index.js'; +import type { MutingsRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { MutingEntityService } from '@/core/entities/MutingEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -33,9 +38,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, @@ -48,7 +52,7 @@ export default class extends Endpoint { .andWhere('muting.muterId = :meId', { meId: me.id }); const mutings = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await this.mutingEntityService.packMany(mutings, me); diff --git a/packages/backend/src/server/api/endpoints/my/apps.ts b/packages/backend/src/server/api/endpoints/my/apps.ts index 4b7ed80123..98c317346f 100644 --- a/packages/backend/src/server/api/endpoints/my/apps.ts +++ b/packages/backend/src/server/api/endpoints/my/apps.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { AppsRepository } from '@/models/index.js'; +import type { AppsRepository } from '@/models/_.js'; import { AppEntityService } from '@/core/entities/AppEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -29,9 +34,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.appsRepository) private appsRepository: AppsRepository, diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts index 5fbc7aba58..95ba5e8b64 100644 --- a/packages/backend/src/server/api/endpoints/notes.ts +++ b/packages/backend/src/server/api/endpoints/notes.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -34,9 +39,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -53,34 +57,34 @@ export default class extends Endpoint { .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - + if (ps.local) { query.andWhere('note.userHost IS NULL'); } - + if (ps.reply !== undefined) { query.andWhere(ps.reply ? 'note.replyId IS NOT NULL' : 'note.replyId IS NULL'); } - + if (ps.renote !== undefined) { query.andWhere(ps.renote ? 'note.renoteId IS NOT NULL' : 'note.renoteId IS NULL'); } - + if (ps.withFiles !== undefined) { query.andWhere(ps.withFiles ? 'note.fileIds != \'{}\'' : 'note.fileIds = \'{}\''); } - + if (ps.poll !== undefined) { query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE'); } - + // TODO //if (bot != undefined) { // query.isBot = bot; //} - - const notes = await query.take(ps.limit).getMany(); - + + const notes = await query.limit(ps.limit).getMany(); + return await this.noteEntityService.packMany(notes); }); } diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index 26f2d6772d..1a82a4b5d7 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -33,9 +38,8 @@ export const paramDef = { required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -68,7 +72,7 @@ export default class extends Endpoint { this.queryService.generateBlockedUserQuery(query, me); } - const notes = await query.take(ps.limit).getMany(); + const notes = await query.limit(ps.limit).getMany(); return await this.noteEntityService.packMany(notes, me); }); diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts index 0a5542f497..677c0ea307 100644 --- a/packages/backend/src/server/api/endpoints/notes/clips.ts +++ b/packages/backend/src/server/api/endpoints/notes/clips.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; +import type { ClipNotesRepository, ClipsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -39,9 +44,8 @@ export const paramDef = { required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts index 5ecf7cf458..b94a019da4 100644 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { Note } from '@/models/entities/Note.js'; -import type { NotesRepository } from '@/models/index.js'; +import type { MiNote } from '@/models/Note.js'; +import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['notes'], @@ -41,9 +46,8 @@ export const paramDef = { required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -57,7 +61,7 @@ export default class extends Endpoint { throw err; }); - const conversation: Note[] = []; + const conversation: MiNote[] = []; let i = 0; const get = async (id: any) => { diff --git a/packages/backend/src/server/api/endpoints/notes/create.test.ts b/packages/backend/src/server/api/endpoints/notes/create.test.ts index 6bff7fc0c9..6d34aaccf3 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.test.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import { readFile } from 'node:fs/promises'; diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 96be5ed844..2e4d316c47 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; import { In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { User } from '@/models/entities/User.js'; -import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/index.js'; -import type { DriveFile } from '@/models/entities/DriveFile.js'; -import type { Note } from '@/models/entities/Note.js'; -import type { Channel } from '@/models/entities/Channel.js'; +import type { MiUser } from '@/models/User.js'; +import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/_.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { MiNote } from '@/models/Note.js'; +import type { MiChannel } from '@/models/Channel.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -157,9 +162,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -180,15 +184,15 @@ export default class extends Endpoint { private noteCreateService: NoteCreateService, ) { super(meta, paramDef, async (ps, me) => { - let visibleUsers: User[] = []; + let visibleUsers: MiUser[] = []; if (ps.visibleUserIds) { visibleUsers = await this.usersRepository.findBy({ id: In(ps.visibleUserIds), }); } - let files: DriveFile[] = []; - const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; + let files: MiDriveFile[] = []; + const fileIds = ps.fileIds ?? ps.mediaIds ?? null; if (fileIds != null) { files = await this.driveFilesRepository.createQueryBuilder('file') .where('file.userId = :userId AND file.id IN (:...fileIds)', { @@ -204,7 +208,7 @@ export default class extends Endpoint { } } - let renote: Note | null = null; + let renote: MiNote | null = null; if (ps.renoteId != null) { // Fetch renote to note renote = await this.notesRepository.findOneBy({ id: ps.renoteId }); @@ -217,17 +221,19 @@ export default class extends Endpoint { // Check blocking if (renote.userId !== me.id) { - const block = await this.blockingsRepository.findOneBy({ - blockerId: renote.userId, - blockeeId: me.id, + const blockExist = await this.blockingsRepository.exist({ + where: { + blockerId: renote.userId, + blockeeId: me.id, + }, }); - if (block) { + if (blockExist) { throw new ApiError(meta.errors.youHaveBeenBlocked); } } } - let reply: Note | null = null; + let reply: MiNote | null = null; if (ps.replyId != null) { // Fetch reply reply = await this.notesRepository.findOneBy({ id: ps.replyId }); @@ -240,11 +246,13 @@ export default class extends Endpoint { // Check blocking if (reply.userId !== me.id) { - const block = await this.blockingsRepository.findOneBy({ - blockerId: reply.userId, - blockeeId: me.id, + const blockExist = await this.blockingsRepository.exist({ + where: { + blockerId: reply.userId, + blockeeId: me.id, + }, }); - if (block) { + if (blockExist) { throw new ApiError(meta.errors.youHaveBeenBlocked); } } @@ -260,7 +268,7 @@ export default class extends Endpoint { } } - let channel: Channel | null = null; + let channel: MiChannel | null = null; if (ps.channelId != null) { channel = await this.channelsRepository.findOneBy({ id: ps.channelId, isArchived: false }); diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts index 16c4c01387..74062a58f5 100644 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/delete.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteDeleteService } from '@/core/NoteDeleteService.js'; import { DI } from '@/di-symbols.js'; @@ -44,9 +49,8 @@ export const paramDef = { required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts index 611ea19560..cc648e22a8 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; -import type { NoteFavoritesRepository } from '@/models/index.js'; +import type { NoteFavoritesRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; @@ -44,9 +49,8 @@ export const paramDef = { required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.noteFavoritesRepository) private noteFavoritesRepository: NoteFavoritesRepository, @@ -63,12 +67,14 @@ export default class extends Endpoint { }); // if already favorited - const exist = await this.noteFavoritesRepository.findOneBy({ - noteId: note.id, - userId: me.id, + const exist = await this.noteFavoritesRepository.exist({ + where: { + noteId: note.id, + userId: me.id, + }, }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyFavorited); } diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts index bb3a7c501a..8ab9775a2c 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; -import type { NoteFavoritesRepository } from '@/models/index.js'; +import type { NoteFavoritesRepository } from '@/models/_.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -35,9 +40,8 @@ export const paramDef = { required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.noteFavoritesRepository) private noteFavoritesRepository: NoteFavoritesRepository, diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index bdb06498bc..5283b0e0bc 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -33,9 +38,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -65,7 +69,7 @@ export default class extends Endpoint { let notes = await query .orderBy('note.score', 'DESC') - .take(100) + .limit(100) .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/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 88c1ca7f58..0b3b5c902e 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -1,9 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { MetaService } from '@/core/MetaService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; @@ -45,16 +49,14 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, private queryService: QueryService, - private metaService: MetaService, private roleService: RoleService, private activeUsersChart: ActiveUsersChart, ) { @@ -88,7 +90,7 @@ export default class extends Endpoint { } //#endregion - const timeline = await query.take(ps.limit).getMany(); + const timeline = await query.limit(ps.limit).getMany(); process.nextTick(() => { if (me) { diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 7a3581e6e4..e9ae5dc755 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -1,10 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, FollowingsRepository } from '@/models/index.js'; +import type { NotesRepository, FollowingsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; -import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; @@ -52,9 +56,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -64,7 +67,6 @@ export default class extends Endpoint { private noteEntityService: NoteEntityService, private queryService: QueryService, - private metaService: MetaService, private roleService: RoleService, private activeUsersChart: ActiveUsersChart, private idService: IdService, @@ -137,7 +139,7 @@ export default class extends Endpoint { } //#endregion - const timeline = await query.take(ps.limit).getMany(); + const timeline = await query.limit(ps.limit).getMany(); process.nextTick(() => { this.activeUsersChart.read(me); 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 2ee549232c..af1e0398dc 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -1,10 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { MetaService } from '@/core/MetaService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; @@ -51,16 +55,14 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, private queryService: QueryService, - private metaService: MetaService, private roleService: RoleService, private activeUsersChart: ActiveUsersChart, private idService: IdService, @@ -110,7 +112,7 @@ export default class extends Endpoint { } //#endregion - const timeline = await query.take(ps.limit).getMany(); + const timeline = await query.limit(ps.limit).getMany(); process.nextTick(() => { if (me) { diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 4e9f604d8d..65e7bd8cd5 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, FollowingsRepository } from '@/models/index.js'; +import type { NotesRepository, FollowingsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -35,9 +40,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -59,6 +63,8 @@ export default class extends Endpoint { .where(`'{"${me.id}"}' <@ note.mentions`) .orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`); })) + // Avoid scanning primary key index + .orderBy('CONCAT(note.id)', 'DESC') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') @@ -79,7 +85,7 @@ export default class extends Endpoint { query.setParameters(followingQuery.getParameters()); } - const mentions = await query.take(ps.limit).getMany(); + const mentions = await query.limit(ps.limit).getMany(); this.noteReadService.read(me.id, mentions); diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts index 6cdc9b902c..29190af62a 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Brackets, In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, MutingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; +import type { NotesRepository, MutingsRepository, PollsRepository, PollVotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -30,9 +35,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -82,8 +86,8 @@ export default class extends Endpoint { const polls = await query .orderBy('poll.noteId', 'DESC') - .take(ps.limit) - .skip(ps.offset) + .limit(ps.limit) + .offset(ps.offset) .getMany(); if (polls.length === 0) return []; 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 3a33b037f8..a58bf09b85 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; -import type { RemoteUser } from '@/models/entities/User.js'; +import type { UsersRepository, PollsRepository, PollVotesRepository } from '@/models/_.js'; +import type { MiRemoteUser } from '@/models/User.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; @@ -71,9 +76,8 @@ export const paramDef = { // TODO: ロジックをサービスに切り出す -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -159,7 +163,7 @@ export default class extends Endpoint { // リモート投票の場合リプライ送信 if (note.userHost != null) { - const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as RemoteUser; + const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as MiRemoteUser; this.queueService.deliver(me, this.apRendererService.addContext(await this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox, false); } diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index 4772c4f809..a2c1778199 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -1,10 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { NoteReactionsRepository } from '@/models/index.js'; -import type { NoteReaction } from '@/models/entities/NoteReaction.js'; +import { Brackets, type FindOptionsWhere } from 'typeorm'; +import type { NoteReactionsRepository } from '@/models/_.js'; +import type { MiNoteReaction } from '@/models/NoteReaction.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js'; import { DI } from '@/di-symbols.js'; -import type { FindOptionsWhere } from 'typeorm'; +import { QueryService } from '@/core/QueryService.js'; export const meta = { tags: ['notes', 'reactions'], @@ -39,44 +45,36 @@ export const paramDef = { noteId: { type: 'string', format: 'misskey:id' }, type: { type: 'string', nullable: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - offset: { type: 'integer', default: 0 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, }, required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.noteReactionsRepository) private noteReactionsRepository: NoteReactionsRepository, private noteReactionEntityService: NoteReactionEntityService, + private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = { - noteId: ps.noteId, - } as FindOptionsWhere; + const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'), ps.sinceId, ps.untilId) + .andWhere('reaction.noteId = :noteId', { noteId: ps.noteId }) + .leftJoinAndSelect('reaction.user', 'user') + .leftJoinAndSelect('reaction.note', 'note'); if (ps.type) { // ローカルリアクションはホスト名が . とされているが // DB 上ではそうではないので、必要に応じて変換 const suffix = '@.:'; const type = ps.type.endsWith(suffix) ? ps.type.slice(0, ps.type.length - suffix.length) + ':' : ps.type; - query.reaction = type; + query.andWhere('reaction.reaction = :type', { type }); } - const reactions = await this.noteReactionsRepository.find({ - where: query, - take: ps.limit, - skip: ps.offset, - order: { - id: -1, - }, - relations: ['user', 'note'], - }); + const reactions = await query.limit(ps.limit).getMany(); return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me))); }); 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 97cb026779..ff22ef1322 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; @@ -43,9 +48,8 @@ export const paramDef = { required: ['noteId', 'reaction'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private getterService: GetterService, private reactionService: ReactionService, 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 207f0b4cf2..b43ab044fa 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -41,9 +46,8 @@ export const paramDef = { required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private getterService: GetterService, private reactionService: ReactionService, diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index d406855660..9f16181a30 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -42,9 +47,8 @@ export const paramDef = { required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -71,7 +75,7 @@ export default class extends Endpoint { if (me) this.queryService.generateMutedUserQuery(query, me); if (me) this.queryService.generateBlockedUserQuery(query, me); - const renotes = await query.take(ps.limit).getMany(); + const renotes = await query.limit(ps.limit).getMany(); return await this.noteEntityService.packMany(renotes, me); }); diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts index f2af71d55f..70142c9818 100644 --- a/packages/backend/src/server/api/endpoints/notes/replies.ts +++ b/packages/backend/src/server/api/endpoints/notes/replies.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -32,9 +37,8 @@ export const paramDef = { required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -55,7 +59,7 @@ export default class extends Endpoint { if (me) this.queryService.generateMutedUserQuery(query, me); if (me) this.queryService.generateBlockedUserQuery(query, me); - const timeline = await query.take(ps.limit).getMany(); + const timeline = await query.limit(ps.limit).getMany(); return await this.noteEntityService.packMany(timeline, me); }); 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 742df0ca95..b00f5207d8 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 @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/_.js'; import { safeForSql } from '@/misc/safe-for-sql.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -58,9 +63,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -130,7 +134,7 @@ export default class extends Endpoint { } // Search notes - const notes = await query.take(ps.limit).getMany(); + const notes = await query.limit(ps.limit).getMany(); return await this.noteEntityService.packMany(notes, me); }); diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index f6385400c3..4425d4593c 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -1,10 +1,12 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { SearchService } from '@/core/SearchService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import type { Config } from '@/config.js'; -import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; @@ -52,13 +54,9 @@ export const paramDef = { // TODO: ロジックをサービスに切り出す -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.config) - private config: Config, - private noteEntityService: NoteEntityService, private searchService: SearchService, private roleService: RoleService, @@ -68,7 +66,7 @@ export default class extends Endpoint { if (!policies.canSearchNotes) { throw new ApiError(meta.errors.unavailable); } - + const notes = await this.searchService.searchNote(ps.query, me, { userId: ps.userId, channelId: ps.channelId, diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index 6b1b84a18e..5bb8196543 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -1,10 +1,13 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['notes'], @@ -34,13 +37,9 @@ export const paramDef = { required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - private noteEntityService: NoteEntityService, private getterService: GetterService, ) { diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts index 93517ab10c..b5fd47723c 100644 --- a/packages/backend/src/server/api/endpoints/notes/state.ts +++ b/packages/backend/src/server/api/endpoints/notes/state.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, NoteThreadMutingsRepository, NoteFavoritesRepository } from '@/models/index.js'; +import type { NotesRepository, NoteThreadMutingsRepository, NoteFavoritesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -32,9 +37,8 @@ export const paramDef = { required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts index abea069da8..449a838604 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; -import type { NotesRepository, NoteThreadMutingsRepository } from '@/models/index.js'; +import type { NotesRepository, NoteThreadMutingsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; @@ -37,9 +42,8 @@ export const paramDef = { required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts index 30016d48bc..d3f1787ee4 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { NoteThreadMutingsRepository } from '@/models/index.js'; +import type { NoteThreadMutingsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; @@ -29,9 +34,8 @@ export const paramDef = { required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.noteThreadMutingsRepository) private noteThreadMutingsRepository: NoteThreadMutingsRepository, diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index e1f286439b..042115ab84 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, FollowingsRepository } from '@/models/index.js'; +import type { NotesRepository, FollowingsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; @@ -41,9 +46,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -65,7 +69,8 @@ export default class extends Endpoint { //#region Construct query const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで + // パフォーマンス上の利点が無さそう? + //.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') @@ -123,7 +128,7 @@ export default class extends Endpoint { } //#endregion - const timeline = await query.take(ps.limit).getMany(); + const timeline = await query.limit(ps.limit).getMany(); process.nextTick(() => { this.activeUsersChart.read(me); diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 66655234a1..00cb9a0a28 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -1,9 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { URLSearchParams } from 'node:url'; -import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { Config } from '@/config.js'; -import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; @@ -38,16 +40,9 @@ export const paramDef = { required: ['noteId', 'targetLang'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.config) - private config: Config, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - private noteEntityService: NoteEntityService, private getterService: GetterService, private metaService: MetaService, diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts index 74e459b426..f67e9365fc 100644 --- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts +++ b/packages/backend/src/server/api/endpoints/notes/unrenote.ts @@ -1,11 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, NotesRepository } from '@/models/index.js'; +import type { UsersRepository, NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteDeleteService } from '@/core/NoteDeleteService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['notes'], @@ -37,9 +42,8 @@ export const paramDef = { required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index afc9bc4213..6932073791 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; +import type { NotesRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -53,9 +58,8 @@ export const paramDef = { required: ['listId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -91,6 +95,10 @@ export default class extends Endpoint { .andWhere('userListJoining.userListId = :userListId', { userListId: list.id }); this.queryService.generateVisibilityQuery(query, me); + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateMutedNoteQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserRenotesQueryForNotes(query, me); if (ps.includeMyRenotes === false) { query.andWhere(new Brackets(qb => { @@ -127,7 +135,7 @@ export default class extends Endpoint { } //#endregion - const timeline = await query.take(ps.limit).getMany(); + const timeline = await query.limit(ps.limit).getMany(); this.activeUsersChart.read(me); diff --git a/packages/backend/src/server/api/endpoints/notifications/create.ts b/packages/backend/src/server/api/endpoints/notifications/create.ts index 4102a924ad..268628cf76 100644 --- a/packages/backend/src/server/api/endpoints/notifications/create.ts +++ b/packages/backend/src/server/api/endpoints/notifications/create.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NotificationService } from '@/core/NotificationService.js'; @@ -9,6 +14,11 @@ export const meta = { kind: 'write:notifications', + limit: { + duration: 1000 * 60, + max: 10, + }, + errors: { }, } as const; @@ -23,9 +33,8 @@ export const paramDef = { required: ['body'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private notificationService: NotificationService, ) { diff --git a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts index e601bf9d5b..dc092c1f3a 100644 --- a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts @@ -1,6 +1,10 @@ -import { Inject, Injectable } from '@nestjs/common'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; import { NotificationService } from '@/core/NotificationService.js'; export const meta = { @@ -17,9 +21,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private notificationService: NotificationService, ) { diff --git a/packages/backend/src/server/api/endpoints/notifications/test-notification.ts b/packages/backend/src/server/api/endpoints/notifications/test-notification.ts new file mode 100644 index 0000000000..8f5f8485c3 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notifications/test-notification.ts @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NotificationService } from '@/core/NotificationService.js'; + +export const meta = { + tags: ['notifications'], + + requireCredential: true, + + kind: 'write:notifications', + + limit: { + duration: 1000 * 60, + max: 10, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private notificationService: NotificationService, + ) { + super(meta, paramDef, async (ps, user) => { + this.notificationService.createNotification(user.id, 'test', {}); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/page-push.ts b/packages/backend/src/server/api/endpoints/page-push.ts index 1d6fb567f0..0a68516586 100644 --- a/packages/backend/src/server/api/endpoints/page-push.ts +++ b/packages/backend/src/server/api/endpoints/page-push.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { PagesRepository } from '@/models/index.js'; +import type { PagesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -29,9 +34,8 @@ export const paramDef = { required: ['pageId', 'event'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index e08ab399f8..c0e8fab16c 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import type { DriveFilesRepository, PagesRepository } from '@/models/index.js'; +import type { DriveFilesRepository, PagesRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; -import { Page } from '@/models/entities/Page.js'; +import { MiPage } from '@/models/Page.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -63,9 +68,8 @@ export const paramDef = { required: ['title', 'name', 'content', 'variables', 'script'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, @@ -98,7 +102,7 @@ export default class extends Endpoint { } }); - const page = await this.pagesRepository.insert(new Page({ + const page = await this.pagesRepository.insert(new MiPage({ id: this.idService.genId(), createdAt: new Date(), updatedAt: new Date(), diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts index e64733131c..1291c0d209 100644 --- a/packages/backend/src/server/api/endpoints/pages/delete.ts +++ b/packages/backend/src/server/api/endpoints/pages/delete.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { PagesRepository } from '@/models/index.js'; +import type { PagesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -34,9 +39,8 @@ export const paramDef = { required: ['pageId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, diff --git a/packages/backend/src/server/api/endpoints/pages/featured.ts b/packages/backend/src/server/api/endpoints/pages/featured.ts index 31844165e2..1f43d6606c 100644 --- a/packages/backend/src/server/api/endpoints/pages/featured.ts +++ b/packages/backend/src/server/api/endpoints/pages/featured.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { PagesRepository } from '@/models/index.js'; +import type { PagesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -26,9 +31,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, @@ -41,7 +45,7 @@ export default class extends Endpoint { .andWhere('page.likedCount > 0') .orderBy('page.likedCount', 'DESC'); - const pages = await query.take(10).getMany(); + const pages = await query.limit(10).getMany(); return await this.pageEntityService.packMany(pages, me); }); diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts index 543c126d9c..6c69cad9d5 100644 --- a/packages/backend/src/server/api/endpoints/pages/like.ts +++ b/packages/backend/src/server/api/endpoints/pages/like.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { PagesRepository, PageLikesRepository } from '@/models/index.js'; +import type { PagesRepository, PageLikesRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -43,9 +48,8 @@ export const paramDef = { required: ['pageId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, @@ -66,12 +70,14 @@ export default class extends Endpoint { } // if already liked - const exist = await this.pageLikesRepository.findOneBy({ - pageId: page.id, - userId: me.id, + const exist = await this.pageLikesRepository.exist({ + where: { + pageId: page.id, + userId: me.id, + }, }); - if (exist != null) { + if (exist) { throw new ApiError(meta.errors.alreadyLiked); } diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts index bf2b2a431e..efb0bd0677 100644 --- a/packages/backend/src/server/api/endpoints/pages/show.ts +++ b/packages/backend/src/server/api/endpoints/pages/show.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, PagesRepository } from '@/models/index.js'; -import type { Page } from '@/models/entities/Page.js'; +import type { UsersRepository, PagesRepository } from '@/models/_.js'; +import type { MiPage } from '@/models/Page.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -40,9 +45,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -53,7 +57,7 @@ export default class extends Endpoint { private pageEntityService: PageEntityService, ) { super(meta, paramDef, async (ps, me) => { - let page: Page | null = null; + let page: MiPage | null = null; if (ps.pageId) { page = await this.pagesRepository.findOneBy({ id: ps.pageId }); diff --git a/packages/backend/src/server/api/endpoints/pages/unlike.ts b/packages/backend/src/server/api/endpoints/pages/unlike.ts index f0c0198460..7a76cd7408 100644 --- a/packages/backend/src/server/api/endpoints/pages/unlike.ts +++ b/packages/backend/src/server/api/endpoints/pages/unlike.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { PagesRepository, PageLikesRepository } from '@/models/index.js'; +import type { PagesRepository, PageLikesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -36,9 +41,8 @@ export const paramDef = { required: ['pageId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index 751274067e..aaea1efa87 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; import { Not } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { PagesRepository, DriveFilesRepository } from '@/models/index.js'; +import type { PagesRepository, DriveFilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -68,9 +73,8 @@ export const paramDef = { required: ['pageId', 'title', 'name', 'content', 'variables', 'script'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, @@ -112,13 +116,17 @@ export default class extends Endpoint { await this.pagesRepository.update(page.id, { updatedAt: new Date(), title: ps.title, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing name: ps.name === undefined ? page.name : ps.name, summary: ps.summary === undefined ? page.summary : ps.summary, content: ps.content, variables: ps.variables, script: ps.script, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing font: ps.font === undefined ? page.font : ps.font, eyeCatchingImageId: ps.eyeCatchingImageId === null ? null diff --git a/packages/backend/src/server/api/endpoints/ping.ts b/packages/backend/src/server/api/endpoints/ping.ts index 5807bf101e..ee2fe48834 100644 --- a/packages/backend/src/server/api/endpoints/ping.ts +++ b/packages/backend/src/server/api/endpoints/ping.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -24,9 +29,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( ) { super(meta, paramDef, async () => { diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index f2c6e798ef..390042c815 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/_.js'; import * as Acct from '@/misc/acct.js'; -import type { User } from '@/models/entities/User.js'; +import type { MiUser } from '@/models/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { MetaService } from '@/core/MetaService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -30,9 +35,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -48,7 +52,7 @@ export default class extends Endpoint { host: acct.host ?? IsNull(), }))); - return await this.userEntityService.packMany(users.filter(x => x !== null) as User[], me, { detail: true }); + return await this.userEntityService.packMany(users.filter(x => x !== null) as MiUser[], me, { detail: true }); }); } } diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts index 90febdbce7..b197756acc 100644 --- a/packages/backend/src/server/api/endpoints/promo/read.ts +++ b/packages/backend/src/server/api/endpoints/promo/read.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { PromoReadsRepository } from '@/models/index.js'; +import type { PromoReadsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['notes'], @@ -28,9 +33,8 @@ export const paramDef = { required: ['noteId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.promoReadsRepository) private promoReadsRepository: PromoReadsRepository, @@ -44,12 +48,14 @@ export default class extends Endpoint { throw err; }); - const exist = await this.promoReadsRepository.findOneBy({ - noteId: note.id, - userId: me.id, + const exist = await this.promoReadsRepository.exist({ + where: { + noteId: note.id, + userId: me.id, + }, }); - if (exist != null) { + if (exist) { return; } diff --git a/packages/backend/src/server/api/endpoints/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts index beb5850d78..3c9d266e21 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/create.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/create.ts @@ -1,10 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; -import type { RenoteMutingsRepository } from '@/models/index.js'; -import type { RenoteMuting } from '@/models/entities/RenoteMuting.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; +import type { RenoteMutingsRepository } from '@/models/_.js'; +import type { MiRenoteMuting } from '@/models/RenoteMuting.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; @@ -51,14 +55,12 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.renoteMutingsRepository) private renoteMutingsRepository: RenoteMutingsRepository, - private globalEventService: GlobalEventService, private getterService: GetterService, private idService: IdService, ) { @@ -92,7 +94,7 @@ export default class extends Endpoint { createdAt: new Date(), muterId: muter.id, muteeId: mutee.id, - } as RenoteMuting); + } as MiRenoteMuting); }); } } diff --git a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts index 70901a1406..f4969896d9 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts @@ -1,7 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RenoteMutingsRepository } from '@/models/index.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; +import type { RenoteMutingsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; @@ -42,14 +46,12 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.renoteMutingsRepository) private renoteMutingsRepository: RenoteMutingsRepository, - private globalEventService: GlobalEventService, private getterService: GetterService, ) { super(meta, paramDef, async (ps, me) => { diff --git a/packages/backend/src/server/api/endpoints/renote-mute/list.ts b/packages/backend/src/server/api/endpoints/renote-mute/list.ts index b2d7addb64..493593ae2d 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/list.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/list.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RenoteMutingsRepository } from '@/models/index.js'; +import type { RenoteMutingsRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { RenoteMutingEntityService } from '@/core/entities/RenoteMutingEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -33,9 +38,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.renoteMutingsRepository) private renoteMutingsRepository: RenoteMutingsRepository, @@ -48,7 +52,7 @@ export default class extends Endpoint { .andWhere('muting.muterId = :meId', { meId: me.id }); const mutings = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await this.renoteMutingEntityService.packMany(mutings, me); 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 3b6ebfe281..adb160c58b 100644 --- a/packages/backend/src/server/api/endpoints/request-reset-password.ts +++ b/packages/backend/src/server/api/endpoints/request-reset-password.ts @@ -1,13 +1,18 @@ -import rndstr from 'rndstr'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { PasswordResetRequestsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { PasswordResetRequestsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; 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 { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; export const meta = { tags: ['reset password'], @@ -35,13 +40,12 @@ export const paramDef = { required: ['username', 'email'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.config) private config: Config, - + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -77,7 +81,7 @@ export default class extends Endpoint { return; } - const token = rndstr('a-z0-9', 64); + const token = secureRndstr(64, { chars: L_CHARS }); await this.passwordResetRequestsRepository.insert({ id: this.idService.genId(), diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index 1d4825f812..0eeee81580 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import * as Redis from 'ioredis'; @@ -23,9 +28,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.db) private db: DataSource, diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts index e6f1af7b22..1858c922a0 100644 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/reset-password.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; -import type { UserProfilesRepository, PasswordResetRequestsRepository } from '@/models/index.js'; +import type { UserProfilesRepository, PasswordResetRequestsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -25,9 +30,8 @@ export const paramDef = { required: ['token', 'password'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.passwordResetRequestsRepository) private passwordResetRequestsRepository: PasswordResetRequestsRepository, diff --git a/packages/backend/src/server/api/endpoints/retention.ts b/packages/backend/src/server/api/endpoints/retention.ts index e9c0fd4dcd..dac6d65407 100644 --- a/packages/backend/src/server/api/endpoints/retention.ts +++ b/packages/backend/src/server/api/endpoints/retention.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { RetentionAggregationsRepository } from '@/models/index.js'; +import type { RetentionAggregationsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -21,9 +26,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.retentionAggregationsRepository) private retentionAggregationsRepository: RetentionAggregationsRepository, diff --git a/packages/backend/src/server/api/endpoints/roles/list.ts b/packages/backend/src/server/api/endpoints/roles/list.ts index d61c6b8dc6..d1de73ad32 100644 --- a/packages/backend/src/server/api/endpoints/roles/list.ts +++ b/packages/backend/src/server/api/endpoints/roles/list.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RolesRepository } from '@/models/index.js'; +import type { RolesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; @@ -18,9 +23,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, @@ -30,6 +34,7 @@ export default class extends Endpoint { super(meta, paramDef, async (ps, me) => { const roles = await this.rolesRepository.findBy({ isPublic: true, + isExplorable: true, }); return await this.roleEntityService.packMany(roles, me); }); diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index 42e36cb04a..6dc35907e1 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { NotesRepository, RolesRepository } from '@/models/index.js'; +import type { NotesRepository, RolesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -45,9 +50,8 @@ export const paramDef = { required: ['roleId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.redis) private redisClient: Redis.Redis, @@ -71,7 +75,7 @@ export default class extends Endpoint { if (role == null) { throw new ApiError(meta.errors.noSuchRole); } - if (!role.isExplorable) { + if (!role.isExplorable) { return []; } const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 diff --git a/packages/backend/src/server/api/endpoints/roles/show.ts b/packages/backend/src/server/api/endpoints/roles/show.ts index cc755dcc76..2afa0e7b7f 100644 --- a/packages/backend/src/server/api/endpoints/roles/show.ts +++ b/packages/backend/src/server/api/endpoints/roles/show.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { RolesRepository } from '@/models/index.js'; +import type { RolesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; @@ -27,9 +32,8 @@ export const paramDef = { required: ['roleId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts index 607dc24206..37aac908b5 100644 --- a/packages/backend/src/server/api/endpoints/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; -import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; +import type { RoleAssignmentsRepository, RolesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; @@ -32,9 +37,8 @@ export const paramDef = { required: ['roleId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.rolesRepository) private rolesRepository: RolesRepository, @@ -49,6 +53,7 @@ export default class extends Endpoint { const role = await this.rolesRepository.findOneBy({ id: ps.roleId, isPublic: true, + isExplorable: true, }); if (role == null) { @@ -64,7 +69,7 @@ export default class extends Endpoint { .innerJoinAndSelect('assign.user', 'user'); const assigns = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await Promise.all(assigns.map(async assign => ({ diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts index 1620e8ae52..c8cb63e6b3 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -1,10 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as os from 'node:os'; import si from 'systeminformation'; import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { MetaService } from '@/core/MetaService.js'; export const meta = { requireCredential: false, + allowGet: true, + cacheSec: 60 * 1, tags: ['meta'], } as const; @@ -15,12 +23,27 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + private metaService: MetaService, ) { super(meta, paramDef, async () => { + if (!(await this.metaService.fetch()).enableServerMachineStats) return { + machine: '?', + cpu: { + model: '?', + cores: 0, + }, + mem: { + total: 0, + }, + fs: { + total: 0, + used: 0, + }, + }; + const memStats = await si.mem(); const fsStats = await si.fsSize(); diff --git a/packages/backend/src/server/api/endpoints/stats.ts b/packages/backend/src/server/api/endpoints/stats.ts index 48a85758a0..05468240d3 100644 --- a/packages/backend/src/server/api/endpoints/stats.ts +++ b/packages/backend/src/server/api/endpoints/stats.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { InstancesRepository, NoteReactionsRepository, NotesRepository, UsersRepository } from '@/models/index.js'; +import type { InstancesRepository, NoteReactionsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import NotesChart from '@/core/chart/charts/notes.js'; @@ -52,16 +57,9 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index bfd5de7b00..5cfbeab73f 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { IdService } from '@/core/IdService.js'; -import type { SwSubscriptionsRepository } from '@/models/index.js'; +import type { SwSubscriptionsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; @@ -52,9 +57,8 @@ export const paramDef = { required: ['endpoint', 'auth', 'publickey'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, diff --git a/packages/backend/src/server/api/endpoints/sw/show-registration.ts b/packages/backend/src/server/api/endpoints/sw/show-registration.ts index bede10be5c..126299e3f7 100644 --- a/packages/backend/src/server/api/endpoints/sw/show-registration.ts +++ b/packages/backend/src/server/api/endpoints/sw/show-registration.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { SwSubscriptionsRepository } from '@/models/index.js'; +import type { SwSubscriptionsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -38,9 +43,8 @@ export const paramDef = { required: ['endpoint'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, diff --git a/packages/backend/src/server/api/endpoints/sw/unregister.ts b/packages/backend/src/server/api/endpoints/sw/unregister.ts index f12b98617d..f00fdd6697 100644 --- a/packages/backend/src/server/api/endpoints/sw/unregister.ts +++ b/packages/backend/src/server/api/endpoints/sw/unregister.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { SwSubscriptionsRepository } from '@/models/index.js'; +import type { SwSubscriptionsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; @@ -19,9 +24,8 @@ export const paramDef = { required: ['endpoint'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, diff --git a/packages/backend/src/server/api/endpoints/sw/update-registration.ts b/packages/backend/src/server/api/endpoints/sw/update-registration.ts index 9f08c8148d..a1a97df0be 100644 --- a/packages/backend/src/server/api/endpoints/sw/update-registration.ts +++ b/packages/backend/src/server/api/endpoints/sw/update-registration.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { SwSubscriptionsRepository } from '@/models/index.js'; +import type { SwSubscriptionsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; @@ -35,7 +40,7 @@ export const meta = { code: 'NO_SUCH_REGISTRATION', id: ' b09d8066-8064-5613-efb6-0e963b21d012', }, - } + }, } as const; export const paramDef = { @@ -47,9 +52,8 @@ export const paramDef = { required: ['endpoint'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, diff --git a/packages/backend/src/server/api/endpoints/test.ts b/packages/backend/src/server/api/endpoints/test.ts index c88f7f2daf..6d6d44f752 100644 --- a/packages/backend/src/server/api/endpoints/test.ts +++ b/packages/backend/src/server/api/endpoints/test.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -21,9 +26,8 @@ export const paramDef = { required: ['required'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( ) { super(meta, paramDef, async (ps, me) => { diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts index 6293c5cb50..e37df62c0c 100644 --- a/packages/backend/src/server/api/endpoints/username/available.ts +++ b/packages/backend/src/server/api/endpoints/username/available.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsedUsernamesRepository, UsersRepository } from '@/models/index.js'; +import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { localUsernameSchema } from '@/models/entities/User.js'; +import { localUsernameSchema } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; @@ -31,9 +36,8 @@ export const paramDef = { required: ['username'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts index 28cd9f6ce5..21c585f1ad 100644 --- a/packages/backend/src/server/api/endpoints/users.ts +++ b/packages/backend/src/server/api/endpoints/users.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/index.js'; +import type { UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -39,9 +44,8 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -80,8 +84,8 @@ export default class extends Endpoint { if (me) this.queryService.generateMutedUserQueryForUsers(query, me); if (me) this.queryService.generateBlockQueryForUsers(query, me); - query.take(ps.limit); - query.skip(ps.offset); + query.limit(ps.limit); + query.offset(ps.offset); const users = await query.getMany(); diff --git a/packages/backend/src/server/api/endpoints/users/achievements.ts b/packages/backend/src/server/api/endpoints/users/achievements.ts index 2a095d83ea..e4845d57bf 100644 --- a/packages/backend/src/server/api/endpoints/users/achievements.ts +++ b/packages/backend/src/server/api/endpoints/users/achievements.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserProfilesRepository } from '@/models/index.js'; +import type { UserProfilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -15,9 +20,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts index c5aa93baaf..725e07db39 100644 --- a/packages/backend/src/server/api/endpoints/users/clips.ts +++ b/packages/backend/src/server/api/endpoints/users/clips.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { ClipsRepository } from '@/models/index.js'; +import type { ClipsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; @@ -32,9 +37,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.clipsRepository) private clipsRepository: ClipsRepository, @@ -48,7 +52,7 @@ export default class extends Endpoint { .andWhere('clip.isPublic = true'); const clips = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await this.clipEntityService.packMany(clips, me); diff --git a/packages/backend/src/server/api/endpoints/users/flashs.ts b/packages/backend/src/server/api/endpoints/users/flashs.ts new file mode 100644 index 0000000000..18026dcefb --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/flashs.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; +import type { FlashsRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['users', 'flashs'], + + description: 'Show all flashs this user created.', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Flash', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.flashsRepository) + private flashsRepository: FlashsRepository, + + private flashEntityService: FlashEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.flashsRepository.createQueryBuilder('flash'), ps.sinceId, ps.untilId) + .andWhere('flash.userId = :userId', { userId: ps.userId }) + .andWhere('flash.visibility = \'public\''); + + const flashs = await query + .limit(ps.limit) + .getMany(); + + return await this.flashEntityService.packMany(flashs); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index 97f1310c36..b22fd2ff7a 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/index.js'; +import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; @@ -61,9 +66,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -97,11 +101,13 @@ export default class extends Endpoint { if (me == null) { throw new ApiError(meta.errors.forbidden); } else if (me.id !== user.id) { - const following = await this.followingsRepository.findOneBy({ - followeeId: user.id, - followerId: me.id, + const isFollowing = await this.followingsRepository.exist({ + where: { + followeeId: user.id, + followerId: me.id, + }, }); - if (following == null) { + if (!isFollowing) { throw new ApiError(meta.errors.forbidden); } } @@ -112,7 +118,7 @@ export default class extends Endpoint { .innerJoinAndSelect('following.follower', 'follower'); const followings = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await this.followingEntityService.packMany(followings, me, { populateFollower: true }); diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index d406594a2e..03487275a3 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/index.js'; +import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; @@ -61,9 +66,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -97,11 +101,13 @@ export default class extends Endpoint { if (me == null) { throw new ApiError(meta.errors.forbidden); } else if (me.id !== user.id) { - const following = await this.followingsRepository.findOneBy({ - followeeId: user.id, - followerId: me.id, + const isFollowing = await this.followingsRepository.exist({ + where: { + followeeId: user.id, + followerId: me.id, + }, }); - if (following == null) { + if (!isFollowing) { throw new ApiError(meta.errors.forbidden); } } @@ -112,7 +118,7 @@ export default class extends Endpoint { .innerJoinAndSelect('following.followee', 'followee'); const followings = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await this.followingEntityService.packMany(followings, me, { populateFollowee: true }); diff --git a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts index 6e57eee5fb..757af98e00 100644 --- a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryPostsRepository } from '@/models/index.js'; +import type { GalleryPostsRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -32,9 +37,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, @@ -47,7 +51,7 @@ export default class extends Endpoint { .andWhere('post.userId = :userId', { userId: ps.userId }); const posts = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await this.galleryPostEntityService.packMany(posts, me); diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts index 09f6acde9c..d6fb65cecb 100644 --- a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts +++ b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts @@ -1,12 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Not, In, IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { maximum } from '@/misc/prelude/array.js'; -import type { NotesRepository, UsersRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['users'], @@ -53,13 +58,9 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts index 8591e4ab96..fd1bb48a4e 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js'; +import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; -import type { UserList } from '@/models/entities/UserList.js'; +import type { MiUserList } from '@/models/UserList.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; @@ -66,7 +71,7 @@ export const paramDef = { } as const; @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, @@ -84,24 +89,26 @@ export default class extends Endpoint { private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const list = await this.userListsRepository.findOneBy({ - id: ps.listId, - isPublic: true, + const listExist = await this.userListsRepository.exist({ + where: { + id: ps.listId, + isPublic: true, + }, }); - if (list === null) throw new ApiError(meta.errors.noSuchList); + if (!listExist) throw new ApiError(meta.errors.noSuchList); const currentCount = await this.userListsRepository.countBy({ userId: me.id, }); if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) { throw new ApiError(meta.errors.tooManyUserLists); } - + const userList = await this.userListsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), userId: me.id, name: ps.name, - } as UserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); + } as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); const users = (await this.userListJoiningsRepository.findBy({ userListId: ps.listId, @@ -114,20 +121,24 @@ export default class extends Endpoint { }); if (currentUser.id !== me.id) { - const block = await this.blockingsRepository.findOneBy({ - blockerId: currentUser.id, - blockeeId: me.id, + const blockExist = await this.blockingsRepository.exist({ + where: { + blockerId: currentUser.id, + blockeeId: me.id, + }, }); - if (block) { + if (blockExist) { throw new ApiError(meta.errors.youHaveBeenBlocked); } } - const exist = await this.userListJoiningsRepository.findOneBy({ - userListId: userList.id, - userId: currentUser.id, + const exist = await this.userListJoiningsRepository.exist({ + where: { + userListId: userList.id, + userId: currentUser.id, + }, }); - + if (exist) { throw new ApiError(meta.errors.alreadyAdded); } diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts index 7510889526..60b2b3f17e 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository } from '@/models/index.js'; +import type { UserListsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; -import type { UserList } from '@/models/entities/UserList.js'; +import type { MiUserList } from '@/models/UserList.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -42,9 +47,8 @@ export const paramDef = { required: ['name'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, @@ -66,7 +70,7 @@ export default class extends Endpoint { createdAt: new Date(), userId: me.id, name: ps.name, - } as UserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); + } as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); return await this.userListEntityService.pack(userList); }); diff --git a/packages/backend/src/server/api/endpoints/users/lists/delete.ts b/packages/backend/src/server/api/endpoints/users/lists/delete.ts index 237cb075ab..763f5afd9d 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/delete.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository } from '@/models/index.js'; +import type { UserListsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -30,9 +35,8 @@ export const paramDef = { required: ['listId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, diff --git a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts index 263852fde1..1707afee60 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserListFavoritesRepository, UserListsRepository } from '@/models/index.js'; +import type { UserListFavoritesRepository, UserListsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { ApiError } from '@/server/api/error.js'; import { DI } from '@/di-symbols.js'; @@ -41,21 +46,25 @@ export default class extends Endpoint { private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - const userList = await this.userListsRepository.findOneBy({ - id: ps.listId, - isPublic: true, + const userListExist = await this.userListsRepository.exist({ + where: { + id: ps.listId, + isPublic: true, + }, }); - if (userList === null) { + if (!userListExist) { throw new ApiError(meta.errors.noSuchList); } - const exist = await this.userListFavoritesRepository.findOneBy({ - userId: me.id, - userListId: ps.listId, + const exist = await this.userListFavoritesRepository.exist({ + where: { + userId: me.id, + userListId: ps.listId, + }, }); - if (exist !== null) { + if (exist) { throw new ApiError(meta.errors.alreadyFavorited); } diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts index eab29944b2..0e86dd3a68 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/list.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository, UsersRepository } from '@/models/index.js'; +import type { UserListsRepository, UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; import { ApiError } from '@/server/api/error.js'; diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts index d50b70efc2..0b01061740 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; +import type { UserListsRepository, UserListJoiningsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GetterService } from '@/server/api/GetterService.js'; @@ -42,9 +47,8 @@ export const paramDef = { required: ['listId', 'userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, 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 925037e484..9bb1a71f58 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; -import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js'; +import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { UserListService } from '@/core/UserListService.js'; @@ -65,9 +70,8 @@ export const paramDef = { required: ['listId', 'userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, @@ -100,18 +104,22 @@ export default class extends Endpoint { // Check blocking if (user.id !== me.id) { - const block = await this.blockingsRepository.findOneBy({ - blockerId: user.id, - blockeeId: me.id, + const blockExist = await this.blockingsRepository.exist({ + where: { + blockerId: user.id, + blockeeId: me.id, + }, }); - if (block) { + if (blockExist) { throw new ApiError(meta.errors.youHaveBeenBlocked); } } - const exist = await this.userListJoiningsRepository.findOneBy({ - userListId: userList.id, - userId: user.id, + const exist = await this.userListJoiningsRepository.exist({ + where: { + userListId: userList.id, + userId: user.id, + }, }); if (exist) { diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts index 8077841c8c..df44870b04 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository, UserListFavoritesRepository } from '@/models/index.js'; +import type { UserListsRepository, UserListFavoritesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -69,10 +74,12 @@ export default class extends Endpoint { userListId: ps.listId, }); if (me !== null) { - additionalProperties.isLiked = (await this.userListFavoritesRepository.findOneBy({ - userId: me.id, - userListId: ps.listId, - }) !== null); + additionalProperties.isLiked = await this.userListFavoritesRepository.exist({ + where: { + userId: me.id, + userListId: ps.listId, + }, + }); } else { additionalProperties.isLiked = false; } diff --git a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts index be8e317816..23611ab8c4 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserListFavoritesRepository, UserListsRepository } from '@/models/index.js'; +import type { UserListFavoritesRepository, UserListsRepository } from '@/models/_.js'; import { ApiError } from '@/server/api/error.js'; import { DI } from '@/di-symbols.js'; @@ -39,12 +44,14 @@ export default class extends Endpoint { private userListFavoritesRepository: UserListFavoritesRepository, ) { super(meta, paramDef, async (ps, me) => { - const userList = await this.userListsRepository.findOneBy({ - id: ps.listId, - isPublic: true, + const userListExist = await this.userListsRepository.exist({ + where: { + id: ps.listId, + isPublic: true, + }, }); - if (userList === null) { + if (!userListExist) { throw new ApiError(meta.errors.noSuchList); } diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts index b0a95a2f28..eb6cfbaf26 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository } from '@/models/index.js'; +import type { UserListsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -39,9 +44,8 @@ export const paramDef = { required: ['listId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index aaf94734a3..5934baef47 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/index.js'; +import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -52,9 +57,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -76,9 +80,15 @@ export default class extends Endpoint { .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('note.channel', 'channel') .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); + query.andWhere(new Brackets(qb => { + qb.orWhere('note.channelId IS NULL'); + qb.orWhere('channel.isSensitive = false'); + })); + this.queryService.generateVisibilityQuery(query, me); if (me) { this.queryService.generateMutedUserQuery(query, me, user); @@ -120,7 +130,7 @@ export default class extends Endpoint { //#endregion - const timeline = await query.take(ps.limit).getMany(); + const timeline = await query.limit(ps.limit).getMany(); return await this.noteEntityService.packMany(timeline, me); }); diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts index a105103f16..cf2f274c70 100644 --- a/packages/backend/src/server/api/endpoints/users/pages.ts +++ b/packages/backend/src/server/api/endpoints/users/pages.ts @@ -1,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; -import type { PagesRepository } from '@/models/index.js'; +import type { PagesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -32,9 +37,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, @@ -48,7 +52,7 @@ export default class extends Endpoint { .andWhere('page.visibility = \'public\''); const pages = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await this.pageEntityService.packMany(pages); diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index ac401a60ee..372ab80c4c 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UserProfilesRepository, NoteReactionsRepository } from '@/models/index.js'; +import type { UserProfilesRepository, NoteReactionsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js'; @@ -45,9 +50,8 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, @@ -73,7 +77,7 @@ export default class extends Endpoint { this.queryService.generateVisibilityQuery(query, me); const reactions = await query - .take(ps.limit) + .limit(ps.limit) .getMany(); return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me, { withNote: true }))); diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts index 5498b8c854..1b30e99b15 100644 --- a/packages/backend/src/server/api/endpoints/users/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; +import type { UsersRepository, FollowingsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -35,16 +40,15 @@ export const paramDef = { required: [], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - + private userEntityService: UserEntityService, private queryService: QueryService, ) { @@ -70,7 +74,7 @@ export default class extends Endpoint { query.setParameters(followingQuery.getParameters()); - const users = await query.take(ps.limit).skip(ps.offset).getMany(); + const users = await query.limit(ps.limit).offset(ps.offset).getMany(); return await this.userEntityService.packMany(users, me, { detail: true }); }); diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts index 3267c18846..326042ed3d 100644 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -1,8 +1,11 @@ -import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/index.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { DI } from '@/di-symbols.js'; export const meta = { tags: ['users'], @@ -122,13 +125,9 @@ export const paramDef = { required: ['userId'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, me) => { diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index d19d4007d6..50aa6fa09e 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -1,6 +1,11 @@ -import * as sanitizeHtml from 'sanitize-html'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import sanitizeHtml from 'sanitize-html'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, AbuseUserReportsRepository } from '@/models/index.js'; +import type { AbuseUserReportsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -48,13 +53,9 @@ export const paramDef = { required: ['userId', 'comment'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.abuseUserReportsRepository) private abuseUserReportsRepository: AbuseUserReportsRepository, 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 b001159ee8..74408cc64a 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,8 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; +import type { UsersRepository, FollowingsRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; -import type { User } from '@/models/entities/User.js'; +import type { MiUser } from '@/models/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -41,9 +46,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.config) private config: Config, @@ -77,7 +81,7 @@ export default class extends Endpoint { const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 - let users: User[] = []; + let users: MiUser[] = []; if (me) { const followingQuery = this.followingsRepository.createQueryBuilder('following') @@ -97,7 +101,7 @@ export default class extends Endpoint { users = await query .orderBy('user.usernameLower', 'ASC') - .take(ps.limit) + .limit(ps.limit) .getMany(); if (users.length < ps.limit) { @@ -110,7 +114,7 @@ export default class extends Endpoint { const otherUsers = await otherQuery .orderBy('user.updatedAt', 'DESC') - .take(ps.limit - users.length) + .limit(ps.limit - users.length) .getMany(); users = users.concat(otherUsers); @@ -122,7 +126,7 @@ export default class extends Endpoint { users = await query .orderBy('user.updatedAt', 'DESC') - .take(ps.limit - users.length) + .limit(ps.limit - users.length) .getMany(); } diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index d7a60f0437..aff5b98779 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, UserProfilesRepository } from '@/models/index.js'; -import type { User } from '@/models/entities/User.js'; +import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; +import type { MiUser } from '@/models/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -37,9 +42,8 @@ export const paramDef = { required: ['query'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -52,9 +56,10 @@ export default class extends Endpoint { super(meta, paramDef, async (ps, me) => { const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 + ps.query = ps.query.trim(); const isUsername = ps.query.startsWith('@'); - let users: User[] = []; + let users: MiUser[] = []; if (isUsername) { const usernameQuery = this.usersRepository.createQueryBuilder('user') @@ -73,12 +78,12 @@ export default class extends Endpoint { users = await usernameQuery .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') - .take(ps.limit) - .skip(ps.offset) + .limit(ps.limit) + .offset(ps.offset) .getMany(); } else { const nameQuery = this.usersRepository.createQueryBuilder('user') - .where(new Brackets(qb => { + .where(new Brackets(qb => { qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); // Also search username if it qualifies as username @@ -100,8 +105,8 @@ export default class extends Endpoint { users = await nameQuery .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') - .take(ps.limit) - .skip(ps.offset) + .limit(ps.limit) + .offset(ps.offset) .getMany(); if (users.length < ps.limit) { @@ -126,8 +131,8 @@ export default class extends Endpoint { users = users.concat(await query .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') - .take(ps.limit) - .skip(ps.offset) + .limit(ps.limit) + .offset(ps.offset) .getMany(), ); } diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index ba432c273b..389497301d 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { In, IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/index.js'; -import type { User } from '@/models/entities/User.js'; +import type { UsersRepository } from '@/models/_.js'; +import type { MiUser } from '@/models/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; @@ -74,9 +79,8 @@ export const paramDef = { ], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -91,6 +95,7 @@ export default class extends Endpoint { let user; const isModerator = await this.roleService.isModerator(me); + ps.username = ps.username?.trim(); if (ps.userIds) { if (ps.userIds.length === 0) { @@ -105,7 +110,7 @@ export default class extends Endpoint { }); // リクエストされた通りに並べ替え - const _users: User[] = []; + const _users: MiUser[] = []; for (const id of ps.userIds) { _users.push(users.find(x => x.id === id)!); } @@ -121,7 +126,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.failedToResolveRemoteUser); }); } else { - const q: FindOptionsWhere = ps.userId != null + const q: FindOptionsWhere = ps.userId != null ? { id: ps.userId } : { usernameLower: ps.username!.toLowerCase(), host: IsNull() }; diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts deleted file mode 100644 index 7479793afe..0000000000 --- a/packages/backend/src/server/api/endpoints/users/stats.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { awaitAll } from '@/misc/prelude/await-all.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; -import { DI } from '@/di-symbols.js'; -import type { UsersRepository, NotesRepository, FollowingsRepository, DriveFilesRepository, NoteReactionsRepository, PageLikesRepository, NoteFavoritesRepository, PollVotesRepository } from '@/models/index.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['users'], - - requireCredential: false, - - description: 'Show statistics about a user.', - - errors: { - noSuchUser: { - message: 'No such user.', - code: 'NO_SUCH_USER', - id: '9e638e45-3b25-4ef7-8f95-07e8498f1819', - }, - }, - - res: { - type: 'object', - optional: false, nullable: false, - properties: { - notesCount: { - type: 'integer', - optional: false, nullable: false, - }, - repliesCount: { - type: 'integer', - optional: false, nullable: false, - }, - renotesCount: { - type: 'integer', - optional: false, nullable: false, - }, - repliedCount: { - type: 'integer', - optional: false, nullable: false, - }, - renotedCount: { - type: 'integer', - optional: false, nullable: false, - }, - pollVotesCount: { - type: 'integer', - optional: false, nullable: false, - }, - pollVotedCount: { - type: 'integer', - optional: false, nullable: false, - }, - localFollowingCount: { - type: 'integer', - optional: false, nullable: false, - }, - remoteFollowingCount: { - type: 'integer', - optional: false, nullable: false, - }, - localFollowersCount: { - type: 'integer', - optional: false, nullable: false, - }, - remoteFollowersCount: { - type: 'integer', - optional: false, nullable: false, - }, - followingCount: { - type: 'integer', - optional: false, nullable: false, - }, - followersCount: { - type: 'integer', - optional: false, nullable: false, - }, - sentReactionsCount: { - type: 'integer', - optional: false, nullable: false, - }, - receivedReactionsCount: { - type: 'integer', - optional: false, nullable: false, - }, - noteFavoritesCount: { - type: 'integer', - optional: false, nullable: false, - }, - pageLikesCount: { - type: 'integer', - optional: false, nullable: false, - }, - pageLikedCount: { - type: 'integer', - optional: false, nullable: false, - }, - driveFilesCount: { - type: 'integer', - optional: false, nullable: false, - }, - driveUsage: { - type: 'integer', - optional: false, nullable: false, - description: 'Drive usage in bytes', - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, - - @Inject(DI.noteReactionsRepository) - private noteReactionsRepository: NoteReactionsRepository, - - @Inject(DI.pageLikesRepository) - private pageLikesRepository: PageLikesRepository, - - @Inject(DI.noteFavoritesRepository) - private noteFavoritesRepository: NoteFavoritesRepository, - - @Inject(DI.pollVotesRepository) - private pollVotesRepository: PollVotesRepository, - - private driveFileEntityService: DriveFileEntityService, - ) { - super(meta, paramDef, async (ps, me) => { - const user = await this.usersRepository.findOneBy({ id: ps.userId }); - if (user == null) { - throw new ApiError(meta.errors.noSuchUser); - } - - const result = await awaitAll({ - notesCount: this.notesRepository.createQueryBuilder('note') - .where('note.userId = :userId', { userId: user.id }) - .getCount(), - repliesCount: this.notesRepository.createQueryBuilder('note') - .where('note.userId = :userId', { userId: user.id }) - .andWhere('note.replyId IS NOT NULL') - .getCount(), - renotesCount: this.notesRepository.createQueryBuilder('note') - .where('note.userId = :userId', { userId: user.id }) - .andWhere('note.renoteId IS NOT NULL') - .getCount(), - repliedCount: this.notesRepository.createQueryBuilder('note') - .where('note.replyUserId = :userId', { userId: user.id }) - .getCount(), - renotedCount: this.notesRepository.createQueryBuilder('note') - .where('note.renoteUserId = :userId', { userId: user.id }) - .getCount(), - pollVotesCount: this.pollVotesRepository.createQueryBuilder('vote') - .where('vote.userId = :userId', { userId: user.id }) - .getCount(), - pollVotedCount: this.pollVotesRepository.createQueryBuilder('vote') - .innerJoin('vote.note', 'note') - .where('note.userId = :userId', { userId: user.id }) - .getCount(), - localFollowingCount: this.followingsRepository.createQueryBuilder('following') - .where('following.followerId = :userId', { userId: user.id }) - .andWhere('following.followeeHost IS NULL') - .getCount(), - remoteFollowingCount: this.followingsRepository.createQueryBuilder('following') - .where('following.followerId = :userId', { userId: user.id }) - .andWhere('following.followeeHost IS NOT NULL') - .getCount(), - localFollowersCount: this.followingsRepository.createQueryBuilder('following') - .where('following.followeeId = :userId', { userId: user.id }) - .andWhere('following.followerHost IS NULL') - .getCount(), - remoteFollowersCount: this.followingsRepository.createQueryBuilder('following') - .where('following.followeeId = :userId', { userId: user.id }) - .andWhere('following.followerHost IS NOT NULL') - .getCount(), - sentReactionsCount: this.noteReactionsRepository.createQueryBuilder('reaction') - .where('reaction.userId = :userId', { userId: user.id }) - .getCount(), - receivedReactionsCount: this.noteReactionsRepository.createQueryBuilder('reaction') - .innerJoin('reaction.note', 'note') - .where('note.userId = :userId', { userId: user.id }) - .getCount(), - noteFavoritesCount: this.noteFavoritesRepository.createQueryBuilder('favorite') - .where('favorite.userId = :userId', { userId: user.id }) - .getCount(), - pageLikesCount: this.pageLikesRepository.createQueryBuilder('like') - .where('like.userId = :userId', { userId: user.id }) - .getCount(), - pageLikedCount: this.pageLikesRepository.createQueryBuilder('like') - .innerJoin('like.page', 'page') - .where('page.userId = :userId', { userId: user.id }) - .getCount(), - driveFilesCount: this.driveFilesRepository.createQueryBuilder('file') - .where('file.userId = :userId', { userId: user.id }) - .getCount(), - driveUsage: this.driveFileEntityService.calcDriveUsageOf(user), - }); - - return { - ...result, - followingCount: result.localFollowingCount + result.remoteFollowingCount, - followersCount: result.localFollowersCount + result.remoteFollowersCount, - }; - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/users/update-memo.ts b/packages/backend/src/server/api/endpoints/users/update-memo.ts index ca7756ef75..194d488052 100644 --- a/packages/backend/src/server/api/endpoints/users/update-memo.ts +++ b/packages/backend/src/server/api/endpoints/users/update-memo.ts @@ -1,7 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { IdService } from '@/core/IdService.js'; -import type { UserMemoRepository } from '@/models/index.js'; +import type { UserMemoRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; @@ -35,9 +40,8 @@ export const paramDef = { required: ['userId', 'memo'], } as const; -// eslint-disable-next-line import/no-default-export @Injectable() -export default class extends Endpoint { +export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( @Inject(DI.userMemosRepository) private userMemosRepository: UserMemoRepository, diff --git a/packages/backend/src/server/api/error.ts b/packages/backend/src/server/api/error.ts index 34f4521606..6506565a0d 100644 --- a/packages/backend/src/server/api/error.ts +++ b/packages/backend/src/server/api/error.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + type E = { message: string, code: string, id: string, kind?: 'client' | 'server' | 'permission', httpStatusCode?: number }; export class ApiError extends Error { diff --git a/packages/backend/src/server/api/openapi/OpenApiServerService.ts b/packages/backend/src/server/api/openapi/OpenApiServerService.ts index e804ba276c..cb22d0f7c9 100644 --- a/packages/backend/src/server/api/openapi/OpenApiServerService.ts +++ b/packages/backend/src/server/api/openapi/OpenApiServerService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { fileURLToPath } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import type { Config } from '@/config.js'; diff --git a/packages/backend/src/server/api/openapi/errors.ts b/packages/backend/src/server/api/openapi/errors.ts index d7f791c6da..84c3c638fa 100644 --- a/packages/backend/src/server/api/openapi/errors.ts +++ b/packages/backend/src/server/api/openapi/errors.ts @@ -1,3 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ export const errors = { '400': { diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index fa62480c02..4f972d3f7e 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import type { Config } from '@/config.js'; import endpoints from '../endpoints.js'; import { errors as basicErrors } from './errors.js'; diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts index 0cef361caf..0b9eb4fe24 100644 --- a/packages/backend/src/server/api/openapi/schemas.ts +++ b/packages/backend/src/server/api/openapi/schemas.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import type { Schema } from '@/misc/json-schema.js'; import { refs } from '@/misc/json-schema.js'; diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts index c77ba66028..8fd106c10c 100644 --- a/packages/backend/src/server/api/stream/ChannelsService.ts +++ b/packages/backend/src/server/api/stream/ChannelsService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; import { HybridTimelineChannelService } from './channels/hybrid-timeline.js'; @@ -52,7 +57,7 @@ export class ChannelsService { case 'serverStats': return this.serverStatsChannelService; case 'queueStats': return this.queueStatsChannelService; case 'admin': return this.adminChannelService; - + default: throw new Error(`no such channel: ${name}`); } diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/Connection.ts similarity index 93% rename from packages/backend/src/server/api/stream/index.ts rename to packages/backend/src/server/api/stream/Connection.ts index 8b1c2c09c9..fd91681fc1 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/Connection.ts @@ -1,12 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as WebSocket from 'ws'; -import type { User } from '@/models/entities/User.js'; -import type { AccessToken } from '@/models/entities/AccessToken.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiAccessToken } from '@/models/AccessToken.js'; import type { Packed } from '@/misc/json-schema.js'; import type { NoteReadService } from '@/core/NoteReadService.js'; import type { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; -import { UserProfile } from '@/models/index.js'; +import { MiUserProfile } from '@/models/_.js'; import type { ChannelsService } from './ChannelsService.js'; import type { EventEmitter } from 'events'; import type Channel from './channel.js'; @@ -15,21 +20,22 @@ import type { StreamEventEmitter, StreamMessages } from './types.js'; /** * Main stream connection */ +// eslint-disable-next-line import/no-default-export export default class Connection { - public user?: User; - public token?: AccessToken; + public user?: MiUser; + public token?: MiAccessToken; private wsConnection: WebSocket.WebSocket; public subscriber: StreamEventEmitter; private channels: Channel[] = []; private subscribingNotes: any = {}; private cachedNotes: Packed<'Note'>[] = []; - public userProfile: UserProfile | null = null; + public userProfile: MiUserProfile | null = null; public following: Set = new Set(); public followingChannels: Set = new Set(); public userIdsWhoMeMuting: Set = new Set(); public userIdsWhoBlockingMe: Set = new Set(); public userIdsWhoMeMutingRenotes: Set = new Set(); - private fetchIntervalId: NodeJS.Timer | null = null; + private fetchIntervalId: NodeJS.Timeout | null = null; constructor( private channelsService: ChannelsService, @@ -37,8 +43,8 @@ export default class Connection { private notificationService: NotificationService, private cacheService: CacheService, - user: User | null | undefined, - token: AccessToken | null | undefined, + user: MiUser | null | undefined, + token: MiAccessToken | null | undefined, ) { if (user) this.user = user; if (token) this.token = token; diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index e67aec9ecd..ad32d08fee 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -1,9 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { bindThis } from '@/decorators.js'; -import type Connection from '.'; +import type Connection from './Connection.js'; /** * Stream channel */ +// eslint-disable-next-line import/no-default-export export default abstract class Channel { protected connection: Connection; public id: string; diff --git a/packages/backend/src/server/api/stream/channels/admin.ts b/packages/backend/src/server/api/stream/channels/admin.ts index 157fcd6aa3..bfb36d9cb8 100644 --- a/packages/backend/src/server/api/stream/channels/admin.ts +++ b/packages/backend/src/server/api/stream/channels/admin.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + 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 d48dea7258..87648a3a77 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { isUserRelated } from '@/misc/is-user-related.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 9e5b40997b..a01714e76d 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; diff --git a/packages/backend/src/server/api/stream/channels/drive.ts b/packages/backend/src/server/api/stream/channels/drive.ts index 52bb29fabe..83f53c1836 100644 --- a/packages/backend/src/server/api/stream/channels/drive.ts +++ b/packages/backend/src/server/api/stream/channels/drive.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + 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 d3339072c1..a33f1a956a 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { checkWordMute } from '@/misc/check-word-mute.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index 0268fdedde..3945b1a1eb 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { isUserRelated } from '@/misc/is-user-related.js'; @@ -49,7 +54,7 @@ class HashtagChannel extends Channel { if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; - + if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; this.connection.cacheNote(note); 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 1755aa94cf..bd8888f679 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { checkWordMute } from '@/misc/check-word-mute.js'; import { isUserRelated } from '@/misc/is-user-related.js'; @@ -26,7 +31,7 @@ class HomeTimelineChannel extends Channel { @bindThis public async init(params: any) { this.withReplies = params.withReplies as boolean; - + this.subscriber.on('notesStream', this.onNote); } 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 5a33e13cf5..760fb8d19f 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { checkWordMute } from '@/misc/check-word-mute.js'; import { isUserRelated } from '@/misc/is-user-related.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 9ca4db8ced..f32f8c5cec 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { checkWordMute } from '@/misc/check-word-mute.js'; import { isUserRelated } from '@/misc/is-user-related.js'; diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts index 139320ce35..f969d02337 100644 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ b/packages/backend/src/server/api/stream/channels/main.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; 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 7f48c54999..f0dc472303 100644 --- a/packages/backend/src/server/api/stream/channels/queue-stats.ts +++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Xev from 'xev'; import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts index ab9c1aa0b5..76b5875343 100644 --- a/packages/backend/src/server/api/stream/channels/role-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts @@ -1,18 +1,23 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; import Channel from '../channel.js'; import { StreamMessages } from '../types.js'; -import { RoleService } from '@/core/RoleService.js'; class RoleTimelineChannel extends Channel { public readonly chName = 'roleTimeline'; public static shouldShare = false; public static requireCredential = false; private roleId: string; - + constructor( private noteEntityService: NoteEntityService, private roleservice: RoleService, 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 9eae0cf2d3..cacae275a8 100644 --- a/packages/backend/src/server/api/stream/channels/server-stats.ts +++ b/packages/backend/src/server/api/stream/channels/server-stats.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Xev from 'xev'; import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.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 8802fc5ab8..8bbba0b6db 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; -import type { UserListJoiningsRepository, UserListsRepository } from '@/models/index.js'; -import type { User } from '@/models/entities/User.js'; +import type { UserListJoiningsRepository, UserListsRepository } from '@/models/_.js'; +import type { MiUser } from '@/models/User.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -13,14 +18,14 @@ class UserListChannel extends Channel { public static shouldShare = false; public static requireCredential = false; private listId: string; - public listUsers: User['id'][] = []; - private listUsersClock: NodeJS.Timer; + public listUsers: MiUser['id'][] = []; + private listUsersClock: NodeJS.Timeout; constructor( private userListsRepository: UserListsRepository, private userListJoiningsRepository: UserListJoiningsRepository, private noteEntityService: NoteEntityService, - + id: string, connection: Channel['connection'], ) { @@ -34,11 +39,13 @@ class UserListChannel extends Channel { this.listId = params.listId as string; // Check existence and owner - const list = await this.userListsRepository.findOneBy({ - id: this.listId, - userId: this.user!.id, + const listExist = await this.userListsRepository.exist({ + where: { + id: this.listId, + userId: this.user!.id, + }, }); - if (!list) return; + if (!listExist) return; // Subscribe stream this.subscriber.on(`userListStream:${this.listId}`, this.send); diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts index d9dba682cd..90e0a61f26 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -1,48 +1,53 @@ -import type { Channel } from '@/models/entities/Channel.js'; -import type { User } from '@/models/entities/User.js'; -import type { UserProfile } from '@/models/entities/UserProfile.js'; -import type { Note } from '@/models/entities/Note.js'; -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 { AbuseUserReport } from '@/models/entities/AbuseUserReport.js'; -import type { Signin } from '@/models/entities/Signin.js'; -import type { Page } from '@/models/entities/Page.js'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { MiChannel } from '@/models/Channel.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiUserProfile } from '@/models/UserProfile.js'; +import type { MiNote } from '@/models/Note.js'; +import type { MiAntenna } from '@/models/Antenna.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { MiDriveFolder } from '@/models/DriveFolder.js'; +import type { MiUserList } from '@/models/UserList.js'; +import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; +import type { MiSignin } from '@/models/Signin.js'; +import type { MiPage } from '@/models/Page.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { Webhook } from '@/models/entities/Webhook.js'; -import type { Meta } from '@/models/entities/Meta.js'; -import { Role, RoleAssignment } from '@/models'; +import type { MiWebhook } from '@/models/Webhook.js'; +import type { MiMeta } from '@/models/Meta.js'; +import { MiRole, MiRoleAssignment } from '@/models/_.js'; import type Emitter from 'strict-event-emitter-types'; import type { EventEmitter } from 'events'; //#region Stream type-body definitions export interface InternalStreamTypes { - userChangeSuspendedState: { id: User['id']; isSuspended: User['isSuspended']; }; - userTokenRegenerated: { id: User['id']; oldToken: string; newToken: string; }; - remoteUserUpdated: { id: User['id']; }; - follow: { followerId: User['id']; followeeId: User['id']; }; - unfollow: { followerId: User['id']; followeeId: User['id']; }; - blockingCreated: { blockerId: User['id']; blockeeId: User['id']; }; - blockingDeleted: { blockerId: User['id']; blockeeId: User['id']; }; - policiesUpdated: Role['policies']; - roleCreated: Role; - roleDeleted: Role; - roleUpdated: Role; - userRoleAssigned: RoleAssignment; - userRoleUnassigned: RoleAssignment; - webhookCreated: Webhook; - webhookDeleted: Webhook; - webhookUpdated: Webhook; - antennaCreated: Antenna; - antennaDeleted: Antenna; - antennaUpdated: Antenna; - metaUpdated: Meta; - followChannel: { userId: User['id']; channelId: Channel['id']; }; - unfollowChannel: { userId: User['id']; channelId: Channel['id']; }; - updateUserProfile: UserProfile; - mute: { muterId: User['id']; muteeId: User['id']; }; - unmute: { muterId: User['id']; muteeId: User['id']; }; + userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; }; + userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; }; + remoteUserUpdated: { id: MiUser['id']; }; + follow: { followerId: MiUser['id']; followeeId: MiUser['id']; }; + unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; }; + blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; }; + blockingDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; }; + policiesUpdated: MiRole['policies']; + roleCreated: MiRole; + roleDeleted: MiRole; + roleUpdated: MiRole; + userRoleAssigned: MiRoleAssignment; + userRoleUnassigned: MiRoleAssignment; + webhookCreated: MiWebhook; + webhookDeleted: MiWebhook; + webhookUpdated: MiWebhook; + antennaCreated: MiAntenna; + antennaDeleted: MiAntenna; + antennaUpdated: MiAntenna; + metaUpdated: MiMeta; + followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; }; + unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; }; + updateUserProfile: MiUserProfile; + mute: { muterId: MiUser['id']; muteeId: MiUser['id']; }; + unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; }; } export interface BroadcastTypes { @@ -59,6 +64,9 @@ export interface BroadcastTypes { [other: string]: any; }[]; }; + announcementCreated: { + announcement: Packed<'Announcement'>; + }; } export interface MainStreamTypes { @@ -71,10 +79,10 @@ export interface MainStreamTypes { unfollow: Packed<'User'>; meUpdated: Packed<'User'>; pageEvent: { - pageId: Page['id']; + pageId: MiPage['id']; event: string; var: any; - userId: User['id']; + userId: MiUser['id']; user: Packed<'User'>; }; urlUploadFinished: { @@ -83,38 +91,41 @@ export interface MainStreamTypes { }; readAllNotifications: undefined; unreadNotification: Packed<'Notification'>; - unreadMention: Note['id']; + unreadMention: MiNote['id']; readAllUnreadMentions: undefined; - unreadSpecifiedNote: Note['id']; + unreadSpecifiedNote: MiNote['id']; readAllUnreadSpecifiedNotes: undefined; readAllAntennas: undefined; - unreadAntenna: Antenna; + unreadAntenna: MiAntenna; readAllAnnouncements: undefined; myTokenRegenerated: undefined; - signin: Signin; + signin: MiSignin; registryUpdated: { scope?: string[]; key: string; value: any | null; }; driveFileCreated: Packed<'DriveFile'>; - readAntenna: Antenna; + readAntenna: MiAntenna; receiveFollowRequest: Packed<'User'>; + announcementCreated: { + announcement: Packed<'Announcement'>; + }; } export interface DriveStreamTypes { fileCreated: Packed<'DriveFile'>; - fileDeleted: DriveFile['id']; + fileDeleted: MiDriveFile['id']; fileUpdated: Packed<'DriveFile'>; folderCreated: Packed<'DriveFolder'>; - folderDeleted: DriveFolder['id']; + folderDeleted: MiDriveFolder['id']; folderUpdated: Packed<'DriveFolder'>; } export interface NoteStreamTypes { pollVoted: { choice: number; - userId: User['id']; + userId: MiUser['id']; }; deleted: { deletedAt: Date; @@ -125,16 +136,16 @@ export interface NoteStreamTypes { name: string; url: string; } | null; - userId: User['id']; + userId: MiUser['id']; }; unreacted: { reaction: string; - userId: User['id']; + userId: MiUser['id']; }; } type NoteStreamEventTypes = { [key in keyof NoteStreamTypes]: { - id: Note['id']; + id: MiNote['id']; body: NoteStreamTypes[key]; }; }; @@ -145,7 +156,7 @@ export interface UserListStreamTypes { } export interface AntennaStreamTypes { - note: Note; + note: MiNote; } export interface RoleTimelineStreamTypes { @@ -154,9 +165,9 @@ export interface RoleTimelineStreamTypes { export interface AdminStreamTypes { newAbuseUserReport: { - id: AbuseUserReport['id']; - targetUserId: User['id'], - reporterId: User['id'], + id: MiAbuseUserReport['id']; + targetUserId: MiUser['id'], + reporterId: MiUser['id'], comment: string; }; } @@ -198,31 +209,31 @@ export type StreamMessages = { payload: EventUnionFromDictionary>; }; main: { - name: `mainStream:${User['id']}`; + name: `mainStream:${MiUser['id']}`; payload: EventUnionFromDictionary>; }; drive: { - name: `driveStream:${User['id']}`; + name: `driveStream:${MiUser['id']}`; payload: EventUnionFromDictionary>; }; note: { - name: `noteStream:${Note['id']}`; + name: `noteStream:${MiNote['id']}`; payload: EventUnionFromDictionary>; }; userList: { - name: `userListStream:${UserList['id']}`; + name: `userListStream:${MiUserList['id']}`; payload: EventUnionFromDictionary>; }; roleTimeline: { - name: `roleTimelineStream:${Role['id']}`; + name: `roleTimelineStream:${MiRole['id']}`; payload: EventUnionFromDictionary>; }; antenna: { - name: `antennaStream:${Antenna['id']}`; + name: `antennaStream:${MiAntenna['id']}`; payload: EventUnionFromDictionary>; }; admin: { - name: `adminStream:${User['id']}`; + name: `adminStream:${MiUser['id']}`; payload: EventUnionFromDictionary>; }; notes: { @@ -233,7 +244,7 @@ export type StreamMessages = { // API event definitions // ストリームごとのEmitterの辞書を用意 -type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter void }> }; +type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter.default void }> }; // 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; // Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts new file mode 100644 index 0000000000..c3a78561c2 --- /dev/null +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -0,0 +1,487 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import dns from 'node:dns/promises'; +import { fileURLToPath } from 'node:url'; +import { Inject, Injectable } from '@nestjs/common'; +import { JSDOM } from 'jsdom'; +import httpLinkHeader from 'http-link-header'; +import ipaddr from 'ipaddr.js'; +import oauth2orize, { type OAuth2, AuthorizationError, ValidateFunctionArity2, OAuth2Req, MiddlewareRequest } from 'oauth2orize'; +import oauth2Pkce from 'oauth2orize-pkce'; +import fastifyView from '@fastify/view'; +import pug from 'pug'; +import bodyParser from 'body-parser'; +import fastifyExpress from '@fastify/express'; +import { verifyChallenge } from 'pkce-challenge'; +import { mf2 } from 'microformats-parser'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; +import { kinds } from '@/misc/api-permissions.js'; +import type { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import type { AccessTokensRepository, UsersRepository } from '@/models/_.js'; +import { IdService } from '@/core/IdService.js'; +import { CacheService } from '@/core/CacheService.js'; +import type { MiLocalUser } from '@/models/User.js'; +import { MemoryKVCache } from '@/misc/cache.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import Logger from '@/logger.js'; +import { StatusError } from '@/misc/status-error.js'; +import type { ServerResponse } from 'node:http'; +import type { FastifyInstance } from 'fastify'; + +// TODO: Consider migrating to @node-oauth/oauth2-server once +// https://github.com/node-oauth/node-oauth2-server/issues/180 is figured out. +// Upstream the various validations and RFC9207 implementation in that case. + +// Follows https://indieauth.spec.indieweb.org/#client-identifier +// This is also mostly similar to https://developers.google.com/identity/protocols/oauth2/web-server#uri-validation +// although Google has stricter rule. +function validateClientId(raw: string): URL { + // "Clients are identified by a [URL]." + const url = ((): URL => { + try { + return new URL(raw); + } catch { throw new AuthorizationError('client_id must be a valid URL', 'invalid_request'); } + })(); + + // "Client identifier URLs MUST have either an https or http scheme" + // But then again: + // https://datatracker.ietf.org/doc/html/rfc6749.html#section-3.1.2.1 + // 'The redirection endpoint SHOULD require the use of TLS as described + // in Section 1.6 when the requested response type is "code" or "token"' + const allowedProtocols = process.env.NODE_ENV === 'test' ? ['http:', 'https:'] : ['https:']; + if (!allowedProtocols.includes(url.protocol)) { + throw new AuthorizationError('client_id must be a valid HTTPS URL', 'invalid_request'); + } + + // "MUST contain a path component (new URL() implicitly adds one)" + + // "MUST NOT contain single-dot or double-dot path segments," + const segments = url.pathname.split('/'); + if (segments.includes('.') || segments.includes('..')) { + throw new AuthorizationError('client_id must not contain dot path segments', 'invalid_request'); + } + + // ("MAY contain a query string component") + + // "MUST NOT contain a fragment component" + if (url.hash) { + throw new AuthorizationError('client_id must not contain a fragment component', 'invalid_request'); + } + + // "MUST NOT contain a username or password component" + if (url.username || url.password) { + throw new AuthorizationError('client_id must not contain a username or a password', 'invalid_request'); + } + + // ("MAY contain a port") + + // "host names MUST be domain names or a loopback interface and MUST NOT be + // IPv4 or IPv6 addresses except for IPv4 127.0.0.1 or IPv6 [::1]." + if (!url.hostname.match(/\.\w+$/) && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) { + throw new AuthorizationError('client_id must have a domain name as a host name', 'invalid_request'); + } + + return url; +} + +interface ClientInformation { + id: string; + redirectUris: string[]; + name: string; +} + +// https://indieauth.spec.indieweb.org/#client-information-discovery +// "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id, +// and if there is an [h-app] with a url property matching the client_id URL, +// then it should use the name and icon and display them on the authorization prompt." +// (But we don't display any icon for now) +// https://indieauth.spec.indieweb.org/#redirect-url +// "The client SHOULD publish one or more tags or Link HTTP headers with a rel attribute +// of redirect_uri at the client_id URL. +// Authorization endpoints verifying that a redirect_uri is allowed for use by a client MUST +// look for an exact match of the given redirect_uri in the request against the list of +// redirect_uris discovered after resolving any relative URLs." +async function discoverClientInformation(logger: Logger, httpRequestService: HttpRequestService, id: string): Promise { + try { + const res = await httpRequestService.send(id); + const redirectUris: string[] = []; + + const linkHeader = res.headers.get('link'); + if (linkHeader) { + redirectUris.push(...httpLinkHeader.parse(linkHeader).get('rel', 'redirect_uri').map(r => r.uri)); + } + + const text = await res.text(); + const fragment = JSDOM.fragment(text); + + redirectUris.push(...[...fragment.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.href)); + + let name = id; + if (text) { + const microformats = mf2(text, { baseUrl: res.url }); + const nameProperty = microformats.items.find(item => item.type?.includes('h-app') && item.properties.url.includes(id))?.properties.name[0]; + if (typeof nameProperty === 'string') { + name = nameProperty; + } + } + + return { + id, + redirectUris: redirectUris.map(uri => new URL(uri, res.url).toString()), + name: typeof name === 'string' ? name : id, + }; + } catch (err) { + console.error(err); + logger.error('Error while fetching client information', { err }); + if (err instanceof StatusError) { + throw new AuthorizationError('Failed to fetch client information', 'invalid_request'); + } else { + throw new AuthorizationError('Failed to parse client information', 'server_error'); + } + } +} + +type OmitFirstElement = T extends [unknown, ...(infer R)] + ? R + : []; + +interface OAuthParsedRequest extends OAuth2Req { + codeChallenge: string; + codeChallengeMethod: string; +} + +interface OAuthHttpResponse extends ServerResponse { + redirect(location: string): void; +} + +interface OAuth2DecisionRequest extends MiddlewareRequest { + body: { + transaction_id: string; + cancel: boolean; + login_token: string; + } +} + +function getQueryMode(issuerUrl: string): oauth2orize.grant.Options['modes'] { + return { + query: (txn, res, params): void => { + // https://datatracker.ietf.org/doc/html/rfc9207#name-response-parameter-iss + // "In authorization responses to the client, including error responses, + // an authorization server supporting this specification MUST indicate its + // identity by including the iss parameter in the response." + params.iss = issuerUrl; + + const parsed = new URL(txn.redirectURI); + for (const [key, value] of Object.entries(params)) { + parsed.searchParams.append(key, value as string); + } + + return (res as OAuthHttpResponse).redirect(parsed.toString()); + }, + }; +} + +/** + * Maps the transaction ID and the oauth/authorize parameters. + * + * Flow: + * 1. oauth/authorize endpoint will call store() to store the parameters + * and puts the generated transaction ID to the dialog page + * 2. oauth/decision will call load() to retrieve the parameters and then remove() + */ +class OAuth2Store { + #cache = new MemoryKVCache(1000 * 60 * 5); // expires after 5min + + load(req: OAuth2DecisionRequest, cb: (err: Error | null, txn?: OAuth2) => void): void { + const { transaction_id } = req.body; + if (!transaction_id) { + cb(new AuthorizationError('Missing transaction ID', 'invalid_request')); + return; + } + const loaded = this.#cache.get(transaction_id); + if (!loaded) { + cb(new AuthorizationError('Invalid or expired transaction ID', 'access_denied')); + return; + } + cb(null, loaded); + } + + store(req: OAuth2DecisionRequest, oauth2: OAuth2, cb: (err: Error | null, transactionID?: string) => void): void { + const transactionId = secureRndstr(128); + this.#cache.set(transactionId, oauth2); + cb(null, transactionId); + } + + remove(req: OAuth2DecisionRequest, tid: string, cb: () => void): void { + this.#cache.delete(tid); + cb(); + } +} + +@Injectable() +export class OAuth2ProviderService { + #server = oauth2orize.createServer({ + store: new OAuth2Store(), + }); + #logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + private httpRequestService: HttpRequestService, + @Inject(DI.accessTokensRepository) + accessTokensRepository: AccessTokensRepository, + idService: IdService, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + private cacheService: CacheService, + loggerService: LoggerService, + ) { + this.#logger = loggerService.getLogger('oauth'); + + const grantCodeCache = new MemoryKVCache<{ + clientId: string, + userId: string, + redirectUri: string, + codeChallenge: string, + scopes: string[], + + // fields to prevent multiple code use + grantedToken?: string, + revoked?: boolean, + used?: boolean, + }>(1000 * 60 * 5); // expires after 5m + + // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics + // "Authorization servers MUST support PKCE [RFC7636]." + this.#server.grant(oauth2Pkce.extensions()); + this.#server.grant(oauth2orize.grant.code({ + modes: getQueryMode(config.url), + }, (client, redirectUri, token, ares, areq, locals, done) => { + (async (): Promise>> => { + this.#logger.info(`Checking the user before sending authorization code to ${client.id}`); + + if (!token) { + throw new AuthorizationError('No user', 'invalid_request'); + } + const user = await this.cacheService.localUserByNativeTokenCache.fetch(token, + () => this.usersRepository.findOneBy({ token }) as Promise); + if (!user) { + throw new AuthorizationError('No such user', 'invalid_request'); + } + + this.#logger.info(`Sending authorization code on behalf of user ${user.id} to ${client.id} through ${redirectUri}, with scope: [${areq.scope}]`); + + const code = secureRndstr(128); + grantCodeCache.set(code, { + clientId: client.id, + userId: user.id, + redirectUri, + codeChallenge: (areq as OAuthParsedRequest).codeChallenge, + scopes: areq.scope, + }); + return [code]; + })().then(args => done(null, ...args), err => done(err)); + })); + this.#server.exchange(oauth2orize.exchange.authorizationCode((client, code, redirectUri, body, authInfo, done) => { + (async (): Promise> | undefined> => { + this.#logger.info('Checking the received authorization code for the exchange'); + const granted = grantCodeCache.get(code); + if (!granted) { + return; + } + + // https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1.2 + // "If an authorization code is used more than once, the authorization server + // MUST deny the request and SHOULD revoke (when possible) all tokens + // previously issued based on that authorization code." + if (granted.used) { + this.#logger.info(`Detected multiple code use from ${granted.clientId} for user ${granted.userId}. Revoking the code.`); + grantCodeCache.delete(code); + granted.revoked = true; + if (granted.grantedToken) { + await accessTokensRepository.delete({ token: granted.grantedToken }); + } + return; + } + granted.used = true; + + // https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1.3 + if (body.client_id !== granted.clientId) return; + if (redirectUri !== granted.redirectUri) return; + + // https://datatracker.ietf.org/doc/html/rfc7636.html#section-4.6 + if (!body.code_verifier) return; + if (!(await verifyChallenge(body.code_verifier as string, granted.codeChallenge))) return; + + const accessToken = secureRndstr(128); + const now = new Date(); + + // NOTE: we don't have a setup for automatic token expiration + await accessTokensRepository.insert({ + id: idService.genId(), + createdAt: now, + lastUsedAt: now, + userId: granted.userId, + token: accessToken, + hash: accessToken, + name: granted.clientId, + permission: granted.scopes, + }); + + if (granted.revoked) { + this.#logger.info('Canceling the token as the authorization code was revoked in parallel during the process.'); + await accessTokensRepository.delete({ token: accessToken }); + return; + } + + granted.grantedToken = accessToken; + this.#logger.info(`Generated access token for ${granted.clientId} for user ${granted.userId}, with scope: [${granted.scopes}]`); + + return [accessToken, undefined, { scope: granted.scopes.join(' ') }]; + })().then(args => done(null, ...args ?? []), err => done(err)); + })); + } + + @bindThis + public async createServer(fastify: FastifyInstance): Promise { + // https://datatracker.ietf.org/doc/html/rfc8414.html + // https://indieauth.spec.indieweb.org/#indieauth-server-metadata + fastify.get('/.well-known/oauth-authorization-server', async (_request, reply) => { + reply.send({ + issuer: this.config.url, + authorization_endpoint: new URL('/oauth/authorize', this.config.url), + token_endpoint: new URL('/oauth/token', this.config.url), + scopes_supported: kinds, + response_types_supported: ['code'], + grant_types_supported: ['authorization_code'], + service_documentation: 'https://misskey-hub.net', + code_challenge_methods_supported: ['S256'], + authorization_response_iss_parameter_supported: true, + }); + }); + + fastify.get('/oauth/authorize', async (request, reply) => { + const oauth2 = (request.raw as MiddlewareRequest).oauth2; + if (!oauth2) { + throw new Error('Unexpected lack of authorization information'); + } + + this.#logger.info(`Rendering authorization page for "${oauth2.client.name}"`); + + reply.header('Cache-Control', 'no-store'); + return await reply.view('oauth', { + transactionId: oauth2.transactionID, + clientName: oauth2.client.name, + scope: oauth2.req.scope.join(' '), + }); + }); + fastify.post('/oauth/decision', async () => { }); + fastify.post('/oauth/token', async () => { }); + + fastify.register(fastifyView, { + root: fileURLToPath(new URL('../web/views', import.meta.url)), + engine: { pug }, + defaultContext: { + version: this.config.version, + config: this.config, + }, + }); + + await fastify.register(fastifyExpress); + fastify.use('/oauth/authorize', this.#server.authorize(((areq, done) => { + (async (): Promise> => { + // This should return client/redirectURI AND the error, or + // the handler can't send error to the redirection URI + + const { codeChallenge, codeChallengeMethod, clientID, redirectURI, scope } = areq as OAuthParsedRequest; + + this.#logger.info(`Validating authorization parameters, with client_id: ${clientID}, redirect_uri: ${redirectURI}, scope: ${scope}`); + + const clientUrl = validateClientId(clientID); + + // https://indieauth.spec.indieweb.org/#client-information-discovery + // "the server may want to resolve the domain name first and avoid fetching the document + // if the IP address is within the loopback range defined by [RFC5735] + // or any other implementation-specific internal IP address." + if (process.env.NODE_ENV !== 'test' || process.env.MISSKEY_TEST_CHECK_IP_RANGE === '1') { + const lookup = await dns.lookup(clientUrl.hostname); + if (ipaddr.parse(lookup.address).range() !== 'unicast') { + throw new AuthorizationError('client_id resolves to disallowed IP range.', 'invalid_request'); + } + } + + // Find client information from the remote. + const clientInfo = await discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href); + + // Require the redirect URI to be included in an explicit list, per + // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.1.3 + if (!clientInfo.redirectUris.includes(redirectURI)) { + throw new AuthorizationError('Invalid redirect_uri', 'invalid_request'); + } + + try { + const scopes = [...new Set(scope)].filter(s => kinds.includes(s)); + if (!scopes.length) { + throw new AuthorizationError('`scope` parameter has no known scope', 'invalid_scope'); + } + areq.scope = scopes; + + // Require PKCE parameters. + // Recommended by https://indieauth.spec.indieweb.org/#authorization-request, but also prevents downgrade attack: + // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-pkce-downgrade-attack + if (typeof codeChallenge !== 'string') { + throw new AuthorizationError('`code_challenge` parameter is required', 'invalid_request'); + } + if (codeChallengeMethod !== 'S256') { + throw new AuthorizationError('`code_challenge_method` parameter must be set as S256', 'invalid_request'); + } + } catch (err) { + return [err as Error, clientInfo, redirectURI]; + } + + return [null, clientInfo, redirectURI]; + })().then(args => done(...args), err => done(err)); + }) as ValidateFunctionArity2)); + fastify.use('/oauth/authorize', this.#server.errorHandler({ + mode: 'indirect', + modes: getQueryMode(this.config.url), + })); + fastify.use('/oauth/authorize', this.#server.errorHandler()); + + fastify.use('/oauth/decision', bodyParser.urlencoded({ extended: false })); + fastify.use('/oauth/decision', this.#server.decision((req, done) => { + const { body } = req as OAuth2DecisionRequest; + this.#logger.info(`Received the decision. Cancel: ${!!body.cancel}`); + req.user = body.login_token; + done(null, undefined); + })); + fastify.use('/oauth/decision', this.#server.errorHandler()); + + // Clients may use JSON or urlencoded + fastify.use('/oauth/token', bodyParser.urlencoded({ extended: false })); + fastify.use('/oauth/token', bodyParser.json({ strict: true })); + fastify.use('/oauth/token', this.#server.token()); + fastify.use('/oauth/token', this.#server.errorHandler()); + + // Return 404 for any unknown paths under /oauth so that clients can know + // whether a certain endpoint is supported or not. + fastify.all('/oauth/*', async (_request, reply) => { + reply.code(404); + reply.send({ + error: { + message: 'Unknown OAuth endpoint.', + code: 'UNKNOWN_OAUTH_ENDPOINT', + id: 'aa49e620-26cb-4e28-aad6-8cbcb58db147', + kind: 'client', + }, + }); + }); + } +} diff --git a/packages/backend/src/server/web/ClientLoggerService.ts b/packages/backend/src/server/web/ClientLoggerService.ts index 6a882aa766..213266f59c 100644 --- a/packages/backend/src/server/web/ClientLoggerService.ts +++ b/packages/backend/src/server/web/ClientLoggerService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index f780280c1f..1faff24201 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -1,9 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { randomUUID } from 'node:crypto'; import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; -import { v4 as uuid } from 'uuid'; import { createBullBoard } from '@bull-board/api'; -import { BullAdapter } from '@bull-board/api/bullAdapter.js'; +import { BullMQAdapter } from '@bull-board/api/bullMQAdapter.js'; import { FastifyAdapter } from '@bull-board/fastify'; import ms from 'ms'; import sharp from 'sharp'; @@ -26,13 +31,12 @@ 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, FlashsRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { deepClone } from '@/misc/clone.js'; import { bindThis } from '@/decorators.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { RoleService } from '@/core/RoleService.js'; -import manifest from './manifest.json' assert { type: 'json' }; import { FeedService } from './FeedService.js'; import { UrlPreviewService } from './UrlPreviewService.js'; import { ClientLoggerService } from './ClientLoggerService.js'; @@ -105,16 +109,73 @@ export class ClientServerService { @bindThis private async manifestHandler(reply: FastifyReply) { - const res = deepClone(manifest); - const instance = await this.metaService.fetch(true); - res.short_name = instance.name ?? 'Misskey'; - res.name = instance.name ?? 'Misskey'; - if (instance.themeColor) res.theme_color = instance.themeColor; + let manifest = { + // 空文字列の場合右辺を使いたいため + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + 'short_name': instance.shortName || instance.name || this.config.host, + // 空文字列の場合右辺を使いたいため + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + 'name': instance.name || this.config.host, + 'start_url': '/', + 'display': 'standalone', + 'background_color': '#313a42', + // 空文字列の場合右辺を使いたいため + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + 'theme_color': instance.themeColor || '#86b300', + 'icons': [{ + // 空文字列の場合右辺を使いたいため + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + 'src': instance.app192IconUrl || '/static-assets/icons/192.png', + 'sizes': '192x192', + 'type': 'image/png', + 'purpose': 'maskable', + }, { + // 空文字列の場合右辺を使いたいため + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + 'src': instance.app512IconUrl || '/static-assets/icons/512.png', + 'sizes': '512x512', + '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', + 'url': 'url', + }, + }, + }; + + manifest = { + ...manifest, + ...JSON.parse(instance.manifestJsonOverride === '' ? '{}' : instance.manifestJsonOverride), + }; reply.header('Cache-Control', 'max-age=300'); - return (res); + return (manifest); + } + + @bindThis + private generateCommonPugData(meta: MiMeta) { + return { + instanceName: meta.name ?? 'Misskey', + icon: meta.iconUrl, + appleTouchIcon: meta.app512IconUrl, + themeColor: meta.themeColor, + serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg', + infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg', + notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg', + }; } @bindThis @@ -126,21 +187,23 @@ export class ClientServerService { // Authenticate fastify.addHook('onRequest', async (request, reply) => { - if (request.url === bullBoardPath || request.url.startsWith(bullBoardPath + '/')) { + // %71ueueとかでリクエストされたら困るため + const url = decodeURI(request.url); + if (url === bullBoardPath || url.startsWith(bullBoardPath + '/')) { const token = request.cookies.token; if (token == null) { - reply.code(401); - throw new Error('login required'); + reply.code(401).send('Login required'); + return; } const user = await this.usersRepository.findOneBy({ token }); if (user == null) { - reply.code(403); - throw new Error('no such user'); + reply.code(403).send('No such user'); + return; } const isAdministrator = await this.roleService.isAdministrator(user); if (!isAdministrator) { - reply.code(403); - throw new Error('access denied'); + reply.code(403).send('Access denied'); + return; } } }); @@ -156,7 +219,7 @@ export class ClientServerService { this.dbQueue, this.objectStorageQueue, this.webhookDeliverQueue, - ].map(q => new BullAdapter(q)), + ].map(q => new BullMQAdapter(q)), serverAdapter, }); @@ -341,12 +404,10 @@ export class ClientServerService { reply.header('Cache-Control', 'public, max-age=30'); return await reply.view('base', { img: meta.bannerUrl, - title: meta.name ?? 'Misskey', - instanceName: meta.name ?? 'Misskey', url: this.config.url, + title: meta.name ?? 'Misskey', desc: meta.description, - icon: meta.iconUrl, - themeColor: meta.themeColor, + ...this.generateCommonPugData(meta), }); }; @@ -431,9 +492,7 @@ export class ClientServerService { user, profile, me, avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), sub: request.params.sub, - instanceName: meta.name ?? 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, + ...this.generateCommonPugData(meta), }); } else { // リモートユーザーなので @@ -481,9 +540,7 @@ export class ClientServerService { avatarUrl: _note.user.avatarUrl, // TODO: Let locale changeable by instance setting summary: getNoteSummary(_note), - instanceName: meta.name ?? 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -522,9 +579,7 @@ export class ClientServerService { page: _page, profile, avatarUrl: _page.user.avatarUrl, - instanceName: meta.name ?? 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -550,9 +605,7 @@ export class ClientServerService { flash: _flash, profile, avatarUrl: _flash.user.avatarUrl, - instanceName: meta.name ?? 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -578,9 +631,7 @@ export class ClientServerService { clip: _clip, profile, avatarUrl: _clip.user.avatarUrl, - instanceName: meta.name ?? 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -604,9 +655,7 @@ export class ClientServerService { post: _post, profile, avatarUrl: _post.user.avatarUrl, - instanceName: meta.name ?? 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -625,9 +674,7 @@ export class ClientServerService { reply.header('Cache-Control', 'public, max-age=15'); return await reply.view('channel', { channel: _channel, - instanceName: meta.name ?? 'Misskey', - icon: meta.iconUrl, - themeColor: meta.themeColor, + ...this.generateCommonPugData(meta), }); } else { return await renderBase(reply); @@ -680,8 +727,8 @@ export class ClientServerService { }); fastify.setErrorHandler(async (error, request, reply) => { - const errId = uuid(); - this.clientLoggerService.logger.error(`Internal error occured in ${request.routerPath}: ${error.message}`, { + const errId = randomUUID(); + this.clientLoggerService.logger.error(`Internal error occurred in ${request.routerPath}: ${error.message}`, { path: request.routerPath, params: request.params, query: request.query, diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts index 0c0e92cc04..78551e800b 100644 --- a/packages/backend/src/server/web/FeedService.ts +++ b/packages/backend/src/server/web/FeedService.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { In, IsNull } from 'typeorm'; import { Feed } from 'feed'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { DriveFilesRepository, NotesRepository, UserProfilesRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; -import type { User } from '@/models/entities/User.js'; +import type { MiUser } from '@/models/User.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -15,9 +20,6 @@ export class FeedService { @Inject(DI.config) private config: Config, - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, @@ -33,14 +35,14 @@ export class FeedService { } @bindThis - public async packFeed(user: User) { + public async packFeed(user: MiUser) { const author = { link: `${this.config.url}/@${user.username}`, name: user.name ?? user.username, }; - + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - + const notes = await this.notesRepository.find({ where: { userId: user.id, @@ -50,7 +52,7 @@ export class FeedService { order: { createdAt: -1 }, take: 20, }); - + const feed = new Feed({ id: author.link, title: `${author.name} (@${user.username}@${this.config.host})`, @@ -66,13 +68,13 @@ export class FeedService { author, copyright: user.name ?? user.username, }); - + for (const note of notes) { const files = note.fileIds.length > 0 ? await this.driveFilesRepository.findBy({ id: In(note.fileIds), }) : []; const file = files.find(file => file.type.startsWith('image/')); - + feed.addItem({ title: `New note by ${author.name}`, link: `${this.config.url}/notes/${note.id}`, @@ -82,7 +84,7 @@ export class FeedService { image: file ? this.driveFileEntityService.getPublicUrl(file) ?? undefined : undefined, }); } - + return feed; } } diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index e61e92c623..d590244e34 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { summaly } from 'summaly'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/web/bios.css b/packages/backend/src/server/web/bios.css index b0da3ee39b..c934a55fa9 100644 --- a/packages/backend/src/server/web/bios.css +++ b/packages/backend/src/server/web/bios.css @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + * { font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace; } diff --git a/packages/backend/src/server/web/bios.js b/packages/backend/src/server/web/bios.js index c2ce5c3814..029eb92aad 100644 --- a/packages/backend/src/server/web/bios.js +++ b/packages/backend/src/server/web/bios.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + 'use strict'; window.onload = async () => { @@ -8,7 +13,7 @@ window.onload = async () => { const promise = new Promise((resolve, reject) => { // Append a credential if (i) data.i = i; - + // Send request window.fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, { method: 'POST', @@ -17,7 +22,7 @@ window.onload = async () => { cache: 'no-cache' }).then(async (res) => { const body = res.status === 204 ? null : await res.json(); - + if (res.status === 200) { resolve(body); } else if (res.status === 204) { @@ -27,7 +32,7 @@ window.onload = async () => { } }).catch(reject); }); - + return promise; }; diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 825f02e835..48939ef7a0 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + /** * BOOT LOADER * サーバーからレスポンスされるHTMLに埋め込まれるスクリプトで、以下の役割を持ちます。 @@ -116,9 +121,9 @@ } } } - const colorSchema = localStorage.getItem('colorSchema'); - if (colorSchema) { - document.documentElement.style.setProperty('color-schema', colorSchema); + const colorScheme = localStorage.getItem('colorScheme'); + if (colorScheme) { + document.documentElement.style.setProperty('color-scheme', colorScheme); } //#endregion diff --git a/packages/backend/src/server/web/cli.css b/packages/backend/src/server/web/cli.css index 07cd27830b..b7737c3f21 100644 --- a/packages/backend/src/server/web/cli.css +++ b/packages/backend/src/server/web/cli.css @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + * { font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace; } diff --git a/packages/backend/src/server/web/cli.js b/packages/backend/src/server/web/cli.js index 3467f7ac2a..e63a80327c 100644 --- a/packages/backend/src/server/web/cli.js +++ b/packages/backend/src/server/web/cli.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + 'use strict'; window.onload = async () => { @@ -8,7 +13,7 @@ window.onload = async () => { const promise = new Promise((resolve, reject) => { // Append a credential if (i) data.i = i; - + // Send request fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, { headers: { @@ -20,7 +25,7 @@ window.onload = async () => { cache: 'no-cache' }).then(async (res) => { const body = res.status === 204 ? null : await res.json(); - + if (res.status === 200) { resolve(body); } else if (res.status === 204) { @@ -30,7 +35,7 @@ window.onload = async () => { } }).catch(reject); }); - + return promise; }; diff --git a/packages/backend/src/server/web/error.css b/packages/backend/src/server/web/error.css index ab913f7a9f..ea3056bdaf 100644 --- a/packages/backend/src/server/web/error.css +++ b/packages/backend/src/server/web/error.css @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + * { font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; } diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css index d59f00fe16..952be9bf0b 100644 --- a/packages/backend/src/server/web/style.css +++ b/packages/backend/src/server/web/style.css @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + html { background-color: var(--bg); color: var(--fg); diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 69b3f68e05..71bcf9462f 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -7,15 +7,15 @@ doctype html // - - _____ _ _ - | |_|___ ___| |_ ___ _ _ + _____ _ _ + | |_|___ ___| |_ ___ _ _ | | | | |_ -|_ -| '_| -_| | | |_|_|_|_|___|___|_,_|___|_ | |___| Thank you for using Misskey! If you are reading this message... how about joining the development? https://github.com/misskey-dev/misskey - + html @@ -28,14 +28,14 @@ html meta(property='og:site_name' content= instanceName || 'Misskey') meta(name='viewport' content='width=device-width, initial-scale=1') link(rel='icon' href= icon || '/favicon.ico') - link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png') + link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png') link(rel='manifest' href='/manifest.json') link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`) - link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg') - link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg') - link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg') + link(rel='prefetch' href=serverErrorImageUrl) + link(rel='prefetch' href=infoImageUrl) + link(rel='prefetch' href=notFoundImageUrl) //- https://github.com/misskey-dev/misskey/issues/9842 - link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.21.0') + link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.35.0') link(rel='modulepreload' href=`/vite/${clientEntry.file}`) if !config.clientManifestExists @@ -55,8 +55,8 @@ html block meta block og - meta(property='og:title' content= title || 'Misskey') - meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨') + meta(property='og:title' content= title || 'Misskey') + meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨') meta(property='og:image' content= img) meta(property='twitter:card' content='summary') diff --git a/packages/backend/src/server/web/views/error.pug b/packages/backend/src/server/web/views/error.pug index b177ae4110..44ebf53cf7 100644 --- a/packages/backend/src/server/web/views/error.pug +++ b/packages/backend/src/server/web/views/error.pug @@ -32,12 +32,12 @@ body path(stroke="none", d="M0 0h24v24H0z", fill="none") path(d="M12 9v2m0 4v.01") path(d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75") - + h1 An error has occurred! button.button-big(onclick="location.reload();") span.button-label-big Refresh - + p.dont-worry Don't worry, it's (probably) not your fault. p If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID. diff --git a/packages/backend/src/server/web/views/gallery-post.pug b/packages/backend/src/server/web/views/gallery-post.pug index a458d7f8c7..9ae25d9ac8 100644 --- a/packages/backend/src/server/web/views/gallery-post.pug +++ b/packages/backend/src/server/web/views/gallery-post.pug @@ -16,8 +16,12 @@ block og meta(property='og:title' content= title) meta(property='og:description' content= post.description) meta(property='og:url' content= url) - meta(property='og:image' content= post.files[0].thumbnailUrl) - meta(property='twitter:card' content='summary_large_image') + if post.isSensitive + meta(property='og:image' content= avatarUrl) + meta(property='twitter:card' content='summary') + else + meta(property='og:image' content= post.files[0].thumbnailUrl) + meta(property='twitter:card' content='summary_large_image') block meta if user.host || profile.noCrawle diff --git a/packages/backend/src/server/web/views/info-card.pug b/packages/backend/src/server/web/views/info-card.pug index 1d62778ce1..2a4954ec8b 100644 --- a/packages/backend/src/server/web/views/info-card.pug +++ b/packages/backend/src/server/web/views/info-card.pug @@ -47,4 +47,4 @@ html header#banner(style=`background-image: url(${meta.bannerUrl})`) div#title= meta.name || host div#content - div#description= meta.description + div#description!= meta.description diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug index 874c48c602..9bc652b6a1 100644 --- a/packages/backend/src/server/web/views/note.pug +++ b/packages/backend/src/server/web/views/note.pug @@ -5,8 +5,8 @@ block vars - const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`; - const url = `${config.url}/notes/${note.id}`; - const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null; - - const image = (note.files || []).find(file => file.type.startsWith('image/') && !file.type.isSensitive) - - const video = (note.files || []).find(file => file.type.startsWith('video/') && !file.type.isSensitive) + - const images = (note.files || []).filter(file => file.type.startsWith('image/') && !file.isSensitive) + - const videos = (note.files || []).filter(file => file.type.startsWith('video/') && !file.isSensitive) block title = `${title} | ${instanceName}` @@ -19,15 +19,17 @@ block og meta(property='og:title' content= title) meta(property='og:description' content= summary) meta(property='og:url' content= url) - if video - meta(property='og:video:url' content= video.url) - meta(property='og:video:secure_url' content= video.url) - meta(property='og:video:type' content= video.type) - // FIXME: add width and height - // FIXME: add embed player for Twitter - if image + if videos.length + each video in videos + meta(property='og:video:url' content= video.url) + meta(property='og:video:secure_url' content= video.url) + meta(property='og:video:type' content= video.type) + // FIXME: add width and height + // FIXME: add embed player for Twitter + if images.length meta(property='twitter:card' content='summary_large_image') - meta(property='og:image' content= image.url) + each image in images + meta(property='og:image' content= image.url) else meta(property='twitter:card' content='summary') meta(property='og:image' content= avatarUrl) @@ -43,7 +45,7 @@ block meta meta(name='misskey:user-username' content=user.username) meta(name='misskey:user-id' content=user.id) meta(name='misskey:note-id' content=note.id) - + // todo if user.twitter meta(name='twitter:creator' content=`@${user.twitter.screenName}`) diff --git a/packages/backend/src/server/web/views/oauth.pug b/packages/backend/src/server/web/views/oauth.pug new file mode 100644 index 0000000000..1470dbfbdf --- /dev/null +++ b/packages/backend/src/server/web/views/oauth.pug @@ -0,0 +1,9 @@ +extends ./base + +block meta + //- Should be removed by the page when it loads, so that it won't needlessly + //- stay when user navigates away via the navigation bar + //- XXX: Remove navigation bar in auth page? + meta(name='misskey:oauth:transaction-id' content=transactionId) + meta(name='misskey:oauth:client-name' content=clientName) + meta(name='misskey:oauth:scope' content=scope) diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 7c6a1e5199..0a28d88d08 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -1,4 +1,24 @@ -export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] as const; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * note - 通知オンにしているユーザーが投稿した + * follow - フォローされた + * mention - 投稿で自分が言及された + * reply - 投稿に返信された + * renote - 投稿がRenoteされた + * quote - 投稿が引用Renoteされた + * reaction - 投稿にリアクションされた + * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した + * receiveFollowRequest - フォローリクエストされた + * followRequestAccepted - 自分の送ったフォローリクエストが承認された + * achievementEarned - 実績を獲得 + * app - アプリ通知 + * test - テスト通知(サーバー側) + */ +export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test'] 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/2fa.ts b/packages/backend/test/e2e/2fa.ts index 0addb430c9..ed967d2620 100644 --- a/packages/backend/test/e2e/2fa.ts +++ b/packages/backend/test/e2e/2fa.ts @@ -1,16 +1,30 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as crypto from 'node:crypto'; -import * as cbor from 'cbor'; +import cbor from 'cbor'; import * as OTPAuth from 'otpauth'; -import { loadConfig } from '../../src/config.js'; -import { signup, api, post, react, startServer, waitFire } from '../utils.js'; +import { loadConfig } from '@/config.js'; +import { api, signup, startServer } from '../utils.js'; +import type { + AuthenticationResponseJSON, + AuthenticatorAssertionResponseJSON, + AuthenticatorAttestationResponseJSON, + PublicKeyCredentialCreationOptionsJSON, + PublicKeyCredentialRequestOptionsJSON, + RegistrationResponseJSON, +} from '@simplewebauthn/typescript-types'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('2要素認証', () => { let app: INestApplicationContext; - let alice: unknown; + let alice: misskey.entities.MeSignup; const config = loadConfig(); const password = 'test'; @@ -41,21 +55,20 @@ describe('2要素認証', () => { const rpIdHash = (): Buffer => { return crypto.createHash('sha256') - .update(Buffer.from(config.hostname, 'utf-8')) + .update(Buffer.from(config.host, 'utf-8')) .digest(); }; const keyDoneParam = (param: { + token: string, keyName: string, - challengeId: string, - challenge: string, credentialId: Buffer, + creationOptions: PublicKeyCredentialCreationOptionsJSON, }): { - attestationObject: string, - challengeId: string, - clientDataJSON: string, + token: string, password: string, name: string, + credential: RegistrationResponseJSON, } => { // A COSE encoded public key const credentialPublicKey = cbor.encode(new Map([ @@ -68,9 +81,9 @@ describe('2要素認証', () => { ])); // AuthenticatorAssertionResponse.authenticatorData - // https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData + // https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData const credentialIdLength = Buffer.allocUnsafe(2); - credentialIdLength.writeUInt16BE(param.credentialId.length); + credentialIdLength.writeUInt16BE(param.credentialId.length, 0); const authData = Buffer.concat([ rpIdHash(), // rpIdHash(32) Buffer.from([0x45]), // flags(1) @@ -80,25 +93,33 @@ describe('2要素認証', () => { param.credentialId, credentialPublicKey, ]); - + return { - attestationObject: cbor.encode({ - fmt: 'none', - attStmt: {}, - authData, - }).toString('hex'), - challengeId: param.challengeId, - clientDataJSON: JSON.stringify({ - type: 'webauthn.create', - challenge: param.challenge, - origin: config.scheme + '://' + config.host, - androidPackageName: 'org.mozilla.firefox', - }), password, + token: param.token, name: param.keyName, + credential: { + id: param.credentialId.toString('base64url'), + rawId: param.credentialId.toString('base64url'), + response: { + clientDataJSON: Buffer.from(JSON.stringify({ + type: 'webauthn.create', + challenge: param.creationOptions.challenge, + origin: config.scheme + '://' + config.host, + androidPackageName: 'org.mozilla.firefox', + }), 'utf-8').toString('base64url'), + attestationObject: cbor.encode({ + fmt: 'none', + attStmt: {}, + authData, + }).toString('base64url'), + }, + clientExtensionResults: {}, + type: 'public-key', + }, }; }; - + const signinParam = (): { username: string, password: string, @@ -115,22 +136,17 @@ describe('2要素認証', () => { const signinWithSecurityKeyParam = (param: { keyName: string, - challengeId: string, - challenge: string, credentialId: Buffer, + requestOptions: PublicKeyCredentialRequestOptionsJSON, }): { - authenticatorData: string, - credentialId: string, - challengeId: string, - clientDataJSON: string, username: string, password: string, - signature: string, + credential: AuthenticationResponseJSON, 'g-recaptcha-response'?: string | null, 'hcaptcha-response'?: string | null, } => { // AuthenticatorAssertionResponse.authenticatorData - // https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData + // https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData const authenticatorData = Buffer.concat([ rpIdHash(), Buffer.from([0x05]), // flags(1) @@ -138,25 +154,31 @@ describe('2要素認証', () => { ]); const clientDataJSONBuffer = Buffer.from(JSON.stringify({ type: 'webauthn.get', - challenge: param.challenge, + challenge: param.requestOptions.challenge, origin: config.scheme + '://' + config.host, androidPackageName: 'org.mozilla.firefox', - })); + }), 'utf-8'); const hashedclientDataJSON = crypto.createHash('sha256') .update(clientDataJSONBuffer) .digest(); const privateKey = crypto.createPrivateKey(pemToSign); - const signature = crypto.createSign('SHA256') + const signature = crypto.createSign('SHA256') .update(Buffer.concat([authenticatorData, hashedclientDataJSON])) .sign(privateKey); return { - authenticatorData: authenticatorData.toString('hex'), - credentialId: param.credentialId.toString('base64'), - challengeId: param.challengeId, - clientDataJSON: clientDataJSONBuffer.toString('hex'), username, password, - signature: signature.toString('hex'), + credential: { + id: param.credentialId.toString('base64url'), + rawId: param.credentialId.toString('base64url'), + response: { + clientDataJSON: clientDataJSONBuffer.toString('base64url'), + authenticatorData: authenticatorData.toString('base64url'), + signature: signature.toString('base64url'), + }, + clientExtensionResults: {}, + type: 'public-key', + }, 'g-recaptcha-response': null, 'hcaptcha-response': null, }; @@ -185,20 +207,26 @@ describe('2要素認証', () => { const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); - assert.strictEqual(doneResponse.status, 204); - + assert.strictEqual(doneResponse.status, 200); + const usersShowResponse = await api('/users/show', { username, }, alice); assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true); - - const signinResponse = await api('/signin', { + + const signinResponse = await api('/signin', { ...signinParam(), token: otpToken(registerResponse.body.secret), }); assert.strictEqual(signinResponse.status, 200); assert.notEqual(signinResponse.body.i, undefined); + + // 後片付け + await api('/i/2fa/unregister', { + password, + token: otpToken(registerResponse.body.secret), + }, alice); }); test('が設定でき、セキュリティキーでログインできる。', async () => { @@ -210,27 +238,28 @@ describe('2要素認証', () => { const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); - assert.strictEqual(doneResponse.status, 204); - + assert.strictEqual(doneResponse.status, 200); + const registerKeyResponse = await api('/i/2fa/register-key', { password, + token: otpToken(registerResponse.body.secret), }, alice); assert.strictEqual(registerKeyResponse.status, 200); - assert.notEqual(registerKeyResponse.body.challengeId, undefined); + assert.notEqual(registerKeyResponse.body.rp, undefined); assert.notEqual(registerKeyResponse.body.challenge, undefined); const keyName = 'example-key'; const credentialId = crypto.randomBytes(0x41); const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ + token: otpToken(registerResponse.body.secret), keyName, - challengeId: registerKeyResponse.body.challengeId, - challenge: registerKeyResponse.body.challenge, credentialId, + creationOptions: registerKeyResponse.body, }), alice); assert.strictEqual(keyDoneResponse.status, 200); - assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('hex')); + assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url')); assert.strictEqual(keyDoneResponse.body.name, keyName); - + const usersShowResponse = await api('/users/show', { username, }); @@ -242,19 +271,23 @@ describe('2要素認証', () => { }); assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.body.i, undefined); - assert.notEqual(signinResponse.body.challengeId, undefined); assert.notEqual(signinResponse.body.challenge, undefined); - assert.notEqual(signinResponse.body.securityKeys, undefined); - assert.strictEqual(signinResponse.body.securityKeys[0].id, credentialId.toString('hex')); + assert.notEqual(signinResponse.body.allowCredentials, undefined); + assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url')); const signinResponse2 = await api('/signin', signinWithSecurityKeyParam({ keyName, - challengeId: signinResponse.body.challengeId, - challenge: signinResponse.body.challenge, credentialId, + requestOptions: signinResponse.body, })); assert.strictEqual(signinResponse2.status, 200); assert.notEqual(signinResponse2.body.i, undefined); + + // 後片付け + await api('/i/2fa/unregister', { + password, + token: otpToken(registerResponse.body.secret), + }, alice); }); test('が設定でき、セキュリティキーでパスワードレスログインできる。', async () => { @@ -266,9 +299,10 @@ describe('2要素認証', () => { const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); - assert.strictEqual(doneResponse.status, 204); - + assert.strictEqual(doneResponse.status, 200); + const registerKeyResponse = await api('/i/2fa/register-key', { + token: otpToken(registerResponse.body.secret), password, }, alice); assert.strictEqual(registerKeyResponse.status, 200); @@ -276,13 +310,13 @@ describe('2要素認証', () => { const keyName = 'example-key'; const credentialId = crypto.randomBytes(0x41); const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ + token: otpToken(registerResponse.body.secret), keyName, - challengeId: registerKeyResponse.body.challengeId, - challenge: registerKeyResponse.body.challenge, credentialId, + creationOptions: registerKeyResponse.body, }), alice); assert.strictEqual(keyDoneResponse.status, 200); - + const passwordLessResponse = await api('/i/2fa/password-less', { value: true, }, alice); @@ -301,17 +335,22 @@ describe('2要素認証', () => { assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.body.i, undefined); - const signinResponse2 = await api('/signin', { + const signinResponse2 = await api('/signin', { ...signinWithSecurityKeyParam({ keyName, - challengeId: signinResponse.body.challengeId, - challenge: signinResponse.body.challenge, credentialId, + requestOptions: signinResponse.body, }), password: '', }); assert.strictEqual(signinResponse2.status, 200); assert.notEqual(signinResponse2.body.i, undefined); + + // 後片付け + await api('/i/2fa/unregister', { + password, + token: otpToken(registerResponse.body.secret), + }, alice); }); test('が設定でき、設定したセキュリティキーの名前を変更できる。', async () => { @@ -323,9 +362,10 @@ describe('2要素認証', () => { const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); - assert.strictEqual(doneResponse.status, 204); - + assert.strictEqual(doneResponse.status, 200); + const registerKeyResponse = await api('/i/2fa/register-key', { + token: otpToken(registerResponse.body.secret), password, }, alice); assert.strictEqual(registerKeyResponse.status, 200); @@ -333,27 +373,33 @@ describe('2要素認証', () => { const keyName = 'example-key'; const credentialId = crypto.randomBytes(0x41); const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ + token: otpToken(registerResponse.body.secret), keyName, - challengeId: registerKeyResponse.body.challengeId, - challenge: registerKeyResponse.body.challenge, credentialId, + creationOptions: registerKeyResponse.body, }), alice); assert.strictEqual(keyDoneResponse.status, 200); - + const renamedKey = 'other-key'; const updateKeyResponse = await api('/i/2fa/update-key', { name: renamedKey, - credentialId: credentialId.toString('hex'), + credentialId: credentialId.toString('base64url'), }, alice); assert.strictEqual(updateKeyResponse.status, 200); - + const iResponse = await api('/i', { }, alice); assert.strictEqual(iResponse.status, 200); - const securityKeys = iResponse.body.securityKeysList.filter(s => s.id === credentialId.toString('hex')); + const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url')); assert.strictEqual(securityKeys.length, 1); assert.strictEqual(securityKeys[0].name, renamedKey); assert.notEqual(securityKeys[0].lastUsed, undefined); + + // 後片付け + await api('/i/2fa/unregister', { + password, + token: otpToken(registerResponse.body.secret), + }, alice); }); test('が設定でき、設定したセキュリティキーを削除できる。', async () => { @@ -365,9 +411,10 @@ describe('2要素認証', () => { const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); - assert.strictEqual(doneResponse.status, 204); - + assert.strictEqual(doneResponse.status, 200); + const registerKeyResponse = await api('/i/2fa/register-key', { + token: otpToken(registerResponse.body.secret), password, }, alice); assert.strictEqual(registerKeyResponse.status, 200); @@ -375,19 +422,20 @@ describe('2要素認証', () => { const keyName = 'example-key'; const credentialId = crypto.randomBytes(0x41); const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({ + token: otpToken(registerResponse.body.secret), keyName, - challengeId: registerKeyResponse.body.challengeId, - challenge: registerKeyResponse.body.challenge, credentialId, + creationOptions: registerKeyResponse.body, }), alice); assert.strictEqual(keyDoneResponse.status, 200); - + // テストの実行順によっては複数残ってるので全部消す const iResponse = await api('/i', { }, alice); assert.strictEqual(iResponse.status, 200); for (const key of iResponse.body.securityKeysList) { const removeKeyResponse = await api('/i/2fa/remove-key', { + token: otpToken(registerResponse.body.secret), password, credentialId: key.id, }, alice); @@ -400,14 +448,20 @@ describe('2要素認証', () => { assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.body.securityKeys, false); - const signinResponse = await api('/signin', { + const signinResponse = await api('/signin', { ...signinParam(), token: otpToken(registerResponse.body.secret), }); assert.strictEqual(signinResponse.status, 200); assert.notEqual(signinResponse.body.i, undefined); + + // 後片付け + await api('/i/2fa/unregister', { + password, + token: otpToken(registerResponse.body.secret), + }, alice); }); - + test('が設定でき、設定解除できる。(パスワードのみでログインできる。)', async () => { const registerResponse = await api('/i/2fa/register', { password, @@ -417,8 +471,8 @@ describe('2要素認証', () => { const doneResponse = await api('/i/2fa/done', { token: otpToken(registerResponse.body.secret), }, alice); - assert.strictEqual(doneResponse.status, 204); - + assert.strictEqual(doneResponse.status, 200); + const usersShowResponse = await api('/users/show', { username, }); @@ -426,6 +480,7 @@ describe('2要素認証', () => { assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true); const unregisterResponse = await api('/i/2fa/unregister', { + token: otpToken(registerResponse.body.secret), password, }, alice); assert.strictEqual(unregisterResponse.status, 204); @@ -435,5 +490,11 @@ describe('2要素認証', () => { }); assert.strictEqual(signinResponse.status, 200); assert.notEqual(signinResponse.body.i, undefined); + + // 後片付け + await api('/i/2fa/unregister', { + password, + token: otpToken(registerResponse.body.secret), + }, alice); }); }); diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts index dd3b09f85a..7e2beab1ab 100644 --- a/packages/backend/test/e2e/antennas.ts +++ b/packages/backend/test/e2e/antennas.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; @@ -32,7 +37,7 @@ describe('アンテナ', () => { // - srcのenumにgroupが残っている // - userGroupIdが残っている, isActiveがない type Antenna = misskey.entities.Antenna | Packed<'Antenna'>; - type User = misskey.entities.MeDetailed & { token: string }; + type User = misskey.entities.MeSignup; type Note = misskey.entities.Note; // アンテナを作成できる最小のパラメタ diff --git a/packages/backend/test/e2e/api-visibility.ts b/packages/backend/test/e2e/api-visibility.ts index 3af0d35182..33c8d03fdb 100644 --- a/packages/backend/test/e2e/api-visibility.ts +++ b/packages/backend/test/e2e/api-visibility.ts @@ -1,8 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { signup, api, post, startServer } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('API visibility', () => { let app: INestApplicationContext; @@ -18,15 +24,15 @@ describe('API visibility', () => { describe('Note visibility', () => { //#region vars /** ヒロイン */ - let alice: any; + let alice: misskey.entities.MeSignup; /** フォロワー */ - let follower: any; + let follower: misskey.entities.MeSignup; /** 非フォロワー */ - let other: any; + let other: misskey.entities.MeSignup; /** 非フォロワーでもリプライやメンションをされた人 */ - let target: any; + let target: misskey.entities.MeSignup; /** specified mentionでmentionを飛ばされる人 */ - let target2: any; + let target2: misskey.entities.MeSignup; /** public-post */ let pub: any; diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts index a46f336a70..15da74931d 100644 --- a/packages/backend/test/e2e/api.ts +++ b/packages/backend/test/e2e/api.ts @@ -1,14 +1,21 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, startServer } from '../utils.js'; +import { IncomingMessage } from 'http'; +import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream, relativeFetch } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('API', () => { let app: INestApplicationContext; - let alice: any; - let bob: any; - let carol: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { app = await startServer(); @@ -80,4 +87,178 @@ describe('API', () => { assert.strictEqual(res.body.nullableDefault, 'hello'); }); }); + + test('管理者専用のAPIのアクセス制限', async () => { + // aliceは管理者、APIを使える + await successfulApiCall({ + endpoint: '/admin/get-index-stats', + parameters: {}, + user: alice, + }); + + // bobは一般ユーザーだからダメ + await failedApiCall({ + endpoint: '/admin/get-index-stats', + parameters: {}, + user: bob, + }, { + status: 403, + code: 'ROLE_PERMISSION_DENIED', + id: 'c3d38592-54c0-429d-be96-5636b0431a61', + }); + + // publicアクセスももちろんダメ + await failedApiCall({ + endpoint: '/admin/get-index-stats', + parameters: {}, + user: undefined, + }, { + status: 401, + code: 'CREDENTIAL_REQUIRED', + id: '1384574d-a912-4b81-8601-c7b1c4085df1', + }); + + // ごまがしもダメ + await failedApiCall({ + endpoint: '/admin/get-index-stats', + parameters: {}, + user: { token: 'tsukawasete' }, + }, { + status: 401, + code: 'AUTHENTICATION_FAILED', + id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', + }); + }); + + describe('Authentication header', () => { + test('一般リクエスト', async () => { + await successfulApiCall({ + endpoint: '/admin/get-index-stats', + parameters: {}, + user: { + token: alice.token, + bearer: true, + }, + }); + }); + + test('multipartリクエスト', async () => { + const result = await uploadFile({ + token: alice.token, + bearer: true, + }); + assert.strictEqual(result.status, 200); + }); + + test('streaming', async () => { + const fired = await waitFire( + { + token: alice.token, + bearer: true, + }, + 'homeTimeline', + () => api('notes/create', { text: 'foo' }, alice), + msg => msg.type === 'note' && msg.body.text === 'foo', + ); + assert.strictEqual(fired, true); + }); + }); + + describe('tokenエラー応答でWWW-Authenticate headerを送る', () => { + describe('invalid_token', () => { + test('一般リクエスト', async () => { + const result = await api('/admin/get-index-stats', {}, { + token: 'syuilo', + bearer: true, + }); + assert.strictEqual(result.status, 401); + assert.ok(result.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="invalid_token", error_description')); + }); + + test('multipartリクエスト', async () => { + const result = await uploadFile({ + token: 'syuilo', + bearer: true, + }); + assert.strictEqual(result.status, 401); + assert.ok(result.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="invalid_token", error_description')); + }); + + test('streaming', async () => { + await assert.rejects(connectStream( + { + token: 'syuilo', + bearer: true, + }, + 'homeTimeline', + () => { }, + ), (err: IncomingMessage) => { + assert.strictEqual(err.statusCode, 401); + assert.ok(err.headers['www-authenticate']?.startsWith('Bearer realm="Misskey", error="invalid_token", error_description')); + return true; + }); + }); + }); + + describe('tokenがないとrealmだけおくる', () => { + test('一般リクエスト', async () => { + const result = await api('/admin/get-index-stats', {}); + assert.strictEqual(result.status, 401); + assert.strictEqual(result.headers.get('WWW-Authenticate'), 'Bearer realm="Misskey"'); + }); + + test('multipartリクエスト', async () => { + const result = await uploadFile(); + assert.strictEqual(result.status, 401); + assert.strictEqual(result.headers.get('WWW-Authenticate'), 'Bearer realm="Misskey"'); + }); + }); + + test('invalid_request', async () => { + const result = await api('/notes/create', { text: true }, { + token: alice.token, + bearer: true, + }); + assert.strictEqual(result.status, 400); + assert.ok(result.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="invalid_request", error_description')); + }); + + describe('invalid bearer format', () => { + test('No preceding bearer', async () => { + const result = await relativeFetch('api/notes/create', { + method: 'POST', + headers: { + Authorization: alice.token, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ text: 'test' }), + }); + assert.strictEqual(result.status, 401); + }); + + test('Lowercase bearer', async () => { + const result = await relativeFetch('api/notes/create', { + method: 'POST', + headers: { + Authorization: `bearer ${alice.token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ text: 'test' }), + }); + assert.strictEqual(result.status, 401); + }); + + test('No space after bearer', async () => { + const result = await relativeFetch('api/notes/create', { + method: 'POST', + headers: { + Authorization: `Bearer${alice.token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ text: 'test' }), + }); + assert.strictEqual(result.status, 401); + }); + }); + }); }); diff --git a/packages/backend/test/e2e/block.ts b/packages/backend/test/e2e/block.ts index 57a46ab38a..4445d9036c 100644 --- a/packages/backend/test/e2e/block.ts +++ b/packages/backend/test/e2e/block.ts @@ -1,16 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { signup, api, post, startServer } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('Block', () => { let app: INestApplicationContext; // alice blocks bob - let alice: any; - let bob: any; - let carol: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { app = await startServer(); diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts index f35aae9dc6..dfdc044caa 100644 --- a/packages/backend/test/e2e/clips.ts +++ b/packages/backend/test/e2e/clips.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; @@ -13,12 +18,12 @@ import { paramDef as UnfavoriteParamDef } from '@/server/api/endpoints/clips/unf import { paramDef as AddNoteParamDef } from '@/server/api/endpoints/clips/add-note.js'; import { paramDef as RemoveNoteParamDef } from '@/server/api/endpoints/clips/remove-note.js'; import { paramDef as NotesParamDef } from '@/server/api/endpoints/clips/notes.js'; -import { - signup, - post, - startServer, +import { + signup, + post, + startServer, api, - successfulApiCall, + successfulApiCall, failedApiCall, ApiRequest, hiddenNote, @@ -82,14 +87,14 @@ describe('クリップ', () => { const update = async (parameters: Partial, request: Partial = {}): Promise => { const clip = await successfulApiCall({ endpoint: '/clips/update', - parameters: { + parameters: { name: 'updated', ...parameters, }, user: alice, ...request, }); - + // 入力が結果として入っていること。clipIdはidになるので消しておく delete (parameters as { clipId?: string }).clipId; assert.deepStrictEqual(clip, { @@ -98,7 +103,7 @@ describe('クリップ', () => { }); return clip; }; - + type DeleteParam = JTDDataType; const deleteClip = async (parameters: DeleteParam, request: Partial = {}): Promise => { return await successfulApiCall({ @@ -129,7 +134,7 @@ describe('クリップ', () => { ...request, }); }; - + const usersClips = async (request: Partial): Promise => { return await successfulApiCall({ endpoint: '/users/clips', @@ -145,14 +150,14 @@ describe('クリップ', () => { bob = await signup({ username: 'bob' }); // FIXME: misskey-jsのNoteはoutdatedなので直接変換できない - aliceNote = await post(alice, { text: 'test' }) as any; - aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' }) as any; - aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' }) as any; - aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' }) as any; - bobNote = await post(bob, { text: 'test' }) as any; - bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' }) as any; - bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' }) as any; - bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any; + aliceNote = await post(alice, { text: 'test' }) as any; + aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' }) as any; + aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' }) as any; + aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' }) as any; + bobNote = await post(bob, { text: 'test' }) as any; + bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' }) as any; + bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' }) as any; + bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any; }, 1000 * 60 * 2); afterAll(async () => { @@ -172,7 +177,7 @@ describe('クリップ', () => { test('の作成ができる', async () => { const res = await create(); // ISO 8601で日付が返ってくること - assert.strictEqual(res.createdAt, new Date(res.createdAt).toISOString()); + assert.strictEqual(res.createdAt, new Date(res.createdAt).toISOString()); assert.strictEqual(res.lastClippedAt, null); assert.strictEqual(res.name, 'test'); assert.strictEqual(res.description, null); @@ -217,7 +222,7 @@ describe('クリップ', () => { ]; test.each(createClipDenyPattern)('の作成は$labelならできない', async ({ parameters }) => failedApiCall({ endpoint: '/clips/create', - parameters: { + parameters: { ...defaultCreate(), ...parameters, }, @@ -229,7 +234,7 @@ describe('クリップ', () => { })); test('の更新ができる', async () => { - const res = await update({ + const res = await update({ clipId: (await create()).id, name: 'updated', description: 'new description', @@ -237,7 +242,7 @@ describe('クリップ', () => { }); // ISO 8601で日付が返ってくること - assert.strictEqual(res.createdAt, new Date(res.createdAt).toISOString()); + assert.strictEqual(res.createdAt, new Date(res.createdAt).toISOString()); assert.strictEqual(res.lastClippedAt, null); assert.strictEqual(res.name, 'updated'); assert.strictEqual(res.description, 'new description'); @@ -251,7 +256,7 @@ describe('クリップ', () => { name: 'updated', ...parameters, })); - + test.each([ { label: 'clipIdがnull', parameters: { clipId: null } }, { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assertion: { @@ -265,7 +270,7 @@ describe('クリップ', () => { ...createClipDenyPattern as any, ])('の更新は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ endpoint: '/clips/update', - parameters: { + parameters: { clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, name: 'updated', ...parameters, @@ -279,7 +284,7 @@ describe('クリップ', () => { })); test('の削除ができる', async () => { - await deleteClip({ + await deleteClip({ clipId: (await create()).id, }); assert.deepStrictEqual(await list({}), []); @@ -297,7 +302,7 @@ describe('クリップ', () => { } }, ])('の削除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ endpoint: '/clips/delete', - parameters: { + parameters: { clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, ...parameters, }, @@ -329,14 +334,14 @@ describe('クリップ', () => { }); test.each([ - { label: 'clipId未指定', parameters: { clipId: undefined } }, - { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assetion: { + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assetion: { code: 'NO_SUCH_CLIP', id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20', } }, ])('のID指定取得は$labelならできない', async ({ parameters, assetion }) => failedApiCall({ endpoint: '/clips/show', - parameters: { + parameters: { ...parameters, }, user: alice, @@ -361,14 +366,14 @@ describe('クリップ', () => { // 返ってくる配列には順序保障がないのでidでソートして厳密比較 assert.deepStrictEqual( - res.sort(compareBy(s => s.id)), + res.sort(compareBy(s => s.id)), clips.sort(compareBy(s => s.id)), ); }); test('の一覧が取得できる(空)', async () => { const res = await usersClips({ - parameters: { + parameters: { userId: alice.id, }, }); @@ -381,14 +386,14 @@ describe('クリップ', () => { ])('の一覧が$label取得できる', async () => { const clips = await createMany({ isPublic: true }); const res = await usersClips({ - parameters: { + parameters: { userId: alice.id, }, }); // 返ってくる配列には順序保障がないのでidでソートして厳密比較 assert.deepStrictEqual( - res.sort(compareBy(s => s.id)), + res.sort(compareBy(s => s.id)), clips.sort(compareBy(s => s.id))); // 認証状態で見たときだけisFavoritedが入っている @@ -421,7 +426,7 @@ describe('クリップ', () => { await create({ isPublic: false }); const aliceClip = await create({ isPublic: true }); const res = await usersClips({ - parameters: { + parameters: { userId: alice.id, limit: 2, }, @@ -433,7 +438,7 @@ describe('クリップ', () => { const clips = await createMany({ isPublic: true }, 7); clips.sort(compareBy(s => s.id)); const res = await usersClips({ - parameters: { + parameters: { userId: alice.id, sinceId: clips[1].id, untilId: clips[5].id, @@ -443,7 +448,7 @@ describe('クリップ', () => { // Promise.allで返ってくる配列には順序保障がないのでidでソートして厳密比較 assert.deepStrictEqual( - res.sort(compareBy(s => s.id)), + res.sort(compareBy(s => s.id)), [clips[2], clips[3], clips[4]], // sinceIdとuntilId自体は結果に含まれない clips[1].id + ' ... ' + clips[3].id + ' with ' + clips.map(s => s.id) + ' vs. ' + res.map(s => s.id)); }); @@ -454,7 +459,7 @@ describe('クリップ', () => { { label: 'limit最大+1', parameters: { limit: 101 } }, ])('の一覧は$labelだと取得できない', async ({ parameters }) => failedApiCall({ endpoint: '/users/clips', - parameters: { + parameters: { userId: alice.id, ...parameters, }, @@ -520,7 +525,7 @@ describe('クリップ', () => { ...request, }); }; - + beforeEach(async () => { aliceClip = await create(); }); @@ -544,7 +549,7 @@ describe('クリップ', () => { assert.strictEqual(clip2.favoritedCount, 1); assert.strictEqual(clip2.isFavorited, false); }); - + test('は1つのクリップに対して複数人が設定できる。', async () => { const publicClip = await create({ isPublic: true }); await favorite({ clipId: publicClip.id }, { user: bob }); @@ -552,7 +557,7 @@ describe('クリップ', () => { const clip = await show({ clipId: publicClip.id }, { user: bob }); assert.strictEqual(clip.favoritedCount, 2); assert.strictEqual(clip.isFavorited, true); - + const clip2 = await show({ clipId: publicClip.id }); assert.strictEqual(clip2.favoritedCount, 2); assert.strictEqual(clip2.isFavorited, true); @@ -581,7 +586,7 @@ describe('クリップ', () => { await favorite({ clipId: aliceClip.id }); await failedApiCall({ endpoint: '/clips/favorite', - parameters: { + parameters: { clipId: aliceClip.id, }, user: alice, @@ -604,7 +609,7 @@ describe('クリップ', () => { } }, ])('の設定は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ endpoint: '/clips/favorite', - parameters: { + parameters: { clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, ...parameters, }, @@ -615,7 +620,7 @@ describe('クリップ', () => { id: '3d81ceae-475f-4600-b2a8-2bc116157532', ...assertion, })); - + test('を設定解除できる。', async () => { await favorite({ clipId: aliceClip.id }); await unfavorite({ clipId: aliceClip.id }); @@ -641,7 +646,7 @@ describe('クリップ', () => { } }, ])('の設定解除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ endpoint: '/clips/unfavorite', - parameters: { + parameters: { clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, ...parameters, }, @@ -652,7 +657,7 @@ describe('クリップ', () => { id: '3d81ceae-475f-4600-b2a8-2bc116157532', ...assertion, })); - + test('を取得できる。', async () => { await favorite({ clipId: aliceClip.id }); const favorited = await myFavorites(); @@ -716,8 +721,8 @@ describe('クリップ', () => { await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); const res = await show({ clipId: aliceClip.id }); assert.strictEqual(res.lastClippedAt, new Date(res.lastClippedAt ?? '').toISOString()); - assert.deepStrictEqual(await notes({ clipId: aliceClip.id }), [aliceNote]); - + assert.deepStrictEqual((await notes({ clipId: aliceClip.id })).map(x => x.id), [aliceNote.id]); + // 他人の非公開ノートも突っ込める await addNote({ clipId: aliceClip.id, noteId: bobHomeNote.id }); await addNote({ clipId: aliceClip.id, noteId: bobFollowersNote.id }); @@ -728,7 +733,7 @@ describe('クリップ', () => { await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); await failedApiCall({ endpoint: '/clips/add-note', - parameters: { + parameters: { clipId: aliceClip.id, noteId: aliceNote.id, }, @@ -747,10 +752,10 @@ describe('クリップ', () => { text: `test ${i}`, }) as unknown)) as Note[]; await Promise.all(noteList.map(s => addNote({ clipId: aliceClip.id, noteId: s.id }))); - + await failedApiCall({ endpoint: '/clips/add-note', - parameters: { + parameters: { clipId: aliceClip.id, noteId: aliceNote.id, }, @@ -764,7 +769,7 @@ describe('クリップ', () => { test('は他人のクリップへ追加できない。', async () => await failedApiCall({ endpoint: '/clips/add-note', - parameters: { + parameters: { clipId: aliceClip.id, noteId: aliceNote.id, }, @@ -776,9 +781,9 @@ describe('クリップ', () => { })); test.each([ - { label: 'clipId未指定', parameters: { clipId: undefined } }, - { label: 'noteId未指定', parameters: { noteId: undefined } }, - { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assetion: { + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: 'noteId未指定', parameters: { noteId: undefined } }, + { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assetion: { code: 'NO_SUCH_CLIP', id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf', } }, @@ -792,7 +797,7 @@ describe('クリップ', () => { } }, ])('の追加は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({ endpoint: '/clips/add-note', - parameters: { + parameters: { clipId: aliceClip.id, noteId: aliceNote.id, ...parameters, @@ -810,11 +815,11 @@ describe('クリップ', () => { await removeNote({ clipId: aliceClip.id, noteId: aliceNote.id }); assert.deepStrictEqual(await notes({ clipId: aliceClip.id }), []); }); - + test.each([ - { label: 'clipId未指定', parameters: { clipId: undefined } }, - { label: 'noteId未指定', parameters: { noteId: undefined } }, - { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assetion: { + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: 'noteId未指定', parameters: { noteId: undefined } }, + { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assetion: { code: 'NO_SUCH_CLIP', id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', // add-noteと異なる } }, @@ -828,7 +833,7 @@ describe('クリップ', () => { } }, ])('の削除は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({ endpoint: '/clips/remove-note', - parameters: { + parameters: { clipId: aliceClip.id, noteId: aliceNote.id, ...parameters, @@ -848,16 +853,16 @@ describe('クリップ', () => { } const res = await notes({ clipId: aliceClip.id }); - + // 自分のノートは非公開でも入れられるし、見える // 他人の非公開ノートは入れられるけど、除外される const expects = [ aliceNote, aliceHomeNote, aliceFollowersNote, aliceSpecifiedNote, - bobNote, bobHomeNote, + bobNote, bobHomeNote, ]; assert.deepStrictEqual( - res.sort(compareBy(s => s.id)), - expects.sort(compareBy(s => s.id))); + res.sort(compareBy(s => s.id)).map(x => x.id), + expects.sort(compareBy(s => s.id)).map(x => x.id)); }); test('を始端IDとlimitで取得できる。', async () => { @@ -867,7 +872,7 @@ describe('クリップ', () => { await addNote({ clipId: aliceClip.id, noteId: note.id }); } - const res = await notes({ + const res = await notes({ clipId: aliceClip.id, sinceId: noteList[2].id, limit: 3, @@ -876,8 +881,8 @@ describe('クリップ', () => { // Promise.allで返ってくる配列はID順で並んでないのでソートして厳密比較 const expects = [noteList[3], noteList[4], noteList[5]]; assert.deepStrictEqual( - res.sort(compareBy(s => s.id)), - expects.sort(compareBy(s => s.id))); + res.sort(compareBy(s => s.id)).map(x => x.id), + expects.sort(compareBy(s => s.id)).map(x => x.id)); }); test('をID範囲指定で取得できる。', async () => { @@ -892,12 +897,12 @@ describe('クリップ', () => { sinceId: noteList[1].id, untilId: noteList[4].id, }); - + // Promise.allで返ってくる配列はID順で並んでないのでソートして厳密比較 const expects = [noteList[2], noteList[3]]; assert.deepStrictEqual( - res.sort(compareBy(s => s.id)), - expects.sort(compareBy(s => s.id))); + res.sort(compareBy(s => s.id)).map(x => x.id), + expects.sort(compareBy(s => s.id)).map(x => x.id)); }); test.todo('Remoteのノートもクリップできる。どうテストしよう?'); @@ -906,7 +911,7 @@ describe('クリップ', () => { const bobClip = await create({ isPublic: true }, { user: bob } ); await addNote({ clipId: bobClip.id, noteId: aliceNote.id }, { user: bob }); const res = await notes({ clipId: bobClip.id }); - assert.deepStrictEqual(res, [aliceNote]); + assert.deepStrictEqual(res.map(x => x.id), [aliceNote.id]); }); test('はPublicなクリップなら認証なしでも取得できる。(非公開ノートはhideされて返ってくる)', async () => { @@ -918,15 +923,15 @@ describe('クリップ', () => { const res = await notes({ clipId: publicClip.id }, { user: undefined }); const expects = [ - aliceNote, aliceHomeNote, + aliceNote, aliceHomeNote, // 認証なしだと非公開ノートは結果には含むけどhideされる。 hiddenNote(aliceFollowersNote), hiddenNote(aliceSpecifiedNote), ]; assert.deepStrictEqual( - res.sort(compareBy(s => s.id)), - expects.sort(compareBy(s => s.id))); + res.sort(compareBy(s => s.id)).map(x => x.id), + expects.sort(compareBy(s => s.id)).map(x => x.id)); }); - + test.todo('ブロック、ミュートされたユーザーからの設定&取得etc.'); test.each([ @@ -947,7 +952,7 @@ describe('クリップ', () => { } }, ])('は$labelだと取得できない', async ({ parameters, user, assertion }) => failedApiCall({ endpoint: '/clips/notes', - parameters: { + parameters: { clipId: aliceClip.id, ...parameters, }, diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index f885209b7f..2ef3434bca 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -1,20 +1,26 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; // node-fetch only supports it's own Blob yet // https://github.com/node-fetch/node-fetch/pull/1664 import { Blob } from 'node-fetch'; +import { MiUser } from '@/models/_.js'; import { startServer, signup, post, api, uploadFile, simpleGet, initTestDb } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; -import { User } from '@/models/index.js'; +import type * as misskey from 'misskey-js'; describe('Endpoints', () => { let app: INestApplicationContext; - let alice: any; - let bob: any; - let carol: any; - let dave: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; + let dave: misskey.entities.MeSignup; beforeAll(async () => { app = await startServer(); @@ -292,7 +298,7 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 200); const connection = await initTestDb(true); - const Users = connection.getRepository(User); + const Users = connection.getRepository(MiUser); const newBob = await Users.findOneByOrFail({ id: bob.id }); assert.strictEqual(newBob.followersCount, 0); assert.strictEqual(newBob.followingCount, 1); @@ -354,7 +360,7 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 200); const connection = await initTestDb(true); - const Users = connection.getRepository(User); + const Users = connection.getRepository(MiUser); const newBob = await Users.findOneByOrFail({ id: bob.id }); assert.strictEqual(newBob.followersCount, 0); assert.strictEqual(newBob.followingCount, 0); diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts index 78ca8b43ba..1cbfec3e5f 100644 --- a/packages/backend/test/e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -1,9 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { startServer, channel, clip, cookie, galleryPost, signup, page, play, post, simpleGet, uploadFile } from '../utils.js'; import type { SimpleGetResponse } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; // Request Accept const ONLY_AP = 'application/activity+json'; @@ -19,7 +25,7 @@ const JSON_UTF8 = 'application/json; charset=utf-8'; describe('Webリソース', () => { let app: INestApplicationContext; - let alice: any; + let alice: misskey.entities.MeSignup; let aliceUploadedFile: any; let alicesPost: any; let alicePage: any; @@ -28,8 +34,10 @@ describe('Webリソース', () => { let aliceGalleryPost: any; let aliceChannel: any; - type Request = { - path: string, + let bob: misskey.entities.MeSignup; + + type Request = { + path: string, accept?: string, cookie?: string, }; @@ -46,7 +54,7 @@ describe('Webリソース', () => { const notOk = async (param: Request & { status?: number, code?: string, - }): Promise => { + }): Promise => { const { path, accept, cookie, status, code } = param; const res = await simpleGet(path, accept, cookie); assert.notStrictEqual(res.status, 200); @@ -58,8 +66,8 @@ describe('Webリソース', () => { } return res; }; - - const notFound = async (param: Request): Promise => { + + const notFound = async (param: Request): Promise => { return await notOk({ ...param, status: 404, @@ -84,6 +92,8 @@ describe('Webリソース', () => { fileIds: [aliceUploadedFile.body.id], }); aliceChannel = await channel(alice, {}); + + bob = await signup({ username: 'alice' }); }, 1000 * 60 * 2); afterAll(async () => { @@ -94,23 +104,23 @@ describe('Webリソース', () => { { path: '/', type: HTML }, { path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。" // fastify-static gives charset=UTF-8 instead of utf-8 and that's okay - { path: '/api-doc', type: 'text/html; charset=UTF-8' }, - { path: '/api.json', type: JSON_UTF8 }, - { path: '/api-console', type: HTML }, - { path: '/_info_card_', type: HTML }, - { path: '/bios', type: HTML }, - { path: '/cli', type: HTML }, - { path: '/flush', type: HTML }, + { path: '/api-doc', type: 'text/html; charset=UTF-8' }, + { path: '/api.json', type: JSON_UTF8 }, + { path: '/api-console', type: HTML }, + { path: '/_info_card_', type: HTML }, + { path: '/bios', type: HTML }, + { path: '/cli', type: HTML }, + { path: '/flush', type: HTML }, { path: '/robots.txt', type: 'text/plain; charset=UTF-8' }, - { path: '/favicon.ico', type: 'image/vnd.microsoft.icon' }, + { path: '/favicon.ico', type: 'image/vnd.microsoft.icon' }, { path: '/opensearch.xml', type: 'application/opensearchdescription+xml' }, - { path: '/apple-touch-icon.png', type: 'image/png' }, - { path: '/twemoji/2764.svg', type: 'image/svg+xml' }, - { path: '/twemoji/2764-fe0f-200d-1f525.svg', type: 'image/svg+xml' }, - { path: '/twemoji-badge/2764.png', type: 'image/png' }, + { path: '/apple-touch-icon.png', type: 'image/png' }, + { path: '/twemoji/2764.svg', type: 'image/svg+xml' }, + { path: '/twemoji/2764-fe0f-200d-1f525.svg', type: 'image/svg+xml' }, + { path: '/twemoji-badge/2764.png', type: 'image/png' }, { path: '/twemoji-badge/2764-fe0f-200d-1f525.png', type: 'image/png' }, - { path: '/fluent-emoji/2764.png', type: 'image/png' }, - { path: '/fluent-emoji/2764-fe0f-200d-1f525.png', type: 'image/png' }, + { path: '/fluent-emoji/2764.png', type: 'image/png' }, + { path: '/fluent-emoji/2764-fe0f-200d-1f525.png', type: 'image/png' }, ])('$path', (p) => { test('がGETできる。', async () => await ok({ ...p })); @@ -120,58 +130,64 @@ describe('Webリソース', () => { }); describe.each([ - { path: '/twemoji/2764.png' }, - { path: '/twemoji/2764-fe0f-200d-1f525.png' }, - { path: '/twemoji-badge/2764.svg' }, + { path: '/twemoji/2764.png' }, + { path: '/twemoji/2764-fe0f-200d-1f525.png' }, + { path: '/twemoji-badge/2764.svg' }, { path: '/twemoji-badge/2764-fe0f-200d-1f525.svg' }, - { path: '/fluent-emoji/2764.svg' }, - { path: '/fluent-emoji/2764-fe0f-200d-1f525.svg' }, + { path: '/fluent-emoji/2764.svg' }, + { path: '/fluent-emoji/2764-fe0f-200d-1f525.svg' }, ])('$path', ({ path }) => { test('はGETできない。', async () => await notFound({ path })); }); describe.each([ - { ext: 'rss', type: 'application/rss+xml; charset=utf-8' }, - { ext: 'atom', type: 'application/atom+xml; charset=utf-8' }, - { ext: 'json', type: 'application/json; charset=utf-8' }, + { ext: 'rss', type: 'application/rss+xml; charset=utf-8' }, + { ext: 'atom', type: 'application/atom+xml; charset=utf-8' }, + { ext: 'json', type: 'application/json; charset=utf-8' }, ])('/@:username.$ext', ({ ext, type }) => { const path = (username: string): string => `/@${username}.${ext}`; - test('がGETできる。', async () => await ok({ + test('がGETできる。', async () => await ok({ path: path(alice.username), type, })); - test('は存在しないユーザーはGETできない。', async () => await notOk({ + test('は存在しないユーザーはGETできない。', async () => await notOk({ path: path('nonexisting'), - status: 404, + status: 404, })); }); describe.each([{ path: '/api/foo' }])('$path', ({ path }) => { - test('はGETできない。', async () => await notOk({ + test('はGETできない。', async () => await notOk({ path, - status: 404, + status: 404, code: 'UNKNOWN_API_ENDPOINT', })); }); describe.each([{ path: '/queue' }])('$path', ({ path }) => { - test('はadminでなければGETできない。', async () => await notOk({ + test('はログインしないとGETできない。', async () => await notOk({ path, - status: 500, // FIXME? 403ではない。 + status: 401, })); - - test('はadminならGETできる。', async () => await ok({ + + test('はadminでなければGETできない。', async () => await notOk({ + path, + cookie: cookie(bob), + status: 403, + })); + + test('はadminならGETできる。', async () => await ok({ path, cookie: cookie(alice), - })); + })); }); describe.each([{ path: '/streaming' }])('$path', ({ path }) => { - test('はGETできない。', async () => await notOk({ + test('はGETできない。', async () => await notOk({ path, - status: 503, + status: 503, })); }); @@ -183,21 +199,21 @@ describe('Webリソース', () => { { accept: UNSPECIFIED }, ])('(Acceptヘッダ: $accept)', ({ accept }) => { test('はHTMLとしてGETできる。', async () => { - const res = await ok({ - path: path(alice.username), - accept, + const res = await ok({ + path: path(alice.username), + accept, type: HTML, }); assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); - + // TODO ogタグの検証 // TODO profile.noCrawleの検証 // TODO twitter:creatorの検証 // TODO の検証 }); - test('はHTMLとしてGETできる。(存在しないIDでも。)', async () => await ok({ - path: path('xxxxxxxxxx'), + test('はHTMLとしてGETできる。(存在しないIDでも。)', async () => await ok({ + path: path('xxxxxxxxxx'), type: HTML, })); }); @@ -207,22 +223,22 @@ describe('Webリソース', () => { { accept: PREFER_AP }, ])('(Acceptヘッダ: $accept)', ({ accept }) => { test('はActivityPubとしてGETできる。', async () => { - const res = await ok({ - path: path(alice.username), - accept, + const res = await ok({ + path: path(alice.username), + accept, type: AP, }); assert.strictEqual(res.body.type, 'Person'); }); - test('は存在しないIDのときActivityPubとしてGETできない。', async () => await notFound({ - path: path('xxxxxxxxxx'), + test('は存在しないIDのときActivityPubとしてGETできない。', async () => await notFound({ + path: path('xxxxxxxxxx'), accept, })); }); }); - describe.each([ + describe.each([ // 実際のハンドルはフロントエンド(index.vue)で行われる { sub: 'home' }, { sub: 'notes' }, @@ -236,32 +252,32 @@ describe('Webリソース', () => { const path = (username: string): string => `/@${username}/${sub}`; test('はHTMLとしてGETできる。', async () => { - const res = await ok({ - path: path(alice.username), + const res = await ok({ + path: path(alice.username), }); assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); }); }); - + describe('/@:user/pages/:page', () => { const path = (username: string, pagename: string): string => `/@${username}/pages/${pagename}`; test('はHTMLとしてGETできる。', async () => { - const res = await ok({ - path: path(alice.username, alicePage.name), + const res = await ok({ + path: path(alice.username, alicePage.name), }); assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); assert.strictEqual(metaTag(res, 'misskey:page-id'), alicePage.id); - + // TODO ogタグの検証 // TODO profile.noCrawleの検証 // TODO twitter:creatorの検証 }); - - test('はGETできる。(存在しないIDでも。)', async () => await ok({ - path: path(alice.username, 'xxxxxxxxxx'), + + test('はGETできる。(存在しないIDでも。)', async () => await ok({ + path: path(alice.username, 'xxxxxxxxxx'), })); }); @@ -278,7 +294,7 @@ describe('Webリソース', () => { assert.strictEqual(res.location, `/@${alice.username}`); }); - test('は存在しないユーザーはGETできない。', async () => await notFound({ + test('は存在しないユーザーはGETできない。', async () => await notFound({ path: path('xxxxxxxx'), })); }); @@ -288,24 +304,24 @@ describe('Webリソース', () => { { accept: PREFER_AP }, ])('(Acceptヘッダ: $accept)', ({ accept }) => { test('はActivityPubとしてGETできる。', async () => { - const res = await ok({ - path: path(alice.id), - accept, + const res = await ok({ + path: path(alice.id), + accept, type: AP, }); assert.strictEqual(res.body.type, 'Person'); }); - test('は存在しないIDのときActivityPubとしてGETできない。', async () => await notOk({ + test('は存在しないIDのときActivityPubとしてGETできない。', async () => await notOk({ path: path('xxxxxxxx'), accept, status: 404, })); }); }); - + describe('/users/inbox', () => { - test('がGETできる。(POST専用だけど4xx/5xxにならずHTMLが返ってくる)', async () => await ok({ + test('がGETできる。(POST専用だけど4xx/5xxにならずHTMLが返ってくる)', async () => await ok({ path: '/inbox', })); @@ -315,7 +331,7 @@ describe('Webリソース', () => { describe('/users/:id/inbox', () => { const path = (id: string): string => `/users/${id}/inbox`; - test('がGETできる。(POST専用だけど4xx/5xxにならずHTMLが返ってくる)', async () => await ok({ + test('がGETできる。(POST専用だけど4xx/5xxにならずHTMLが返ってくる)', async () => await ok({ path: path(alice.id), })); @@ -326,14 +342,14 @@ describe('Webリソース', () => { const path = (id: string): string => `/users/${id}/outbox`; test('がGETできる。', async () => { - const res = await ok({ - path: path(alice.id), + const res = await ok({ + path: path(alice.id), type: AP, }); assert.strictEqual(res.body.type, 'OrderedCollection'); }); }); - + describe('/notes/:id', () => { const path = (noteId: string): string => `/notes/${noteId}`; @@ -342,22 +358,22 @@ describe('Webリソース', () => { { accept: UNSPECIFIED }, ])('(Acceptヘッダ: $accept)', ({ accept }) => { test('はHTMLとしてGETできる。', async () => { - const res = await ok({ - path: path(alicesPost.id), - accept, + const res = await ok({ + path: path(alicesPost.id), + accept, type: HTML, }); assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); - assert.strictEqual(metaTag(res, 'misskey:note-id'), alicesPost.id); - + assert.strictEqual(metaTag(res, 'misskey:note-id'), alicesPost.id); + // TODO ogタグの検証 // TODO profile.noCrawleの検証 // TODO twitter:creatorの検証 }); - test('はHTMLとしてGETできる。(存在しないIDでも。)', async () => await ok({ - path: path('xxxxxxxxxx'), + test('はHTMLとしてGETできる。(存在しないIDでも。)', async () => await ok({ + path: path('xxxxxxxxxx'), })); }); @@ -366,48 +382,48 @@ describe('Webリソース', () => { { accept: PREFER_AP }, ])('(Acceptヘッダ: $accept)', ({ accept }) => { test('はActivityPubとしてGETできる。', async () => { - const res = await ok({ - path: path(alicesPost.id), + const res = await ok({ + path: path(alicesPost.id), accept, type: AP, }); assert.strictEqual(res.body.type, 'Note'); }); - test('は存在しないIDのときActivityPubとしてGETできない。', async () => await notFound({ - path: path('xxxxxxxxxx'), + test('は存在しないIDのときActivityPubとしてGETできない。', async () => await notFound({ + path: path('xxxxxxxxxx'), accept, })); }); }); - + describe('/play/:id', () => { const path = (playid: string): string => `/play/${playid}`; test('がGETできる。', async () => { - const res = await ok({ - path: path(alicePlay.id), + const res = await ok({ + path: path(alicePlay.id), }); assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); assert.strictEqual(metaTag(res, 'misskey:flash-id'), alicePlay.id); - + // TODO ogタグの検証 // TODO profile.noCrawleの検証 // TODO twitter:creatorの検証 }); - test('がGETできる。(存在しないIDでも。)', async () => await ok({ - path: path('xxxxxxxxxx'), + test('がGETできる。(存在しないIDでも。)', async () => await ok({ + path: path('xxxxxxxxxx'), })); }); - + describe('/clips/:clip', () => { const path = (clip: string): string => `/clips/${clip}`; test('がGETできる。', async () => { - const res = await ok({ - path: path(aliceClip.id), + const res = await ok({ + path: path(aliceClip.id), }); assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); @@ -416,9 +432,9 @@ describe('Webリソース', () => { // TODO ogタグの検証 // TODO profile.noCrawleの検証 }); - - test('がGETできる。(存在しないIDでも。)', async () => await ok({ - path: path('xxxxxxxxxx'), + + test('がGETできる。(存在しないIDでも。)', async () => await ok({ + path: path('xxxxxxxxxx'), })); }); @@ -426,8 +442,8 @@ describe('Webリソース', () => { const path = (post: string): string => `/gallery/${post}`; test('がGETできる。', async () => { - const res = await ok({ - path: path(aliceGalleryPost.id), + const res = await ok({ + path: path(aliceGalleryPost.id), }); assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); @@ -436,26 +452,26 @@ describe('Webリソース', () => { // TODO profile.noCrawleの検証 // TODO twitter:creatorの検証 }); - - test('がGETできる。(存在しないIDでも。)', async () => await ok({ - path: path('xxxxxxxxxx'), + + test('がGETできる。(存在しないIDでも。)', async () => await ok({ + path: path('xxxxxxxxxx'), })); }); - + describe('/channels/:channel', () => { const path = (channel: string): string => `/channels/${channel}`; test('はGETできる。', async () => { const res = await ok({ - path: path(aliceChannel.id), + path: path(aliceChannel.id), }); // FIXME: misskey関連のmetaタグの設定がない // TODO ogタグの検証 }); - - test('がGETできる。(存在しないIDでも。)', async () => await ok({ - path: path('xxxxxxxxxx'), + + test('がGETできる。(存在しないIDでも。)', async () => await ok({ + path: path('xxxxxxxxxx'), })); }); }); diff --git a/packages/backend/test/e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts index 7b75005a39..7841e057bf 100644 --- a/packages/backend/test/e2e/ff-visibility.ts +++ b/packages/backend/test/e2e/ff-visibility.ts @@ -1,14 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { signup, api, startServer, simpleGet } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('FF visibility', () => { let app: INestApplicationContext; - let alice: any; - let bob: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; beforeAll(async () => { app = await startServer(); diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts index 7d6c646090..3f158f9f13 100644 --- a/packages/backend/test/e2e/move.ts +++ b/packages/backend/test/e2e/move.ts @@ -1,12 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import rndstr from 'rndstr'; import { loadConfig } from '@/config.js'; -import { User, UsersRepository } from '@/models/index.js'; +import { MiUser, UsersRepository } from '@/models/_.js'; import { jobQueue } from '@/boot/common.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; import { uploadFile, signup, startServer, initTestDb, api, sleep, successfulApiCall } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('Account Move', () => { let app: INestApplicationContext; @@ -14,12 +20,12 @@ describe('Account Move', () => { let url: URL; let root: any; - let alice: any; - let bob: any; - let carol: any; - let dave: any; - let eve: any; - let frank: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; + let dave: misskey.entities.MeSignup; + let eve: misskey.entities.MeSignup; + let frank: misskey.entities.MeSignup; let Users: UsersRepository; @@ -36,7 +42,7 @@ describe('Account Move', () => { dave = await signup({ username: 'dave' }); eve = await signup({ username: 'eve' }); frank = await signup({ username: 'frank' }); - Users = connection.getRepository(User); + Users = connection.getRepository(MiUser); }, 1000 * 60 * 2); afterAll(async () => { @@ -162,7 +168,7 @@ describe('Account Move', () => { alsoKnownAs: [`@alice@${url.hostname}`], }, root); const listRoot = await api('/users/lists/create', { - name: rndstr('0-9a-z', 8), + name: secureRndstr(8), }, root); await api('/users/lists/push', { listId: listRoot.body.id, @@ -176,9 +182,9 @@ describe('Account Move', () => { userId: eve.id, }, alice); const antenna = await api('/antennas/create', { - name: rndstr('0-9a-z', 8), + name: secureRndstr(8), src: 'home', - keywords: [rndstr('0-9a-z', 8)], + keywords: [secureRndstr(8)], excludeKeywords: [], users: [], caseSensitive: false, @@ -210,7 +216,7 @@ describe('Account Move', () => { userId: dave.id, }, eve); const listEve = await api('/users/lists/create', { - name: rndstr('0-9a-z', 8), + name: secureRndstr(8), }, eve); await api('/users/lists/push', { listId: listEve.body.id, @@ -419,9 +425,9 @@ describe('Account Move', () => { test('Prohibit access after moving: /antennas/update', async () => { const res = await api('/antennas/update', { antennaId, - name: rndstr('0-9a-z', 8), + name: secureRndstr(8), src: 'users', - keywords: [rndstr('0-9a-z', 8)], + keywords: [secureRndstr(8)], excludeKeywords: [], users: [eve.id], caseSensitive: false, diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts index 25bd532cfb..a4b57a1eba 100644 --- a/packages/backend/test/e2e/mute.ts +++ b/packages/backend/test/e2e/mute.ts @@ -1,16 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { signup, api, post, react, startServer, waitFire } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('Mute', () => { let app: INestApplicationContext; // alice mutes carol - let alice: any; - let bob: any; - let carol: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { app = await startServer(); diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index d2eb8f01d7..961df99cc2 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -1,21 +1,28 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { Note } from '@/models/entities/Note.js'; +import { MiNote } from '@/models/Note.js'; +import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { signup, post, uploadUrl, startServer, initTestDb, api, uploadFile } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('Note', () => { let app: INestApplicationContext; let Notes: any; - let alice: any; - let bob: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; beforeAll(async () => { app = await startServer(); const connection = await initTestDb(true); - Notes = connection.getRepository(Note); + Notes = connection.getRepository(MiNote); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); }, 1000 * 60 * 2); @@ -163,7 +170,7 @@ describe('Note', () => { test('文字数ぎりぎりで怒られない', async () => { const post = { - text: '!'.repeat(3000), + text: '!'.repeat(MAX_NOTE_TEXT_LENGTH), // 3000文字 }; const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 200); @@ -171,7 +178,7 @@ describe('Note', () => { test('文字数オーバーで怒られる', async () => { const post = { - text: '!'.repeat(3001), + text: '!'.repeat(MAX_NOTE_TEXT_LENGTH + 1), // 3001文字 }; const res = await api('/notes/create', post, alice); assert.strictEqual(res.status, 400); @@ -378,7 +385,7 @@ describe('Note', () => { }, }, }, alice); - + assert.strictEqual(res.status, 200); const assign = await api('admin/roles/assign', { @@ -545,8 +552,8 @@ describe('Note', () => { test('センシティブな投稿はhomeになる (単語指定)', async () => { const sensitive = await api('admin/update-meta', { sensitiveWords: [ - "test", - ] + 'test', + ], }, alice); assert.strictEqual(sensitive.status, 204); @@ -559,14 +566,13 @@ describe('Note', () => { assert.strictEqual(note1.status, 200); assert.strictEqual(note1.body.createdNote.visibility, 'home'); - }); test('センシティブな投稿はhomeになる (正規表現)', async () => { const sensitive = await api('admin/update-meta', { sensitiveWords: [ - "/Test/i", - ] + '/Test/i', + ], }, alice); assert.strictEqual(sensitive.status, 204); @@ -582,8 +588,8 @@ describe('Note', () => { test('センシティブな投稿はhomeになる (スペースアンド)', async () => { const sensitive = await api('admin/update-meta', { sensitiveWords: [ - "Test hoge" - ] + 'Test hoge', + ], }, alice); assert.strictEqual(sensitive.status, 204); @@ -594,7 +600,6 @@ describe('Note', () => { assert.strictEqual(note2.status, 200); assert.strictEqual(note2.body.createdNote.visibility, 'home'); - }); }); diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts new file mode 100644 index 0000000000..a029a0d4be --- /dev/null +++ b/packages/backend/test/e2e/oauth.ts @@ -0,0 +1,944 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * Basic OAuth tests to make sure the library is correctly integrated to Misskey + * and not regressed by version updates or potential migration to another library. + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { AuthorizationCode, ResourceOwnerPassword, type AuthorizationTokenConfig, ClientCredentials, ModuleOptions } from 'simple-oauth2'; +import pkceChallenge from 'pkce-challenge'; +import { JSDOM } from 'jsdom'; +import Fastify, { type FastifyReply, type FastifyInstance } from 'fastify'; +import { api, port, signup, startServer } from '../utils.js'; +import type * as misskey from 'misskey-js'; +import type { INestApplicationContext } from '@nestjs/common'; + +const host = `http://127.0.0.1:${port}`; + +const clientPort = port + 1; +const redirect_uri = `http://127.0.0.1:${clientPort}/redirect`; + +const basicAuthParams: AuthorizationParamsExtended = { + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', +}; + +interface AuthorizationParamsExtended { + redirect_uri: string; + scope: string | string[]; + state: string; + code_challenge?: string; + code_challenge_method?: string; +} + +interface AuthorizationTokenConfigExtended extends AuthorizationTokenConfig { + code_verifier: string | undefined; +} + +interface GetTokenError { + data: { + payload: { + error: string; + } + } +} + +const clientConfig: ModuleOptions<'client_id'> = { + client: { + id: `http://127.0.0.1:${clientPort}/`, + secret: '', + }, + auth: { + tokenHost: host, + tokenPath: '/oauth/token', + authorizePath: '/oauth/authorize', + }, + options: { + authorizationMethod: 'body', + }, +}; + +function getMeta(html: string): { transactionId: string | undefined, clientName: string | undefined } { + const fragment = JSDOM.fragment(html); + return { + transactionId: fragment.querySelector('meta[name="misskey:oauth:transaction-id"]')?.content, + clientName: fragment.querySelector('meta[name="misskey:oauth:client-name"]')?.content, + }; +} + +function fetchDecision(transactionId: string, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise { + return fetch(new URL('/oauth/decision', host), { + method: 'post', + body: new URLSearchParams({ + transaction_id: transactionId, + login_token: user.token, + cancel: cancel ? 'cancel' : '', + }), + redirect: 'manual', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + }); +} + +async function fetchDecisionFromResponse(response: Response, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise { + const { transactionId } = getMeta(await response.text()); + assert.ok(transactionId); + + return await fetchDecision(transactionId, user, { cancel }); +} + +async function fetchAuthorizationCode(user: misskey.entities.MeSignup, scope: string, code_challenge: string): Promise<{ client: AuthorizationCode, code: string }> { + const client = new AuthorizationCode(clientConfig); + + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope, + state: 'state', + code_challenge, + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + assert.strictEqual(response.status, 200); + + const decisionResponse = await fetchDecisionFromResponse(response, user); + assert.strictEqual(decisionResponse.status, 302); + + const locationHeader = decisionResponse.headers.get('location'); + assert.ok(locationHeader); + + const location = new URL(locationHeader); + assert.ok(location.searchParams.has('code')); + + const code = new URL(location).searchParams.get('code'); + assert.ok(code); + + return { client, code }; +} + +function assertIndirectError(response: Response, error: string): void { + assert.strictEqual(response.status, 302); + + const locationHeader = response.headers.get('location'); + assert.ok(locationHeader); + + const location = new URL(locationHeader); + assert.strictEqual(location.searchParams.get('error'), error); + + // https://datatracker.ietf.org/doc/html/rfc9207#name-response-parameter-iss + assert.strictEqual(location.searchParams.get('iss'), 'http://misskey.local'); + // https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1.2.1 + assert.ok(location.searchParams.has('state')); +} + +async function assertDirectError(response: Response, status: number, error: string): Promise { + assert.strictEqual(response.status, status); + + const data = await response.json(); + assert.strictEqual(data.error, error); +} + +describe('OAuth', () => { + let app: INestApplicationContext; + let fastify: FastifyInstance; + + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + + let sender: (reply: FastifyReply) => void; + + beforeAll(async () => { + app = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + + fastify = Fastify(); + fastify.get('/', async (request, reply) => { + sender(reply); + }); + await fastify.listen({ port: clientPort }); + }, 1000 * 60 * 2); + + beforeEach(async () => { + process.env.MISSKEY_TEST_CHECK_IP_RANGE = ''; + sender = (reply): void => { + reply.send(` + + +

Misklient + `); + }; + }); + + afterAll(async () => { + await fastify.close(); + await app.close(); + }); + + test('Full flow', async () => { + const { code_challenge, code_verifier } = await pkceChallenge(128); + + const client = new AuthorizationCode(clientConfig); + + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge, + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + assert.strictEqual(response.status, 200); + + const meta = getMeta(await response.text()); + assert.strictEqual(typeof meta.transactionId, 'string'); + assert.ok(meta.transactionId); + assert.strictEqual(meta.clientName, 'Misklient'); + + const decisionResponse = await fetchDecision(meta.transactionId, alice); + assert.strictEqual(decisionResponse.status, 302); + assert.ok(decisionResponse.headers.has('location')); + + const locationHeader = decisionResponse.headers.get('location'); + assert.ok(locationHeader); + + const location = new URL(locationHeader); + assert.strictEqual(location.origin + location.pathname, redirect_uri); + assert.ok(location.searchParams.has('code')); + assert.strictEqual(location.searchParams.get('state'), 'state'); + // https://datatracker.ietf.org/doc/html/rfc9207#name-response-parameter-iss + assert.strictEqual(location.searchParams.get('iss'), 'http://misskey.local'); + + const code = new URL(location).searchParams.get('code'); + assert.ok(code); + + const token = await client.getToken({ + code, + redirect_uri, + code_verifier, + } as AuthorizationTokenConfigExtended); + assert.strictEqual(typeof token.token.access_token, 'string'); + assert.strictEqual(token.token.token_type, 'Bearer'); + assert.strictEqual(token.token.scope, 'write:notes'); + + const createResult = await api('notes/create', { text: 'test' }, { + token: token.token.access_token as string, + bearer: true, + }); + assert.strictEqual(createResult.status, 200); + + const createResultBody = createResult.body as misskey.Endpoints['notes/create']['res']; + assert.strictEqual(createResultBody.createdNote.text, 'test'); + }); + + test('Two concurrent flows', async () => { + const client = new AuthorizationCode(clientConfig); + + const pkceAlice = await pkceChallenge(128); + const pkceBob = await pkceChallenge(128); + + const responseAlice = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: pkceAlice.code_challenge, + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + assert.strictEqual(responseAlice.status, 200); + + const responseBob = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: pkceBob.code_challenge, + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + assert.strictEqual(responseBob.status, 200); + + const decisionResponseAlice = await fetchDecisionFromResponse(responseAlice, alice); + assert.strictEqual(decisionResponseAlice.status, 302); + + const decisionResponseBob = await fetchDecisionFromResponse(responseBob, bob); + assert.strictEqual(decisionResponseBob.status, 302); + + const locationHeaderAlice = decisionResponseAlice.headers.get('location'); + assert.ok(locationHeaderAlice); + const locationAlice = new URL(locationHeaderAlice); + + const locationHeaderBob = decisionResponseBob.headers.get('location'); + assert.ok(locationHeaderBob); + const locationBob = new URL(locationHeaderBob); + + const codeAlice = locationAlice.searchParams.get('code'); + assert.ok(codeAlice); + const codeBob = locationBob.searchParams.get('code'); + assert.ok(codeBob); + + const tokenAlice = await client.getToken({ + code: codeAlice, + redirect_uri, + code_verifier: pkceAlice.code_verifier, + } as AuthorizationTokenConfigExtended); + + const tokenBob = await client.getToken({ + code: codeBob, + redirect_uri, + code_verifier: pkceBob.code_verifier, + } as AuthorizationTokenConfigExtended); + + const createResultAlice = await api('notes/create', { text: 'test' }, { + token: tokenAlice.token.access_token as string, + bearer: true, + }); + assert.strictEqual(createResultAlice.status, 200); + + const createResultBob = await api('notes/create', { text: 'test' }, { + token: tokenBob.token.access_token as string, + bearer: true, + }); + assert.strictEqual(createResultAlice.status, 200); + + const createResultBodyAlice = await createResultAlice.body as misskey.Endpoints['notes/create']['res']; + assert.strictEqual(createResultBodyAlice.createdNote.user.username, 'alice'); + + const createResultBodyBob = await createResultBob.body as misskey.Endpoints['notes/create']['res']; + assert.strictEqual(createResultBodyBob.createdNote.user.username, 'bob'); + }); + + // https://datatracker.ietf.org/doc/html/rfc7636.html + describe('PKCE', () => { + // https://datatracker.ietf.org/doc/html/rfc7636.html#section-4.4.1 + // '... the authorization endpoint MUST return the authorization + // error response with the "error" value set to "invalid_request".' + test('Require PKCE', async () => { + const client = new AuthorizationCode(clientConfig); + + // Pattern 1: No PKCE fields at all + let response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + }), { redirect: 'manual' }); + assertIndirectError(response, 'invalid_request'); + + // Pattern 2: Only code_challenge + response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + } as AuthorizationParamsExtended), { redirect: 'manual' }); + assertIndirectError(response, 'invalid_request'); + + // Pattern 3: Only code_challenge_method + response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended), { redirect: 'manual' }); + assertIndirectError(response, 'invalid_request'); + + // Pattern 4: Unsupported code_challenge_method + response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'SSSS', + } as AuthorizationParamsExtended), { redirect: 'manual' }); + assertIndirectError(response, 'invalid_request'); + }); + + // Use precomputed challenge/verifier set here for deterministic test + const code_challenge = '4w2GDuvaxXlw2l46k5PFIoIcTGHdzw2i3hrn-C_Q6f7u0-nTYKd-beVEYy9XinYsGtAix.Nnvr.GByD3lAii2ibPRsSDrZgIN0YQb.kfevcfR9aDKoTLyOUm4hW4ABhs'; + const code_verifier = 'Ew8VSBiH59JirLlg7ocFpLQ6NXuFC1W_rn8gmRzBKc8'; + + const tests: Record = { + 'Code followed by some junk code': code_verifier + 'x', + 'Clipped code': code_verifier.slice(0, 80), + 'Some part of code is replaced': code_verifier.slice(0, -10) + 'x'.repeat(10), + 'No verifier': undefined, + }; + + describe('Verify PKCE', () => { + for (const [title, wrong_verifier] of Object.entries(tests)) { + test(title, async () => { + const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge); + + await assert.rejects(client.getToken({ + code, + redirect_uri, + code_verifier: wrong_verifier, + } as AuthorizationTokenConfigExtended), (err: GetTokenError) => { + assert.strictEqual(err.data.payload.error, 'invalid_grant'); + return true; + }); + }); + } + }); + }); + + // https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1.2 + // "If an authorization code is used more than once, the authorization server + // MUST deny the request and SHOULD revoke (when possible) all tokens + // previously issued based on that authorization code." + describe('Revoking authorization code', () => { + test('On success', async () => { + const { code_challenge, code_verifier } = await pkceChallenge(128); + const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge); + + await client.getToken({ + code, + redirect_uri, + code_verifier, + } as AuthorizationTokenConfigExtended); + + await assert.rejects(client.getToken({ + code, + redirect_uri, + code_verifier, + } as AuthorizationTokenConfigExtended), (err: GetTokenError) => { + assert.strictEqual(err.data.payload.error, 'invalid_grant'); + return true; + }); + }); + + test('On failure', async () => { + const { code_challenge, code_verifier } = await pkceChallenge(128); + const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge); + + await assert.rejects(client.getToken({ code, redirect_uri }), (err: GetTokenError) => { + assert.strictEqual(err.data.payload.error, 'invalid_grant'); + return true; + }); + + await assert.rejects(client.getToken({ + code, + redirect_uri, + code_verifier, + } as AuthorizationTokenConfigExtended), (err: GetTokenError) => { + assert.strictEqual(err.data.payload.error, 'invalid_grant'); + return true; + }); + }); + + test('Revoke the already granted access token', async () => { + const { code_challenge, code_verifier } = await pkceChallenge(128); + const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge); + + const token = await client.getToken({ + code, + redirect_uri, + code_verifier, + } as AuthorizationTokenConfigExtended); + + const createResult = await api('notes/create', { text: 'test' }, { + token: token.token.access_token as string, + bearer: true, + }); + assert.strictEqual(createResult.status, 200); + + await assert.rejects(client.getToken({ + code, + redirect_uri, + code_verifier, + } as AuthorizationTokenConfigExtended), (err: GetTokenError) => { + assert.strictEqual(err.data.payload.error, 'invalid_grant'); + return true; + }); + + const createResult2 = await api('notes/create', { text: 'test' }, { + token: token.token.access_token as string, + bearer: true, + }); + assert.strictEqual(createResult2.status, 401); + }); + }); + + test('Cancellation', async () => { + const client = new AuthorizationCode(clientConfig); + + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + assert.strictEqual(response.status, 200); + + const decisionResponse = await fetchDecisionFromResponse(response, alice, { cancel: true }); + assert.strictEqual(decisionResponse.status, 302); + + const locationHeader = decisionResponse.headers.get('location'); + assert.ok(locationHeader); + + const location = new URL(locationHeader); + assert.ok(!location.searchParams.has('code')); + assert.ok(location.searchParams.has('error')); + }); + + // https://datatracker.ietf.org/doc/html/rfc6749.html#section-3.3 + describe('Scope', () => { + // "If the client omits the scope parameter when requesting + // authorization, the authorization server MUST either process the + // request using a pre-defined default value or fail the request + // indicating an invalid scope." + // (And Misskey does the latter) + test('Missing scope', async () => { + const client = new AuthorizationCode(clientConfig); + + const response = await fetch(client.authorizeURL({ + redirect_uri, + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended), { redirect: 'manual' }); + assertIndirectError(response, 'invalid_scope'); + }); + + test('Empty scope', async () => { + const client = new AuthorizationCode(clientConfig); + + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: '', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended), { redirect: 'manual' }); + assertIndirectError(response, 'invalid_scope'); + }); + + test('Unknown scopes', async () => { + const client = new AuthorizationCode(clientConfig); + + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'test:unknown test:unknown2', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended), { redirect: 'manual' }); + assertIndirectError(response, 'invalid_scope'); + }); + + // "If the issued access token scope + // is different from the one requested by the client, the authorization + // server MUST include the "scope" response parameter to inform the + // client of the actual scope granted." + // (Although Misskey always return scope, which is also fine) + test('Partially known scopes', async () => { + const { code_challenge, code_verifier } = await pkceChallenge(128); + + // Just get the known scope for this case for backward compatibility + const { client, code } = await fetchAuthorizationCode( + alice, + 'write:notes test:unknown test:unknown2', + code_challenge, + ); + + const token = await client.getToken({ + code, + redirect_uri, + code_verifier, + } as AuthorizationTokenConfigExtended); + + assert.strictEqual(token.token.scope, 'write:notes'); + }); + + test('Known scopes', async () => { + const client = new AuthorizationCode(clientConfig); + + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes read:account', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + + assert.strictEqual(response.status, 200); + }); + + test('Duplicated scopes', async () => { + const { code_challenge, code_verifier } = await pkceChallenge(128); + + const { client, code } = await fetchAuthorizationCode( + alice, + 'write:notes write:notes read:account read:account', + code_challenge, + ); + + const token = await client.getToken({ + code, + redirect_uri, + code_verifier, + } as AuthorizationTokenConfigExtended); + assert.strictEqual(token.token.scope, 'write:notes read:account'); + }); + + test('Scope check by API', async () => { + const { code_challenge, code_verifier } = await pkceChallenge(128); + + const { client, code } = await fetchAuthorizationCode(alice, 'read:account', code_challenge); + + const token = await client.getToken({ + code, + redirect_uri, + code_verifier, + } as AuthorizationTokenConfigExtended); + assert.strictEqual(typeof token.token.access_token, 'string'); + + const createResult = await api('notes/create', { text: 'test' }, { + token: token.token.access_token as string, + bearer: true, + }); + assert.strictEqual(createResult.status, 403); + assert.ok(createResult.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="insufficient_scope", error_description')); + }); + }); + + // https://datatracker.ietf.org/doc/html/rfc6749.html#section-3.1.2.4 + // "If an authorization request fails validation due to a missing, + // invalid, or mismatching redirection URI, the authorization server + // SHOULD inform the resource owner of the error and MUST NOT + // automatically redirect the user-agent to the invalid redirection URI." + describe('Redirection', () => { + test('Invalid redirect_uri at authorization endpoint', async () => { + const client = new AuthorizationCode(clientConfig); + + const response = await fetch(client.authorizeURL({ + redirect_uri: 'http://127.0.0.2/', + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + await assertDirectError(response, 400, 'invalid_request'); + }); + + test('Invalid redirect_uri including the valid one at authorization endpoint', async () => { + const client = new AuthorizationCode(clientConfig); + + const response = await fetch(client.authorizeURL({ + redirect_uri: 'http://127.0.0.1/redirection', + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + await assertDirectError(response, 400, 'invalid_request'); + }); + + test('No redirect_uri at authorization endpoint', async () => { + const client = new AuthorizationCode(clientConfig); + + const response = await fetch(client.authorizeURL({ + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + await assertDirectError(response, 400, 'invalid_request'); + }); + + test('Invalid redirect_uri at token endpoint', async () => { + const { code_challenge, code_verifier } = await pkceChallenge(128); + + const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge); + + await assert.rejects(client.getToken({ + code, + redirect_uri: 'http://127.0.0.2/', + code_verifier, + } as AuthorizationTokenConfigExtended), (err: GetTokenError) => { + assert.strictEqual(err.data.payload.error, 'invalid_grant'); + return true; + }); + }); + + test('Invalid redirect_uri including the valid one at token endpoint', async () => { + const { code_challenge, code_verifier } = await pkceChallenge(128); + + const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge); + + await assert.rejects(client.getToken({ + code, + redirect_uri: 'http://127.0.0.1/redirection', + code_verifier, + } as AuthorizationTokenConfigExtended), (err: GetTokenError) => { + assert.strictEqual(err.data.payload.error, 'invalid_grant'); + return true; + }); + }); + + test('No redirect_uri at token endpoint', async () => { + const { code_challenge, code_verifier } = await pkceChallenge(128); + + const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge); + + await assert.rejects(client.getToken({ + code, + code_verifier, + } as AuthorizationTokenConfigExtended), (err: GetTokenError) => { + assert.strictEqual(err.data.payload.error, 'invalid_grant'); + return true; + }); + }); + }); + + // https://datatracker.ietf.org/doc/html/rfc8414 + test('Server metadata', async () => { + const response = await fetch(new URL('.well-known/oauth-authorization-server', host)); + assert.strictEqual(response.status, 200); + + const body = await response.json(); + assert.strictEqual(body.issuer, 'http://misskey.local'); + assert.ok(body.scopes_supported.includes('write:notes')); + }); + + // Any error on decision endpoint is solely on Misskey side and nothing to do with the client. + // Do not use indirect error here. + describe('Decision endpoint', () => { + test('No login token', async () => { + const client = new AuthorizationCode(clientConfig); + + const response = await fetch(client.authorizeURL(basicAuthParams)); + assert.strictEqual(response.status, 200); + + const { transactionId } = getMeta(await response.text()); + assert.ok(transactionId); + + const decisionResponse = await fetch(new URL('/oauth/decision', host), { + method: 'post', + body: new URLSearchParams({ + transaction_id: transactionId, + }), + redirect: 'manual', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + }); + await assertDirectError(decisionResponse, 400, 'invalid_request'); + }); + + test('No transaction ID', async () => { + const decisionResponse = await fetch(new URL('/oauth/decision', host), { + method: 'post', + body: new URLSearchParams({ + login_token: alice.token, + }), + redirect: 'manual', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + }); + await assertDirectError(decisionResponse, 400, 'invalid_request'); + }); + + test('Invalid transaction ID', async () => { + const decisionResponse = await fetch(new URL('/oauth/decision', host), { + method: 'post', + body: new URLSearchParams({ + login_token: alice.token, + transaction_id: 'invalid_id', + }), + redirect: 'manual', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + }); + await assertDirectError(decisionResponse, 403, 'access_denied'); + }); + }); + + // Only authorization code grant is supported + describe('Grant type', () => { + test('Implicit grant is not supported', async () => { + const url = new URL('/oauth/authorize', host); + url.searchParams.append('response_type', 'token'); + const response = await fetch(url); + assertDirectError(response, 501, 'unsupported_response_type'); + }); + + test('Resource owner grant is not supported', async () => { + const client = new ResourceOwnerPassword({ + ...clientConfig, + auth: { + tokenHost: host, + tokenPath: '/oauth/token', + }, + }); + + await assert.rejects(client.getToken({ + username: 'alice', + password: 'test', + }), (err: GetTokenError) => { + assert.strictEqual(err.data.payload.error, 'unsupported_grant_type'); + return true; + }); + }); + + test('Client credential grant is not supported', async () => { + const client = new ClientCredentials({ + ...clientConfig, + auth: { + tokenHost: host, + tokenPath: '/oauth/token', + }, + }); + + await assert.rejects(client.getToken({}), (err: GetTokenError) => { + assert.strictEqual(err.data.payload.error, 'unsupported_grant_type'); + return true; + }); + }); + }); + + // https://indieauth.spec.indieweb.org/#client-information-discovery + describe('Client Information Discovery', () => { + describe('Redirection', () => { + const tests: Record void> = { + 'Read HTTP header': reply => { + reply.header('Link', '; rel="redirect_uri"'); + reply.send(` + +
Misklient + `); + }, + 'Mixed links': reply => { + reply.header('Link', '; rel="redirect_uri"'); + reply.send(` + + +
Misklient + `); + }, + 'Multiple items in Link header': reply => { + reply.header('Link', '; rel="redirect_uri",; rel="redirect_uri"'); + reply.send(` + +
Misklient + `); + }, + 'Multiple items in HTML': reply => { + reply.send(` + + + +
Misklient + `); + }, + }; + + for (const [title, replyFunc] of Object.entries(tests)) { + test(title, async () => { + sender = replyFunc; + + const client = new AuthorizationCode(clientConfig); + + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + assert.strictEqual(response.status, 200); + }); + } + + test('No item', async () => { + sender = (reply): void => { + reply.send(` + +
Misklient + `); + }; + + const client = new AuthorizationCode(clientConfig); + + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + + // direct error because there's no redirect URI to ping + await assertDirectError(response, 400, 'invalid_request'); + }); + }); + + test('Disallow loopback', async () => { + process.env.MISSKEY_TEST_CHECK_IP_RANGE = '1'; + + const client = new AuthorizationCode(clientConfig); + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + await assertDirectError(response, 400, 'invalid_request'); + }); + + test('Missing name', async () => { + sender = (reply): void => { + reply.header('Link', '; rel="redirect_uri"'); + reply.send(); + }; + + const client = new AuthorizationCode(clientConfig); + + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + assert.strictEqual(response.status, 200); + assert.strictEqual(getMeta(await response.text()).clientName, `http://127.0.0.1:${clientPort}/`); + }); + + test('Mismatching URL in h-app', async () => { + sender = (reply): void => { + reply.header('Link', '; rel="redirect_uri"'); + reply.send(` + +
Misklient + `); + reply.send(); + }; + + const client = new AuthorizationCode(clientConfig); + + const response = await fetch(client.authorizeURL({ + redirect_uri, + scope: 'write:notes', + state: 'state', + code_challenge: 'code', + code_challenge_method: 'S256', + } as AuthorizationParamsExtended)); + assert.strictEqual(response.status, 200); + assert.strictEqual(getMeta(await response.text()).clientName, `http://127.0.0.1:${clientPort}/`); + }); + }); + + test('Unknown OAuth endpoint', async () => { + const response = await fetch(new URL('/oauth/foo', host)); + assert.strictEqual(response.status, 404); + }); +}); diff --git a/packages/backend/test/e2e/renote-mute.ts b/packages/backend/test/e2e/renote-mute.ts index 0f73b8d09f..c9e1ccc304 100644 --- a/packages/backend/test/e2e/renote-mute.ts +++ b/packages/backend/test/e2e/renote-mute.ts @@ -1,16 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { signup, api, post, react, startServer, waitFire } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('Renote Mute', () => { let app: INestApplicationContext; // alice mutes carol - let alice: any; - let bob: any; - let carol: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { app = await startServer(); diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts index d1394ef7a8..77de144882 100644 --- a/packages/backend/test/e2e/streaming.ts +++ b/packages/backend/test/e2e/streaming.ts @@ -1,9 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { Following } from '@/models/entities/Following.js'; +import { MiFollowing } from '@/models/Following.js'; import { connectStream, signup, api, post, startServer, initTestDb, waitFire } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('Streaming', () => { let app: INestApplicationContext; @@ -26,13 +32,13 @@ describe('Streaming', () => { describe('Streaming', () => { // Local users - let ayano: any; - let kyoko: any; - let chitose: any; + let ayano: misskey.entities.MeSignup; + let kyoko: misskey.entities.MeSignup; + let chitose: misskey.entities.MeSignup; // Remote users - let akari: any; - let chinatsu: any; + let akari: misskey.entities.MeSignup; + let chinatsu: misskey.entities.MeSignup; let kyokoNote: any; let list: any; @@ -40,7 +46,7 @@ describe('Streaming', () => { beforeAll(async () => { app = await startServer(); const connection = await initTestDb(true); - Followings = connection.getRepository(Following); + Followings = connection.getRepository(MiFollowing); ayano = await signup({ username: 'ayano' }); kyoko = await signup({ username: 'kyoko' }); diff --git a/packages/backend/test/e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts index 2ae2eb67c1..0e487976dc 100644 --- a/packages/backend/test/e2e/thread-mute.ts +++ b/packages/backend/test/e2e/thread-mute.ts @@ -1,15 +1,21 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { signup, api, post, connectStream, startServer } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('Note thread mute', () => { let app: INestApplicationContext; - let alice: any; - let bob: any; - let carol: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { app = await startServer(); diff --git a/packages/backend/test/e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts index c11099e7b5..121070787d 100644 --- a/packages/backend/test/e2e/user-notes.ts +++ b/packages/backend/test/e2e/user-notes.ts @@ -1,13 +1,19 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { signup, api, post, uploadUrl, startServer } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('users/notes', () => { let app: INestApplicationContext; - let alice: any; + let alice: misskey.entities.MeSignup; let jpgNote: any; let pngNote: any; let jpgPngNote: any; diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 02684c93b8..0b3f200260 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -1,17 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { inspect } from 'node:util'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import type { Packed } from '@/misc/json-schema.js'; -import { - signup, - post, +import { + signup, + post, page, role, - startServer, + startServer, api, - successfulApiCall, + successfulApiCall, failedApiCall, uploadFile, } from '../utils.js'; @@ -36,19 +41,19 @@ describe('ユーザー', () => { badgeRoles: any[], }; - type UserDetailedNotMe = UserLite & + type UserDetailedNotMe = UserLite & misskey.entities.UserDetailed & { roles: any[], }; - type MeDetailed = UserDetailedNotMe & + type MeDetailed = UserDetailedNotMe & misskey.entities.MeDetailed & { achievements: object[], loggedInDays: number, policies: object, }; - - type User = MeDetailed & { token: string }; + + type User = MeDetailed & { token: string }; const show = async (id: string, me = root): Promise => { return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me }) as any; @@ -97,6 +102,7 @@ describe('ユーザー', () => { birthday: user.birthday, lang: user.lang, fields: user.fields, + verifiedLinks: user.verifiedLinks, followersCount: user.followersCount, followingCount: user.followingCount, notesCount: user.notesCount, @@ -126,6 +132,7 @@ describe('ユーザー', () => { isBlocked: user.isBlocked ?? false, isMuted: user.isMuted ?? false, isRenoteMuted: user.isRenoteMuted ?? false, + notify: user.notify ?? 'none', }); }; @@ -147,6 +154,7 @@ describe('ユーザー', () => { preventAiLearning: user.preventAiLearning, isExplorable: user.isExplorable, isDeleted: user.isDeleted, + twoFactorBackupCodesStock: user.twoFactorBackupCodesStock, hideOnlineStatus: user.hideOnlineStatus, hasUnreadSpecifiedNotes: user.hasUnreadSpecifiedNotes, hasUnreadMentions: user.hasUnreadMentions, @@ -155,11 +163,12 @@ describe('ユーザー', () => { hasUnreadChannel: user.hasUnreadChannel, hasUnreadNotification: user.hasUnreadNotification, hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest, + unreadAnnouncements: user.unreadAnnouncements, mutedWords: user.mutedWords, mutedInstances: user.mutedInstances, mutingNotificationTypes: user.mutingNotificationTypes, emailNotificationTypes: user.emailNotificationTypes, - achievements: user.achievements, + achievements: user.achievements, loggedInDays: user.loggedInDays, policies: user.policies, ...(security ? { @@ -222,11 +231,11 @@ describe('ユーザー', () => { beforeAll(async () => { root = await signup({ username: 'root' }); alice = await signup({ username: 'alice' }); - aliceNote = await post(alice, { text: 'test' }) as any; + aliceNote = await post(alice, { text: 'test' }) as any; alicePage = await page(alice); aliceList = (await api('users/list/create', { name: 'aliceList' }, alice)).body; bob = await signup({ username: 'bob' }); - bobNote = await post(bob, { text: 'test' }) as any; + bobNote = await post(bob, { text: 'test' }) as any; carol = await signup({ username: 'carol' }); dave = await signup({ username: 'dave' }); ellen = await signup({ username: 'ellen' }); @@ -236,10 +245,10 @@ describe('ユーザー', () => { usersReplying = await [...Array(10)].map((_, i) => i).reduce(async (acc, i) => { const u = await signup({ username: `replying${i}` }); for (let j = 0; j < 10 - i; j++) { - const p = await post(u, { text: `test${j}` }); + const p = await post(u, { text: `test${j}` }); await post(alice, { text: `@${u.username} test${j}`, replyId: p.id }); } - + return (await acc).concat(u); }, Promise.resolve([] as User[])); @@ -362,6 +371,7 @@ describe('ユーザー', () => { assert.strictEqual(response.birthday, null); assert.strictEqual(response.lang, null); assert.deepStrictEqual(response.fields, []); + assert.deepStrictEqual(response.verifiedLinks, []); assert.strictEqual(response.followersCount, 0); assert.strictEqual(response.followingCount, 0); assert.strictEqual(response.notesCount, 0); @@ -376,7 +386,7 @@ describe('ユーザー', () => { assert.strictEqual(response.securityKeys, false); assert.deepStrictEqual(response.roles, []); assert.strictEqual(response.memo, null); - + // MeDetailedOnly assert.strictEqual(response.avatarId, null); assert.strictEqual(response.bannerId, null); @@ -392,6 +402,7 @@ describe('ユーザー', () => { assert.strictEqual(response.preventAiLearning, true); assert.strictEqual(response.isExplorable, true); assert.strictEqual(response.isDeleted, false); + assert.strictEqual(response.twoFactorBackupCodesStock, 'none'); assert.strictEqual(response.hideOnlineStatus, false); assert.strictEqual(response.hasUnreadSpecifiedNotes, false); assert.strictEqual(response.hasUnreadMentions, false); @@ -400,13 +411,14 @@ describe('ユーザー', () => { assert.strictEqual(response.hasUnreadChannel, false); assert.strictEqual(response.hasUnreadNotification, false); assert.strictEqual(response.hasPendingReceivedFollowRequest, false); + assert.deepStrictEqual(response.unreadAnnouncements, []); assert.deepStrictEqual(response.mutedWords, []); assert.deepStrictEqual(response.mutedInstances, []); assert.deepStrictEqual(response.mutingNotificationTypes, []); assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']); assert.deepStrictEqual(response.achievements, []); assert.deepStrictEqual(response.loggedInDays, 0); - assert.deepStrictEqual(response.policies, DEFAULT_POLICIES); + assert.deepStrictEqual(response.policies, DEFAULT_POLICIES); assert.notStrictEqual(response.email, undefined); assert.strictEqual(response.emailVerified, false); assert.deepStrictEqual(response.securityKeysList, []); @@ -483,7 +495,7 @@ describe('ユーザー', () => { { parameters: (): object => ({ mutedWords: [] }) }, { parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) }, { parameters: (): object => ({ mutedInstances: [] }) }, - { parameters: (): object => ({ mutingNotificationTypes: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) }, + { parameters: (): object => ({ mutingNotificationTypes: ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) }, { parameters: (): object => ({ mutingNotificationTypes: [] }) }, { parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) }, { parameters: (): object => ({ emailNotificationTypes: [] }) }, @@ -499,8 +511,8 @@ describe('ユーザー', () => { const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice }); assert.match(response.avatarUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); assert.match(response.avatarBlurhash ?? '.', /[ -~]{54}/); - const expected = { - ...meDetailed(alice, true), + const expected = { + ...meDetailed(alice, true), avatarId: aliceFile.id, avatarBlurhash: response.avatarBlurhash, avatarUrl: response.avatarUrl, @@ -509,8 +521,8 @@ describe('ユーザー', () => { const parameters2 = { avatarId: null }; const response2 = await successfulApiCall({ endpoint: 'i/update', parameters: parameters2, user: alice }); - const expected2 = { - ...meDetailed(alice, true), + const expected2 = { + ...meDetailed(alice, true), avatarId: null, avatarBlurhash: null, avatarUrl: alice.avatarUrl, // 解除した場合、identiconになる @@ -524,8 +536,8 @@ describe('ユーザー', () => { const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice }); assert.match(response.bannerUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); assert.match(response.bannerBlurhash ?? '.', /[ -~]{54}/); - const expected = { - ...meDetailed(alice, true), + const expected = { + ...meDetailed(alice, true), bannerId: aliceFile.id, bannerBlurhash: response.bannerBlurhash, bannerUrl: response.bannerUrl, @@ -534,8 +546,8 @@ describe('ユーザー', () => { const parameters2 = { bannerId: null }; const response2 = await successfulApiCall({ endpoint: 'i/update', parameters: parameters2, user: alice }); - const expected2 = { - ...meDetailed(alice, true), + const expected2 = { + ...meDetailed(alice, true), bannerId: null, bannerBlurhash: null, bannerUrl: null, @@ -551,7 +563,7 @@ describe('ユーザー', () => { const response = await successfulApiCall({ endpoint: 'i/pin', parameters, user: alice }); const expected = { ...meDetailed(alice, false), pinnedNoteIds: [aliceNote.id], pinnedNotes: [aliceNote] }; assert.deepStrictEqual(response, expected); - + const response2 = await successfulApiCall({ endpoint: 'i/unpin', parameters, user: alice }); const expected2 = meDetailed(alice, false); assert.deepStrictEqual(response2, expected2); @@ -612,7 +624,7 @@ describe('ユーザー', () => { }); test.todo('をリスト形式で取得することができる(リモート, hostname指定)'); test.todo('をリスト形式で取得することができる(pagenation)'); - + //#endregion //#region ユーザー情報(users/show) @@ -684,9 +696,9 @@ describe('ユーザー', () => { const parameters = { userIds: [bob.id, alice.id, carol.id] }; const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: alice }); const expected = [ - await successfulApiCall({ endpoint: 'users/show', parameters: { userId: bob.id }, user: alice }), - await successfulApiCall({ endpoint: 'users/show', parameters: { userId: alice.id }, user: alice }), - await successfulApiCall({ endpoint: 'users/show', parameters: { userId: carol.id }, user: alice }), + await successfulApiCall({ endpoint: 'users/show', parameters: { userId: bob.id }, user: alice }), + await successfulApiCall({ endpoint: 'users/show', parameters: { userId: alice.id }, user: alice }), + await successfulApiCall({ endpoint: 'users/show', parameters: { userId: carol.id }, user: alice }), ]; assert.deepStrictEqual(response, expected); }); @@ -701,7 +713,7 @@ describe('ユーザー', () => { // BUG サスペンドユーザーを一般ユーザーから見るとrootユーザーが返ってくる //{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: (): User => userSuspended, me: (): User => bob, excluded: true }, { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, - { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, ] as const)('をID指定のリスト形式で取得することができ、結果に$label', async ({ user, me, excluded }) => { const parameters = { userIds: [user().id] }; const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: me?.() ?? alice }); @@ -734,7 +746,7 @@ describe('ユーザー', () => { { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, - { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, ] as const)('を検索することができ、結果に$labelが含まれる', async ({ user, excluded }) => { const parameters = { query: user().username, limit: 1 }; const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice }); @@ -747,7 +759,7 @@ describe('ユーザー', () => { //#endregion //#region ID指定検索(users/search-by-username-and-host) - test.each([ + test.each([ { label: '自分', parameters: { username: 'alice' }, user: (): User[] => [alice] }, { label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: (): User[] => [alice] }, { label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: (): User[] => [userFollowedByAlice] }, @@ -786,7 +798,7 @@ describe('ユーザー', () => { test('がよくリプライをするユーザーのリストを取得できる', async () => { const parameters = { userId: alice.id, limit: 5 }; const response = await successfulApiCall({ endpoint: 'users/get-frequently-replied-users', parameters, user: alice }); - const expected = await Promise.all(usersReplying.slice(0, parameters.limit).map(async (s, i) => ({ + const expected = await Promise.all(usersReplying.slice(0, parameters.limit).map(async (s, i) => ({ user: await show(s.id, alice), weight: (usersReplying.length - i) / usersReplying.length, }))); diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index a7bcd859ae..7cba7a2aa8 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import type { Config } from '@/config.js'; import type { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import type { ApRendererService } from '@/core/activitypub/ApRendererService.js'; @@ -10,7 +15,7 @@ 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'; +import type { NoteReactionsRepository, NotesRepository, PollsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js'; type MockResponse = { type: string; @@ -18,7 +23,8 @@ type MockResponse = { }; export class MockResolver extends Resolver { - private _rs = new Map(); + #responseMap = new Map(); + #remoteGetTrials: string[] = []; constructor(loggerService: LoggerService) { super( @@ -27,6 +33,7 @@ export class MockResolver extends Resolver { {} as NotesRepository, {} as PollsRepository, {} as NoteReactionsRepository, + {} as FollowRequestsRepository, {} as UtilityService, {} as InstanceActorService, {} as MetaService, @@ -38,18 +45,28 @@ export class MockResolver extends Resolver { ); } - public async _register(uri: string, content: string | Record, type = 'application/activity+json') { - this._rs.set(uri, { + public register(uri: string, content: string | Record, type = 'application/activity+json'): void { + this.#responseMap.set(uri, { type, content: typeof content === 'string' ? content : JSON.stringify(content), }); } + public clear(): void { + this.#responseMap.clear(); + this.#remoteGetTrials.length = 0; + } + + public remoteGetTrials(): string[] { + return this.#remoteGetTrials; + } + @bindThis public async resolve(value: string | IObject): Promise { if (typeof value !== 'string') return value; - const r = this._rs.get(value); + this.#remoteGetTrials.push(value); + const r = this.#responseMap.get(value); if (!r) { throw new Error('Not registed for mock'); diff --git a/packages/backend/test/prelude/get-api-validator.ts b/packages/backend/test/prelude/get-api-validator.ts index 1f4a2dbc95..cccd63299a 100644 --- a/packages/backend/test/prelude/get-api-validator.ts +++ b/packages/backend/test/prelude/get-api-validator.ts @@ -1,11 +1,16 @@ -import { Schema } from '@/misc/schema'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import Ajv from 'ajv'; +import { Schema } from '@/misc/schema'; export const getValidator = (paramDef: Schema) => { - const ajv = new Ajv({ - useDefaults: true, - }); - ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); + const ajv = new Ajv({ + useDefaults: true, + }); + ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); - return ajv.compile(paramDef); -} + return ajv.compile(paramDef); +}; diff --git a/packages/backend/test/prelude/maybe.ts b/packages/backend/test/prelude/maybe.ts index b8679c1071..37ccfbf7fe 100644 --- a/packages/backend/test/prelude/maybe.ts +++ b/packages/backend/test/prelude/maybe.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as assert from 'assert'; import { just, nothing } from '../../src/misc/prelude/maybe.js'; diff --git a/packages/backend/test/prelude/url.ts b/packages/backend/test/prelude/url.ts index 23b6b22bb0..340c6451ce 100644 --- a/packages/backend/test/prelude/url.ts +++ b/packages/backend/test/prelude/url.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as assert from 'assert'; import { query } from '../../src/misc/prelude/url.js'; diff --git a/packages/backend/test/tsconfig.json b/packages/backend/test/tsconfig.json index 8a024a678b..4597ff8780 100644 --- a/packages/backend/test/tsconfig.json +++ b/packages/backend/test/tsconfig.json @@ -9,9 +9,9 @@ "noFallthroughCasesInSwitch": true, "declaration": false, "sourceMap": true, - "target": "es2021", - "module": "es2020", - "moduleResolution": "node", + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", "allowSyntheticDefaultImports": true, "removeComments": false, "noLib": false, @@ -39,6 +39,6 @@ "include": [ "./**/*.ts", "../src/**/*.test.ts", - "../src/@types/**/*.ts", + "../src/@types/**/*.ts" ] } diff --git a/packages/backend/test/unit/AnnouncementService.ts b/packages/backend/test/unit/AnnouncementService.ts new file mode 100644 index 0000000000..721fbb7345 --- /dev/null +++ b/packages/backend/test/unit/AnnouncementService.ts @@ -0,0 +1,194 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; + +import { jest } from '@jest/globals'; +import { ModuleMocker } from 'jest-mock'; +import { Test } from '@nestjs/testing'; +import { GlobalModule } from '@/GlobalModule.js'; +import { AnnouncementService } from '@/core/AnnouncementService.js'; +import type { MiAnnouncement, AnnouncementsRepository, AnnouncementReadsRepository, UsersRepository, MiUser } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { genAidx } from '@/misc/id/aidx.js'; +import { CacheService } from '@/core/CacheService.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; +import type { TestingModule } from '@nestjs/testing'; +import type { MockFunctionMetadata } from 'jest-mock'; + +const moduleMocker = new ModuleMocker(global); + +describe('AnnouncementService', () => { + let app: TestingModule; + let announcementService: AnnouncementService; + let usersRepository: UsersRepository; + let announcementsRepository: AnnouncementsRepository; + let announcementReadsRepository: AnnouncementReadsRepository; + let globalEventService: jest.Mocked; + + function createUser(data: Partial = {}) { + const un = secureRndstr(16); + return usersRepository.insert({ + id: genAidx(new Date()), + createdAt: new Date(), + username: un, + usernameLower: un, + ...data, + }) + .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); + } + + function createAnnouncement(data: Partial = {}) { + return announcementsRepository.insert({ + id: genAidx(new Date()), + createdAt: new Date(), + updatedAt: null, + title: 'Title', + text: 'Text', + ...data, + }) + .then(x => announcementsRepository.findOneByOrFail(x.identifiers[0])); + } + + beforeEach(async () => { + app = await Test.createTestingModule({ + imports: [ + GlobalModule, + ], + providers: [ + AnnouncementService, + CacheService, + IdService, + ], + }) + .useMocker((token) => { + if (token === GlobalEventService) { + return { + publishMainStream: jest.fn(), + publishBroadcastStream: jest.fn(), + }; + } + if (typeof token === 'function') { + const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata; + const Mock = moduleMocker.generateFromMetadata(mockMetadata); + return new Mock(); + } + }) + .compile(); + + app.enableShutdownHooks(); + + announcementService = app.get(AnnouncementService); + usersRepository = app.get(DI.usersRepository); + announcementsRepository = app.get(DI.announcementsRepository); + announcementReadsRepository = app.get(DI.announcementReadsRepository); + globalEventService = app.get(GlobalEventService) as jest.Mocked; + }); + + afterEach(async () => { + await Promise.all([ + app.get(DI.metasRepository).delete({}), + usersRepository.delete({}), + announcementsRepository.delete({}), + announcementReadsRepository.delete({}), + ]); + + await app.close(); + }); + + describe('getUnreadAnnouncements', () => { + test('通常', async () => { + const user = await createUser(); + const announcement = await createAnnouncement({ + title: '1', + }); + + const result = await announcementService.getUnreadAnnouncements(user); + + expect(result.length).toBe(1); + expect(result[0].title).toBe(announcement.title); + }); + + test('isActiveがfalseは除外', async () => { + const user = await createUser(); + await createAnnouncement({ + isActive: false, + }); + + const result = await announcementService.getUnreadAnnouncements(user); + + expect(result.length).toBe(0); + }); + + test('forExistingUsers', async () => { + const user = await createUser(); + const [announcementAfter, announcementBefore, announcementBefore2] = await Promise.all([ + createAnnouncement({ + title: 'after', + createdAt: new Date(), + forExistingUsers: true, + }), + createAnnouncement({ + title: 'before', + createdAt: new Date(Date.now() - 1000), + forExistingUsers: true, + }), + createAnnouncement({ + title: 'before2', + createdAt: new Date(Date.now() - 1000), + forExistingUsers: false, + }), + ]); + + const result = await announcementService.getUnreadAnnouncements(user); + + expect(result.length).toBe(2); + expect(result.some(a => a.title === announcementAfter.title)).toBe(true); + expect(result.some(a => a.title === announcementBefore.title)).toBe(false); + expect(result.some(a => a.title === announcementBefore2.title)).toBe(true); + }); + }); + + describe('create', () => { + test('通常', async () => { + const result = await announcementService.create({ + title: 'Title', + text: 'Text', + }); + + expect(result.raw.title).toBe('Title'); + expect(result.packed.title).toBe('Title'); + + expect(globalEventService.publishBroadcastStream).toHaveBeenCalled(); + expect(globalEventService.publishBroadcastStream.mock.lastCall![0]).toBe('announcementCreated'); + expect((globalEventService.publishBroadcastStream.mock.lastCall![1] as any).announcement).toBe(result.packed); + }); + + test('ユーザー指定', async () => { + const user = await createUser(); + const result = await announcementService.create({ + title: 'Title', + text: 'Text', + userId: user.id, + }); + + expect(result.raw.title).toBe('Title'); + expect(result.packed.title).toBe('Title'); + + expect(globalEventService.publishBroadcastStream).not.toHaveBeenCalled(); + expect(globalEventService.publishMainStream).toHaveBeenCalled(); + expect(globalEventService.publishMainStream.mock.lastCall![0]).toBe(user.id); + expect(globalEventService.publishMainStream.mock.lastCall![1]).toBe('announcementCreated'); + expect((globalEventService.publishMainStream.mock.lastCall![2] as any).announcement).toBe(result.packed); + }); + }); + + describe('read', () => { + // TODO + }); +}); + diff --git a/packages/backend/test/unit/DriveService.ts b/packages/backend/test/unit/DriveService.ts index 4065665579..7234da2e36 100644 --- a/packages/backend/test/unit/DriveService.ts +++ b/packages/backend/test/unit/DriveService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import { Test } from '@nestjs/testing'; @@ -34,7 +39,7 @@ describe('DriveService', () => { test('delete a file', async () => { s3Mock.on(DeleteObjectCommand) .resolves({} as DeleteObjectCommandOutput); - + await driveService.deleteObjectStorageFile('peace of the world'); }); diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts new file mode 100644 index 0000000000..34200899d4 --- /dev/null +++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts @@ -0,0 +1,114 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; + +import { jest } from '@jest/globals'; +import { Test } from '@nestjs/testing'; +import { Redis } from 'ioredis'; +import { GlobalModule } from '@/GlobalModule.js'; +import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { IdService } from '@/core/IdService.js'; +import { DI } from '@/di-symbols.js'; +import type { TestingModule } from '@nestjs/testing'; + +function mockRedis() { + const hash = {}; + const set = jest.fn((key, value) => { + const ret = hash[key]; + hash[key] = value; + return ret; + }); + return set; +} + +describe('FetchInstanceMetadataService', () => { + let app: TestingModule; + let fetchInstanceMetadataService: jest.Mocked; + let federatedInstanceService: jest.Mocked; + let httpRequestService: jest.Mocked; + let redisClient: jest.Mocked; + + beforeEach(async () => { + app = await Test + .createTestingModule({ + imports: [ + GlobalModule, + ], + providers: [ + FetchInstanceMetadataService, + LoggerService, + UtilityService, + IdService, + ], + }) + .useMocker((token) => { + if (token === HttpRequestService) { + return { getJson: jest.fn(), getHtml: jest.fn(), send: jest.fn() }; + } else if (token === FederatedInstanceService) { + return { fetch: jest.fn() }; + } else if (token === DI.redis) { + return mockRedis; + }}) + .compile(); + + app.enableShutdownHooks(); + + fetchInstanceMetadataService = app.get(FetchInstanceMetadataService); + federatedInstanceService = app.get(FederatedInstanceService) as jest.Mocked; + redisClient = app.get(DI.redis) as jest.Mocked; + httpRequestService = app.get(HttpRequestService) as jest.Mocked; + }); + + afterEach(async () => { + await app.close(); + }); + + test('Lock and update', async () => { + redisClient.set = mockRedis(); + const now = Date.now(); + federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } }); + httpRequestService.getJson.mockImplementation(() => { throw Error(); }); + const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); + const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); + await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' }); + expect(tryLockSpy).toHaveBeenCalledTimes(1); + expect(unlockSpy).toHaveBeenCalledTimes(1); + expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1); + expect(httpRequestService.getJson).toHaveBeenCalled(); + }); + + test('Lock and don\'t update', async () => { + redisClient.set = mockRedis(); + const now = Date.now(); + federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => now } }); + httpRequestService.getJson.mockImplementation(() => { throw Error(); }); + const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); + const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); + await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' }); + expect(tryLockSpy).toHaveBeenCalledTimes(1); + expect(unlockSpy).toHaveBeenCalledTimes(1); + expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1); + expect(httpRequestService.getJson).toHaveBeenCalledTimes(0); + }); + + test('Do nothing when lock not acquired', async () => { + redisClient.set = mockRedis(); + federatedInstanceService.fetch.mockReturnValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } }); + httpRequestService.getJson.mockImplementation(() => { throw Error(); }); + const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); + const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); + await fetchInstanceMetadataService.tryLock('example.com'); + await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' }); + expect(tryLockSpy).toHaveBeenCalledTimes(2); + expect(unlockSpy).toHaveBeenCalledTimes(0); + expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0); + expect(httpRequestService.getJson).toHaveBeenCalledTimes(0); + }); +}); diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts index f378184c74..de0b31488c 100644 --- a/packages/backend/test/unit/FileInfoService.ts +++ b/packages/backend/test/unit/FileInfoService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; @@ -5,12 +10,12 @@ import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; +import { describe, beforeAll, afterAll, test } from '@jest/globals'; import { GlobalModule } from '@/GlobalModule.js'; import { FileInfoService } from '@/core/FileInfoService.js'; //import { DI } from '@/di-symbols.js'; import { AiService } from '@/core/AiService.js'; import type { TestingModule } from '@nestjs/testing'; -import { describe, beforeAll, afterAll, test } from '@jest/globals'; import type { MockFunctionMetadata } from 'jest-mock'; const _filename = fileURLToPath(import.meta.url); @@ -94,7 +99,7 @@ describe('FileInfoService', () => { orientation: undefined, }); }); - + test('Generic APNG', async () => { const path = `${resources}/anime.png`; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; @@ -114,7 +119,7 @@ describe('FileInfoService', () => { orientation: undefined, }); }); - + test('Generic AGIF', async () => { const path = `${resources}/anime.gif`; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; @@ -134,7 +139,7 @@ describe('FileInfoService', () => { orientation: undefined, }); }); - + test('PNG with alpha', async () => { const path = `${resources}/with-alpha.png`; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; @@ -154,7 +159,7 @@ describe('FileInfoService', () => { orientation: undefined, }); }); - + test('Generic SVG', async () => { const path = `${resources}/image.svg`; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; @@ -174,7 +179,7 @@ describe('FileInfoService', () => { orientation: undefined, }); }); - + test('SVG with XML definition', async () => { // https://github.com/misskey-dev/misskey/issues/4413 const path = `${resources}/with-xml-def.svg`; @@ -195,7 +200,7 @@ describe('FileInfoService', () => { orientation: undefined, }); }); - + test('Dimension limit', async () => { const path = `${resources}/25000x25000.png`; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; @@ -215,7 +220,7 @@ describe('FileInfoService', () => { orientation: undefined, }); }); - + test('Rotate JPEG', async () => { const path = `${resources}/rotate.jpg`; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; @@ -257,7 +262,7 @@ describe('FileInfoService', () => { }, }); }); - + test('WAV', async () => { const path = `${resources}/kick_gaba7.wav`; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; @@ -277,7 +282,7 @@ describe('FileInfoService', () => { }, }); }); - + test('AAC', async () => { const path = `${resources}/kick_gaba7.aac`; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; @@ -297,7 +302,7 @@ describe('FileInfoService', () => { }, }); }); - + test('FLAC', async () => { const path = `${resources}/kick_gaba7.flac`; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; @@ -317,7 +322,7 @@ describe('FileInfoService', () => { }, }); }); - + /* * video/webmとして検出されてしまう test('WEBM AUDIO', async () => { diff --git a/packages/backend/test/unit/MetaService.ts b/packages/backend/test/unit/MetaService.ts index 9efd8bbe70..ab30f48283 100644 --- a/packages/backend/test/unit/MetaService.ts +++ b/packages/backend/test/unit/MetaService.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import { jest } from '@jest/globals'; import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; import { GlobalModule } from '@/GlobalModule.js'; -import type { MetasRepository } from '@/models/index.js'; +import type { MetasRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; import { CoreModule } from '@/core/CoreModule.js'; diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts index 5496738778..bb8e6981d5 100644 --- a/packages/backend/test/unit/MfmService.ts +++ b/packages/backend/test/unit/MfmService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as assert from 'assert'; import * as mfm from 'mfm-js'; import { Test } from '@nestjs/testing'; diff --git a/packages/backend/test/unit/ReactionService.ts b/packages/backend/test/unit/ReactionService.ts index aa68f4117d..7b5bf7d0a0 100644 --- a/packages/backend/test/unit/ReactionService.ts +++ b/packages/backend/test/unit/ReactionService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as assert from 'assert'; import { Test } from '@nestjs/testing'; diff --git a/packages/backend/test/unit/RelayService.ts b/packages/backend/test/unit/RelayService.ts index c2280142a6..f780a25388 100644 --- a/packages/backend/test/unit/RelayService.ts +++ b/packages/backend/test/unit/RelayService.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import { jest } from '@jest/globals'; @@ -10,7 +15,7 @@ import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { QueueService } from '@/core/QueueService.js'; import { IdService } from '@/core/IdService.js'; -import type { RelaysRepository } from '@/models/index.js'; +import type { RelaysRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import type { TestingModule } from '@nestjs/testing'; import type { MockFunctionMetadata } from 'jest-mock'; @@ -61,7 +66,7 @@ describe('RelayService', () => { await app.close(); }); - test('addRelay', async () => { + test('addRelay', async () => { const result = await relayService.addRelay('https://example.com'); expect(result.inbox).toBe('https://example.com'); @@ -72,7 +77,7 @@ describe('RelayService', () => { //expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor'); }); - test('listRelay', async () => { + test('listRelay', async () => { const result = await relayService.listRelay(); expect(result.length).toBe(1); @@ -80,7 +85,7 @@ describe('RelayService', () => { expect(result[0].status).toBe('requesting'); }); - test('removeRelay: succ', async () => { + test('removeRelay: succ', async () => { await relayService.removeRelay('https://example.com'); expect(queueService.deliver).toHaveBeenCalled(); @@ -93,7 +98,7 @@ describe('RelayService', () => { expect(list.length).toBe(0); }); - test('removeRelay: fail', async () => { + test('removeRelay: fail', async () => { await expect(relayService.removeRelay('https://x.example.com')) .rejects.toThrow('relay not found'); }); diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index 907f1f2edc..c6a14702ae 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -1,19 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import { jest } from '@jest/globals'; import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; 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 type { MiRole, RolesRepository, RoleAssignmentsRepository, UsersRepository, MiUser } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; -import { genAid } from '@/misc/id/aid.js'; +import { genAidx } from '@/misc/id/aidx.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; import { sleep } from '../utils.js'; import type { TestingModule } from '@nestjs/testing'; import type { MockFunctionMetadata } from 'jest-mock'; @@ -29,10 +34,10 @@ describe('RoleService', () => { let metaService: jest.Mocked; let clock: lolex.InstalledClock; - function createUser(data: Partial = {}) { - const un = rndstr('a-z0-9', 16); + function createUser(data: Partial = {}) { + const un = secureRndstr(16); return usersRepository.insert({ - id: genAid(new Date()), + id: genAidx(new Date()), createdAt: new Date(), username: un, usernameLower: un, @@ -41,9 +46,9 @@ describe('RoleService', () => { .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); } - function createRole(data: Partial = {}) { + function createRole(data: Partial = {}) { return rolesRepository.insert({ - id: genAid(new Date()), + id: genAidx(new Date()), createdAt: new Date(), updatedAt: new Date(), lastUsedAt: new Date(), @@ -106,19 +111,19 @@ describe('RoleService', () => { }); describe('getUserPolicies', () => { - test('instance default policies', async () => { + test('instance default policies', async () => { const user = await createUser(); metaService.fetch.mockResolvedValue({ policies: { canManageCustomEmojis: false, }, } as any); - + const result = await roleService.getUserPolicies(user.id); - + expect(result.canManageCustomEmojis).toBe(false); }); - + test('instance default policies 2', async () => { const user = await createUser(); metaService.fetch.mockResolvedValue({ @@ -126,12 +131,12 @@ describe('RoleService', () => { canManageCustomEmojis: true, }, } as any); - + const result = await roleService.getUserPolicies(user.id); - + expect(result.canManageCustomEmojis).toBe(true); }); - + test('with role', async () => { const user = await createUser(); const role = await createRole({ @@ -150,9 +155,9 @@ describe('RoleService', () => { canManageCustomEmojis: false, }, } as any); - + const result = await roleService.getUserPolicies(user.id); - + expect(result.canManageCustomEmojis).toBe(true); }); @@ -185,9 +190,9 @@ describe('RoleService', () => { driveCapacityMb: 50, }, } as any); - + const result = await roleService.getUserPolicies(user.id); - + expect(result.driveCapacityMb).toBe(100); }); @@ -199,7 +204,7 @@ describe('RoleService', () => { createdAt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 365)), followersCount: 10, }); - const role = await createRole({ + await createRole({ name: 'a', policies: { canManageCustomEmojis: { @@ -226,7 +231,7 @@ describe('RoleService', () => { canManageCustomEmojis: false, }, } as any); - + const user1Policies = await roleService.getUserPolicies(user1.id); const user2Policies = await roleService.getUserPolicies(user2.id); expect(user1Policies.canManageCustomEmojis).toBe(false); @@ -251,7 +256,7 @@ describe('RoleService', () => { canManageCustomEmojis: false, }, } as any); - + const result = await roleService.getUserPolicies(user.id); expect(result.canManageCustomEmojis).toBe(true); diff --git a/packages/backend/test/unit/S3Service.ts b/packages/backend/test/unit/S3Service.ts index 1dfa22afd2..c1eafc96b7 100644 --- a/packages/backend/test/unit/S3Service.ts +++ b/packages/backend/test/unit/S3Service.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import { Test } from '@nestjs/testing'; @@ -5,8 +10,8 @@ import { UploadPartCommand, CompleteMultipartUploadCommand, CreateMultipartUploa import { mockClient } from 'aws-sdk-client-mock'; import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; -import { S3Service } from '@/core/S3Service'; -import { Meta } from '@/models'; +import { S3Service } from '@/core/S3Service.js'; +import { MiMeta } from '@/models/_.js'; import type { TestingModule } from '@nestjs/testing'; describe('S3Service', () => { @@ -35,7 +40,7 @@ describe('S3Service', () => { test('upload a file', async () => { s3Mock.on(PutObjectCommand).resolves({}); - await s3Service.upload({ objectStorageRegion: 'us-east-1' } as Meta, { + await s3Service.upload({ objectStorageRegion: 'us-east-1' } as MiMeta, { Bucket: 'fake', Key: 'fake', Body: 'x', @@ -47,7 +52,7 @@ describe('S3Service', () => { s3Mock.on(UploadPartCommand).resolves({ ETag: '1' }); s3Mock.on(CompleteMultipartUploadCommand).resolves({ Bucket: 'fake', Key: 'fake' }); - await s3Service.upload({} as Meta, { + await s3Service.upload({} as MiMeta, { Bucket: 'fake', Key: 'fake', Body: 'x'.repeat(8 * 1024 * 1024 + 1), // デフォルトpartSizeにしている 8 * 1024 * 1024 を越えるサイズ @@ -57,7 +62,7 @@ describe('S3Service', () => { test('upload a file error', async () => { s3Mock.on(PutObjectCommand).rejects({ name: 'Fake Error' }); - await expect(s3Service.upload({ objectStorageRegion: 'us-east-1' } as Meta, { + await expect(s3Service.upload({ objectStorageRegion: 'us-east-1' } as MiMeta, { Bucket: 'fake', Key: 'fake', Body: 'x', @@ -67,7 +72,7 @@ describe('S3Service', () => { test('upload a large file error', async () => { s3Mock.on(UploadPartCommand).rejects(); - await expect(s3Service.upload({} as Meta, { + await expect(s3Service.upload({} as MiMeta, { Bucket: 'fake', Key: 'fake', Body: 'x'.repeat(8 * 1024 * 1024 + 1), // デフォルトpartSizeにしている 8 * 1024 * 1024 を越えるサイズ diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 146998937e..dbc446d12d 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -1,10 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import rndstr from 'rndstr'; import { Test } from '@nestjs/testing'; import { jest } from '@jest/globals'; +import { ApImageService } from '@/core/activitypub/models/ApImageService.js'; import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; @@ -12,15 +17,22 @@ import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { LoggerService } from '@/core/LoggerService.js'; -import type { IActor } from '@/core/activitypub/type.js'; +import type { IActor, IApDocument, ICollection, IPost } from '@/core/activitypub/type.js'; +import { MiMeta, MiNote } from '@/models/_.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; +import { DownloadService } from '@/core/DownloadService.js'; +import { MetaService } from '@/core/MetaService.js'; +import type { MiRemoteUser } from '@/models/User.js'; import { MockResolver } from '../misc/mock-resolver.js'; -import { Note } from '@/models/index.js'; const host = 'https://host1.test'; -function createRandomActor(): IActor & { id: string } { - const preferredUsername = `${rndstr('A-Z', 4)}${rndstr('a-z', 4)}`; - const actorId = `${host}/users/${preferredUsername.toLowerCase()}`; +type NonTransientIActor = IActor & { id: string }; +type NonTransientIPost = IPost & { id: string }; + +function createRandomActor({ actorHost = host } = {}): NonTransientIActor { + const preferredUsername = secureRndstr(8); + const actorId = `${actorHost}/users/${preferredUsername.toLowerCase()}`; return { '@context': 'https://www.w3.org/ns/activitystreams', @@ -32,16 +44,75 @@ function createRandomActor(): IActor & { id: string } { }; } +function createRandomNote(actor: NonTransientIActor): NonTransientIPost { + const id = secureRndstr(8); + const noteId = `${new URL(actor.id).origin}/notes/${id}`; + + return { + id: noteId, + type: 'Note', + attributedTo: actor.id, + content: 'test test foo', + }; +} + +function createRandomNotes(actor: NonTransientIActor, length: number): NonTransientIPost[] { + return new Array(length).fill(null).map(() => createRandomNote(actor)); +} + +function createRandomFeaturedCollection(actor: NonTransientIActor, length: number): ICollection { + const items = createRandomNotes(actor, length); + + return { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Collection', + id: actor.outbox as string, + totalItems: items.length, + items, + }; +} + +async function createRandomRemoteUser( + resolver: MockResolver, + personService: ApPersonService, +): Promise { + const actor = createRandomActor(); + resolver.register(actor.id, actor); + + return await personService.createPerson(actor.id, resolver); +} + describe('ActivityPub', () => { + let imageService: ApImageService; let noteService: ApNoteService; let personService: ApPersonService; let rendererService: ApRendererService; let resolver: MockResolver; - beforeEach(async () => { + const metaInitial = { + cacheRemoteFiles: true, + cacheRemoteSensitiveFiles: true, + blockedHosts: [] as string[], + sensitiveWords: [] as string[], + } as MiMeta; + let meta = metaInitial; + + beforeAll(async () => { const app = await Test.createTestingModule({ imports: [GlobalModule, CoreModule], - }).compile(); + }) + .overrideProvider(DownloadService).useValue({ + async downloadUrl(): Promise<{ filename: string }> { + return { + filename: 'dummy.tmp', + }; + }, + }) + .overrideProvider(MetaService).useValue({ + async fetch(): Promise { + return meta; + }, + }).compile(); await app.init(); app.enableShutdownHooks(); @@ -49,11 +120,16 @@ describe('ActivityPub', () => { noteService = app.get(ApNoteService); personService = app.get(ApPersonService); rendererService = app.get(ApRendererService); + imageService = app.get(ApImageService); 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(() => {})); + jest.spyOn(federatedInstanceService, 'fetch').mockImplementation(() => new Promise(() => { })); + }); + + beforeEach(() => { + resolver.clear(); }); describe('Parse minimum object', () => { @@ -61,7 +137,7 @@ describe('ActivityPub', () => { const post = { '@context': 'https://www.w3.org/ns/activitystreams', - id: `${host}/users/${rndstr('0-9a-z', 8)}`, + id: `${host}/users/${secureRndstr(8)}`, type: 'Note', attributedTo: actor.id, to: 'https://www.w3.org/ns/activitystreams#Public', @@ -69,7 +145,7 @@ describe('ActivityPub', () => { }; test('Minimum Actor', async () => { - resolver._register(actor.id, actor); + resolver.register(actor.id, actor); const user = await personService.createPerson(actor.id, resolver); @@ -79,8 +155,8 @@ describe('ActivityPub', () => { }); test('Minimum Note', async () => { - resolver._register(actor.id, actor); - resolver._register(post.id, post); + resolver.register(actor.id, actor); + resolver.register(post.id, post); const note = await noteService.createNote(post.id, resolver, true); @@ -94,10 +170,10 @@ describe('ActivityPub', () => { test('Truncate long name', async () => { const actor = { ...createRandomActor(), - name: rndstr('0-9a-z', 129), + name: secureRndstr(129), }; - resolver._register(actor.id, actor); + resolver.register(actor.id, actor); const user = await personService.createPerson(actor.id, resolver); @@ -110,7 +186,7 @@ describe('ActivityPub', () => { name: '', }; - resolver._register(actor.id, actor); + resolver.register(actor.id, actor); const user = await personService.createPerson(actor.id, resolver); @@ -123,7 +199,167 @@ describe('ActivityPub', () => { rendererService.renderAnnounce(null, { createdAt: new Date(0), visibility: 'followers', - } as Note); + } as MiNote); + }); + }); + + describe('Featured', () => { + test('Fetch featured notes from IActor', async () => { + const actor = createRandomActor(); + actor.featured = `${actor.id}/collections/featured`; + + const featured = createRandomFeaturedCollection(actor, 5); + + resolver.register(actor.id, actor); + resolver.register(actor.featured, featured); + + await personService.createPerson(actor.id, resolver); + + // All notes in `featured` are same-origin, no need to fetch notes again + assert.deepStrictEqual(resolver.remoteGetTrials(), [actor.id, actor.featured]); + + // Created notes without resolving anything + for (const item of featured.items as IPost[]) { + const note = await noteService.fetchNote(item); + assert.ok(note); + assert.strictEqual(note.text, 'test test foo'); + assert.strictEqual(note.uri, item.id); + } + }); + + test('Fetch featured notes from IActor pointing to another remote server', async () => { + const actor1 = createRandomActor(); + actor1.featured = `${actor1.id}/collections/featured`; + const actor2 = createRandomActor({ actorHost: 'https://host2.test' }); + + const actor2Note = createRandomNote(actor2); + const featured = createRandomFeaturedCollection(actor1, 0); + (featured.items as IPost[]).push({ + ...actor2Note, + content: 'test test bar', // fraud! + }); + + resolver.register(actor1.id, actor1); + resolver.register(actor1.featured, featured); + resolver.register(actor2.id, actor2); + resolver.register(actor2Note.id, actor2Note); + + await personService.createPerson(actor1.id, resolver); + + // actor2Note is from a different server and needs to be fetched again + assert.deepStrictEqual( + resolver.remoteGetTrials(), + [actor1.id, actor1.featured, actor2Note.id, actor2.id], + ); + + const note = await noteService.fetchNote(actor2Note.id); + assert.ok(note); + + // Reflects the original content instead of the fraud + assert.strictEqual(note.text, 'test test foo'); + assert.strictEqual(note.uri, actor2Note.id); + }); + + test('Fetch a note that is a featured note of the attributed actor', async () => { + const actor = createRandomActor(); + actor.featured = `${actor.id}/collections/featured`; + + const featured = createRandomFeaturedCollection(actor, 5); + const firstNote = (featured.items as NonTransientIPost[])[0]; + + resolver.register(actor.id, actor); + resolver.register(actor.featured, featured); + resolver.register(firstNote.id, firstNote); + + const note = await noteService.createNote(firstNote.id as string, resolver); + assert.strictEqual(note?.uri, firstNote.id); + }); + }); + + describe('Images', () => { + test('Create images', async () => { + const imageObject: IApDocument = { + type: 'Document', + mediaType: 'image/png', + url: 'http://host1.test/foo.png', + name: '', + }; + const driveFile = await imageService.createImage( + await createRandomRemoteUser(resolver, personService), + imageObject, + ); + assert.ok(!driveFile.isLink); + + const sensitiveImageObject: IApDocument = { + type: 'Document', + mediaType: 'image/png', + url: 'http://host1.test/bar.png', + name: '', + sensitive: true, + }; + const sensitiveDriveFile = await imageService.createImage( + await createRandomRemoteUser(resolver, personService), + sensitiveImageObject, + ); + assert.ok(!sensitiveDriveFile.isLink); + }); + + test('cacheRemoteFiles=false disables caching', async () => { + meta = { ...metaInitial, cacheRemoteFiles: false }; + + const imageObject: IApDocument = { + type: 'Document', + mediaType: 'image/png', + url: 'http://host1.test/foo.png', + name: '', + }; + const driveFile = await imageService.createImage( + await createRandomRemoteUser(resolver, personService), + imageObject, + ); + assert.ok(driveFile.isLink); + + const sensitiveImageObject: IApDocument = { + type: 'Document', + mediaType: 'image/png', + url: 'http://host1.test/bar.png', + name: '', + sensitive: true, + }; + const sensitiveDriveFile = await imageService.createImage( + await createRandomRemoteUser(resolver, personService), + sensitiveImageObject, + ); + assert.ok(sensitiveDriveFile.isLink); + }); + + test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => { + meta = { ...metaInitial, cacheRemoteSensitiveFiles: false }; + + const imageObject: IApDocument = { + type: 'Document', + mediaType: 'image/png', + url: 'http://host1.test/foo.png', + name: '', + }; + const driveFile = await imageService.createImage( + await createRandomRemoteUser(resolver, personService), + imageObject, + ); + assert.ok(!driveFile.isLink); + + const sensitiveImageObject: IApDocument = { + type: 'Document', + mediaType: 'image/png', + url: 'http://host1.test/bar.png', + name: '', + sensitive: true, + }; + const sensitiveDriveFile = await imageService.createImage( + await createRandomRemoteUser(resolver, personService), + sensitiveImageObject, + ); + assert.ok(sensitiveDriveFile.isLink); }); }); }); diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts index 98f352e1c6..9edd53d274 100644 --- a/packages/backend/test/unit/ap-request.ts +++ b/packages/backend/test/unit/ap-request.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as assert from 'assert'; import httpSignature from '@peertube/http-signature'; diff --git a/packages/backend/test/unit/chart.ts b/packages/backend/test/unit/chart.ts index 5ac4cc18a2..036e73fd5e 100644 --- a/packages/backend/test/unit/chart.ts +++ b/packages/backend/test/unit/chart.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + process.env.NODE_ENV = 'test'; import * as assert from 'assert'; @@ -13,7 +18,7 @@ import { entity as TestGroupedChartEntity } from '@/core/chart/charts/entities/t import { entity as TestUniqueChartEntity } from '@/core/chart/charts/entities/test-unique.js'; import { entity as TestIntersectionChartEntity } from '@/core/chart/charts/entities/test-intersection.js'; import { loadConfig } from '@/config.js'; -import type { AppLockService } from '@/core/AppLockService'; +import type { AppLockService } from '@/core/AppLockService.js'; import Logger from '@/logger.js'; describe('Chart', () => { @@ -475,16 +480,16 @@ describe('Chart', () => { await testIntersectionChart.addA('bob'); await testIntersectionChart.addB('carol'); await testIntersectionChart.save(); - + const chartHours = await testIntersectionChart.getChart('hour', 3, null); const chartDays = await testIntersectionChart.getChart('day', 3, null); - + assert.deepStrictEqual(chartHours, { a: [2, 0, 0], b: [1, 0, 0], aAndB: [0, 0, 0], }); - + assert.deepStrictEqual(chartDays, { a: [2, 0, 0], b: [1, 0, 0], @@ -498,16 +503,16 @@ describe('Chart', () => { await testIntersectionChart.addB('carol'); await testIntersectionChart.addB('alice'); await testIntersectionChart.save(); - + const chartHours = await testIntersectionChart.getChart('hour', 3, null); const chartDays = await testIntersectionChart.getChart('day', 3, null); - + assert.deepStrictEqual(chartHours, { a: [2, 0, 0], b: [2, 0, 0], aAndB: [1, 0, 0], }); - + assert.deepStrictEqual(chartDays, { a: [2, 0, 0], b: [2, 0, 0], diff --git a/packages/backend/test/unit/extract-mentions.ts b/packages/backend/test/unit/extract-mentions.ts index 66d32be1c5..5901f33fdc 100644 --- a/packages/backend/test/unit/extract-mentions.ts +++ b/packages/backend/test/unit/extract-mentions.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as assert from 'assert'; import { parse } from 'mfm-js'; diff --git a/packages/backend/test/unit/misc/check-word-mute.ts b/packages/backend/test/unit/misc/check-word-mute.ts index 7ab838bdee..12bfca8bd7 100644 --- a/packages/backend/test/unit/misc/check-word-mute.ts +++ b/packages/backend/test/unit/misc/check-word-mute.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { checkWordMute } from '@/misc/check-word-mute.js'; describe(checkWordMute, () => { diff --git a/packages/backend/test/unit/misc/correct-filename.ts b/packages/backend/test/unit/misc/correct-filename.ts new file mode 100644 index 0000000000..0c4482e0bf --- /dev/null +++ b/packages/backend/test/unit/misc/correct-filename.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { correctFilename } from '@/misc/correct-filename.js'; + +describe(correctFilename, () => { + it('no ext to null', () => { + expect(correctFilename('test', null)).toBe('test.unknown'); + }); + it('no ext to jpg', () => { + expect(correctFilename('test', 'jpg')).toBe('test.jpg'); + }); + it('jpg to webp', () => { + expect(correctFilename('test.jpg', 'webp')).toBe('test.jpg.webp'); + }); + it('jpg to .webp', () => { + expect(correctFilename('test.jpg', '.webp')).toBe('test.jpg.webp'); + }); + it('jpeg to jpg', () => { + expect(correctFilename('test.jpeg', 'jpg')).toBe('test.jpeg'); + }); + it('JPEG to jpg', () => { + expect(correctFilename('test.JPEG', 'jpg')).toBe('test.JPEG'); + }); + it('jpg to jpg', () => { + expect(correctFilename('test.jpg', 'jpg')).toBe('test.jpg'); + }); + it('JPG to jpg', () => { + expect(correctFilename('test.JPG', 'jpg')).toBe('test.JPG'); + }); + it('tiff to tif', () => { + expect(correctFilename('test.tiff', 'tif')).toBe('test.tiff'); + }); + it('skip gz', () => { + expect(correctFilename('test.unitypackage', 'gz')).toBe('test.unitypackage'); + }); + it('skip text file', () => { + expect(correctFilename('test.txt', null)).toBe('test.txt'); + }); + it('unknown', () => { + expect(correctFilename('test.hoge', null)).toBe('test.hoge'); + }); + test('non ascii with space', () => { + expect(correctFilename('ファイル 名前', 'jpg')).toBe('ファイル 名前.jpg'); + }); +}); diff --git a/packages/backend/test/unit/misc/id.ts b/packages/backend/test/unit/misc/id.ts index ecd0e60a31..57b4ea9947 100644 --- a/packages/backend/test/unit/misc/id.ts +++ b/packages/backend/test/unit/misc/id.ts @@ -1,44 +1,57 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { ulid } from 'ulid'; +import { describe, test, expect } from '@jest/globals'; import { aidRegExp, genAid, parseAid } from '@/misc/id/aid.js'; +import { aidxRegExp, genAidx, parseAidx } from '@/misc/id/aidx.js'; import { genMeid, meidRegExp, parseMeid } from '@/misc/id/meid.js'; import { genMeidg, meidgRegExp, parseMeidg } from '@/misc/id/meidg.js'; import { genObjectId, objectIdRegExp, parseObjectId } from '@/misc/id/object-id.js'; import { ulidRegExp, parseUlid } from '@/misc/id/ulid.js'; -import { ulid } from 'ulid'; -import { describe, test, expect } from '@jest/globals'; describe('misc:id', () => { - test('aid', () => { - const date = new Date(); - const gotAid = genAid(date); - expect(gotAid).toMatch(aidRegExp); - expect(parseAid(gotAid).date.getTime()).toBe(date.getTime()); - }); + test('aid', () => { + const date = new Date(); + const gotAid = genAid(date); + expect(gotAid).toMatch(aidRegExp); + expect(parseAid(gotAid).date.getTime()).toBe(date.getTime()); + }); - test('meid', () => { - const date = new Date(); - const gotMeid = genMeid(date); - expect(gotMeid).toMatch(meidRegExp); - expect(parseMeid(gotMeid).date.getTime()).toBe(date.getTime()); - }); + test('aidx', () => { + const date = new Date(); + const gotAidx = genAidx(date); + expect(gotAidx).toMatch(aidxRegExp); + expect(parseAidx(gotAidx).date.getTime()).toBe(date.getTime()); + }); - test('meidg', () => { - const date = new Date(); - const gotMeidg = genMeidg(date); - expect(gotMeidg).toMatch(meidgRegExp); - expect(parseMeidg(gotMeidg).date.getTime()).toBe(date.getTime()); - }); + test('meid', () => { + const date = new Date(); + const gotMeid = genMeid(date); + expect(gotMeid).toMatch(meidRegExp); + expect(parseMeid(gotMeid).date.getTime()).toBe(date.getTime()); + }); - test('objectid', () => { - const date = new Date(); - const gotObjectId = genObjectId(date); - expect(gotObjectId).toMatch(objectIdRegExp); - expect(Math.floor(parseObjectId(gotObjectId).date.getTime() / 1000)).toBe(Math.floor(date.getTime() / 1000)); - }); + test('meidg', () => { + const date = new Date(); + const gotMeidg = genMeidg(date); + expect(gotMeidg).toMatch(meidgRegExp); + expect(parseMeidg(gotMeidg).date.getTime()).toBe(date.getTime()); + }); - test('ulid', () => { - const date = new Date(); - const gotUlid = ulid(date.getTime()); - expect(gotUlid).toMatch(ulidRegExp); - expect(parseUlid(gotUlid).date.getTime()).toBe(date.getTime()); - }); + test('objectid', () => { + const date = new Date(); + const gotObjectId = genObjectId(date); + expect(gotObjectId).toMatch(objectIdRegExp); + expect(Math.floor(parseObjectId(gotObjectId).date.getTime() / 1000)).toBe(Math.floor(date.getTime() / 1000)); + }); + + test('ulid', () => { + const date = new Date(); + const gotUlid = ulid(date.getTime()); + expect(gotUlid).toMatch(ulidRegExp); + expect(parseUlid(gotUlid).date.getTime()).toBe(date.getTime()); + }); }); diff --git a/packages/backend/test/unit/misc/others.ts b/packages/backend/test/unit/misc/others.ts index c476aef33b..b16d26d866 100644 --- a/packages/backend/test/unit/misc/others.ts +++ b/packages/backend/test/unit/misc/others.ts @@ -1,42 +1,19 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { describe, test, expect } from '@jest/globals'; import { contentDisposition } from '@/misc/content-disposition.js'; -import { correctFilename } from '@/misc/correct-filename.js'; describe('misc:content-disposition', () => { - test('inline', () => { - expect(contentDisposition('inline', 'foo bar')).toBe('inline; filename=\"foo_bar\"; filename*=UTF-8\'\'foo%20bar'); - }); - test('attachment', () => { - expect(contentDisposition('attachment', 'foo bar')).toBe('attachment; filename=\"foo_bar\"; filename*=UTF-8\'\'foo%20bar'); - }); - test('non ascii', () => { - expect(contentDisposition('attachment', 'ファイル名')).toBe('attachment; filename=\"_____\"; filename*=UTF-8\'\'%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%90%8D'); - }); -}); - -describe('misc:correct-filename', () => { - test('simple', () => { - expect(correctFilename('filename', 'jpg')).toBe('filename.jpg'); - }); - test('with same ext', () => { - expect(correctFilename('filename.jpg', 'jpg')).toBe('filename.jpg'); - }); - test('.ext', () => { - expect(correctFilename('filename.jpg', '.jpg')).toBe('filename.jpg'); - }); - test('with different ext', () => { - expect(correctFilename('filename.webp', 'jpg')).toBe('filename.webp.jpg'); - }); - test('non ascii with space', () => { - expect(correctFilename('ファイル 名前', 'jpg')).toBe('ファイル 名前.jpg'); - }); - test('jpeg', () => { - expect(correctFilename('filename.jpeg', 'jpg')).toBe('filename.jpeg'); - }); - test('tiff', () => { - expect(correctFilename('filename.tiff', 'tif')).toBe('filename.tiff'); - }); - test('null ext', () => { - expect(correctFilename('filename', null)).toBe('filename.unknown'); - }); + test('inline', () => { + expect(contentDisposition('inline', 'foo bar')).toBe('inline; filename=\"foo_bar\"; filename*=UTF-8\'\'foo%20bar'); + }); + test('attachment', () => { + expect(contentDisposition('attachment', 'foo bar')).toBe('attachment; filename=\"foo_bar\"; filename*=UTF-8\'\'foo%20bar'); + }); + test('non ascii', () => { + expect(contentDisposition('attachment', 'ファイル名')).toBe('attachment; filename=\"_____\"; filename*=UTF-8\'\'%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%90%8D'); + }); }); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 22f7d81e4e..adc532bbe7 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -1,9 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as assert from 'node:assert'; import { readFile } from 'node:fs/promises'; import { isAbsolute, basename } from 'node:path'; import { inspect } from 'node:util'; -import WebSocket from 'ws'; -import fetch, { Blob, File, RequestInit } from 'node-fetch'; +import WebSocket, { ClientOptions } from 'ws'; +import fetch, { File, RequestInit } from 'node-fetch'; import { DataSource } from 'typeorm'; import { JSDOM } from 'jsdom'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; @@ -13,14 +18,19 @@ import type * as misskey from 'misskey-js'; export { server as startServer } from '@/boot/common.js'; +interface UserToken { + token: string; + bearer?: boolean; +} + const config = loadConfig(); export const port = config.port; -export const cookie = (me: any): string => { +export const cookie = (me: UserToken): string => { return `token=${me.token};`; }; -export const api = async (endpoint: string, params: any, me?: any) => { +export const api = async (endpoint: string, params: any, me?: UserToken) => { const normalized = endpoint.replace(/^\//, ''); return await request(`api/${normalized}`, params, me); }; @@ -28,7 +38,7 @@ export const api = async (endpoint: string, params: any, me?: any) => { export type ApiRequest = { endpoint: string, parameters: object, - user: object | undefined, + user: UserToken | undefined, }; export const successfulApiCall = async (request: ApiRequest, assertion: { @@ -55,35 +65,41 @@ export const failedApiCall = async (request: ApiRequest, assertion: { return res.body; }; -const request = async (path: string, params: any, me?: any): Promise<{ body: any, status: number }> => { - const auth = me ? { - i: me.token, - } : {}; +const request = async (path: string, params: any, me?: UserToken): Promise<{ status: number, headers: Headers, body: any }> => { + const bodyAuth: Record = {}; + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (me?.bearer) { + headers.Authorization = `Bearer ${me.token}`; + } else if (me) { + bodyAuth.i = me.token; + } const res = await relativeFetch(path, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(Object.assign(auth, params)), + headers, + body: JSON.stringify(Object.assign(bodyAuth, params)), redirect: 'manual', }); - const status = res.status; const body = res.headers.get('content-type') === 'application/json; charset=utf-8' ? await res.json() : null; return { - body, status, + status: res.status, + headers: res.headers, + body, }; }; -const relativeFetch = async (path: string, init?: RequestInit | undefined) => { +export 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 => { +export const signup = async (params?: Partial): Promise> => { const q = Object.assign({ username: 'test', password: 'test', @@ -94,7 +110,7 @@ export const signup = async (params?: any): Promise => { return res.body; }; -export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise => { +export const post = async (user: UserToken, params?: misskey.Endpoints['notes/create']['req']): Promise => { const q = params; const res = await api('notes/create', q, user); @@ -117,21 +133,21 @@ export const hiddenNote = (note: any): any => { return temp; }; -export const react = async (user: any, note: any, reaction: string): Promise => { +export const react = async (user: UserToken, note: any, reaction: string): Promise => { await api('notes/reactions/create', { noteId: note.id, reaction: reaction, }, user); }; -export const userList = async (user: any, userList: any = {}): Promise => { +export const userList = async (user: UserToken, userList: any = {}): Promise => { const res = await api('users/lists/create', { name: 'test', }, user); return res.body; }; -export const page = async (user: any, page: any = {}): Promise => { +export const page = async (user: UserToken, page: any = {}): Promise => { const res = await api('pages/create', { alignCenter: false, content: [ @@ -154,7 +170,7 @@ export const page = async (user: any, page: any = {}): Promise => { return res.body; }; -export const play = async (user: any, play: any = {}): Promise => { +export const play = async (user: UserToken, play: any = {}): Promise => { const res = await api('flash/create', { permissions: [], script: 'test', @@ -165,7 +181,7 @@ export const play = async (user: any, play: any = {}): Promise => { return res.body; }; -export const clip = async (user: any, clip: any = {}): Promise => { +export const clip = async (user: UserToken, clip: any = {}): Promise => { const res = await api('clips/create', { description: null, isPublic: true, @@ -175,7 +191,7 @@ export const clip = async (user: any, clip: any = {}): Promise => { return res.body; }; -export const galleryPost = async (user: any, channel: any = {}): Promise => { +export const galleryPost = async (user: UserToken, channel: any = {}): Promise => { const res = await api('gallery/posts/create', { description: null, fileIds: [], @@ -186,7 +202,7 @@ export const galleryPost = async (user: any, channel: any = {}): Promise => return res.body; }; -export const channel = async (user: any, channel: any = {}): Promise => { +export const channel = async (user: UserToken, channel: any = {}): Promise => { const res = await api('channels/create', { bannerId: null, description: null, @@ -196,7 +212,7 @@ export const channel = async (user: any, channel: any = {}): Promise => { return res.body; }; -export const role = async (user: any, role: any = {}, policies: any = {}): Promise => { +export const role = async (user: UserToken, role: any = {}, policies: any = {}): Promise => { const res = await api('admin/roles/create', { asBadge: false, canEditMembersByModerator: false, @@ -213,8 +229,8 @@ export const role = async (user: any, role: any = {}, policies: any = {}): Promi isPublic: false, name: 'New Role', target: 'manual', - policies: { - ...Object.entries(DEFAULT_POLICIES).map(([k, v]) => [k, { + policies: { + ...Object.entries(DEFAULT_POLICIES).map(([k, v]) => [k, { priority: 0, useDefault: true, value: v, @@ -239,7 +255,7 @@ interface UploadOptions { * Upload file * @param user User */ -export const uploadFile = async (user: any, { path, name, blob }: UploadOptions = {}): Promise => { +export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{ status: number, headers: Headers, body: misskey.Endpoints['drive/files/create']['res'] | null }> => { const absPath = path == null ? new URL('resources/Lenna.jpg', import.meta.url) : isAbsolute(path.toString()) @@ -247,7 +263,6 @@ export const uploadFile = async (user: any, { path, name, blob }: UploadOptions : new URL(path, new URL('resources/', import.meta.url)); const formData = new FormData(); - formData.append('i', user.token); formData.append('file', blob ?? new File([await readFile(absPath)], basename(absPath.toString()))); formData.append('force', 'true'); @@ -255,20 +270,29 @@ export const uploadFile = async (user: any, { path, name, blob }: UploadOptions formData.append('name', name); } + const headers: Record = {}; + if (user?.bearer) { + headers.Authorization = `Bearer ${user.token}`; + } else if (user) { + formData.append('i', user.token); + } + const res = await relativeFetch('api/drive/files/create', { method: 'POST', body: formData, + headers, }); - const body = res.status !== 204 ? await res.json() : null; + const body = res.status !== 204 ? await res.json() as misskey.Endpoints['drive/files/create']['res'] : null; return { status: res.status, + headers: res.headers, body, }; }; -export const uploadUrl = async (user: any, url: string) => { +export const uploadUrl = async (user: UserToken, url: string) => { let file: any; const marker = Math.random().toString(); @@ -290,10 +314,18 @@ export const uploadUrl = async (user: any, url: string) => { return file; }; -export function connectStream(user: any, channel: string, listener: (message: Record) => any, params?: any): Promise { +export function connectStream(user: UserToken, channel: string, listener: (message: Record) => any, params?: any): Promise { return new Promise((res, rej) => { - const ws = new WebSocket(`ws://127.0.0.1:${port}/streaming?i=${user.token}`); + const url = new URL(`ws://127.0.0.1:${port}/streaming`); + const options: ClientOptions = {}; + if (user.bearer) { + options.headers = { Authorization: `Bearer ${user.token}` }; + } else { + url.searchParams.set('i', user.token); + } + const ws = new WebSocket(url, options); + ws.on('unexpected-response', (req, res) => rej(res)); ws.on('open', () => { ws.on('message', data => { const msg = JSON.parse(data.toString()); @@ -317,7 +349,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) => { +export const waitFire = async (user: UserToken, channel: string, trgr: () => any, cond: (msg: Record) => boolean, params?: any) => { return new Promise(async (res, rej) => { let timer: NodeJS.Timeout | null = null; @@ -351,11 +383,11 @@ export const waitFire = async (user: any, channel: string, trgr: () => any, cond }); }; -export type SimpleGetResponse = { - status: number, - body: any | JSDOM | null, - type: string | null, - location: string | null +export type SimpleGetResponse = { + status: number, + body: any | JSDOM | null, + type: string | null, + location: string | null }; export const simpleGet = async (path: string, accept = '*/*', cookie: any = undefined): Promise => { const res = await relativeFetch(path, { @@ -374,9 +406,9 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde 'text/html; charset=utf-8', ]; - const body = - jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() : - htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) : + const body = + jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() : + htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) : null; return { @@ -420,12 +452,12 @@ export async function testPaginationConsistency id + ':' + createdAt), @@ -453,7 +485,7 @@ export async function testPaginationConsistency id + ':' + createdAt), diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index faadbcdfc6..2b15a5cc7a 100644 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -9,9 +9,9 @@ "noFallthroughCasesInSwitch": true, "declaration": false, "sourceMap": false, - "target": "es2021", - "module": "esnext", - "moduleResolution": "node", + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", "allowSyntheticDefaultImports": true, "removeComments": false, "noLib": false, @@ -33,8 +33,9 @@ "node" ], "typeRoots": [ + "./src/@types", "./node_modules/@types", - "./src/@types" + "./node_modules" ], "lib": [ "esnext" diff --git a/packages/backend/watch.mjs b/packages/backend/watch.mjs index 9c9d2dbd86..81c23a0f50 100644 --- a/packages/backend/watch.mjs +++ b/packages/backend/watch.mjs @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { execa } from 'execa'; (async () => { diff --git a/packages/frontend/.eslintrc.js b/packages/frontend/.eslintrc.cjs similarity index 97% rename from packages/frontend/.eslintrc.js rename to packages/frontend/.eslintrc.cjs index 24c3ad4b83..77038f0dfa 100644 --- a/packages/frontend/.eslintrc.js +++ b/packages/frontend/.eslintrc.cjs @@ -21,9 +21,6 @@ module.exports = { 'allowSingleExtends': true, }, ], - '@typescript-eslint/prefer-nullish-coalescing': [ - 'error', - ], // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため 'id-denylist': ['error', 'window', 'e'], diff --git a/packages/frontend/.storybook/changes.ts b/packages/frontend/.storybook/changes.ts index fc0f0c286b..0cc648fbae 100644 --- a/packages/frontend/.storybook/changes.ts +++ b/packages/frontend/.storybook/changes.ts @@ -1,7 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import fs from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; import path from 'node:path'; import micromatch from 'micromatch'; -import main from './main'; +import main from './main.js'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); interface Stats { readonly modules: readonly { @@ -13,8 +21,8 @@ interface Stats { }[]; } -fs.readFile( - path.resolve(__dirname, '../storybook-static/preview-stats.json') +await fs.readFile( + new URL('../storybook-static/preview-stats.json', import.meta.url) ).then((buffer) => { const stats: Stats = JSON.parse(buffer.toString()); const keys = new Set(stats.modules.map((stat) => stat.id)); diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index 5fd21cdf0a..811c243926 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import type { entities } from 'misskey-js' export function abuseUserReport() { @@ -84,6 +89,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi value: 'https://misskey-hub.net', }, ], + verifiedLinks: [], followersCount: 1024, followingCount: 16, hasPendingFollowRequestFromYou: false, @@ -110,8 +116,34 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi publicReactions: false, securityKeys: false, twoFactorEnabled: false, + twoFactorBackupCodesStock: 'none', updatedAt: null, uri: null, url: null, + notify: 'none', }; } + +export function inviteCode(isUsed = false, hasExpiration = false, isExpired = false, isCreatedBySystem = false) { + const date = new Date(); + const createdAt = new Date(); + createdAt.setDate(date.getDate() - 1) + const expiresAt = new Date(); + + if (isExpired) { + expiresAt.setHours(date.getHours() - 1) + } else { + expiresAt.setHours(date.getHours() + 1) + } + + return { + id: "9gyqzizw77", + code: "SLF3JKF7UV2H9", + expiresAt: hasExpiration ? expiresAt.toISOString() : null, + createdAt: createdAt.toISOString(), + createdBy: isCreatedBySystem ? null : userDetailed('8i3rvznx32'), + usedBy: isUsed ? userDetailed('3i3r2znx1v') : null, + usedAt: isUsed ? date.toISOString() : null, + used: isUsed, + } +} diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index f442422109..d61df9e7be 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { existsSync, readFileSync } from 'node:fs'; import { writeFile } from 'node:fs/promises'; import { basename, dirname } from 'node:path/posix'; @@ -96,7 +101,7 @@ declare global { } } -function toStories(component: string): string { +function toStories(component: string): Promise { const msw = `${component.slice(0, -'.vue'.length)}.msw`; const implStories = `${component.slice(0, -'.vue'.length)}.stories.impl`; const metaStories = `${component.slice(0, -'.vue'.length)}.stories.meta`; @@ -394,18 +399,21 @@ function toStories(component: string): string { } // glob('src/{components,pages,ui,widgets}/**/*.vue') -Promise.all([ - glob('src/components/global/*.vue'), - glob('src/components/Mk{A,B}*.vue'), - glob('src/components/MkDigitalClock.vue'), - glob('src/components/MkGalleryPostPreview.vue'), - glob('src/components/MkSignupServerRules.vue'), - glob('src/components/MkUserSetupDialog.vue'), - glob('src/components/MkUserSetupDialog.*.vue'), - glob('src/pages/user/home.vue'), -]) - .then((globs) => globs.flat()) - .then((components) => Promise.all(components.map((component) => { +(async () => { + const globs = await Promise.all([ + glob('src/components/global/*.vue'), + glob('src/components/Mk{A,B}*.vue'), + glob('src/components/MkDigitalClock.vue'), + glob('src/components/MkGalleryPostPreview.vue'), + glob('src/components/MkSignupServerRules.vue'), + glob('src/components/MkUserSetupDialog.vue'), + glob('src/components/MkUserSetupDialog.*.vue'), + glob('src/components/MkInviteCode.vue'), + glob('src/pages/user/home.vue'), + ]); + const components = globs.flat(); + await Promise.all(components.map(async (component) => { const stories = component.replace(/\.vue$/, '.stories.ts'); - return writeFile(stories, toStories(component)); - }))); + await writeFile(stories, await toStories(component)); + })) +})(); diff --git a/packages/frontend/.storybook/main.ts b/packages/frontend/.storybook/main.ts index 1d0ce5ab63..a450f8b46b 100644 --- a/packages/frontend/.storybook/main.ts +++ b/packages/frontend/.storybook/main.ts @@ -1,7 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; import type { StorybookConfig } from '@storybook/vue3-vite'; import { type Plugin, mergeConfig } from 'vite'; import turbosnap from 'vite-plugin-turbosnap'; + +const dirname = fileURLToPath(new URL('.', import.meta.url)); + const config = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], addons: [ @@ -9,7 +18,7 @@ const config = { '@storybook/addon-interactions', '@storybook/addon-links', '@storybook/addon-storysource', - resolve(__dirname, '../node_modules/storybook-addon-misskey-theme'), + resolve(dirname, '../node_modules/storybook-addon-misskey-theme'), ], framework: { name: '@storybook/vue3-vite', @@ -28,7 +37,8 @@ const config = { } return mergeConfig(config, { plugins: [ - turbosnap({ + // XXX: https://github.com/IanVS/vite-plugin-turbosnap/issues/8 + (turbosnap as any as typeof turbosnap['default'])({ rootDir: config.root ?? process.cwd(), }), ], diff --git a/packages/frontend/.storybook/manager.ts b/packages/frontend/.storybook/manager.ts index 5653deee84..8f501111d0 100644 --- a/packages/frontend/.storybook/manager.ts +++ b/packages/frontend/.storybook/manager.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { addons } from '@storybook/manager-api'; import { create } from '@storybook/theming/create'; diff --git a/packages/frontend/.storybook/mocks.ts b/packages/frontend/.storybook/mocks.ts index 4091e39686..b60755feea 100644 --- a/packages/frontend/.storybook/mocks.ts +++ b/packages/frontend/.storybook/mocks.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { type SharedOptions, rest } from 'msw'; export const onUnhandledRequest = ((req, print) => { diff --git a/packages/frontend/.storybook/package.json b/packages/frontend/.storybook/package.json new file mode 100644 index 0000000000..bedb411a91 --- /dev/null +++ b/packages/frontend/.storybook/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/frontend/.storybook/preload-locale.ts b/packages/frontend/.storybook/preload-locale.ts index a54164742a..349cc13508 100644 --- a/packages/frontend/.storybook/preload-locale.ts +++ b/packages/frontend/.storybook/preload-locale.ts @@ -1,9 +1,13 @@ -import { writeFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; -import * as locales from '../../../locales'; +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ -writeFile( - resolve(__dirname, 'locale.ts'), +import { writeFile } from 'node:fs/promises'; +import locales from '../../../locales/index.js'; + +await writeFile( + new URL('locale.ts', import.meta.url), `export default ${JSON.stringify(locales['ja-JP'], undefined, 2)} as const;`, 'utf8', ) diff --git a/packages/frontend/.storybook/preload-theme.ts b/packages/frontend/.storybook/preload-theme.ts index 1ff8f71ecd..ad2cf18a35 100644 --- a/packages/frontend/.storybook/preload-theme.ts +++ b/packages/frontend/.storybook/preload-theme.ts @@ -1,6 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { readFile, writeFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; -import * as JSON5 from 'json5'; +import JSON5 from 'json5'; const keys = [ '_dark', @@ -26,9 +30,9 @@ const keys = [ 'd-u0', ] -Promise.all(keys.map((key) => readFile(resolve(__dirname, `../src/themes/${key}.json5`), 'utf8'))).then((sources) => { +await Promise.all(keys.map((key) => readFile(new URL(`../src/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => { writeFile( - resolve(__dirname, './themes.ts'), + new URL('./themes.ts', import.meta.url), `export default ${JSON.stringify( Object.fromEntries(sources.map((source, i) => [keys[i], JSON5.parse(source)])), undefined, diff --git a/packages/frontend/.storybook/preview-head.html b/packages/frontend/.storybook/preview-head.html index f6a9a4875d..ed38e49647 100644 --- a/packages/frontend/.storybook/preview-head.html +++ b/packages/frontend/.storybook/preview-head.html @@ -1,6 +1,6 @@ - + diff --git a/packages/frontend/src/components/MkAsUi.stories.impl.ts b/packages/frontend/src/components/MkAsUi.stories.impl.ts index b67c0e679d..564fa902ba 100644 --- a/packages/frontend/src/components/MkAsUi.stories.impl.ts +++ b/packages/frontend/src/components/MkAsUi.stories.impl.ts @@ -1,2 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import MkAsUi from './MkAsUi.vue'; void MkAsUi; diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue index 8bfcfa6aa6..099baf0d72 100644 --- a/packages/frontend/src/components/MkAsUi.vue +++ b/packages/frontend/src/components/MkAsUi.vue @@ -1,3 +1,8 @@ + +