diff --git a/.config/example.yml b/.config/example.yml index b9644a1597..33ab68fe80 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -2,6 +2,63 @@ # CherryPick configuration #━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# ┌──────────────────────────────┐ +#───┘ a boring but important thing └──────────────────────────── + +# +# First of all, let me tell you a story that may possibly be +# boring to you and possibly important to you. +# +# Misskey is licensed under the AGPLv3 license. This license is +# known to be often misunderstood. Please read the following +# instructions carefully and select the appropriate option so +# that you do not negligently cause a license violation. +# + +# -------- +# Option 1: If you host Misskey AS-IS (without any changes to +# the source code. forks are not included). +# +# Step 1: Congratulations! You don't need to do anything. + +# -------- +# Option 2: If you have made changes to the source code (forks +# are included) and publish a Git repository of source +# code. There should be no access restrictions on +# this repository. Strictly speaking, it doesn't have +# to be a Git repository, but you'll probably use Git! +# +# Step 1: Build and run the Misskey server first. +# Step 2: Open in +# your browser with the administrator account. +# Step 3: Enter the URL of your Git repository in the +# "Repository URL" field. + +# -------- +# Option 3: If neither of the above applies to you. +# (In this case, the source code should be published +# on the Misskey interface. IT IS NOT ENOUGH TO +# DISCLOSE THE SOURCE CODE WEHN A USER REQUESTS IT BY +# E-MAIL OR OTHER MEANS. If you are not satisfied +# with this, it is recommended that you read the +# license again carefully. Anyway, enabling this +# option will automatically generate and publish a +# tarball at build time, protecting you from +# inadvertent license violations. (There is no legal +# guarantee, of course.) The tarball will generated +# from the root directory of your codebase. So it is +# also recommended to check directory +# once after building and before activating the server +# to avoid ACCIDENTAL LEAKING OF SENSITIVE INFORMATION. +# To prevent certain files from being included in the +# tarball, add a glob pattern after line 15 in +# . DO NOT FORGET TO BUILD AFTER +# ENABLING THIS OPTION!) +# +# Step 1: Uncomment the following line. +# +# publishTarballInsteadOfProvideRepositoryUrl: true + # ┌─────┐ #───┘ URL └───────────────────────────────────────────────────── @@ -118,7 +175,7 @@ redis: # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── -# You can set scope to local (default value) or global +# You can set scope to local (default value) or global # (include notes from remote). #meilisearch: @@ -223,7 +280,7 @@ proxyRemoteFiles: true signToActivityPubGet: true # For security reasons, uploading attachments from the intranet is prohibited, -# but exceptions can be made from the following settings. Default value is "undefined". +# but exceptions can be made from the following settings. Default value is "undefined". # Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). #allowedPrivateNetworks: [ # '127.0.0.1/32' diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml index d4cdf64f70..f254af0d1f 100644 --- a/.github/workflows/changelog-check.yml +++ b/.github/workflows/changelog-check.yml @@ -14,7 +14,7 @@ jobs: - name: Checkout head uses: actions/checkout@v4.1.1 - name: Setup Node.js - uses: actions/setup-node@v4.0.1 + uses: actions/setup-node@v4.0.2 with: node-version-file: '.node-version' diff --git a/.github/workflows/check-cherrypick-js-autogen.yml b/.github/workflows/check-cherrypick-js-autogen.yml index 4752c64736..285eb1ea28 100644 --- a/.github/workflows/check-cherrypick-js-autogen.yml +++ b/.github/workflows/check-cherrypick-js-autogen.yml @@ -19,19 +19,19 @@ jobs: steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v4.1.1 with: submodules: true ref: ${{ github.event.pull_request.head.sha }} - name: setup pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v3 with: version: 8 - name: setup node id: setup-node - uses: actions/setup-node@v4 + uses: actions/setup-node@v4.0.2 with: node-version-file: '.node-version' cache: pnpm @@ -48,7 +48,7 @@ jobs: wait-interval: 30 - name: Download artifact - uses: actions/github-script@v7 + uses: actions/github-script@v7.0.1 with: script: | const fs = require('fs'); diff --git a/.github/workflows/deploy-test-environment.yml b/.github/workflows/deploy-test-environment.yml index acfe7dec2d..77cdcfaf88 100644 --- a/.github/workflows/deploy-test-environment.yml +++ b/.github/workflows/deploy-test-environment.yml @@ -1,23 +1,87 @@ name: deploy-test-environment on: - #push: + issue_comment: + types: [created] workflow_dispatch: inputs: repository: - description: 'Repository to deploy (optional)' - type: string + description: 'Repository to deploy (optional, use the repository where this workflow is stored by default)' required: false + default: '' branch_or_hash: - description: 'Branch or Commit hash to deploy (optional)' - type: string + description: 'Branch or Commit hash to deploy (optional, use the branch where this workflow is stored by default)' required: false + default: '' + wait_time: + description: 'Time to wait in seconds (optional, 1800 seconds by default)' + required: false + default: '' jobs: - deploy-test-environment: + get-pr-ref: + runs-on: ubuntu-latest + if: github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/preview') + outputs: + is-allowed-user: ${{ steps.check-allowed-users.outputs.is-allowed-user }} + pr-ref: ${{ steps.get-ref.outputs.pr-ref }} + wait_time: ${{ steps.get-wait-time.outputs.wait_time }} + steps: + - name: Checkout + uses: actions/checkout@v4.1.1 + + - name: Check allowed users + id: check-allowed-users + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ORG_ID: ${{ github.repository_owner_id }} + COMMENT_AUTHOR: ${{ github.event.comment.user.login }} + run: | + MEMBERSHIP_STATUS=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/organizations/$ORG_ID/public_members/$COMMENT_AUTHOR" \ + -o /dev/null -w '%{http_code}\n' -s) + if [ "$MEMBERSHIP_STATUS" -eq 204 ]; then + echo "is-allowed-user=true" > $GITHUB_OUTPUT + else + echo "is-allowed-user=false" > $GITHUB_OUTPUT + fi + + - name: Get PR ref + id: get-ref + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER=$(jq --raw-output .issue.number $GITHUB_EVENT_PATH) + PR_REF=$(gh pr view $PR_NUMBER --json headRefName -q '.headRefName') + echo "pr-ref=$PR_REF" > $GITHUB_OUTPUT + + - name: Extract wait time + id: get-wait-time + env: + COMMENT_BODY: ${{ github.event.comment.body }} + run: | + WAIT_TIME=$(echo "$COMMENT_BODY" | grep -oP '(?<=/preview\s)\d+' || echo "1800") + echo "wait_time=$WAIT_TIME" > $GITHUB_OUTPUT + + deploy-test-environment-pr-comment: + needs: get-pr-ref + if: needs.get-pr-ref.outputs.is-allowed-user == 'true' uses: joinmisskey/misskey-tga/.github/workflows/deploy-test-environment.yml@main with: - repository: ${{ github.event.inputs.repository }} - branch_or_hash: ${{ github.event.inputs.branch_or_hash }} + repository: ${{ github.repository }} + branch_or_hash: ${{ needs.get-pr-ref.outputs.pr-ref }} + wait_time: ${{ needs.get-pr-ref.outputs.wait_time }} + secrets: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + + deploy-test-environment-wd: + if: github.event_name == 'workflow_dispatch' + uses: joinmisskey/misskey-tga/.github/workflows/deploy-test-environment.yml@main + with: + repository: ${{ inputs.repository || github.repository }} + branch_or_hash: ${{ inputs.branch_or_hash || github.ref_name }} + wait_time: ${{ inputs.wait_time || '1800' }} secrets: DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} diff --git a/.github/workflows/on-release-created.yml b/.github/workflows/on-release-created.yml index 657213c5e5..e95cb0169d 100644 --- a/.github/workflows/on-release-created.yml +++ b/.github/workflows/on-release-created.yml @@ -20,16 +20,16 @@ jobs: node-version: [20.10.0] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4.1.1 with: submodules: true - name: Install pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v3 with: version: 8 run_install: false - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v4.0.2 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml new file mode 100644 index 0000000000..87481b12cf --- /dev/null +++ b/.github/workflows/storybook.yml @@ -0,0 +1,113 @@ +name: Storybook + +on: + push: + branches: + - master + - develop + - dev/storybook8 # for testing + pull_request_target: + +jobs: + build: + runs-on: ubuntu-latest + + env: + NODE_OPTIONS: "--max_old_space_size=7168" + + steps: + - uses: actions/checkout@v4.1.1 + if: github.event_name != 'pull_request_target' + with: + fetch-depth: 0 + submodules: true + - uses: actions/checkout@v4.1.1 + if: github.event_name == 'pull_request_target' + with: + fetch-depth: 0 + submodules: true + ref: "refs/pull/${{ github.event.number }}/merge" + - name: Checkout actual HEAD + if: github.event_name == 'pull_request_target' + id: rev + run: | + echo "base=$(git rev-list --parents -n1 HEAD | cut -d" " -f2)" >> $GITHUB_OUTPUT + git checkout $(git rev-list --parents -n1 HEAD | cut -d" " -f3) + - name: Install pnpm + uses: pnpm/action-setup@v3 + with: + version: 8 + run_install: false + - name: Use Node.js 20.x + uses: actions/setup-node@v4.0.2 + with: + node-version-file: '.node-version' + cache: 'pnpm' + - run: corepack enable + - run: pnpm i --frozen-lockfile + - name: Check pnpm-lock.yaml + run: git diff --exit-code pnpm-lock.yaml + - name: Build misskey-js + run: pnpm --filter misskey-js build + - name: Build storybook + run: pnpm --filter frontend build-storybook + - name: Publish to Chromatic + if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/master' + run: pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static + env: + CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + - name: Publish to Chromatic + if: github.event_name != 'pull_request_target' && github.ref != 'refs/heads/master' + id: chromatic_push + run: | + DIFF="${{ github.event.before }} HEAD" + if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then + DIFF="HEAD" + fi + CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff-tree --no-commit-id --name-only -r $(echo "$DIFF") | xargs))" + if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + fi + if pnpm --filter frontend chromatic -d storybook-static $(echo "$CHROMATIC_PARAMETER"); then + echo "success=true" >> $GITHUB_OUTPUT + else + echo "success=false" >> $GITHUB_OUTPUT + fi + env: + CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + - name: Publish to Chromatic + if: github.event_name == 'pull_request_target' + id: chromatic_pull_request + run: | + DIFF="${{ steps.rev.outputs.base }} HEAD" + if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then + DIFF="HEAD" + fi + CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff-tree --no-commit-id --name-only -r $(echo "$DIFF") | xargs))" + if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then + echo "skip=true" >> $GITHUB_OUTPUT + fi + BRANCH="${{ github.event.pull_request.head.user.login }}:${{ github.event.pull_request.head.ref }}" + if [ "$BRANCH" = "misskey-dev:${{ github.event.pull_request.head.ref }}" ]; then + BRANCH="${{ github.event.pull_request.head.ref }}" + fi + pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static --branch-name $BRANCH $(echo "$CHROMATIC_PARAMETER") + env: + CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + - name: Notify that Chromatic detects changes + uses: actions/github-script@v7.0.1 + if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false' + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.repos.createCommitComment({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: context.sha, + body: 'Chromatic detects changes. Please [review the changes on Chromatic](https://www.chromatic.com/builds?appId=6428f7d7b962f0b79f97d6e4).' + }) + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: storybook + path: packages/frontend/storybook-static diff --git a/.github/workflows/validate-api-json.yml b/.github/workflows/validate-api-json.yml index 93c4cf4cd1..36ed8d273f 100644 --- a/.github/workflows/validate-api-json.yml +++ b/.github/workflows/validate-api-json.yml @@ -24,12 +24,12 @@ jobs: with: submodules: true - name: Install pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v3 with: version: 8 run_install: false - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.1 + uses: actions/setup-node@v4.0.2 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dd51db687..817dde7bf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,18 +12,23 @@ --> -## 202x.x.x (Unreleased) +## 2024.2.0 ### Note -- 外部サイトからプラグインをインストールする場合のパスが`/install-extentions`から`/install-extensions`に変わります。現時点では以前のパスも利用できますが、非推奨です。 +- 外部サイトからプラグインをインストールする場合のパスが`/install-extentions`から`/install-extensions`に変わります。以前のパスからは自動でリダイレクトされるようになっていますが、新しいパスに変更することをお勧めします。 ### General - Feat: [mCaptcha](https://github.com/mCaptcha/mCaptcha)のサポートを追加 -- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正 - Feat: Add support for TrueMail +- Feat: AGPLv3ライセンスに誤って違反するのを防止する機能を追加 + - 管理者がrepositoryUrlを変更したり、またはソースコードを直接頒布することを選択できるようになります + - 本体のソースコードに改変を加えた際に、ライセンスに基づく適切な案内を表示します +- Enhance: モデレーターはすべてのユーザーのリアクション一覧を見られるように +- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正 - Fix: リモートユーザーのリアクション一覧がすべて見えてしまうのを修正 * すべてのリモートユーザーのリアクション一覧を見えないようにします -- Enhance: モデレーターはすべてのユーザーのリアクション一覧を見られるように +- Fix: 特定のキーワード及び正規表現にマッチする文字列を含むノートが投稿された際、エラーに出来るような設定項目を追加 #13207 + * デフォルトは空欄なので適用前と同等の動作になります ### Client - Feat: 新しいゲームを追加 @@ -55,6 +60,10 @@ - リモートのユーザーにローカルのみのカスタム絵文字をリアクションしようとした場合 - センシティブなリアクションを認めていないユーザーにセンシティブなカスタム絵文字をリアクションしようとした場合 - ロールが必要な絵文字をリアクションしようとした場合 +- Enhance: ページ遷移時にPlayerを閉じるように +- Enhance: 通報ページのユーザをクリックした際にユーザをウィンドウで開くように +- Enhance: ノートの通報時にリモートのノートであっても自インスタンスにおけるノートのリンクを含むように +- Enhance: オフライン表示のデザインを改善・多言語対応 - Fix: ネイティブモードの絵文字がモノクロにならないように - Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正 - Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正 @@ -65,7 +74,6 @@ - Fix: デッキのプロファイル作成時に名前を空にできる問題を修正 - Fix: テーマ作成時に名称が空欄でも作成できてしまう問題を修正 - Fix: プラグインで`Plugin:register_note_post_interruptor`を使用すると、ノートが投稿できなくなる問題を修正 -- Enhance: ページ遷移時にPlayerを閉じるように - Fix: iOSで大きな画像を変換してアップロードできない問題を修正 - Fix: 「アニメーション画像を再生しない」もしくは「データセーバー(アイコン)」を有効にしていても、アイコンデコレーションのアニメーションが停止されない問題を修正 - Fix: 画像をクロップするとクロップ後の解像度が異様に低くなる問題の修正 @@ -77,11 +85,12 @@ - Fix: Summaly proxy利用時にプレイヤーが動作しないことがあるのを修正 #13196 ### Server -- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました +- Enhance: 連合先のレートリミットを超過した際にリトライするようになりました - Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916) - Enhance: クリップをエクスポートできるように - Enhance: `/files`のファイルに対してHTTP Rangeリクエストを行えるように - Enhance: `api.json`のOpenAPI Specificationを3.1.0に更新 +- Enhance: 連合向けのノート配信を軽量化 #13192 - Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正 - Fix: `notes/create`で、`text`が空白文字のみで構成されているか`null`であって、かつ`text`だけであるリクエストに対するレスポンスが400になるように変更 - Fix: `notes/create`で、`text`が空白文字のみで構成されていてかつリノート、ファイルまたは投票を含んでいるリクエストに対するレスポンスの`text`が`""`から`null`になるように変更 @@ -89,10 +98,7 @@ - Fix: properly handle cc followers - Fix: ジョブに関する設定の名前を修正 relashionshipJobPerSec -> relationshipJobPerSec - Fix: コントロールパネル->モデレーション->「誰でも新規登録できるようにする」の初期値をONからOFFに変更 #13122 -- Enhance: 連合向けのノート配信を軽量化 #13192 - -### Service Worker -- Enhance: オフライン表示のデザインを改善・多言語対応 +- Fix: リモートユーザーが復活してもキャッシュにより該当ユーザーのActivityが受け入れられないのを修正 #13273 ## 2023.12.2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 824a818d9b..a21b0f6f61 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -122,6 +122,19 @@ command. If you have not changed it from the default, it will be "http://localhost:3000". If "port" in .config/default.yml is set to something other than 3000, you need to change the proxy settings in packages/frontend/vite.config.local-dev.ts. +### `MK_DEV_PREFER=backend pnpm dev` +pnpm dev has another mode with `MK_DEV_PREFER=backend`. + +``` +MK_DEV_PREFER=backend pnpm dev +``` + +- This mode is closer to the production environment than the default mode. +- Vite runs behind the backend (the backend will proxy Vite at /vite). +- You can see Misskey by accessing `http://localhost:3000` (Replace `3000` with the port configured with `port` in .config/default.yml). +- To change the port of Vite, specify with `VITE_PORT` environment variable. +- HMR may not work in some environments such as Windows. + ### Dev Container Instead of running `pnpm` locally, you can use Dev Container to set up your development environment. To use Dev Container, open the project directory on VSCode with Dev Containers installed. diff --git a/Dockerfile b/Dockerfile index 1b30ad04fd..4814a6c48a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,13 +27,13 @@ COPY --link ["packages/cherrypick-js/package.json", "./packages/cherrypick-js/"] COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"] COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"] +ARG NODE_ENV=production + RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm i --frozen-lockfile --aggregate-output COPY --link . ./ -ARG NODE_ENV=production - RUN git submodule update --init RUN pnpm build RUN rm -rf .git/ @@ -57,6 +57,8 @@ COPY --link ["packages/cherrypick-js/package.json", "./packages/cherrypick-js/"] COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"] COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"] +ARG NODE_ENV=production + RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm i --frozen-lockfile --aggregate-output diff --git a/cypress/e2e/basic.cy.js b/cypress/e2e/basic.cy.js index 8a942b4137..6531c9f1b4 100644 --- a/cypress/e2e/basic.cy.js +++ b/cypress/e2e/basic.cy.js @@ -162,12 +162,12 @@ describe('After user signed in', () => { it('successfully loads', () => { // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup-continue]', { timeout: 12000 }).should('be.visible'); + cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).should('be.visible'); }); it('account setup wizard', () => { // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup-continue]', { timeout: 12000 }).click(); + cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).click(); cy.get('[data-cy-user-setup-user-name] input').type('ありす'); cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ'); @@ -217,7 +217,7 @@ describe('After user setup', () => { // アカウント初期設定ウィザード // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 12000 }).click(); + cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click(); cy.get('[data-cy-modal-dialog-ok]').click(); }); diff --git a/cypress/e2e/router.cy.js b/cypress/e2e/router.cy.js index 81f497b5b8..6de27be5f4 100644 --- a/cypress/e2e/router.cy.js +++ b/cypress/e2e/router.cy.js @@ -14,7 +14,7 @@ describe('Router transition', () => { // アカウント初期設定ウィザード // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 12000 }).click(); + cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click(); cy.wait(500); cy.get('[data-cy-modal-dialog-ok]').click(); }); diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index dca1fb1254..38f6a21d62 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -1025,6 +1025,7 @@ expired: "منتهية صلاحيته" icon: "الصورة الرمزية" replies: "رد" renotes: "أعد النشر" +sourceCode: "الشفرة المصدرية" flip: "اقلب" lastNDays: "آخر {n} أيام" _initialAccountSetting: diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 6527bde03f..ad4da8f84c 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -868,6 +868,7 @@ youFollowing: "অনুসরণ করা হচ্ছে" icon: "প্রোফাইল ছবি" replies: "জবাব" renotes: "রিনোট" +sourceCode: "সোর্স কোড" flip: "উল্টান" _role: priority: "অগ্রাধিকার" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 1b889a05f0..6409ac47b4 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -1041,6 +1041,9 @@ resetPasswordConfirm: "Vols canviar la teva contrasenya?" sensitiveWords: "Paraules sensibles" sensitiveWordsDescription: "La visibilitat de totes les notes que continguin qualsevol de les paraules configurades seran, automàticament, afegides a \"Inici\". Pots llistar diferents paraules separant les per línies noves." sensitiveWordsDescription2: "Fent servir espais crearà expressions AND si l'expressió s'envolta amb barres inclinades es converteix en una expressió regular." +prohibitedWords: "Paraules prohibides" +prohibitedWordsDescription: "Quan intenteu publicar una Nota que conté una paraula prohibida, feu que es converteixi en un error. Es poden dividir i establir múltiples línies." +prohibitedWordsDescription2: "Fent servir espais crearà expressions AND si l'expressió s'envolta amb barres inclinades es converteix en una expressió regular." hiddenTags: "Etiquetes ocultes" hiddenTagsDescription: "La visibilitat de totes les notes que continguin qualsevol de les paraules configurades seran, automàticament, afegides a \"Inici\". Pots llistar diferents paraules separant les per línies noves." notesSearchNotAvailable: "La cerca de notes no es troba disponible." @@ -1164,6 +1167,7 @@ hideRepliesToOthersInTimelineAll: "Ocultar les teves respostes a tots els usuari confirmShowRepliesAll: "Aquesta opció no té marxa enrere. Vols mostrar les teves respostes a tots els que segueixes a la teva línia de temps?" confirmHideRepliesAll: "Aquesta opció no té marxa enrere. Vols ocultar les teves respostes a tots els usuaris que segueixes a la línia de temps?" externalServices: "Serveis externs" +sourceCode: "Codi font" impressum: "Impressum" impressumUrl: "Adreça URL impressum" impressumDescription: "A països, com Alemanya, la inclusió de la informació de contacte de l'operador (un Impressum) és requereix de manera legal per llocs comercials." @@ -1518,12 +1522,82 @@ _achievements: title: "Nocturn" description: "Publica una nota a altes hores de la nit " flavor: "És hora d'anar a dormir." + _postedAt0min0sec: + title: "Rellotge xerraire" + description: "Publica una nota a les 0:00" + flavor: "Tic tac, tic tac, tic tac, DING!" + _selfQuote: + title: "Autoreferència " + description: "Cita una nota teva" + _htl20npm: + title: "Línia de temps fluida" + description: "La teva línia de temps va a més de 20npm (notes per minut)" + _viewInstanceChart: + title: "Analista " + description: "Mira els gràfics de la teva instància " + _outputHelloWorldOnScratchpad: + title: "Hola, món!" + description: "Escriu \"hola, món\" al bloc de notes" _open3windows: title: "Multi finestres" description: "I va obrir més de tres finestres" _driveFolderCircularReference: title: "Consulteu la secció de bucle" + description: "Intenta crear carpetes recursives al Disc" + _reactWithoutRead: + title: "De veritat has llegit això?" + description: "Reaccions a una nota de més de 100 caràcters publicada fa menys de 3 segons " + _clickedClickHere: + title: "Fer clic" + description: "Has fet clic aquí " + _justPlainLucky: + title: "Ha sigut sort" + description: "Oportunitat de guanyar-lo amb una probabilitat d'un 0.005% cada 10 segons" + _setNameToSyuilo: + title: "soc millor" + description: "Posat \"siuylo\" com a nom" + _passedSinceAccountCreated1: + title: "Primer aniversari" + description: "Ja ha passat un any d'ençà que vas crear el teu compte" + _passedSinceAccountCreated2: + title: "Segon aniversari" + description: "Ja han passat dos anys d'ençà que vas crear el teu compte" + _passedSinceAccountCreated3: + title: "Tres anys" + description: "Ja han passat tres anys d'ençà que vas crear el teu compte" + _loggedInOnBirthday: + title: "Felicitats!" + description: "T'has identificat el dia del teu aniversari" + _loggedInOnNewYearsDay: + title: "Bon any nou!" + description: "T'has identificat el primer dia de l'any " + flavor: "A per un altre any memorable a la teva instància " + _cookieClicked: + title: "Un joc en què fas clic a les galetes" + description: "Pica galetes" + flavor: "Espera, ets al lloc web correcte?" + _brainDiver: + title: "Busseja Ments" + description: "Publica un enllaç al Busseja Ments" + flavor: "Misskey-Misskey La-Tu-Ma" + _smashTestNotificationButton: + title: "Sobrecàrrega de proves" + description: "Envia moltes notificacions de prova en un període de temps molt curt" + _tutorialCompleted: + title: "Diploma del Curs Elemental de Misskey" + description: "Has completat el tutorial" + _bubbleGameExplodingHead: + title: "🤯" + description: "L'objecte més gran del joc de la bombolla " + _bubbleGameDoubleExplodingHead: + title: "Doble 🤯" + description: "Dos dels objectes més grans del joc de la bombolla al mateix temps" + flavor: "Pots emplenar una carmanyola com aquesta 🤯🤯 una mica" _role: + new: "Nou rol" + edit: "Editar el rol" + name: "Nom del rol" + description: "Descripció del rol" permission: "Permisos de rol" descriptionOfPermission: "Els Moderadors poden fer operacions bàsiques de moderació.\nEls Administradors poden canviar tots els ajustos del servidor." assignTarget: "Assignar " @@ -1545,37 +1619,400 @@ _role: asBadge: "Mostrar com a insígnia " descriptionOfAsBadge: "La icona d'aquest rol es mostrarà al costat dels noms d'usuaris que tinguin assignats aquest rol." isExplorable: "Fer el rol explorable" + descriptionOfIsExplorable: "La línia de temps d'aquest rol i la llista d'usuaris seran públics si s'activa." + displayOrder: "Posició " + descriptionOfDisplayOrder: "Com més gran és el número, més dalt la seva posició a la interfície." + canEditMembersByModerator: "Permetre que els moderadors editin la llista d'usuaris en aquest rol" + descriptionOfCanEditMembersByModerator: "Quan s'activa, els moderadors, així com els administradors, podran afegir i treure usuaris d'aquest rol. Si es troba desactivat, només els administradors poden assignar usuaris." priority: "Prioritat" _priority: low: "Baixa" middle: "Mitjà" high: "Alta" _options: + gtlAvailable: "Pot veure la línia de temps global" + ltlAvailable: "Pot veure la línia de temps local" + canPublicNote: "Pot enviar notes públiques" + canInvite: "Pot crear invitacions a la instància " + inviteLimit: "Límit d'invitacions " + inviteLimitCycle: "Temps de refresc de les invitacions" + inviteExpirationTime: "Interval de caducitat de les invitacions" canManageCustomEmojis: "Gestiona els emojis personalitzats" canManageAvatarDecorations: "Gestiona les decoracions dels avatars " + driveCapacity: "Capacitat del disc" + alwaysMarkNsfw: "Marca sempre els fitxers com a sensibles" + pinMax: "Nombre màxim de notes fixades" antennaMax: "Nombre màxim d'antenes" + wordMuteMax: "Nombre màxim de caràcters permesos a les paraules silenciades" + webhookMax: "Nombre màxim de Webhooks" + clipMax: "Nombre màxim de clips" + noteEachClipsMax: "Nombre màxim de notes dintre d'un clip" + userListMax: "Nombre màxim de llistes d'usuaris " + userEachUserListsMax: "Nombre màxim d'usuaris dintre d'una llista d'usuaris " + rateLimitFactor: "Limitador" + descriptionOfRateLimitFactor: "Límits baixos són menys restrictius, límits alts són més restrictius." + canHideAds: "Pot amagar els anuncis" + canSearchNotes: "Pot cercar notes" + canUseTranslator: "Pot fer servir el traductor" + avatarDecorationLimit: "Nombre màxim de decoracions que es poden aplicar els avatars" + _condition: + isLocal: "Usuari local" + isRemote: "Usuari remot" + createdLessThan: "Han passat menys de X a passat des de la creació del compte" + createdMoreThan: "Han passat més de X des de la creació del compte" + followersLessThanOrEq: "Té menys de X seguidors" + followersMoreThanOrEq: "Té X o més seguidors" + followingLessThanOrEq: "Segueix X o menys comptes" + followingMoreThanOrEq: "Segueix a X o més comptes" + notesLessThanOrEq: "Les publicacions són menys o igual a " + notesMoreThanOrEq: "Les publicacions són més o igual a " + and: "AND condicional " + or: "OR condicional" + not: "NOT condicional" +_sensitiveMediaDetection: + description: "Redueix els esforços de moderació gràcies al reconeixement automàtic dels fitxers amb contingut sensible mitjançant Machine Learing. Això augmentarà la càrrega del servidor." + sensitivity: "Sensibilitat de la detecció " + sensitivityDescription: "Reduint la sensibilitat provocarà menys falsos positius. D'altra banda incrementant-ho generarà més falsos negatius." + setSensitiveFlagAutomatically: "Marcar com a sensible" + setSensitiveFlagAutomaticallyDescription: "Els resultats de la detecció interna seran desats, inclòs si aquesta opció es troba desactivada." + analyzeVideos: "Activar anàlisis de vídeos " + analyzeVideosDescription: "Analitzar els vídeos a més de les imatges. Això incrementarà lleugerament la càrrega del servidor." +_emailUnavailable: + used: "Aquest correu electrònic ja s'està fent servir" + format: "El format del correu electrònic és invàlid " + disposable: "No es poden fer servir adreces de correu electrònic d'un sol ús " + mx: "Aquest servidor de correu electrònic no és vàlid " + smtp: "Aquest servidor de correu electrònic no respon" + banned: "No pots registrar-te amb aquesta adreça de correu electrònic " _ffVisibility: public: "Publicar" + followers: "Visible només per a seguidors " + private: "Privat" +_signup: + almostThere: "Ja quasi estem" + emailAddressInfo: "Si us plau, escriu la teva adreça de correu electrònic. No es farà pública." + emailSent: "S'ha enviat un correu de confirmació a ({email}). Si us plau, fes clic a l'enllaç per completar el registre." +_accountDelete: + accountDelete: "Eliminar el compte" + mayTakeTime: "Com l'eliminació d'un compte consumeix bastants recursos, pot trigar un temps perquè es completi l'esborrat, depenent si tens molt contingut i la quantitat de fitxer que hagis pujat." + sendEmail: "Una vegada hagi finalitzat l'esborrat del compte rebràs un correu electrònic a l'adreça que tinguis registrada en aquest compte." + requestAccountDelete: "Demanar l'eliminació del compte" + started: "Ha començat l'esborrat del compte." + inProgress: "L'esborrat es troba en procés " _ad: back: "Tornar" + reduceFrequencyOfThisAd: "Mostrar menys aquest anunci" + hide: "No mostrar mai" + timezoneinfo: "El dia de la setmana ve determinat del fus horari del servidor." + adsSettings: "Configuració d'anuncis " + notesPerOneAd: "Interval d'emplaçament d'anuncis en temps real (Notes per anuncis)" + setZeroToDisable: "Ajusta aquest valor a 0 per deshabilitar l'actualització d'anuncis en temps real" + adsTooClose: "L'interval actual pot fer que l'experiència de l'usuari sigui dolenta perquè l'interval és molt baix." +_forgotPassword: + enterEmail: "Escriu l'adreça de correu electrònic amb la que et vas registrar. S'enviarà un correu electrònic amb un enllaç perquè puguis canviar-la." + ifNoEmail: "Si no vas fer servir una adreça de correu electrònic per registrar-te, si us plau posa't en contacte amb l'administrador." + contactAdmin: "Aquesta instància no suporta registrar-se amb correu electrònic. Si us plau, contacta amb l'administrador del servidor." +_gallery: + my: "La meva Galeria " + liked: "Publicacions que t'han agradat" + like: "M'agrada " + unlike: "Ja no m'agrada" _email: _follow: title: "t'ha seguit" + _receiveFollowRequest: + title: "Has rebut una sol·licitud de seguiment" +_plugin: + install: "Instal·lar un afegit " + installWarn: "Si us plau, no instal·lis afegits que no siguin de confiança." + manage: "Gestionar els afegits" + viewSource: "Veure l'origen " +_preferencesBackups: + list: "Llista de còpies de seguretat" + saveNew: "Fer una còpia de seguretat nova" + loadFile: "Carregar des d'un fitxer" + apply: "Aplicar en aquest dispositiu" + save: "Desar els canvis" + inputName: "Escriu un nom per aquesta còpia de seguretat" + cannotSave: "No s'ha pogut desar" + nameAlreadyExists: "Ja existeix una còpia de seguretat anomenada \"{name}\". Escriu un nom diferent." + applyConfirm: "Vols aplicar la còpia de seguretat \"{name}\" a aquest dispositiu? La configuració actual del dispositiu serà esborrada." + saveConfirm: "Desar còpia de seguretat com {name}?" + deleteConfirm: "Esborrar la còpia de seguretat {name}?" + renameConfirm: "Vols canvia el nom de la còpia de seguretat de \"{old}\" a \"{new}\"?" + noBackups: "No hi ha còpies de seguretat. Pots fer una còpia de seguretat de la configuració d'aquest dispositiu al servidor fent servir \"Crear nova còpia de seguretat\"" + createdAt: "Creat el: {date} {time}" + updatedAt: "Actualitzat el: {date} {time}" + cannotLoad: "Hi ha hagut un error al carregar" + invalidFile: "Format del fitxer no vàlid " +_registry: + scope: "Àmbit " + key: "Clau" + keys: "Claus" + domain: "Domini" + createKey: "Crear una clau" +_aboutMisskey: + about: "Misskey és un programa de codi obert desenvolupar per syuilo des de 2014" + contributors: "Col·laboradors principals" + allContributors: "Tots els col·laboradors " + source: "Codi font" + translation: "Tradueix Misskey" + donate: "Fes un donatiu a Misskey" + morePatrons: "També agraïm el suport d'altres col·laboradors que no surten en aquesta llista. Gràcies! 🥰" + patrons: "Patrocinadors" + projectMembers: "Membres del projecte" +_displayOfSensitiveMedia: + respect: "Ocultar imatges o vídeos marcats com a sensibles" + ignore: "Mostrar imatges o vídeos marcats com a sensibles" + force: "Ocultar totes les imatges o vídeos " +_instanceTicker: + none: "No mostrar mai" + remote: "Mostrar per usuaris remots" + always: "Mostrar sempre" +_serverDisconnectedBehavior: + reload: "Recarregar automàticament " + dialog: "Mostrar finestres de confirmació " + quiet: "Mostrar un avís que no molesti" +_channel: + create: "Crear un canal" + edit: "Editar canal" + setBanner: "Estableix el bàner " + removeBanner: "Eliminar el.bàner" + featured: "Popular" + owned: "Propietat" + following: "Seguin" + usersCount: "{n} Participants" + notesCount: "{n} Notes" + nameAndDescription: "Nom i descripció " + nameOnly: "Nom només " + allowRenoteToExternal: "Permet la citació i l'impuls fora del canal" +_menuDisplay: + sideFull: "Horitzontal " + sideIcon: "Horitzontal (icones)" + top: "A dalt" + hide: "Amagar" +_wordMute: + muteWords: "Paraules silenciades" + muteWordsDescription: "Separar amb espais per la condició AND o amb salts de línia per la condició OR." + muteWordsDescription2: "Envolta les paraules amb barres per fer servir expressions regulars." _instanceMute: instanceMuteDescription: "Silencia tots els impulsos dels servidors seleccionats, també els usuaris que responen a altres d'un servidor silenciat." + instanceMuteDescription2: "Separar amb salts de línia" + title: "Ocultar notes de les instàncies en la llista." + heading: "Llista d'instàncies a silenciar" _theme: + explore: "Explorar els temes " + install: "Instal·lar un tema" + manage: "Gestionar els temes " + code: "Codi del tema" description: "Descripció" + installed: "{name} Instal·lat " + installedThemes: "Temes instal·lats " + builtinThemes: "Temes integrats" + alreadyInstalled: "Aquest tema ja es troba instal·lat " + invalid: "El format d'aquest tema no és correcte" + make: "Crear un tema" + base: "Base" + addConstant: "Afegir constant " + constant: "Constant" + defaultValue: "Valor per defecte" + color: "Color" + refProp: "Referència a una propietat" + refConst: "Referència a una constant " + key: "Clau" + func: "Funcions" + funcKind: "Tipus de funció " + argument: "Argument" + basedProp: "Propietat referenciada" + alpha: "Opacitat" + darken: "Enfosquir " + lighten: "Brillantor" + inputConstantName: "Escriu un nom per aquesta constant" + importInfo: "Si escrius el codi del tema aquí, el podràs importar a l'editor del tema" + deleteConstantConfirm: "Vols esborrar la constant {const}?" keys: + accent: "Accent" + bg: "Fons" + fg: "Text" + focus: "Enfocament" + indicator: "Indicador" + panel: "Taulell " + shadow: "Ombra" + header: "Capçalera" + navBg: "Fons de la barra lateral" + navFg: "Text de la barra lateral" + navHoverFg: "Text barra lateral (en passar per sobre)" + navActive: "Text barra lateral (actiu)" + navIndicator: "Indicador barra lateral" + link: "Enllaç" + hashtag: "Etiqueta" mention: "Menció" + mentionMe: "Mencions (jo)" renote: "Renotar" + modalBg: "Fons del modal" divider: "Divisor" + scrollbarHandle: "Maneta de la barra de desplaçament" + scrollbarHandleHover: "Maneta de la barra de desplaçament (en passar-hi per sobre)" + dateLabelFg: "Text de l'etiqueta de la data" + infoBg: "Fons d'informació " + infoFg: "Text d'informació " + infoWarnBg: "Fons avís " + infoWarnFg: "Text avís " + toastBg: "Fons notificació " + toastFg: "Text notificació " + buttonBg: "Fons botó " + buttonHoverBg: "Fons botó (en passar-hi per sobre)" + inputBorder: "Contorn del cap d'introducció " + listItemHoverBg: "Fons dels elements d'una llista" + driveFolderBg: "Fons de la carpeta Disc" + wallpaperOverlay: "Superposició del fons de pantalla " + badge: "Insígnia " + messageBg: "Fons del xat" + accentDarken: "Accent (fosc)" + accentLighten: "Accent (clar)" + fgHighlighted: "Text ressaltat" _sfx: note: "Notes" + noteMy: "Nota (per mi)" notification: "Notificacions" chat: "Xat" antenna: "Antenes" + channel: "Notificacions dels canals" + reaction: "Quan se selecciona una reacció " +_soundSettings: + driveFile: "Fer servir un fitxer d'àudio del disc" + driveFileWarn: "Seleccionar un fitxer d'àudio del disc" + driveFileTypeWarn: "Fitxer no suportat " + driveFileTypeWarnDescription: "Seleccionar un fitxer d'àudio " + driveFileDurationWarn: "L'àudio és massa llarg" + driveFileDurationWarnDescription: "Els àudios molt llargs pot interrompre l'ús de Misskey. Vols continuar?" +_ago: + future: "Futur " + justNow: "Ara mateix" + secondsAgo: "Fa {n} segons" + minutesAgo: "Fa {n} minuts" + hoursAgo: "Fa {n} hores" + daysAgo: "Fa {n} dies" + weeksAgo: "Fa {n} setmanes" + monthsAgo: "Fa {n} mesos" + yearsAgo: "Fa {n} anys" + invalid: "Res" +_timeIn: + seconds: "En {n} segons" + minutes: "En {n} minuts" + hours: "En {n} hores" + days: "En {n} dies" + weeks: "En {n} setmanes" + months: "En {n} mesos" + years: "En {n} anys" +_time: + second: "Segon(s)" + minute: "Minut(s)" + hour: "Hor(a)(es)" + day: "Di(a)(es)" _2fa: + alreadyRegistered: "J has registrat un dispositiu d'autenticació de doble factor." + registerTOTP: "Registrar una aplicació autenticadora" + step1: "Primer instal·la una aplicació autenticadora (com {a} o {b}) al teu dispositiu." + step2: "Després escaneja el codi QR que es mostra en aquesta pantalla." + step2Click: "Fent clic en aquest codi QR et permetrà registrar l'autenticació de doble factor a la teva clau de seguretat o en l'aplicació d'autenticació del teu dispositiu." + step2Uri: "Escriu la següent URI si estàs fent servir una aplicació d'escriptori " + step3Title: "Escriu un codi d'autenticació" + step3: "Escriu el codi d'autenticació (token) que es mostra a la teva aplicació per finalitzar la configuració." + setupCompleted: "Configuració terminada" + step4: "D'ara endavant quan accedeixis se't demanarà el token que has introduït." + securityKeyNotSupported: "El teu navegador no suporta claus de seguretat" + registerTOTPBeforeKey: "Configura una aplicació d'autenticació per registrar una clau de seguretat o una clau de pas." + securityKeyInfo: "A més de l'empremta digital o PIN per autenticar-te, pots configurar autenticació mitjançant maquinari que suporti claus de seguretat FIDO2, per protegir encara més el teu compte." + registerSecurityKey: "Registrar una clau de seguretat o clau de pas" + securityKeyName: "Escriu un nom per la clau" + tapSecurityKey: "Seguiu les instruccions del navegador i registrar les claus de seguretat o la clau de pas" + removeKey: "Esborrar la clau de seguretat" + removeKeyConfirm: "Esborrar la còpia de seguretat {name}?" + whyTOTPOnlyRenew: "L'aplicació d'autenticació no es pot eliminar mentre hi hagi una clau de seguretat registrada." + renewTOTP: "Reconfigurar l'aplicació d'autenticació " + renewTOTPConfirm: "Això farà que els codis de validació de l'antiga aplicació deixin de funcionar" + renewTOTPOk: "Reconfigurar" renewTOTPCancel: "No, gràcies" + checkBackupCodesBeforeCloseThisWizard: "Abans de tancar aquesta finestra, comprova el següent codi de seguretat." + backupCodes: "Codi de seguretat." + backupCodesDescription: "Si l'aplicació d'autenticació no es pot utilitzar, es pot accedir al compte utilitzant els següents codis de còpia de seguretat. Assegura't de mantenir aquests codis en un lloc segur. Cada codi es pot utilitzar només una vegada." + backupCodeUsedWarning: "Es va utilitzar un codi de còpia de seguretat. Si l'aplicació de certificació està disponible, reconfigura l'aplicació d'autenticació tan aviat com sigui possible." + backupCodesExhaustedWarning: "Es van utilitzar tots els codis de còpia de seguretat. Si no es pot utilitzar l'aplicació d'autenticació, ja no es pot accedir al compte. Torna a registrar l'aplicació d'autenticació." +_permissions: + "read:account": "Veure la informació del compte." + "write:account": "Editar la informació del compte." + "read:blocks": "Veure la llista d'usuaris bloquejats" + "write:blocks": "Editar la llista d'usuaris blocats" + "read:drive": "Accedeix als teus fitxers i carpetes del Disc" + "write:drive": "Editar o eliminar els teus fitxers i carpetes al Disc" + "read:favorites": "Veure la teva llista de favorits" + "write:favorites": "Editar la teva llista de favorits" + "read:following": "Veure informació de qui segueixes" + "write:following": "Segueix o deixa de seguir altres comptes" + "read:messaging": "Veure els teus xats" + "write:messaging": "Crear o esborrar missatges de xat" + "read:mutes": "Veure la teva llista d'usuaris silenciats" + "write:mutes": "Editar la teva llista d'usuaris silenciats" + "write:notes": "Crear o esborrar notes" + "read:notifications": "Veure les teves notificacions" + "write:notifications": "Gestionar les teves notificacions" + "read:reactions": "Veure les teves reaccions" + "write:reactions": "Editar les teves reaccions" + "write:votes": "Votar en una enquesta" + "read:pages": "Veure les teves pàgines " + "write:pages": "Editar o esborrar les teves pàgines " + "read:page-likes": "Veure la llista de les pàgines que t'han agradat" + "write:page-likes": "Editar la llista de les pàgines que t'han agradat" + "read:user-groups": "Veure els teus grups d'usuaris " + "write:user-groups": "Editar o esborrar els teus grups d'usuaris " + "read:channels": "Veure els teus canals" + "write:channels": "Editar els teus canals" + "read:gallery": "Veure la teva galeria " + "write:gallery": "Editar la teva galeria" + "read:gallery-likes": "Veure la llista de publicacions de galeries que t'han agradat" + "write:gallery-likes": "Editar la llista de publicacions de galeries que t'han agradat" + "read:flash": "Veure reproduccions" + "write:flash": "Editar reproduccions" + "read:flash-likes": "Veure la llista de reproduccions que t'han agradat" + "write:flash-likes": "Editar la llista de reproduccions que t'han agradat" + "read:admin:abuse-user-reports": "Veure informes d'usuaris " + "write:admin:delete-account": "Esborrar compte d'usuari " + "write:admin:delete-all-files-of-a-user": "Esborrar tots els fitxers d'un usuari" + "read:admin:index-stats": "Veure l'índex de la base de dades" + "read:admin:table-stats": "Veure la informació de les taules a la base de dades" + "read:admin:user-ips": "Veure adreça IP de l'usuari " + "read:admin:meta": "Veure meta-informació del servidor" + "write:admin:reset-password": "Reiniciar contrasenya d'usuari " + "write:admin:resolve-abuse-user-report": "Resoldre informes d'usuaris " + "write:admin:send-email": "Enviar correu electrònic " + "read:admin:server-info": "Veure informació del servidor" + "read:admin:show-moderation-log": "Veure registre de moderació " + "read:admin:show-user": "Veure informació privada de l'usuari " + "read:admin:show-users": "Veure informació privada de l'usuari " + "write:admin:suspend-user": "Suspendre usuari" + "write:admin:unset-user-avatar": "Esborrar avatar d'usuari " + "write:admin:unset-user-banner": "Esborrar bàner de l'usuari " + "write:admin:unsuspend-user": "Treure la suspensió d'un usuari" + "write:admin:meta": "Gestionar les metadades de la instància" + "write:admin:user-note": "Gestionar les notes de moderació " + "write:admin:roles": "Gestionar rols" + "read:admin:roles": "Veure rols" + "write:admin:relays": "Gestionar relé" + "read:admin:relays": "Veure relés" + "write:admin:invite-codes": "Gestionar codis d'invitació " + "read:admin:invite-codes": "Veure codis d'invitació " + "write:admin:announcements": "Gestionar anuncis" + "read:admin:announcements": "Veure anuncis" + "write:admin:avatar-decorations": "Gestionar la decoració dels avatars" + "read:admin:avatar-decorations": "Veure les decoracions dels avatars" + "write:admin:federation": "Gestionar la federació d'instàncies " + "write:admin:account": "Gestionar els comptes d'usuaris " + "read:admin:account": "Veure els comptes d'usuaris " + "write:admin:emoji": "Edició d'emojis" + "read:admin:emoji": "Veure emojis" + "write:admin:queue": "Gestionar la cua de feines" + "read:admin:queue": "Veure la cua de feines" _antennaSources: all: "Totes les publicacions" homeTimeline: "Publicacions dels usuaris seguits" @@ -1588,17 +2025,73 @@ _widgets: timeline: "Línia de temps" activity: "Activitat" federation: "Federació" + button: "Botó " jobQueue: "Cua de tasques" _userList: chooseList: "Tria una llista" _cw: + hide: "Amagar" show: "Carregar més" + chars: "{count} caràcters " + files: "{count} fitxer(s)" +_poll: + noOnlyOneChoice: "Es necessita escollir dues opcions com a mínim " + choiceN: "Opció {n}" + noMore: "No pots afegir més opcions" + canMultipleVote: "Permetre escollir diferents opcions" + expiration: "Finalitza el" + infinite: "Mai" + at: "Finalitza en..." + after: "Finalitza després..." + deadlineDate: "Data de finalització " + deadlineTime: "Hor(a)(es)" + duration: "Duració " + votesCount: "{n} vots" + totalVotes: "{n} vots en total" + vote: "Votar en una enquesta" + showResult: "Veure resultats" + voted: "Has votat" + closed: "Finalitzada" + remainingDays: "Queden {d} dies i {h} hores per finalitzar" + remainingHours: "Queden {h} hores i {m} minuts" + remainingMinutes: "Queden {m} minuts i {s} segons" + remainingSeconds: "Queden {s} segons" _visibility: + public: "Públic " + publicDescription: "La teva nota la podrà veure tothom " home: "Inici" + homeDescription: "Publicar només a la línia de temps d'Inici " followers: "Seguidors" + followersDescription: "Fes només visible per als teus seguidors" + specified: "Directe" + specifiedDescription: "Fer visible només per alguns usuaris" + disableFederation: "Sense federar" + disableFederationDescription: "No enviar a altres servidors" +_postForm: + replyPlaceholder: "Contestar..." + quotePlaceholder: "Citar..." + channelPlaceholder: "Publicar a un canal..." + _placeholders: + a: "Que vols dir?..." + b: "Alguna cosa interessant al teu voltant?..." + c: "Què et passa pel cap?..." + d: "Què vols dir?..." + e: "Escriu alguna cosa..." + f: "Esperant que escriguis qualsevol cosa..." _profile: name: "Nom" username: "Nom d'usuari" + description: "Biografia " + youCanIncludeHashtags: "Pots posar etiquetes a la teva biografia " + metadata: "Informació adicional " + metadataEdit: "Editar la informació adicional " + metadataDescription: "Amb això podràs mostrar camps d'informació adicional al teu perfil." + metadataLabel: "Etiqueta " + metadataContent: "Contingut" + changeAvatar: "Canviar l'avatar " + changeBanner: "Canviar el bàner " + verifiedLinkDescription: "Escrivint una adreça URL que enllaci a aquest perfil, una icona de propietat verificada es mostrarà al costat del camp." + avatarDecorationMax: "Pot afegir un màxim de {max} decoracions." _exportOrImport: allNotes: "Totes les publicacions" clips: "Retalls" @@ -1614,19 +2107,74 @@ _timelines: social: "Social" global: "Global" _play: + viewSource: "Veure l'origen " + featured: "Popular" + title: "Títol " script: "Script" summary: "Descripció" _pages: + viewSource: "Veure l'origen " + viewPage: "Veure les teves pàgines " + like: "M'agrada " + unlike: "Treure m'agrada " + my: "Les meves pàgines " + liked: "Pàgines que m'agraden " + featured: "Popular" + inspector: "Inspeccionar" contents: "Contingut" + content: "Bloquejar la pàgina " + variables: "Variables" + title: "Títol " + url: "URL de la pàgina " + summary: "Resum de la pàgina " + alignCenter: "Centrar elements" + hideTitleWhenPinned: "Amagar el títol de la pàgina quan estigui fixada al perfil" + font: "Lletra tipogràfica" + fontSerif: "Serif" + fontSansSerif: "Sans Serif" + eyeCatchingImageSet: "Escull una miniatura" + eyeCatchingImageRemove: "Esborrar la miniatura" + chooseBlock: "Afegeix un bloc" + selectType: "Seleccionar tipus" + contentBlocks: "Contingut" + inputBlocks: "Entrada " + specialBlocks: "Especial" blocks: + text: "Text" + textarea: "Àrea de text" + section: "Secció " image: "Imatges" + button: "Botó " + note: "Incorporar una Nota" _note: id: "ID de la publicació" + idDescription: "Alternativament pots enganxar l'adreça URL de la nota aquí." detailed: "Mostra els detalls" +_relayStatus: + requesting: "Pendent" + accepted: "Acceptat" + rejected: "Rebutjat" _notification: + fileUploaded: "Fitxer pujat sense cap problema" + youGotMention: "{name} t'ha mencionat" + youGotReply: "{name} t'ha contestat" + youGotQuote: "{name} t'ha citat" youRenoted: "Impulsat per {name}" youWereFollowed: "t'ha seguit" + youReceivedFollowRequest: "Has rebut una petició de seguiment" + yourFollowRequestAccepted: "La teva petició de seguiment ha sigut acceptada" + pollEnded: "Ja pots veure els resultats de l'enquesta " + newNote: "Nota nova" unreadAntennaNote: "Antena {name}" + roleAssigned: "Rol assignat " + emptyPushNotificationMessage: "Les notificacions han sigut actualitzades" + achievementEarned: "Aconseguiment desblocat" + testNotification: "Notificació de prova" + checkNotificationBehavior: "Comprova el comportament de la notificació " + sendTestNotification: "Enviar notificació de prova" + notificationWillBeDisplayedLikeThis: "Les notificacions és veure'n així " + reactedBySomeUsers: "Han reaccionat {n} usuaris" + renotedBySomeUsers: "L'han impulsat {n} usuaris" _types: all: "Tots" follow: "Seguint" @@ -1665,7 +2213,46 @@ _webhookSettings: _moderationLogTypes: suspend: "Suspèn" resetPassword: "Restableix la contrasenya" + suspendRemoteInstance: "Servidor remot suspès " + unsuspendRemoteInstance: "S'ha tret la suspensió del servidor remot" + markSensitiveDriveFile: "Fitxer marcat com a sensible" + unmarkSensitiveDriveFile: "S'ha tret la marca de sensible del fitxer" + resolveAbuseReport: "Informe resolt" createInvitation: "Crear codi d'invitació " + createAd: "Anunci creat" + deleteAd: "Anunci esborrat" + updateAd: "Anunci actualitzat" + createAvatarDecoration: "Decoració de l'avatar creada" + updateAvatarDecoration: "S'ha actualitzat la decoració de l'avatar " + deleteAvatarDecoration: "S'ha esborrat la decoració de l'avatar " + unsetUserAvatar: "Esborrar l'avatar d'aquest usuari" + unsetUserBanner: "Esborrar el bàner d'aquest usuari" +_fileViewer: + title: "Detall del fitxer" + type: "Tipus de fitxer" + size: "Mida" + url: "URL" + uploadedAt: "Pujat el" + attachedNotes: "Notes amb aquest fitxer" + thisPageCanBeSeenFromTheAuthor: "Aquesta pàgina només la pot veure l'usuari que ha pujat aquest fitxer." +_externalResourceInstaller: + title: "Instal·lar des d'un lloc extern" + checkVendorBeforeInstall: "Assegura't que qui distribueix aquest recurs és fiable abans d'instal·lar-ho." + _plugin: + title: "Vols instal·lar aquest afegit?" + metaTitle: "Informació de l'afegit " + _theme: + title: "Vols instal·lar aquest tema?" + metaTitle: "Informació del tema" + _meta: + base: "Paleta de colors base" + _vendorInfo: + title: "Informació del distribuïdor " + endpoint: "Punt final referenciat" + hashVerify: "Verificació d'integritat " + _errors: + _invalidParams: + title: "Paràmetres no vàlids " _reversi: total: "Total" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index a58b14cc5f..44a104a2d5 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -1005,6 +1005,7 @@ 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." +prohibitedWordsDescription2: "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?" @@ -1094,6 +1095,7 @@ iHaveReadXCarefullyAndAgree: "Přečetl jsem si text \"{x}\" a souhlasím s ním icon: "Avatar" replies: "Odpovědět" renotes: "Přeposlat" +sourceCode: "Zdrojový kód" flip: "Otočit" lastNDays: "Posledních {n} dnů" _initialAccountSetting: diff --git a/locales/de-DE.yml b/locales/de-DE.yml index ea85ca7729..8b4c21f36a 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -1050,6 +1050,7 @@ resetPasswordConfirm: "Wirklich Passwort zurücksetzen?" sensitiveWords: "Sensible Wörter" sensitiveWordsDescription: "Die Notizsichtbarkeit aller Notizen, die diese Wörter enthalten, wird automatisch auf \"Startseite\" gesetzt. Durch Zeilenumbrüche können mehrere konfiguriert werden." sensitiveWordsDescription2: "Durch die Verwendung von Leerzeichen können AND-Verknüpfungen angegeben werden und durch das Umgeben von Schrägstrichen können reguläre Ausdrücke verwendet werden." +prohibitedWordsDescription2: "Durch die Verwendung von Leerzeichen können AND-Verknüpfungen angegeben werden und durch das Umgeben von Schrägstrichen können reguläre Ausdrücke verwendet werden." hiddenTags: "Ausgeblendete Hashtags" hiddenTagsDescription: "Die hier eingestellten Tags werden nicht mehr in den Trends angezeigt. Mit der Umschalttaste können mehrere ausgewählt werden." notesSearchNotAvailable: "Die Notizsuche ist nicht verfügbar." @@ -1171,6 +1172,7 @@ hideRepliesToOthersInTimelineAll: "Antworten von allen momentan gefolgten Benutz confirmShowRepliesAll: "Dies ist eine unwiderrufliche Aktion. Wirklich Antworten von allen momentan gefolgten Benutzern in der Chronik anzeigen?" confirmHideRepliesAll: "Dies ist eine unwiderrufliche Aktion. Wirklich Antworten von allen momentan gefolgten Benutzern nicht in der Chronik anzeigen?" externalServices: "Externe Dienste" +sourceCode: "Quellcode" impressum: "Impressum" impressumUrl: "Impressums-URL" impressumDescription: "In manchen Ländern, wie Deutschland und dessen Umgebung, ist die Angabe von Betreiberinformationen (ein Impressum) bei kommerziellem Betrieb zwingend." diff --git a/locales/en-US.yml b/locales/en-US.yml index 0e51870146..924d272755 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1147,6 +1147,7 @@ resetPasswordConfirm: "Really reset your password?" sensitiveWords: "Sensitive words" sensitiveWordsDescription: "The visibility of all notes containing any of the configured words will be set to \"Home\" automatically. You can list multiple by separating them via line breaks." sensitiveWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression." +prohibitedWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression." hiddenTags: "Hidden hashtags" hiddenTagsDescription: "Select tags which will not shown on trend list.\nMultiple tags could be registered by lines." notesSearchNotAvailable: "Note search is unavailable." @@ -1283,6 +1284,7 @@ hideRepliesToOthersInTimelineAll: "Hide replies to others from everyone you foll confirmShowRepliesAll: "This operation is irreversible. Would you really like to show replies to others from everyone you follow in your timeline?" confirmHideRepliesAll: "This operation is irreversible. Would you really like to hide replies to others from everyone you follow in your timeline?" externalServices: "External Services" +sourceCode: "Source code" impressum: "Impressum" impressumUrl: "Impressum URL" impressumDescription: "In some countries, like germany, the inclusion of operator contact information (an Impressum) is legally required for commercial websites." @@ -2302,6 +2304,55 @@ _permissions: "write:flash": "Edit Plays" "read:flash-likes": "View list of liked Plays" "write:flash-likes": "Edit list of liked Plays" + "read:admin:abuse-user-reports": "View user reports" + "write:admin:delete-account": "Delete user account" + "write:admin:delete-all-files-of-a-user": "Delete all files of a user" + "read:admin:index-stats": "View database index stats" + "read:admin:table-stats": "View database table stats" + "read:admin:user-ips": "View user IP addresses" + "read:admin:meta": "View instance metadata" + "write:admin:reset-password": "Reset user password" + "write:admin:resolve-abuse-user-report": "Resolve user report" + "write:admin:send-email": "Send email" + "read:admin:server-info": "View server info" + "read:admin:show-moderation-log": "View moderation log" + "read:admin:show-user": "View private user info" + "read:admin:show-users": "View private user info" + "write:admin:suspend-user": "Suspend user" + "write:admin:unset-user-avatar": "Remove user avatar" + "write:admin:unset-user-banner": "Remove user banner" + "write:admin:unsuspend-user": "Unsuspend user" + "write:admin:meta": "Manage instance metadata" + "write:admin:user-note": "Manage moderation note" + "write:admin:roles": "Manage roles" + "read:admin:roles": "View roles" + "write:admin:relays": "Manage relays" + "read:admin:relays": "View relays" + "write:admin:invite-codes": "Manage invite codes" + "read:admin:invite-codes": "View invite codes" + "write:admin:announcements": "Manage announcements" + "read:admin:announcements": "View announcements" + "write:admin:avatar-decorations": "Manage avatar decorations" + "read:admin:avatar-decorations": "View avatar decorations" + "write:admin:federation": "Manage federation data" + "write:admin:account": "Manage user account" + "read:admin:account": "View user account" + "write:admin:emoji": "Manage emoji" + "read:admin:emoji": "View emoji" + "write:admin:queue": "Manage job queue" + "read:admin:queue": "View job queue info" + "write:admin:promo": "Manage promotion notes" + "write:admin:drive": "Manage user drive" + "read:admin:drive": "View user drive info" + "read:admin:stream": "Use WebSocket API for Admin" + "write:admin:ad": "Manage ads" + "read:admin:ad": "View ads" + "write:invite-codes": "Create invite codes" + "read:invite-codes": "Get invite codes" + "write:clip-favorite": "Manage favorited clips" + "read:clip-favorite": "View favorited clips" + "read:federation": "Get federation data" + "write:report-abuse": "Report violation" _auth: shareAccessTitle: "Granting application permissions" shareAccess: "Would you like to authorize \"{name}\" to access this account?" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 92b4731dc3..7fd6b05f52 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -11,7 +11,7 @@ password: "Contraseña" forgotPassword: "Olvidé mi contraseña" fetchingAsApObject: "Buscando en el fediverso" ok: "OK" -gotIt: "Entendido" +gotIt: "¡Lo tengo!" cancel: "Cancelar" noThankYou: "No gracias" enterUsername: "Introduce el nombre de usuario" @@ -1055,6 +1055,8 @@ resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?" sensitiveWords: "Palabras sensibles" sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de línea" sensitiveWordsDescription2: "Si se usan espacios se crearán expresiones AND y las palabras subsecuentes con barras inclinadas se convertirán en expresiones regulares." +prohibitedWords: "Palabras explícitas" +prohibitedWordsDescription2: "Si se usan espacios se crearán expresiones AND y las palabras subsecuentes con barras inclinadas se convertirán en expresiones regulares." hiddenTags: "Hashtags ocultos" hiddenTagsDescription: "Selecciona las etiquetas que no se mostrarán en tendencias. Una etiqueta por línea." notesSearchNotAvailable: "No se puede buscar una nota" @@ -1073,6 +1075,8 @@ limitWidthOfReaction: "Limitar ancho de las reacciones" noteIdOrUrl: "ID o URL de la nota" video: "Video" videos: "Video" +audio: "Sonido" +audioFiles: "Sonido" dataSaver: "Ahorro de datos" accountMigration: "Migración de cuenta" accountMoved: "Este usuario se movió a una nueva cuenta:" @@ -1176,6 +1180,7 @@ hideRepliesToOthersInTimelineAll: "Ocultar tus respuestas a otros usuarios que s confirmShowRepliesAll: "Esta operación es irreversible. ¿Confirmas que quieres mostrar tus respuestas a otros usuarios que sigues en tu línea de tiempo?" confirmHideRepliesAll: "Esta operación es irreversible. ¿Confirmas que quieres ocultar tus respuestas a otros usuarios que sigues en tu línea de tiempo?" externalServices: "Servicios Externos" +sourceCode: "Código fuente" impressum: "Impressum" impressumUrl: "Impressum URL" impressumDescription: "En algunos países, como Alemania, la inclusión del operador de datos (el Impressum) es requerido legalmente para sitios web comerciales." @@ -1207,14 +1212,23 @@ addMfmFunction: "Añadir función MFM" enableQuickAddMfmFunction: "Activar acceso rápido para añadir funciones MFM" bubbleGame: "Bubble Game" sfx: "Efectos de sonido" -soundWillBePlayed: "Se reproducirán efector sonoros" +soundWillBePlayed: "Se reproducirán efectos sonoros" showReplay: "Ver reproducción" replay: "Reproducir" replaying: "Reproduciendo" ranking: "Clasificación" lastNDays: "Últimos {n} días" +backToTitle: "Regresar al inicio" +hemisphere: "Región" +withSensitive: "Mostrar notas que contengan material sensible" +userSaysSomethingSensitive: "La publicación de {name} contiene material sensible" +enableHorizontalSwipe: "Deslice para cambiar de pestaña" _bubbleGame: howToPlay: "Cómo jugar" + _howToPlay: + section1: "Ajuste la posición y deje caer el objeto en la caja" + section2: "Cuando dos objetos del mismo tipo se tocan, cambian a otro tipo y consigues puntos" + section3: "El juego termina cuando la caja se desborda de objetos. ¡Intenta conseguir una puntuación alta al juntar objetos mientras evitas desbordar la caja!" _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." @@ -2515,6 +2529,11 @@ _dataSaver: _code: title: "Resaltar código" description: "Si se usa resaltado de código en MFM, etc., no se cargará hasta pulsar en ello. El resaltado de sintaxis requiere la descarga de archivos de definición para cada lenguaje de programación. Debido a esto, al deshabilitar la carga automática de estos archivos reducirás el consumo de datos." +_hemisphere: + N: "Hemisferio norte" + S: "Hemisferio sur" _reversi: + reversi: "Reversi" + won: "{name} ha ganado" total: "Total" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 1015fcdf9e..18c98a359d 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -2,7 +2,7 @@ _lang_: "Français" headlineMisskey: "Réseau relié par des notes" introMisskey: "Bienvenue ! CherryPick est un service de microblogage décentralisé, libre et ouvert.\nÉcrivez des « notes » et partagez ce qui se passe à l’instant présent, autour de vous avec les autres 📡\nLa fonction « réactions », vous permet également d’ajouter une réaction rapide aux notes des autres utilisateur·rice·s 👍\nExplorons un nouveau monde 🚀" -poweredByMisskeyDescription: "{nom} est l'un des services propulsés par la plateforme ouverte CherryPick (appelée \"instance CherryPick\")." +poweredByMisskeyDescription: "{name} est l'un des services propulsés par la plateforme ouverte LycheeBridge (appelée \"instance LycheeBridge\")." monthAndDay: "{day}/{month}" search: "Rechercher" notifications: "Notifications" @@ -1150,6 +1150,7 @@ hideRepliesToOthersInTimelineAll: "Masquer les réponses de toutes les personnes confirmShowRepliesAll: "Cette opération est irréversible. Voulez-vous vraiment afficher les réponses de toutes les personnes que vous suivez dans le fil ?" confirmHideRepliesAll: "Cette opération est irréversible. Voulez-vous vraiment masquer les réponses de toutes les personnes que vous suivez dans le fil ?" externalServices: "Services externes" +sourceCode: "Code source" impressum: "Impressum" impressumUrl: "URL de l'impressum" impressumDescription: "Dans certains pays comme l'Allemagne, il est obligatoire d'afficher les informations sur l'opérateur d'un site (un impressum)." @@ -1189,7 +1190,7 @@ _initialAccountSetting: profileSetting: "Paramètres du profil" privacySetting: "Paramètres de confidentialité" initialAccountSettingCompleted: "Configuration du profil terminée avec succès !" - youCanContinueTutorial: "Vous pouvez procéder au tutoriel sur l'utilisation de {nom}(CherryPick) ou vous arrêter ici et commencer à l'utiliser immédiatement." + youCanContinueTutorial: "Vous pouvez procéder au tutoriel sur l'utilisation de {nom}(LycheeBridge) ou vous arrêter ici et commencer à l'utiliser immédiatement." startTutorial: "Démarrer le tutoriel" skipAreYouSure: "Désirez-vous ignorer la configuration du profil ?" _initialTutorial: diff --git a/locales/id-ID.yml b/locales/id-ID.yml index aaa8f1512b..547c554bf5 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -81,7 +81,7 @@ exportRequested: "Kamu telah meminta ekspor. Ini akan memakan waktu sesaat. Sete importRequested: "Kamu telah meminta impor. Ini akan memakan waktu sesaat." lists: "Daftar" noLists: "Kamu tidak memiliki daftar apapun" -note: "Catat" +note: "Catatan" notes: "Catatan" following: "Ikuti" followers: "Pengikut" @@ -381,8 +381,10 @@ enableHcaptcha: "Nyalakan hCaptcha" hcaptchaSiteKey: "Site Key" hcaptchaSecretKey: "Secret Key" mcaptcha: "mCaptcha" +enableMcaptcha: "Nyalakan mCaptcha" mcaptchaSiteKey: "Site key" mcaptchaSecretKey: "Secret Key" +mcaptchaInstanceUrl: "URL instansi mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "Nyalakan reCAPTCHA" recaptchaSiteKey: "Site key" @@ -642,6 +644,7 @@ medium: "Sedang" small: "Kecil" generateAccessToken: "Buat token akses" permission: "Izin" +adminPermission: "Wewenang Izin Admin" enableAll: "Aktifkan semua" disableAll: "Nonaktifkan semua" tokenRequested: "Berikan ijin akses ke akun" @@ -1052,6 +1055,8 @@ resetPasswordConfirm: "Yakin untuk mereset kata sandimu?" sensitiveWords: "Kata sensitif" sensitiveWordsDescription: "Visibilitas dari semua catatan mengandung kata yang telah diatur akan dijadikan \"Beranda\" secara otomatis. Kamu dapat mendaftarkan kata tersebut lebih dari satu dengan menuliskannya di baris baru." sensitiveWordsDescription2: "Menggunakan spasi akan membuat ekspresi AND dan kata kunci disekitarnya dengan garis miring akan mengubahnya menjadi ekspresi reguler." +prohibitedWords: "Kata yang dilarang" +prohibitedWordsDescription2: "Menggunakan spasi akan membuat ekspresi AND dan kata kunci disekitarnya dengan garis miring akan mengubahnya menjadi ekspresi reguler." hiddenTags: "Tagar tersembunyi" hiddenTagsDescription: "Pilih tanda yang mana akan tidak diperlihatkan dalam daftar tren.\nTanda lebih dari satu dapat didaftarkan dengan tiap baris." notesSearchNotAvailable: "Pencarian catatan tidak tersedia." @@ -1070,6 +1075,8 @@ limitWidthOfReaction: "Batasi lebar maksimum reaksi dan tampilkan dalam ukuran t noteIdOrUrl: "ID catatan atau URL" video: "Video" videos: "Video" +audio: "Suara" +audioFiles: "Berkas Suara" dataSaver: "Penghemat data" accountMigration: "Pemindahan akun" accountMoved: "Pengguna ini telah berpindah ke akun baru:" @@ -1173,6 +1180,7 @@ hideRepliesToOthersInTimelineAll: "Sembuyikan balasan ke lainnya dari semua oran confirmShowRepliesAll: "Operasi ini tidak dapat diubah. Apakah kamu yakin untuk menampilkan balasan ke lainnya dari semua orang yang kamu ikuti di lini masa?" confirmHideRepliesAll: "Operasi ini tidak dapat diubah. Apakah kamu yakin untuk menyembunyikan balasan ke lainnya dari semua orang yang kamu ikuti di lini masa?" externalServices: "Layanan eksternal" +sourceCode: "Sumber kode" impressum: "Impressum" impressumUrl: "Tautan Impressum" impressumDescription: "Pada beberapa negara seperti Jerman, inklusi dari informasi kontak operator (sebuah Impressum) diperlukan secara legal untuk situs web komersil." @@ -1204,10 +1212,21 @@ addMfmFunction: "Tambahkan dekorasi" enableQuickAddMfmFunction: "Tampilkan pemilih MFM tingkat lanjut" bubbleGame: "Bubble Game" sfx: "Efek Suara" +soundWillBePlayed: "Suara yang akan dimainkan" +showReplay: "Lihat tayangan ulang" +replay: "Tayangan ulang" +replaying: "Menayangkan Ulang" +ranking: "Peringkat" lastNDays: "{n} hari terakhir" backToTitle: "Ke Judul" +hemisphere: "Letak kamu tinggal" +withSensitive: "Lampirkan catatan dengan berkas sensitif" +userSaysSomethingSensitive: "Postingan oleh {name} mengandung konten sensitif" +enableHorizontalSwipe: "Geser untuk mengganti tab" _bubbleGame: howToPlay: "Cara bermain" + _howToPlay: + section1: "Atur posisi dan jatuhkan obyek ke dalam kotak." _announcement: forExistingUsers: "Hanya pengguna yang telah ada" forExistingUsersDescription: "Pengumuman ini akan dimunculkan ke pengguna yang sudah ada dari titik waktu publikasi jika dinyalakan. Apabila dimatikan, mereka yang baru mendaftar setelah publikasi ini akan juga melihatnya." @@ -1269,6 +1288,8 @@ _initialTutorial: note: "Baru aja makan donat berlapis coklat 🍩😋" _howToMakeAttachmentsSensitive: title: "Bagaimana menandai lampiran sebagai sensitif?" + _done: + title: "Kamu telah menyelesaikan tutorial! 🎉" _serverRules: description: "Daftar peraturan akan ditampilkan sebelum pendaftaran. Mengatur ringkasan dari Syarat dan Ketentuan sangat direkomendasikan." _serverSettings: @@ -1982,6 +2003,55 @@ _permissions: "write:flash": "Sunting Play" "read:flash-likes": "Lihat daftar Play yang disukai" "write:flash-likes": "Sunting daftar Play yang disukai" + "read:admin:abuse-user-reports": "Lihat laporan pengguna" + "write:admin:delete-account": "Hapus akun pengguna" + "write:admin:delete-all-files-of-a-user": "Hapus semua berkas dari seorang pengguna" + "read:admin:index-stats": "Lihat statistik indeks basis data" + "read:admin:table-stats": "Lihat statistik tabel basis data" + "read:admin:user-ips": "Lihat alamat IP pengguna" + "read:admin:meta": "Lihat metadata instansi" + "write:admin:reset-password": "Atur ulang kata sandi pengguna" + "write:admin:resolve-abuse-user-report": "Selesaikan laporan pengguna" + "write:admin:send-email": "Mengirim surel" + "read:admin:server-info": "Lihat informasi peladen" + "read:admin:show-moderation-log": "Lihat log moderasi" + "read:admin:show-user": "Lihat informasi pengguna privat" + "read:admin:show-users": "Lihat informasi pengguna privat" + "write:admin:suspend-user": "Tangguhkan pengguna" + "write:admin:unset-user-avatar": "Hapus avatar pengguna" + "write:admin:unset-user-banner": "Hapus banner pengguna" + "write:admin:unsuspend-user": "Batalkan penangguhan pengguna" + "write:admin:meta": "Kelola metadata instansi" + "write:admin:user-note": "Kelola moderasi catatan" + "write:admin:roles": "Kelola peran" + "read:admin:roles": "Lihat peran" + "write:admin:relays": "Kelola relay" + "read:admin:relays": "Lihat relay" + "write:admin:invite-codes": "Kelola kode undangan" + "read:admin:invite-codes": "Lihat kode undangan" + "write:admin:announcements": "Kelola pengumuman" + "read:admin:announcements": "Lihat Pengumuman" + "write:admin:avatar-decorations": "Kelola dekorasi avatar" + "read:admin:avatar-decorations": "Lihat dekorasi avatar" + "write:admin:federation": "Kelola data federasi" + "write:admin:account": "Kelola akun pengguna" + "read:admin:account": "Lihat akun pengguna" + "write:admin:emoji": "Kelola emoji" + "read:admin:emoji": "Lihat emoji" + "write:admin:queue": "Kelola antrian kerja" + "read:admin:queue": "Lihat informasi antrian kerja" + "write:admin:promo": "Kelola catatan promosi" + "write:admin:drive": "Kelola drive pengguna" + "read:admin:drive": "Kelola informasi drive pengguna" + "read:admin:stream": "Gunakan API WebSocket untuk Admin" + "write:admin:ad": "Kelola iklan" + "read:admin:ad": "Lihat iklan" + "write:invite-codes": "Membuat kode undangan" + "read:invite-codes": "Mendapatkan kode undangan" + "write:clip-favorite": "Kelola klip yang difavoritkan" + "read:clip-favorite": "Lihat klip yang difavoritkan" + "read:federation": "Mendapatkan data federasi" + "write:report-abuse": "Melaporkan pelanggaran" _auth: shareAccessTitle: "Mendapatkan ijin akses aplikasi" shareAccess: "Apakah kamu ingin mengijinkan \"{name}\" untuk mengakses akun ini?" @@ -2037,6 +2107,7 @@ _widgets: _userList: chooseList: "Pilih daftar" clicker: "Pengeklik" + birthdayFollowings: "Pengguna yang merayakan hari ulang tahunnya hari ini" _cw: hide: "Sembunyikan" show: "Lihat konten" @@ -2406,6 +2477,41 @@ _dataSaver: _code: title: "Penyorotan kode" description: "Jika notasi penyorotan kode digunakan di MFM, dll. Fungsi tersebut tidak akan dimuat apabila tidak diketuk. Penyorotan sintaks membutuhkan pengunduhan berkas definisi penyorotan untuk setiap bahasa pemrograman. Oleh sebab itu, menonaktifkan pemuatan otomatis dari berkas ini dilakukan untuk mengurangi jumlah komunikasi data." +_hemisphere: + N: "Bumi belahan utara" + S: "Bumi belahan selatan" + caption: "Digunakan dalam beberapa pengaturan klien untuk menentukan musim." _reversi: + reversi: "Reversi" + gameSettings: "Pengaturan permainan" + chooseBoard: "Pilih papan" + blackOrWhite: "Hitam/Putih" + blackIs: "{name} bermain sebagai Hitam" + rules: "Aturan" + thisGameIsStartedSoon: "Permainan akan segera dimulai" + waitingForOther: "Menunggu langkah giliran dari lawan" + waitingForMe: "Menungguh langkah giliran dari kamu" + waitingBoth: "Bersiap" + ready: "Siap" + cancelReady: "Belum siap" + opponentTurn: "Giliran lawan" + myTurn: "Giliran kamu" + turnOf: "Giliran {name}" + pastTurnOf: "Giliran {name}" + surrender: "Menyerah" + surrendered: "Telah menyerah" + timeout: "Waktu habis" + drawn: "Seri" + won: "{name} menang" + black: "Hitam" + white: "Putih" total: "Jumlah" + turnCount: "Langkah ke {count}" + myGames: "Rondeku" + allGames: "Semua ronde" + ended: "Selesai" + playing: "Sedang bermain" + isLlotheo: "Pemain dengan batu yang sedikit menang (Llotheo)" + loopedMap: "Peta melingkar" + canPutEverywhere: "Keping dapat ditaruh dimana saja" diff --git a/locales/index.d.ts b/locales/index.d.ts index 0c5fe5c7dd..17f253a8ed 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -4415,6 +4415,10 @@ export interface Locale extends ILocale { * LycheeBridgeは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします! */ "pleaseDonate": ParameterizedString<"host">; + /** + * 対応するソースコードは{anchor}から利用可能です。 + */ + "correspondingSourceIsAvailable": ParameterizedString<"anchor">; /** * ロール */ @@ -4619,6 +4623,18 @@ export interface Locale extends ILocale { * スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。 */ "sensitiveWordsDescription2": string; + /** + * 禁止ワード + */ + "prohibitedWords": string; + /** + * 設定したワードが含まれるノートを投稿しようとした際、エラーとなるようにします。改行で区切って複数設定できます。 + */ + "prohibitedWordsDescription": string; + /** + * スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。 + */ + "prohibitedWordsDescription2": string; /** * 非表示ハッシュタグ */ @@ -5163,6 +5179,34 @@ export interface Locale extends ILocale { * 外部サービス */ "externalServices": string; + /** + * ソースコード + */ + "sourceCode": string; + /** + * ソースコードはまだ提供されていません。この問題の修正について管理者に問い合わせてください。 + */ + "sourceCodeIsNotYetProvided": string; + /** + * リポジトリURL + */ + "repositoryUrl": string; + /** + * ソースコードが公開されているリポジトリがある場合、そのURLを記入します。Misskeyを現状のまま(ソースコードにいかなる変更も加えずに)使用している場合は https://github.com/misskey-dev/misskey と記入します。 + */ + "repositoryUrlDescription": string; + /** + * リポジトリを公開していない場合、代わりにtarballを提供する必要があります。詳細は.config/example.ymlを参照してください。 + */ + "repositoryUrlOrTarballRequired": string; + /** + * フィードバック + */ + "feedback": string; + /** + * フィードバックURL + */ + "feedbackUrl": string; /** * 運営者情報 */ @@ -7765,6 +7809,14 @@ export interface Locale extends ILocale { * ソースコード */ "source": string; + /** + * オリジナル + */ + "original": string; + /** + * {name}はオリジナルのMisskeyを改変したバージョンを使用しています。 + */ + "thisIsModifiedVersion": ParameterizedString<"name">; /** * Misskeyを翻訳 */ diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 45b0003d8d..3ae8cab0c2 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -435,7 +435,7 @@ moderation: "moderazione" moderationNote: "Promemoria di moderazione" addModerationNote: "Aggiungi promemoria di moderazione" moderationLogs: "Cronologia di moderazione" -nUsersMentioned: "{n} profili menzionati" +nUsersMentioned: "{n} profili ne parlano" securityKeyAndPasskey: "Chiave di sicurezza e accesso" securityKey: "Chiave di sicurezza" lastUsed: "Ultima attività" @@ -670,7 +670,7 @@ hardWordMute: "Filtro parole forte" regexpError: "errore regex" regexpErrorDescription: "Si è verificato un errore nell'espressione regolare alla riga {line} della parola muta {tab}:" instanceMute: "Silenzia l'istanza" -userSaysSomething: "{name} ha detto qualcosa" +userSaysSomething: "{name} ha parlato" makeActive: "Attiva" display: "Visualizza" copy: "Copia" @@ -774,7 +774,7 @@ reloadToApplySetting: "Le tue preferenze verranno impostate dopo il ricaricament needReloadToApply: "È necessario riavviare per rendere effettive le modifiche." showTitlebar: "Visualizza la barra del titolo" clearCache: "Svuota la cache" -onlineUsersCount: "{n} persone online" +onlineUsersCount: "{n} persone attive adesso" nUsers: "{n} profili" nNotes: "{n}Note" sendErrorReports: "Invia segnalazioni di errori" @@ -1055,6 +1055,9 @@ resetPasswordConfirm: "Vuoi davvero ripristinare la password?" sensitiveWords: "Parole esplicite" sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga." sensitiveWordsDescription2: "Gli spazi creano la relazione \"E\" tra parole (questo E quello). Racchiudere una parola nelle slash \"/\" la trasforma in Espressione Regolare." +prohibitedWords: "Parole proibite" +prohibitedWordsDescription: "Verrà impedito di pubblicare Note che abbiano le parole indicate. Puoi impostare più parole, separatamente, su ogni riga." +prohibitedWordsDescription2: "Gli spazi creano la relazione \"E\" tra parole (questo E quello). Racchiudere una parola nelle slash \"/\" la trasforma in Espressione Regolare." hiddenTags: "Hashtag nascosti" hiddenTagsDescription: "Impedire la visualizzazione del tag impostato nei trend. Puoi impostare più valori, uno per riga." notesSearchNotAvailable: "Non è possibile cercare tra le Note." @@ -1178,6 +1181,7 @@ hideRepliesToOthersInTimelineAll: "Nascondi le risposte dei tuoi follow nella TL confirmShowRepliesAll: "Questa è una attività irreversibile. Vuoi davvero includere tutte le risposte dei following in TL?" confirmHideRepliesAll: "Questa è una attività irreversibile. Vuoi davvero escludere tutte le risposte dei following in TL?" externalServices: "Servizi esterni" +sourceCode: "Codice sorgente" impressum: "Dichiarazione di proprietà" impressumUrl: "URL della dichiarazione di proprietà" impressumDescription: "La dichiarazione di proprietà, è obbligatoria in alcuni paesi come la Germania (Impressum)." @@ -2076,6 +2080,35 @@ _permissions: "write:admin:unsuspend-user": "Togliere la sospensione ai profili" "write:admin:meta": "Modificare i metadati dell'istanza" "write:admin:user-note": "Scrivere annotazioni di moderazione" + "write:admin:roles": "Gestire i ruoli" + "read:admin:roles": "Vedere i ruoli" + "write:admin:relays": "Gestire i Relay" + "read:admin:relays": "Vedere i Relay" + "write:admin:invite-codes": "Gestire codici di invito" + "read:admin:invite-codes": "Vedere codici di invito" + "write:admin:announcements": "Gestire gli annunci" + "read:admin:announcements": "Leggere gli annunci" + "write:admin:avatar-decorations": "Gestire le decorazioni" + "read:admin:avatar-decorations": "Vedere le decorazioni" + "write:admin:federation": "Gestire la federazione" + "write:admin:account": "Vedere la federazione" + "read:admin:account": "Vedere le utenze" + "write:admin:emoji": "Gestire le emoji personalizzate" + "read:admin:emoji": "Vedere le emoji personalizzate" + "write:admin:queue": "Gestire la coda di attività" + "read:admin:queue": "Vedere la coda di attività" + "write:admin:promo": "Gestire le promozioni" + "write:admin:drive": "Gestire il Drive degli account" + "read:admin:drive": "Vedere il Drive degli account" + "read:admin:stream": "Usare le API Websocket" + "write:admin:ad": "Gestire i banner pubblicitari" + "read:admin:ad": "Vedere i banner pubblicitari" + "write:invite-codes": "Creare codici di invito" + "read:invite-codes": "Vedere i codici di invito" + "write:clip-favorite": "Impostare Clip preferite" + "read:clip-favorite": "Vedere Clip preferite" + "read:federation": "Vedere la federazione" + "write:report-abuse": "Inviare segnalazioni" _auth: shareAccessTitle: "Permessi dell'applicazione" shareAccess: "Vuoi autorizzare {name} ad accedere al tuo profilo?" @@ -2121,7 +2154,7 @@ _widgets: postForm: "Finestra di pubblicazione" slideshow: "Diapositive" button: "Pulsante" - onlineUsers: "Persone online" + onlineUsers: "Persone attive adesso" jobQueue: "Coda di lavoro" serverMetric: "Statistiche server" aiscript: "Console AiScript" @@ -2319,6 +2352,7 @@ _notification: pollEnded: "Risultati del sondaggio." newNote: "Nuove Note" unreadAntennaNote: "Antenna {name}" + roleAssigned: "Ruolo assegnato" emptyPushNotificationMessage: "Le notifiche push sono state aggiornate." achievementEarned: "Obiettivo raggiunto" testNotification: "Prova la notifica" @@ -2341,6 +2375,7 @@ _notification: receiveFollowRequest: "Richiesta di follow ricevuta" followRequestAccepted: "Richiesta di follow accettata" groupInvited: "Invito a un gruppo" + roleAssigned: "Ruolo concesso" achievementEarned: "Risultato raggiunto" app: "Notifiche da applicazioni" _actions: @@ -2499,6 +2534,53 @@ _dataSaver: _code: title: "Codice evidenziato" description: "Impedire che il codice sorgente sia automaticamente evidenziato. Evidenziare il codice richiede il caricamento di un file per ogni linguaggio. Puoi evidenziare soltanto il codice che intendi leggere e ridurre il traffico inutilizzato." +_hemisphere: + N: "Emisfero boreale" + S: "Emisfero australe" + caption: "Utile per alcune impostazioni del client, per determinare la stagione." _reversi: + reversi: "Reversi" + gameSettings: "Impostazioni di gioco" + chooseBoard: "Segli la tavola" + blackOrWhite: "Neri / Bianchi" + blackIs: "{name} muove i Neri" + rules: "Regole del gioco" + thisGameIsStartedSoon: "Il gioco sta per iniziare" + waitingForOther: "Attendere l'avversario" + waitingForMe: "Ti stanno aspettando" + waitingBoth: "Preparatevi" + ready: "Pronti" + cancelReady: "Riprendere la preparazione" + opponentTurn: "Turno avversario" + myTurn: "Tocca a te" + turnOf: "Tocca a {name}" + pastTurnOf: "Turno di {name}" + surrender: "Mi arrendo" + surrendered: "Ha ceduto" + timeout: "Tempo scaduto" + drawn: "Pareggio" + won: "Ha vinto {name}" + black: "Neri" + white: "Bianchi" total: "Totale" + turnCount: "Turno N. {count}" + myGames: "Le mie sfide" + allGames: "Tutte le sfide" + ended: "Conclusione" + playing: "In gioco" + isLlotheo: "Vince chi ha meno pietre (Roseo)" + loopedMap: "Mappa ricorsiva" + canPutEverywhere: "Modalità che può essere posizionata ovunque" + timeLimitForEachTurn: "Tempo limite per turno" + freeMatch: "Sfida libera" + lookingForPlayer: "Alla ricerca di un avversario" + gameCanceled: "Sfida cancellata" + shareToTlTheGameWhenStart: "Pubblica l'inizio della partita sulla tua Timeline" + iStartedAGame: "Inizia la sfida! #MisskeyReversi" + opponentHasSettingsChanged: "L'avversario ha cambiato configurazione" + allowIrregularRules: "Regole inconsuete (completamente libere)" + disallowIrregularRules: "Impedire le regole inconsuete" +_offlineScreen: + title: "Scollegato. Impossibile connettersi al server" + header: "Impossibile connettersi al server" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b5089e7f22..d6e88513a9 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1098,6 +1098,7 @@ neverShow: "今後表示しない" remindMeLater: "また後で" didYouLikeMisskey: "LycheeBridgeを気に入っていただけましたか?" pleaseDonate: "LycheeBridgeは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!" +correspondingSourceIsAvailable: "対応するソースコードは{anchor}から利用可能です。" roles: "ロール" role: "ロール" noRole: "ロールはありません" @@ -1149,6 +1150,9 @@ resetPasswordConfirm: "パスワードリセットしますか?" sensitiveWords: "センシティブワード" sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。" sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。" +prohibitedWords: "禁止ワード" +prohibitedWordsDescription: "設定したワードが含まれるノートを投稿しようとした際、エラーとなるようにします。改行で区切って複数設定できます。" +prohibitedWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。" hiddenTags: "非表示ハッシュタグ" hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。" notesSearchNotAvailable: "ノート検索は利用できません。" @@ -1285,6 +1289,13 @@ hideRepliesToOthersInTimelineAll: "TLに現在フォロー中の人全員の返 confirmShowRepliesAll: "この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めるようにしますか?" confirmHideRepliesAll: "この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めないようにしますか?" externalServices: "外部サービス" +sourceCode: "ソースコード" +sourceCodeIsNotYetProvided: "ソースコードはまだ提供されていません。この問題の修正について管理者に問い合わせてください。" +repositoryUrl: "リポジトリURL" +repositoryUrlDescription: "ソースコードが公開されているリポジトリがある場合、そのURLを記入します。Misskeyを現状のまま(ソースコードにいかなる変更も加えずに)使用している場合は https://github.com/misskey-dev/misskey と記入します。" +repositoryUrlOrTarballRequired: "リポジトリを公開していない場合、代わりにtarballを提供する必要があります。詳細は.config/example.ymlを参照してください。" +feedback: "フィードバック" +feedbackUrl: "フィードバックURL" impressum: "運営者情報" impressumUrl: "運営者情報URL" impressumDescription: "ドイツなどの一部の国と地域では表示が義務付けられています(Impressum)。" @@ -2032,6 +2043,8 @@ _aboutMisskey: contributors: "コントリビューター" allContributors: "全てのコントリビューター" source: "ソースコード" + original: "オリジナル" + thisIsModifiedVersion: "{name}はオリジナルのMisskeyを改変したバージョンを使用しています。" translation: "Misskeyを翻訳" donate: "Misskeyに寄付" morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index fb8b848914..e8870c42bf 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -1055,6 +1055,7 @@ resetPasswordConfirm: "パスワード作り直すんでええな?" sensitiveWords: "けったいな単語" sensitiveWordsDescription: "設定した単語が入っとるノートの公開範囲をホームにしたるわ。改行で区切ったら複数設定できるで。" sensitiveWordsDescription2: "スペースで区切るとAND指定、キーワードをスラッシュで囲んだら正規表現や。" +prohibitedWordsDescription2: "スペースで区切るとAND指定、キーワードをスラッシュで囲んだら正規表現や。" hiddenTags: "見えてへんハッシュタグ" hiddenTagsDescription: "設定したタグを最近流行りのとこに見えんようにすんで。複数設定するときは改行で区切ってな。" notesSearchNotAvailable: "なんかノート探せへん。" @@ -1178,6 +1179,7 @@ hideRepliesToOthersInTimelineAll: "タイムラインに今フォローしとる confirmShowRepliesAll: "これは元に戻せへんから慎重に決めてや。本当にタイムラインに今フォローしとる全員の返信を入れるか?" confirmHideRepliesAll: "これは元に戻せへんから慎重に決めてや。本当にタイムラインに今フォローしとる全員の返信を入れへんのか?" externalServices: "他のサイトのサービス" +sourceCode: "ソースコード" impressum: "運営者の情報" impressumUrl: "運営者の情報URL" impressumDescription: "ドイツとかの一部んところではな、表示が義務付けられてんねん(Impressum)。" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 6b86e0d1e7..d35efba49d 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1148,6 +1148,9 @@ resetPasswordConfirm: "비밀번호를 재설정할까요?" sensitiveWords: "민감한 단어" sensitiveWordsDescription: "설정한 단어가 포함된 노트의 공개 범위를 '공개'로 설정할 수 없게 합니다. 한 줄에 하나씩 작성할 수 있습니다." sensitiveWordsDescription2: "공백으로 구분하면 AND로 지정되고, 키워드를 '/'로 둘러싸면 정규 표현식이 됩니다." +prohibitedWords: "차단된 단어" +prohibitedWordsDescription: "설정한 단어가 포함된 노트를 작성하려고 시도하면 노트 작성에 실패하게 합니다. 한 줄에 하나씩 작성할 수 있습니다." +prohibitedWordsDescription2: "공백으로 구분하면 AND로 지정되고, 키워드를 '/'로 둘러싸면 정규 표현식이 됩니다." hiddenTags: "숨긴 해시태그" hiddenTagsDescription: "설정한 태그가 트렌드에 표시되지 않아요. 줄 바꿈으로 하나씩 나눠서 설정할 수 있어요." notesSearchNotAvailable: "노트 검색을 이용할 수 없습니다." @@ -1284,6 +1287,7 @@ hideRepliesToOthersInTimelineAll: "타임라인에 팔로우 중인 모든 사 confirmShowRepliesAll: "이 조작은 되돌릴 수 없어요! 정말로 타임라인에 현재 팔로우 중인 모든 사람의 답글을 표시하도록 설정할까요?" confirmHideRepliesAll: "이 조작은 되돌릴 수 없어요! 정말로 타임라인에 현재 팔로우 중인 모든 사람의 답글을 표시하지 않도록 설정할까요?" externalServices: "외부 서비스" +sourceCode: "소스 코드" impressum: "운영자 정보" impressumUrl: "운영자 정보 URL" impressumDescription: "독일 등의 일부 나라와 지역에서는 꼭 표시해야 해요(Impressum)." diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index fab11b5ba0..1cbcff2041 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -885,6 +885,7 @@ youFollowing: "Śledzeni" icon: "Awatar" replies: "Odpowiedz" renotes: "Udostępnij" +sourceCode: "Kod źródłowy" flip: "Odwróć" _role: priority: "Priorytet" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 86fd76a31e..360ee71a63 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -1029,6 +1029,7 @@ resetPasswordConfirm: "Сбросить пароль?" sensitiveWords: "Чувствительные слова" sensitiveWordsDescription: "Установите общедоступный диапазон заметки, содержащей заданное слово, на домашний. Можно сделать несколько настроек, разделив их переносами строк." sensitiveWordsDescription2: "Разделение пробелом создаёт спецификацию AND, а разделение косой чертой создаёт регулярное выражение." +prohibitedWordsDescription2: "Разделение пробелом создаёт спецификацию AND, а разделение косой чертой создаёт регулярное выражение." notesSearchNotAvailable: "Поиск заметок недоступен" license: "Лицензия" unfavoriteConfirm: "Удалить избранное?" @@ -1095,6 +1096,7 @@ icon: "Аватар" replies: "Ответы" renotes: "Репост" loadReplies: "Показать ответы" +sourceCode: "Исходный код" flip: "Переворот" lastNDays: "Последние {n} сут" _initialAccountSetting: diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index a2547d0d6c..94da1d50e8 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -933,6 +933,7 @@ youFollowing: "Sledované" icon: "Avatar" replies: "Odpovedať" renotes: "Preposlať" +sourceCode: "Zdrojový kód" flip: "Preklopiť" lastNDays: "Posledných {n} dní" _role: diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 2d4c7e0473..bae55a0cee 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -1055,6 +1055,9 @@ resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุ sensitiveWords: "คำที่มีเนื้อหาละเอียดอ่อน" sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ" sensitiveWordsDescription2: "การใช้ช่องว่างนั้นอาจจะสร้างนิพจน์ AND และคำหลักที่มีเครื่องหมายทับล้อมรอบจะเปลี่ยนเป็นนิพจน์ทั่วไปนะ" +prohibitedWords: "คำต้องห้าม" +prohibitedWordsDescription: "จะแจ้งเตือนว่าเกิดข้อผิดพลาดเมื่อพยายามโพสต์โน้ตที่มีคำที่กำหนดไว้ สามารถตั้งได้หลายคำด้วยการขึ้นบรรทัดใหม่" +prohibitedWordsDescription2: "การใช้ช่องว่างนั้นอาจจะสร้างนิพจน์ AND และคำหลักที่มีเครื่องหมายทับล้อมรอบจะเปลี่ยนเป็นนิพจน์ทั่วไปนะ" hiddenTags: "แฮชแท็กที่ซ่อนอยู่" hiddenTagsDescription: "เลือกแท็กที่จะไม่แสดงในรายการเทรนด์ สามารถลงทะเบียนหลายแท็กได้โดยขึ้นบรรทัดใหม่" notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งาน" @@ -1178,6 +1181,7 @@ hideRepliesToOthersInTimelineAll: "ซ่อนตอบกลับจากท confirmShowRepliesAll: "การดำเนินการนี้ไม่สามารถย้อนกลับได้ คุณต้องการแสดงการตอบกลับผู้อื่นจากผู้ใช้ทุกคนที่คุณติดตามอยู่ในไทม์ไลน์ของคุณหรือไม่?" confirmHideRepliesAll: "การดำเนินการนี้ไม่สามารถย้อนกลับได้ คุณต้องการซ่อนการตอบกลับผู้อื่นจากผู้ใช้ทุกคนที่คุณติดตามอยู่ในไทม์ไลน์ของคุณหรือไม่?" externalServices: "บริการภายนอก" +sourceCode: "ซอร์สโค้ด" impressum: "อิมเพรสชั่น" impressumUrl: "URL อิมเพรสชั่น" impressumDescription: "การติดป้ายกำกับ (Impressum) มีผลบังคับใช้ในบางประเทศและภูมิภาค เช่น ประเทศเยอรมนี" @@ -1216,6 +1220,9 @@ replaying: "กำลังรีเพลย์" ranking: "อันดับ" lastNDays: "ล่าสุด {n} วันที่แล้ว" backToTitle: "กลับไปหน้าไตเติ้ล" +hemisphere: "พื้นที่ที่อาศัยอยู่" +withSensitive: "แสดงโน้ตที่มีไฟล์ที่ระบุว่ามีเนื้อหาละเอียดอ่อน" +userSaysSomethingSensitive: "โพสต์ที่มีไฟล์เนื้อหาละเอียดอ่อนของ {name}" enableHorizontalSwipe: "ปัดเพื่อสลับแท็บ" _bubbleGame: howToPlay: "วิธีเล่น" @@ -2528,6 +2535,53 @@ _dataSaver: _code: title: "ไฮไลต์โค้ด" description: "หากใช้สัญลักษณ์ไฮไลต์โค้ดใน MFM ฯลฯ สัญลักษณ์เหล่านั้นจะไม่โหลดจนกว่าจะแตะ การไฮไลต์ไวยากรณ์(syntax)จำเป็นต้องดาวน์โหลดไฟล์คำจำกัดความของไฮไลต์สำหรับแต่ละภาษา ดังนั้นการปิดใช้งานการโหลดไฟล์เหล่านี้โดยอัตโนมัติจึงคาดว่าจะช่วยลดปริมาณข้อมูลการสื่อสารได้" +_hemisphere: + N: "ซีกโลกเหนือ" + S: "ซีกโลกใต้" + caption: "ใช้เพื่อกำหนดฤดูกาลของไคลเอ็นต์" _reversi: + reversi: "รีเวอร์ซี" + gameSettings: "ตั้งค่าการเล่น" + chooseBoard: "เลือกกระดาน" + blackOrWhite: "ดำ/ขาว" + blackIs: "{name}เป็นสีดำ" + rules: "กฎ" + thisGameIsStartedSoon: "การเล่นจะเริ่มแล้ว" + waitingForOther: "กำลังรออีกฝ่ายเตรียมตัวให้เสร็จ" + waitingForMe: "กำลังรอฝ่ายคุณเตรียมตัวให้เสร็จ" + waitingBoth: "กรุณาเตรียมตัว" + ready: "เตรียมตัวพร้อมแล้ว" + cancelReady: "ยกเลิกการเตรียมตัวพร้อม" + opponentTurn: "ตาอีกฝ่าย" + myTurn: "ตาของคุณ" + turnOf: "ตาของ{name}" + pastTurnOf: "ตาของ{name}" + surrender: "ยอมแพ้" + surrendered: "ยอมแพ้แล้ว" + timeout: "หมดเวลาแล้ว" + drawn: "เสมอ" + won: "{name}ชนะ" + black: "ดำ" + white: "ขาว" total: "รวมทั้งหมด" + turnCount: "ตาที่{count}" + myGames: "การเล่นของตัวเอง" + allGames: "การเล่นของทุกคน" + ended: "จบ" + playing: "กำลังเล่น" + isLlotheo: "คนที่มีตัวหมากน้อยกว่าชนะ (Roseo)" + loopedMap: "ลูปแมป" + canPutEverywhere: "โหมดที่สามารถวางได้ทุกที่" + timeLimitForEachTurn: "จำกัดเวลาต่อแต่ละตา" + freeMatch: "ฟรีแมตช์" + lookingForPlayer: "กำลังมองหาคู่ต่อสู้อยู่" + gameCanceled: "ยกเลิกการเล่นแล้ว" + shareToTlTheGameWhenStart: "โพสต์ลงไทม์ไลน์เมื่อเริ่มการเล่น" + iStartedAGame: "เริ่มเล่นหมากรีเวอร์ซีแล้ว! #MisskeyReversi" + opponentHasSettingsChanged: "อีกฝ่ายเปลี่ยนการตั้งค่า" + allowIrregularRules: "อนุญาตกฎที่ไม่ปรกติ (โหมดฟรีทุกอย่าง)" + disallowIrregularRules: "ไม่อนุญาตกฎที่ไม่ปรกติ" +_offlineScreen: + title: "ออฟไลน์ - ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้" + header: "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 2974a5e036..1623a4225d 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -925,6 +925,7 @@ youFollowing: "Підписки" icon: "Аватар" replies: "Відповісти" renotes: "Поширити" +sourceCode: "Вихідний код" flip: "Перевернути" lastNDays: "Останні {n} днів" _achievements: diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index cd8f913d0b..e50c3b7896 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1059,6 +1059,7 @@ 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" +sourceCode: "Mã nguồn" flip: "Lật" lastNDays: "{n} ngày trước" _announcement: diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 2db267ba1d..e77cec3907 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -11,7 +11,7 @@ password: "密码" forgotPassword: "忘记密码" fetchingAsApObject: "在联邦宇宙查询中..." ok: "OK" -gotIt: "我明白了" +gotIt: "好" cancel: "取消" noThankYou: "不用,谢谢" enterUsername: "输入用户名" @@ -1055,6 +1055,8 @@ resetPasswordConfirm: "确定重置密码?" sensitiveWords: "敏感词" sensitiveWordsDescription: "将包含设置词的帖子的可见范围设置为首页。可以通过用换行符分隔来设置多个。" sensitiveWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。" +prohibitedWords: "禁用词" +prohibitedWordsDescription2: "AND 条件用空格分隔,正则表达式用斜线包裹。" hiddenTags: "隐藏标签" hiddenTagsDescription: "设定的标签将不会在时间线上显示。可使用换行来设置多个标签。" notesSearchNotAvailable: "帖子检索不可用" @@ -1178,6 +1180,7 @@ hideRepliesToOthersInTimelineAll: "在时间线中隐藏现在关注的所有人 confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中包含现在关注的所有人的回复吗?" confirmHideRepliesAll: "此操作不可撤销。确认要在时间线中隐藏现在关注的所有人的回复吗?" externalServices: "外部服务" +sourceCode: "源代码" impressum: "运营商信息" impressumUrl: "运营商信息地址" impressumDescription: "德国等国家和地区有义务展示此类信息(Impressum)。" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 027989c997..a8ae2629fd 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -66,7 +66,7 @@ showMore: "載入更多" showLess: "關閉" youGotNewFollower: "您有新的追隨者" receiveFollowRequest: "您有新的追隨請求" -followRequestAccepted: "追隨請求已接受" +followRequestAccepted: "追隨請求已被接受" mention: "提及" mentions: "提及" directNotes: "私訊" @@ -616,7 +616,7 @@ inboxUrl: "收件夾URL" addedRelays: "已加入的中繼器" serviceworkerInfo: "如要使用推播通知,需要啟用此選項並設定金鑰。" deletedNote: "已刪除的貼文" -invisibleNote: "私密的貼文" +invisibleNote: "私人貼文" enableInfiniteScroll: "啟用自動滾動頁面模式" visibility: "可見性" poll: "票選活動" @@ -1055,6 +1055,9 @@ resetPasswordConfirm: "重設密碼?" sensitiveWords: "敏感詞" sensitiveWordsDescription: "將含有設定詞彙的貼文可見性設為發送至首頁。可以用換行來進行複數的設定。" sensitiveWordsDescription2: "空格代表「以及」(AND),斜線包圍關鍵字代表使用正規表達式。" +prohibitedWords: "禁語" +prohibitedWordsDescription: "當要發布包含禁語的貼文時,會出現錯誤。可以用換行分隔來設定多個禁語。" +prohibitedWordsDescription2: "空格代表「以及」(AND),斜線包圍關鍵字代表使用正規表達式。" hiddenTags: "隱藏標籤" hiddenTagsDescription: "設定的標籤不會在趨勢中顯示,換行可以設定多個標籤。" notesSearchNotAvailable: "無法使用搜尋貼文功能。" @@ -1178,6 +1181,7 @@ hideRepliesToOthersInTimelineAll: "在時間軸不包含追隨中所有人的回 confirmShowRepliesAll: "進行此操作後無法復原。您真的希望時間軸「包含」您目前追隨的所有人的回覆嗎?" confirmHideRepliesAll: "進行此操作後無法復原。您真的希望時間軸「不包含」您目前追隨的所有人的回覆嗎?" externalServices: "外部服務" +sourceCode: "原始碼" impressum: "營運者資訊" impressumUrl: "營運者資訊網址" impressumDescription: "在德國與部份地區必須要明確顯示營運者資訊。" @@ -1206,7 +1210,7 @@ overwriteContentConfirm: "確定要覆蓋目前的內容嗎?" seasonalScreenEffect: "隨季節變換畫面的呈現" decorate: "設置頭像裝飾" addMfmFunction: "插入MFM功能語法" -enableQuickAddMfmFunction: "顯示高級MFM選擇器" +enableQuickAddMfmFunction: "顯示高級 MFM 選擇器" bubbleGame: "氣泡遊戲" sfx: "音效" soundWillBePlayed: "將播放音效" @@ -2180,7 +2184,7 @@ _poll: deadlineTime: "小時" duration: "時長" votesCount: "{n} 票" - totalVotes: "一共{n}票" + totalVotes: "合計 {n} 票" vote: "投票" showResult: "顯示結果" voted: "已投票" diff --git a/package.json b/package.json index 7cb98feedd..54e12022b9 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "type": "git", "url": "https://github.com/LycheeBridge/LycheeBridge.git" }, - "packageManager": "pnpm@8.12.1", + "packageManager": "pnpm@8.15.1", "workspaces": [ "packages/frontend", "packages/backend", @@ -23,7 +23,7 @@ "build-assets": "node ./scripts/build-assets.mjs", "build": "pnpm build-pre && pnpm -r build && pnpm build-assets", "build-storybook": "pnpm --filter frontend build-storybook", - "build-cherrypick-js-with-types": "pnpm --filter backend build && pnpm --filter backend generate-api-json && ncp packages/backend/built/api.json packages/cherrypick-js/generator/api.json && pnpm --filter cherrypick-js update-autogen-code && pnpm --filter cherrypick-js build && pnpm --filter cherrypick-js api", + "build-cherrypick-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!cherrypick-js build && pnpm --filter backend generate-api-json && ncp packages/backend/built/api.json packages/cherrypick-js/generator/api.json && pnpm --filter cherrypick-js update-autogen-code && pnpm --filter cherrypick-js build && pnpm --filter cherrypick-js api", "start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js", "start:docker": "pnpm check:connect && cd packages/backend && exec node ./built/boot/entry.js", "start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js", @@ -53,10 +53,13 @@ "lodash": "4.17.21" }, "dependencies": { - "execa": "8.0.1", "cssnano": "6.0.3", + "execa": "8.0.1", + "fast-glob": "3.3.2", + "ignore-walk": "6.0.4", "js-yaml": "4.1.0", "postcss": "8.4.33", + "tar": "6.2.0", "terser": "5.27.0", "typescript": "5.3.3" }, @@ -66,8 +69,8 @@ "cross-env": "7.0.3", "cypress": "13.6.3", "eslint": "8.56.0", - "start-server-and-test": "2.0.3", - "ncp": "2.0.0" + "ncp": "2.0.0", + "start-server-and-test": "2.0.3" }, "optionalDependencies": { "@tensorflow/tfjs-core": "4.4.0" diff --git a/packages/backend/migration/1707429690000-prohibited-words.js b/packages/backend/migration/1707429690000-prohibited-words.js new file mode 100644 index 0000000000..2dd62d8ff8 --- /dev/null +++ b/packages/backend/migration/1707429690000-prohibited-words.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class prohibitedWords1707429690000 { + name = 'prohibitedWords1707429690000' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "prohibitedWords" character varying(1024) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "prohibitedWords"`); + } +} diff --git a/packages/backend/migration/1707808106310-MakeRepositoryUrlNullable.js b/packages/backend/migration/1707808106310-MakeRepositoryUrlNullable.js new file mode 100644 index 0000000000..335b14976c --- /dev/null +++ b/packages/backend/migration/1707808106310-MakeRepositoryUrlNullable.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class MakeRepositoryUrlNullable1707808106310 { + name = 'MakeRepositoryUrlNullable1707808106310' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" DROP NOT NULL`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET NOT NULL`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 5766814263..ce3a134106 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -88,7 +88,7 @@ "@nestjs/core": "10.2.10", "@nestjs/testing": "10.2.10", "@peertube/http-signature": "1.7.0", - "@simplewebauthn/server": "9.0.1", + "@simplewebauthn/server": "9.0.2", "@sinonjs/fake-timers": "11.2.2", "@smithy/node-http-handler": "2.1.10", "@swc/cli": "0.1.63", @@ -102,14 +102,14 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.2", - "bullmq": "5.1.5", + "bullmq": "5.1.9", "cacheable-lookup": "7.0.0", - "cbor": "9.0.1", + "cbor": "9.0.2", "chalk": "5.3.0", "chalk-template": "1.1.0", "cherrypick-js": "workspace:*", "cherrypick-mfm-js": "0.24.0-cherrypick.4", - "chokidar": "3.5.3", + "chokidar": "3.6.0", "cli-highlight": "2.1.11", "color-convert": "2.0.1", "content-disposition": "0.5.4", @@ -190,13 +190,12 @@ "@jest/globals": "29.7.0", "@misskey-dev/eslint-plugin": "1.0.0", "@nestjs/platform-express": "10.3.1", - "@simplewebauthn/typescript-types": "8.3.4", + "@simplewebauthn/types": "9.0.1", "@swc/jest": "0.2.31", "@types/accepts": "1.3.7", "@types/archiver": "6.0.2", "@types/bcryptjs": "2.4.6", "@types/body-parser": "1.19.5", - "@types/cbor": "6.0.0", "@types/color-convert": "2.0.3", "@types/content-disposition": "0.5.8", "@types/fluent-ffmpeg": "2.1.24", @@ -208,7 +207,7 @@ "@types/jsrsasign": "10.5.12", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "20.11.10", + "@types/node": "20.11.17", "@types/node-fetch": "3.0.3", "@types/nodemailer": "6.4.14", "@types/oauth": "0.9.4", @@ -223,7 +222,6 @@ "@types/rename": "1.0.7", "@types/sanitize-html": "2.9.5", "@types/semver": "7.5.6", - "@types/sharp": "0.32.0", "@types/simple-oauth2": "5.0.7", "@types/sinonjs__fake-timers": "8.1.5", "@types/tinycolor2": "1.4.6", diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 531b0515c9..ab33a3b7e6 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -57,6 +57,8 @@ type Source = { scope?: 'local' | 'global' | string[]; }; + publishTarballInsteadOfProvideRepositoryUrl?: boolean; + proxy?: string; proxySmtp?: string; proxyBypassHosts?: string[]; @@ -162,6 +164,7 @@ export type Config = { version: string; basedMisskeyVersion: string; + publishTarballInsteadOfProvideRepositoryUrl: boolean; host: string; hostname: string; scheme: string; @@ -228,6 +231,7 @@ export function loadConfig(): Config { return { version, basedMisskeyVersion, + publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl, url: url.origin, port: config.port ?? parseInt(process.env.PORT ?? '', 10), socket: config.socket, diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index 82fa1ddcc2..d8e322dbe3 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -17,10 +17,10 @@ import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class CacheService implements OnApplicationShutdown { - public userByIdCache: MemoryKVCache; - public localUserByNativeTokenCache: MemoryKVCache; + public userByIdCache: MemoryKVCache; + public localUserByNativeTokenCache: MemoryKVCache; public localUserByIdCache: MemoryKVCache; - public uriPersonCache: MemoryKVCache; + public uriPersonCache: MemoryKVCache; public userProfileCache: RedisKVCache; public flashAccessTokensCache: RedisKVCache; public userMutingsCache: RedisKVCache>; @@ -58,41 +58,10 @@ export class CacheService implements OnApplicationShutdown { ) { //this.onMessage = this.onMessage.bind(this); - const localUserByIdCache = new MemoryKVCache(1000 * 60 * 60 * 6 /* 6h */); - this.localUserByIdCache = localUserByIdCache; - - // ローカルユーザーなら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.userByIdCache = new MemoryKVCache(Infinity); + this.localUserByNativeTokenCache = new MemoryKVCache(Infinity); + this.localUserByIdCache = new MemoryKVCache(Infinity); + this.uriPersonCache = new MemoryKVCache(Infinity); this.userProfileCache = new RedisKVCache(this.redisClient, 'userProfile', { lifetime: 1000 * 60 * 30, // 30m @@ -169,16 +138,25 @@ export class CacheService implements OnApplicationShutdown { switch (type) { case 'userChangeSuspendedState': case 'remoteUserUpdated': { - 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 === user.id) { - this.uriPersonCache.set(k, user); + const user = await this.usersRepository.findOneBy({ id: body.id }); + if (user == null) { + this.userByIdCache.delete(body.id); + for (const [k, v] of this.uriPersonCache.cache.entries()) { + if (v.value?.id === body.id) { + this.uriPersonCache.delete(k); + } + } + } else { + this.userByIdCache.set(user.id, user); + for (const [k, v] of this.uriPersonCache.cache.entries()) { + if (v.value?.id === user.id) { + this.uriPersonCache.set(k, user); + } + } + if (this.userEntityService.isLocalUser(user)) { + this.localUserByNativeTokenCache.set(user.token!, user); + this.localUserByIdCache.set(user.id, user); } - } - if (this.userEntityService.isLocalUser(user)) { - this.localUserByNativeTokenCache.set(user.token!, user); - this.localUserByIdCache.set(user.id, user); } break; } diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts index c1c573c543..4d115ab97b 100644 --- a/packages/backend/src/core/HashtagService.ts +++ b/packages/backend/src/core/HashtagService.ts @@ -163,7 +163,7 @@ export class HashtagService { const instance = await this.metaService.fetch(); const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); if (hiddenTags.includes(hashtag)) return; - if (this.utilityService.isSensitiveWordIncluded(hashtag, instance.sensitiveWords)) return; + if (this.utilityService.isKeyWordIncluded(hashtag, instance.sensitiveWords)) return; // YYYYMMDDHHmm (10分間隔) const now = new Date(); diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 05d708aa5a..a2fa407ea7 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -14,9 +14,16 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { StatusError } from '@/misc/status-error.js'; import { bindThis } from '@/decorators.js'; +import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; +import type { IObject } from '@/core/activitypub/type.js'; import type { Response } from 'node-fetch'; import type { URL } from 'node:url'; +export type HttpRequestSendOptions = { + throwErrorWhenResponseNotOk: boolean; + validators?: ((res: Response) => void)[]; +}; + @Injectable() export class HttpRequestService { /** @@ -104,6 +111,23 @@ export class HttpRequestService { } } + @bindThis + public async getActivityJson(url: string): Promise { + const res = await this.send(url, { + method: 'GET', + headers: { + Accept: 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + timeout: 5000, + size: 1024 * 256, + }, { + throwErrorWhenResponseNotOk: true, + validators: [validateContentTypeSetAsActivityPub], + }); + + return await res.json() as IObject; + } + @bindThis public async getJson(url: string, accept = 'application/json, */*', headers?: Record): Promise { const res = await this.send(url, { @@ -132,17 +156,20 @@ export class HttpRequestService { } @bindThis - public async send(url: string, args: { - method?: string, - body?: string, - headers?: Record, - timeout?: number, - size?: number, - } = {}, extra: { - throwErrorWhenResponseNotOk: boolean; - } = { - throwErrorWhenResponseNotOk: true, - }): Promise { + public async send( + url: string, + args: { + method?: string, + body?: string, + headers?: Record, + timeout?: number, + size?: number, + } = {}, + extra: HttpRequestSendOptions = { + throwErrorWhenResponseNotOk: true, + validators: [], + }, + ): Promise { const timeout = args.timeout ?? 5000; const controller = new AbortController(); @@ -169,6 +196,12 @@ export class HttpRequestService { throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText); } + if (res.ok) { + for (const validator of (extra.validators ?? [])) { + validator(res); + } + } + return res; } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 5344ad999a..92de83a527 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -156,6 +156,8 @@ type Option = { export class NoteCreateService implements OnApplicationShutdown { #shutdownController = new AbortController(); + public static ContainsProhibitedWordsError = class extends Error {}; + constructor( @Inject(DI.config) private config: Config, @@ -260,13 +262,17 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.visibility === 'public' && data.channel == null) { const sensitiveWords = meta.sensitiveWords; - if (this.utilityService.isSensitiveWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) { + if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) { data.visibility = 'home'; } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { data.visibility = 'home'; } } + if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', meta.prohibitedWords)) { + throw new NoteCreateService.ContainsProhibitedWordsError(); + } + const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host); if (data.visibility === 'public' && inSilencedInstance && user.host !== null) { diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 7758586aba..27b13bc820 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -30,12 +30,12 @@ import { RoleService } from '@/core/RoleService.js'; import { FeaturedService } from '@/core/FeaturedService.js'; import { trackPromise } from '@/misc/promise-tracker.js'; -const FALLBACK = '❤'; +const FALLBACK = '\u2764'; const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16; const legacies: Record = { 'like': '👍', - 'love': '❤', // ここに記述する場合は異体字セレクタを入れない + 'love': '\u2764', // ハート、異体字セレクタを入れない 'laugh': '😆', 'hmm': '🤔', 'surprise': '😮', @@ -120,7 +120,7 @@ export class ReactionService { let reaction = _reaction ?? FALLBACK; if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) { - reaction = '❤️'; + reaction = '\u2764'; } else if (_reaction) { const custom = reaction.match(isCustomEmojiRegexp); if (custom) { diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 6f2b398bbb..b00b7c2c39 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -43,13 +43,13 @@ export class UtilityService { } @bindThis - public isSensitiveWordIncluded(text: string, sensitiveWords: string[]): boolean { - if (sensitiveWords.length === 0) return false; + public isKeyWordIncluded(text: string, keyWords: string[]): boolean { + if (keyWords.length === 0) return false; if (text === '') return false; const regexpregexp = /^\/(.+)\/(.*)$/; - const matched = sensitiveWords.some(filter => { + const matched = keyWords.some(filter => { // represents RegExp const regexp = filter.match(regexpregexp); // This should never happen due to input sanitisation. diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts index 1ad740f83a..9dd46a0d33 100644 --- a/packages/backend/src/core/WebAuthnService.ts +++ b/packages/backend/src/core/WebAuthnService.ts @@ -26,7 +26,7 @@ import type { PublicKeyCredentialDescriptorFuture, PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON, -} from '@simplewebauthn/typescript-types'; +} from '@simplewebauthn/types'; @Injectable() export class WebAuthnService { diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index b793929b00..50f7108e13 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -127,12 +127,12 @@ export class ApDbResolverService implements OnApplicationShutdown { return await this.cacheService.userByIdCache.fetchMaybe( parsed.id, - () => this.usersRepository.findOneBy({ id: parsed.id }).then(x => x ?? undefined), + () => this.usersRepository.findOneBy({ id: parsed.id, isDeleted: false }).then(x => x ?? undefined), ) as MiLocalUser | undefined ?? null; } else { return await this.cacheService.uriPersonCache.fetch( parsed.uri, - () => this.usersRepository.findOneBy({ uri: parsed.uri }), + () => this.usersRepository.findOneBy({ uri: parsed.uri, isDeleted: false }), ) as MiRemoteUser | null; } } @@ -157,8 +157,12 @@ export class ApDbResolverService implements OnApplicationShutdown { if (key == null) return null; + const user = await this.cacheService.findUserById(key.userId).catch(() => null) as MiRemoteUser | null; + if (user == null) return null; + if (user.isDeleted) return null; + return { - user: await this.cacheService.findUserById(key.userId) as MiRemoteUser, + user, key, }; } @@ -172,6 +176,7 @@ export class ApDbResolverService implements OnApplicationShutdown { key: MiUserPublickey | null; } | null> { const user = await this.apPersonService.resolvePerson(uri) as MiRemoteUser; + if (user.isDeleted) return null; const key = await this.publicKeyByUserIdCache.fetch( user.id, diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index e20f5edce8..630dd061c8 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -30,6 +30,7 @@ import type { UsersRepository, NotesRepository, FollowingsRepository, MessagingM import { bindThis } from '@/decorators.js'; import type { MiRemoteUser } from '@/models/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { CacheService } from '@/core/CacheService.js'; import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isRead, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { ApNoteService } from './models/ApNoteService.js'; import { ApLoggerService } from './ApLoggerService.js'; @@ -90,6 +91,7 @@ export class ApInboxService { private apQuestionService: ApQuestionService, private queueService: QueueService, private messagingService: MessagingService, + private cacheService: CacheService, private globalEventService: GlobalEventService, ) { this.logger = this.apLoggerService.logger; @@ -456,7 +458,7 @@ export class ApInboxService { if (err instanceof StatusError && !err.isRetryable) { return `skip ${err.statusCode}`; } else if (err.message === 'actor has been suspended') { - return `skip suspended actor`; + return 'skip suspended actor'; } else { throw err; } @@ -528,6 +530,8 @@ export class ApInboxService { isDeleted: true, }); + this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: actor.id }); + return `ok: queued ${job.name} ${job.id}`; } @@ -846,7 +850,7 @@ export class ApInboxService { if (err instanceof StatusError && err.isClientError) { return `skip ${err.statusCode}`; } else if (err.message === 'actor has been suspended') { - return `skip suspended actor`; + return 'skip suspended actor'; } else { throw err; } diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index de98b29752..5f7c43bfa9 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -14,6 +14,7 @@ import { HttpRequestService } from '@/core/HttpRequestService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import type Logger from '@/logger.js'; +import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; type Request = { url: string; @@ -70,7 +71,7 @@ export class ApRequestCreator { url: u.href, method: 'GET', headers: this.#objectAssignWithLcKey({ - 'Accept': 'application/activity+json, application/ld+json', + 'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'Date': new Date().toUTCString(), 'Host': new URL(args.url).host, }, args.additionalHeaders), @@ -195,6 +196,9 @@ export class ApRequestService { const res = await this.httpRequestService.send(url, { method: req.request.method, headers: req.request.headers, + }, { + throwErrorWhenResponseNotOk: true, + validators: [validateContentTypeSetAsActivityPub], }); return await res.json(); diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index d63669fa42..b9ae3d2a4e 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -105,7 +105,7 @@ export class Resolver { const object = (this.user ? await this.apRequestService.signedGet(value, this.user) as IObject - : await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject; + : await this.httpRequestService.getActivityJson(value)) as IObject; if ( Array.isArray(object['@context']) ? diff --git a/packages/backend/src/core/activitypub/LdSignatureService.ts b/packages/backend/src/core/activitypub/LdSignatureService.ts index 28083d0891..a06939a4b9 100644 --- a/packages/backend/src/core/activitypub/LdSignatureService.ts +++ b/packages/backend/src/core/activitypub/LdSignatureService.ts @@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import { CONTEXTS } from './misc/contexts.js'; +import { validateContentTypeSetAsJsonLD } from './misc/validator.js'; import type { JsonLdDocument } from 'jsonld'; import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js'; @@ -133,7 +134,10 @@ class LdSignature { }, timeout: this.loderTimeout, }, - { throwErrorWhenResponseNotOk: false }, + { + throwErrorWhenResponseNotOk: false, + validators: [validateContentTypeSetAsJsonLD], + }, ).then(res => { if (!res.ok) { throw new Error(`${res.status} ${res.statusText}`); diff --git a/packages/backend/src/core/activitypub/misc/validator.ts b/packages/backend/src/core/activitypub/misc/validator.ts new file mode 100644 index 0000000000..690beeffef --- /dev/null +++ b/packages/backend/src/core/activitypub/misc/validator.ts @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { Response } from 'node-fetch'; + +export function validateContentTypeSetAsActivityPub(response: Response): void { + const contentType = (response.headers.get('content-type') ?? '').toLowerCase(); + + if (contentType === '') { + throw new Error('Validate content type of AP response: No content-type header'); + } + if ( + contentType.startsWith('application/activity+json') || + (contentType.startsWith('application/ld+json;') && contentType.includes('https://www.w3.org/ns/activitystreams')) + ) { + return; + } + throw new Error('Validate content type of AP response: Content type is not application/activity+json or application/ld+json'); +} + +const plusJsonSuffixRegex = /^\s*(application|text)\/[a-zA-Z0-9\.\-\+]+\+json\s*(;|$)/; + +export function validateContentTypeSetAsJsonLD(response: Response): void { + const contentType = (response.headers.get('content-type') ?? '').toLowerCase(); + + if (contentType === '') { + throw new Error('Validate content type of JSON LD: No content-type header'); + } + if ( + contentType.startsWith('application/ld+json') || + contentType.startsWith('application/json') || + plusJsonSuffixRegex.test(contentType) + ) { + return; + } + throw new Error('Validate content type of JSON LD: Content type is not application/ld+json or application/json'); +} diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 746d3ad198..1f0f8c269a 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -192,28 +192,14 @@ export class RedisSingleCache { // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? -function nothingToDo(value: T): V { - return value as unknown as V; -} - -export class MemoryKVCache { - public cache: Map; +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, - }) { + constructor(lifetime: MemoryKVCache['lifetime']) { this.cache = new Map(); this.lifetime = lifetime; - this.toMapConverter = options.toMapConverter; - this.fromMapConverter = options.fromMapConverter; this.gcIntervalHandle = setInterval(() => { this.gc(); @@ -224,7 +210,7 @@ export class MemoryKVCache { public set(key: string, value: T): void { this.cache.set(key, { date: Date.now(), - value: this.toMapConverter(value), + value, }); } @@ -236,7 +222,7 @@ export class MemoryKVCache { this.cache.delete(key); return undefined; } - return this.fromMapConverter(cached.value); + return cached.value; } @bindThis @@ -247,10 +233,9 @@ export class MemoryKVCache { /** * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします - * fetcherの引数はcacheに保存されている値があれば渡されます */ @bindThis - public async fetch(key: string, fetcher: (value: V | undefined) => Promise, validator?: (cachedValue: T) => boolean): Promise { + public async fetch(key: string, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { const cachedValue = this.get(key); if (cachedValue !== undefined) { if (validator) { @@ -265,7 +250,7 @@ export class MemoryKVCache { } // Cache MISS - const value = await fetcher(this.cache.get(key)?.value); + const value = await fetcher(); this.set(key, value); return value; } @@ -273,10 +258,9 @@ export class MemoryKVCache { /** * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします - * fetcherの引数はcacheに保存されている値があれば渡されます */ @bindThis - public async fetchMaybe(key: string, fetcher: (value: V | undefined) => Promise, validator?: (cachedValue: T) => boolean): Promise { + public async fetchMaybe(key: string, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { const cachedValue = this.get(key); if (cachedValue !== undefined) { if (validator) { @@ -291,7 +275,7 @@ export class MemoryKVCache { } // Cache MISS - const value = await fetcher(this.cache.get(key)?.value); + const value = await fetcher(); if (value !== undefined) { this.set(key, value); } diff --git a/packages/backend/src/misc/fastify-hook-handlers.ts b/packages/backend/src/misc/fastify-hook-handlers.ts new file mode 100644 index 0000000000..49a48f6a6b --- /dev/null +++ b/packages/backend/src/misc/fastify-hook-handlers.ts @@ -0,0 +1,9 @@ +import type { onRequestHookHandler } from 'fastify'; + +export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => { + const index = request.url.indexOf('?'); + if (~index) { + reply.redirect(301, request.url.slice(0, index)); + } + done(); +}; diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index d940edec7c..d540d70f01 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -39,7 +39,17 @@ import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/jso import { packedFlashSchema } from '@/models/json-schema/flash.js'; import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; import { packedSigninSchema } from '@/models/json-schema/signin.js'; -import { packedRoleLiteSchema, packedRoleSchema, packedRolePoliciesSchema } from '@/models/json-schema/role.js'; +import { + packedRoleLiteSchema, + packedRoleSchema, + packedRolePoliciesSchema, + packedRoleCondFormulaLogicsSchema, + packedRoleCondFormulaValueNot, + packedRoleCondFormulaValueIsLocalOrRemoteSchema, + packedRoleCondFormulaValueCreatedSchema, + packedRoleCondFormulaFollowersOrFollowingOrNotesSchema, + packedRoleCondFormulaValueSchema, +} from '@/models/json-schema/role.js'; import { packedAdSchema } from '@/models/json-schema/ad.js'; import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js'; @@ -82,6 +92,12 @@ export const refs = { EmojiDetailed: packedEmojiDetailedSchema, Flash: packedFlashSchema, Signin: packedSigninSchema, + RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema, + RoleCondFormulaValueNot: packedRoleCondFormulaValueNot, + RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema, + RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema, + RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema, + RoleCondFormulaValue: packedRoleCondFormulaValueSchema, RoleLite: packedRoleLiteSchema, Role: packedRoleSchema, RolePolicies: packedRolePoliciesSchema, diff --git a/packages/backend/src/models/BubbleGameRecord.ts b/packages/backend/src/models/BubbleGameRecord.ts index 05bf6ac49a..f8430139cd 100644 --- a/packages/backend/src/models/BubbleGameRecord.ts +++ b/packages/backend/src/models/BubbleGameRecord.ts @@ -48,7 +48,7 @@ export class MiBubbleGameRecord { @Column('jsonb', { default: [], }) - public logs: any[]; + public logs: number[][]; @Column('boolean', { default: false, diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index c515995276..24bbc5e651 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -76,6 +76,11 @@ export class MiMeta { }) public sensitiveWords: string[]; + @Column('varchar', { + length: 1024, array: true, default: '{}', + }) + public prohibitedWords: string[]; + @Column('varchar', { length: 1024, array: true, default: '{}', }) @@ -388,9 +393,9 @@ export class MiMeta { @Column('varchar', { length: 1024, default: 'https://github.com/LycheeBridge/LycheeBridge', - nullable: false, + nullable: true, }) - public repositoryUrl: string; + public repositoryUrl: string | null; @Column('varchar', { length: 1024, diff --git a/packages/backend/src/models/Role.ts b/packages/backend/src/models/Role.ts index 07b70a6f94..6dca668e4d 100644 --- a/packages/backend/src/models/Role.ts +++ b/packages/backend/src/models/Role.ts @@ -69,7 +69,7 @@ type CondFormulaValueNotesMoreThanOrEq = { value: number; }; -export type RoleCondFormulaValue = +export type RoleCondFormulaValue = { id: string } & ( CondFormulaValueAnd | CondFormulaValueOr | CondFormulaValueNot | @@ -82,7 +82,8 @@ export type RoleCondFormulaValue = CondFormulaValueFollowingLessThanOrEq | CondFormulaValueFollowingMoreThanOrEq | CondFormulaValueNotesLessThanOrEq | - CondFormulaValueNotesMoreThanOrEq; + CondFormulaValueNotesMoreThanOrEq +); @Entity('role') export class MiRole { diff --git a/packages/backend/src/models/json-schema/reversi-game.ts b/packages/backend/src/models/json-schema/reversi-game.ts index e153ee5724..2ea63e7332 100644 --- a/packages/backend/src/models/json-schema/reversi-game.ts +++ b/packages/backend/src/models/json-schema/reversi-game.ts @@ -47,12 +47,12 @@ export const packedReversiGameLiteSchema = { user1: { type: 'object', optional: false, nullable: false, - ref: 'User', + ref: 'UserLite', }, user2: { type: 'object', optional: false, nullable: false, - ref: 'User', + ref: 'UserLite', }, winnerId: { type: 'string', @@ -62,7 +62,7 @@ export const packedReversiGameLiteSchema = { winner: { type: 'object', optional: false, nullable: true, - ref: 'User', + ref: 'UserLite', }, surrenderedUserId: { type: 'string', @@ -165,12 +165,12 @@ export const packedReversiGameDetailedSchema = { user1: { type: 'object', optional: false, nullable: false, - ref: 'User', + ref: 'UserLite', }, user2: { type: 'object', optional: false, nullable: false, - ref: 'User', + ref: 'UserLite', }, winnerId: { type: 'string', @@ -180,7 +180,7 @@ export const packedReversiGameDetailedSchema = { winner: { type: 'object', optional: false, nullable: true, - ref: 'User', + ref: 'UserLite', }, surrenderedUserId: { type: 'string', @@ -226,6 +226,9 @@ export const packedReversiGameDetailedSchema = { items: { type: 'array', optional: false, nullable: false, + items: { + type: 'number', + }, }, }, map: { diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index 55348d4f3d..ef6b279bee 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -1,3 +1,129 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedRoleCondFormulaLogicsSchema = { + type: 'object', + properties: { + id: { + type: 'string', optional: false, + }, + type: { + type: 'string', + nullable: false, optional: false, + enum: ['and', 'or'], + }, + values: { + type: 'array', + nullable: false, optional: false, + items: { + ref: 'RoleCondFormulaValue', + }, + }, + }, +} as const; + +export const packedRoleCondFormulaValueNot = { + type: 'object', + properties: { + id: { + type: 'string', optional: false, + }, + type: { + type: 'string', + nullable: false, optional: false, + enum: ['not'], + }, + value: { + type: 'object', + optional: false, + ref: 'RoleCondFormulaValue', + }, + }, +} as const; + +export const packedRoleCondFormulaValueIsLocalOrRemoteSchema = { + type: 'object', + properties: { + id: { + type: 'string', optional: false, + }, + type: { + type: 'string', + nullable: false, optional: false, + enum: ['isLocal', 'isRemote'], + }, + }, +} as const; + +export const packedRoleCondFormulaValueCreatedSchema = { + type: 'object', + properties: { + id: { + type: 'string', optional: false, + }, + type: { + type: 'string', + nullable: false, optional: false, + enum: [ + 'createdLessThan', + 'createdMoreThan', + ], + }, + sec: { + type: 'number', + nullable: false, optional: false, + }, + }, +} as const; + +export const packedRoleCondFormulaFollowersOrFollowingOrNotesSchema = { + type: 'object', + properties: { + id: { + type: 'string', optional: false, + }, + type: { + type: 'string', + nullable: false, optional: false, + enum: [ + 'followersLessThanOrEq', + 'followersMoreThanOrEq', + 'followingLessThanOrEq', + 'followingMoreThanOrEq', + 'notesLessThanOrEq', + 'notesMoreThanOrEq', + ], + }, + value: { + type: 'number', + nullable: false, optional: false, + }, + }, +} as const; + +export const packedRoleCondFormulaValueSchema = { + type: 'object', + oneOf: [ + { + ref: 'RoleCondFormulaLogics', + }, + { + ref: 'RoleCondFormulaValueNot', + }, + { + ref: 'RoleCondFormulaValueIsLocalOrRemote', + }, + { + ref: 'RoleCondFormulaValueCreated', + }, + { + ref: 'RoleCondFormulaFollowersOrFollowingOrNotes', + }, + ], +} as const; + export const packedRolePoliciesSchema = { type: 'object', optional: false, nullable: false, @@ -174,6 +300,7 @@ export const packedRoleSchema = { condFormula: { type: 'object', optional: false, nullable: false, + ref: 'RoleCondFormulaValue', }, isPublic: { type: 'boolean', diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 6cd3b7504c..f0c8992e5d 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -3,16 +3,38 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -const notificationRecieveConfig = { +export const notificationRecieveConfig = { type: 'object', - nullable: false, optional: true, - properties: { - type: { - type: 'string', - nullable: false, optional: false, - enum: ['all', 'following', 'follower', 'mutualFollow', 'list', 'never'], + oneOf: [ + { + type: 'object', + nullable: false, + properties: { + type: { + type: 'string', + nullable: false, + enum: ['all', 'following', 'follower', 'mutualFollow', 'never'], + }, + }, + required: ['type'], }, - }, + { + type: 'object', + nullable: false, + properties: { + type: { + type: 'string', + nullable: false, + enum: ['list'], + }, + userListId: { + type: 'string', + format: 'misskey:id', + }, + }, + required: ['type', 'userListId'], + }, + ], } as const; export const packedUserLiteSchema = { @@ -558,16 +580,21 @@ export const packedMeDetailedOnlySchema = { type: 'object', nullable: false, optional: false, properties: { - app: notificationRecieveConfig, - quote: notificationRecieveConfig, - reply: notificationRecieveConfig, - follow: notificationRecieveConfig, - renote: notificationRecieveConfig, - mention: notificationRecieveConfig, - reaction: notificationRecieveConfig, - pollEnded: notificationRecieveConfig, - receiveFollowRequest: notificationRecieveConfig, - groupInvited: notificationRecieveConfig, + note: { optional: true, ...notificationRecieveConfig }, + follow: { optional: true, ...notificationRecieveConfig }, + mention: { optional: true, ...notificationRecieveConfig }, + reply: { optional: true, ...notificationRecieveConfig }, + renote: { optional: true, ...notificationRecieveConfig }, + quote: { optional: true, ...notificationRecieveConfig }, + reaction: { optional: true, ...notificationRecieveConfig }, + pollEnded: { optional: true, ...notificationRecieveConfig }, + receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, + followRequestAccepted: { optional: true, ...notificationRecieveConfig }, + groupInvited: { optional: true, ...notificationRecieveConfig }, + roleAssigned: { optional: true, ...notificationRecieveConfig }, + achievementEarned: { optional: true, ...notificationRecieveConfig }, + app: { optional: true, ...notificationRecieveConfig }, + test: { optional: true, ...notificationRecieveConfig }, }, }, emailNotificationTypes: { diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 2544ec9df6..4c86c88ead 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -27,6 +27,7 @@ import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; import { correctFilename } from '@/misc/correct-filename.js'; +import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; const _filename = fileURLToPath(import.meta.url); @@ -67,20 +68,23 @@ export class FileServerService { done(); }); - fastify.get('/files/app-default.jpg', (request, reply) => { - const file = fs.createReadStream(`${_dirname}/assets/dummy.png`); - reply.header('Content-Type', 'image/jpeg'); - reply.header('Cache-Control', 'max-age=31536000, immutable'); - return reply.send(file); - }); + fastify.register((fastify, options, done) => { + fastify.addHook('onRequest', handleRequestRedirectToOmitSearch); + fastify.get('/files/app-default.jpg', (request, reply) => { + const file = fs.createReadStream(`${_dirname}/assets/dummy.png`); + reply.header('Content-Type', 'image/jpeg'); + reply.header('Cache-Control', 'max-age=31536000, immutable'); + return reply.send(file); + }); - fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => { - return await this.sendDriveFile(request, reply) - .catch(err => this.errorHandler(request, reply, err)); - }); - fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => { - return await this.sendDriveFile(request, reply) - .catch(err => this.errorHandler(request, reply, err)); + fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => { + return await this.sendDriveFile(request, reply) + .catch(err => this.errorHandler(request, reply, err)); + }); + fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => { + return await reply.redirect(301, `${this.config.url}/files/${request.params.key}`); + }); + done(); }); fastify.get<{ diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index fcdec234c2..4af48ea222 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -22,7 +22,7 @@ import { WebAuthnService } from '@/core/WebAuthnService.js'; import { UserAuthService } from '@/core/UserAuthService.js'; import { RateLimiterService } from './RateLimiterService.js'; import { SigninService } from './SigninService.js'; -import type { AuthenticationResponseJSON } from '@simplewebauthn/typescript-types'; +import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; import type { FastifyReply, FastifyRequest } from 'fastify'; @Injectable() 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 b6be624545..1b5e01cdce 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-account.ts @@ -15,9 +15,6 @@ export const meta = { requireCredential: true, requireAdmin: true, kind: 'write:admin:delete-account', - - res: { - }, } as const; export const paramDef = { 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 0d35784c04..30a00a6724 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 @@ -84,6 +84,24 @@ export const meta = { properties: { type: 'object', optional: false, nullable: false, + properties: { + width: { + type: 'number', + optional: true, nullable: false, + }, + height: { + type: 'number', + optional: true, nullable: false, + }, + orientation: { + type: 'number', + optional: true, nullable: false, + }, + avgColor: { + type: 'string', + optional: true, nullable: false, + }, + }, }, storedInternal: { type: 'boolean', 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 4db012b35b..2ebc8dc1ca 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 @@ -18,6 +18,18 @@ export const meta = { res: { type: 'object', optional: false, nullable: false, + additionalProperties: { + type: 'object', + properties: { + count: { + type: 'number', + }, + size: { + type: 'number', + }, + }, + required: ['count', 'size'], + }, example: { migrations: { count: 66, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 061fa0c9ea..2771bb8046 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -160,6 +160,13 @@ export const meta = { type: 'string', }, }, + prohibitedWords: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + }, + }, bannedEmailDomains: { type: 'array', optional: true, nullable: false, @@ -478,7 +485,7 @@ export const meta = { }, repositoryUrl: { type: 'string', - optional: false, nullable: false, + optional: false, nullable: true, }, summalyProxy: { type: 'string', @@ -612,6 +619,7 @@ export default class extends Endpoint { // eslint- blockedHosts: instance.blockedHosts, silencedHosts: instance.silencedHosts, sensitiveWords: instance.sensitiveWords, + prohibitedWords: instance.prohibitedWords, preservedUsernames: instance.preservedUsernames, hcaptchaSecretKey: instance.hcaptchaSecretKey, mcaptchaSecretKey: instance.mcaptchaSecretKey, 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 94066210e2..4ab8d10b25 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts @@ -17,7 +17,7 @@ export const meta = { tags: ['admin', 'role', 'users'], requireCredential: false, - requireAdmin: true, + requireModerator: true, kind: 'read:admin:roles', errors: { 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 39a91b7a69..6dc6831e1a 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; import { IdService } from '@/core/IdService.js'; +import { notificationRecieveConfig } from '@/models/json-schema/user.js'; export const meta = { tags: ['admin'], @@ -21,6 +22,157 @@ export const meta = { res: { type: 'object', nullable: false, optional: false, + properties: { + email: { + type: 'string', + optional: false, nullable: true, + }, + emailVerified: { + type: 'boolean', + optional: false, nullable: false, + }, + autoAcceptFollowed: { + type: 'boolean', + optional: false, nullable: false, + }, + noCrawle: { + type: 'boolean', + optional: false, nullable: false, + }, + preventAiLearning: { + type: 'boolean', + optional: false, nullable: false, + }, + alwaysMarkNsfw: { + type: 'boolean', + optional: false, nullable: false, + }, + autoSensitive: { + type: 'boolean', + optional: false, nullable: false, + }, + carefulBot: { + type: 'boolean', + optional: false, nullable: false, + }, + injectFeaturedNote: { + type: 'boolean', + optional: false, nullable: false, + }, + receiveAnnouncementEmail: { + type: 'boolean', + optional: false, nullable: false, + }, + mutedWords: { + type: 'array', + optional: false, nullable: false, + items: { + anyOf: [ + { + type: 'string', + }, + { + type: 'array', + items: { + type: 'string', + }, + }, + ], + }, + }, + mutedInstances: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + }, + }, + notificationRecieveConfig: { + type: 'object', + optional: false, nullable: false, + properties: { + note: { optional: true, ...notificationRecieveConfig }, + follow: { optional: true, ...notificationRecieveConfig }, + mention: { optional: true, ...notificationRecieveConfig }, + reply: { optional: true, ...notificationRecieveConfig }, + renote: { optional: true, ...notificationRecieveConfig }, + quote: { optional: true, ...notificationRecieveConfig }, + reaction: { optional: true, ...notificationRecieveConfig }, + pollEnded: { optional: true, ...notificationRecieveConfig }, + receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, + followRequestAccepted: { optional: true, ...notificationRecieveConfig }, + roleAssigned: { optional: true, ...notificationRecieveConfig }, + achievementEarned: { optional: true, ...notificationRecieveConfig }, + app: { optional: true, ...notificationRecieveConfig }, + test: { optional: true, ...notificationRecieveConfig }, + }, + }, + isModerator: { + type: 'boolean', + optional: false, nullable: false, + }, + isSilenced: { + type: 'boolean', + optional: false, nullable: false, + }, + isSuspended: { + type: 'boolean', + optional: false, nullable: false, + }, + isHibernated: { + type: 'boolean', + optional: false, nullable: false, + }, + lastActiveDate: { + type: 'string', + optional: false, nullable: true, + }, + moderationNote: { + type: 'string', + optional: false, nullable: false, + }, + signins: { + type: 'array', + optional: false, nullable: false, + items: { + ref: 'Signin', + }, + }, + policies: { + type: 'object', + optional: false, nullable: false, + ref: 'RolePolicies', + }, + roles: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + ref: 'Role', + }, + }, + roleAssigns: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + properties: { + createdAt: { + type: 'string', + optional: false, nullable: false, + }, + expiresAt: { + type: 'string', + optional: false, nullable: true, + }, + roleId: { + type: 'string', + optional: false, nullable: false, + }, + }, + }, + }, + }, }, } as const; @@ -89,7 +241,7 @@ export default class extends Endpoint { // eslint- isSilenced: isSilenced, isSuspended: user.isSuspended, isHibernated: user.isHibernated, - lastActiveDate: user.lastActiveDate, + lastActiveDate: user.lastActiveDate ? user.lastActiveDate.toISOString() : null, moderationNote: profile.moderationNote ?? '', signins, policies: await this.roleService.getUserPolicies(user.id), 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 862ef0c6b6..e93dd67e9d 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -43,6 +43,11 @@ export const paramDef = { type: 'string', }, }, + prohibitedWords: { + type: 'array', nullable: true, items: { + type: 'string', + }, + }, themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' }, mascotImageUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true }, @@ -107,8 +112,8 @@ export const paramDef = { swPublicKey: { type: 'string', nullable: true }, swPrivateKey: { type: 'string', nullable: true }, tosUrl: { type: 'string', nullable: true }, - repositoryUrl: { type: 'string' }, - feedbackUrl: { type: 'string' }, + repositoryUrl: { type: 'string', nullable: true }, + feedbackUrl: { type: 'string', nullable: true }, impressumUrl: { type: 'string', nullable: true }, privacyPolicyUrl: { type: 'string', nullable: true }, useObjectStorage: { type: 'boolean' }, @@ -207,6 +212,9 @@ export default class extends Endpoint { // eslint- if (Array.isArray(ps.sensitiveWords)) { set.sensitiveWords = ps.sensitiveWords.filter(Boolean); } + if (Array.isArray(ps.prohibitedWords)) { + set.prohibitedWords = ps.prohibitedWords.filter(Boolean); + } if (Array.isArray(ps.silencedHosts)) { let lastValue = ''; set.silencedHosts = ps.silencedHosts.sort().filter((h) => { @@ -424,7 +432,7 @@ export default class extends Endpoint { // eslint- } if (ps.repositoryUrl !== undefined) { - set.repositoryUrl = ps.repositoryUrl; + set.repositoryUrl = URL.canParse(ps.repositoryUrl!) ? ps.repositoryUrl : null; } if (ps.feedbackUrl !== undefined) { diff --git a/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts b/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts index c02a3909fb..798896de7c 100644 --- a/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts +++ b/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts @@ -24,9 +24,19 @@ export const meta = { type: 'object', optional: false, nullable: false, properties: { - id: { type: 'string', format: 'misskey:id' }, - score: { type: 'integer' }, - user: { ref: 'UserLite' }, + id: { + type: 'string', format: 'misskey:id', + optional: false, nullable: false, + }, + score: { + type: 'integer', + optional: false, nullable: false, + }, + user: { + type: 'object', + optional: true, nullable: false, + ref: 'UserLite', + }, }, }, }, diff --git a/packages/backend/src/server/api/endpoints/bubble-game/register.ts b/packages/backend/src/server/api/endpoints/bubble-game/register.ts index a7cd9059b6..7be0f785f7 100644 --- a/packages/backend/src/server/api/endpoints/bubble-game/register.ts +++ b/packages/backend/src/server/api/endpoints/bubble-game/register.ts @@ -29,9 +29,6 @@ export const meta = { id: 'eb627bc7-574b-4a52-a860-3c3eae772b88', }, }, - - res: { - }, } as const; export const paramDef = { @@ -39,7 +36,15 @@ export const paramDef = { properties: { score: { type: 'integer', minimum: 0 }, seed: { type: 'string', minLength: 1, maxLength: 1024 }, - logs: { type: 'array' }, + logs: { + type: 'array', + items: { + type: 'array', + items: { + type: 'number', + }, + }, + }, gameMode: { type: 'string' }, gameVersion: { type: 'integer' }, }, 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 5f59125309..f0d58f044c 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts @@ -15,6 +15,19 @@ export const meta = { requireCredential: true, secure: true, + + res: { + type: 'object', + properties: { + backupCodes: { + type: 'array', + optional: false, + items: { + type: 'string', + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts index 4d6c328557..74f770f8b4 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 @@ -47,7 +47,7 @@ export const meta = { properties: { id: { type: 'string', - nullable: true, + optional: true, }, }, }, diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts index aca50639f6..10586bfd11 100644 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ b/packages/backend/src/server/api/endpoints/i/apps.ts @@ -21,21 +21,26 @@ export const meta = { properties: { id: { type: 'string', + optional: false, format: 'misskey:id', }, name: { type: 'string', + optional: true, }, createdAt: { type: 'string', + optional: false, format: 'date-time', }, lastUsedAt: { type: 'string', + optional: true, format: 'date-time', }, permission: { type: 'array', + optional: false, uniqueItems: true, items: { type: 'string', 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 5e6d620bc7..6ab6c379ce 100644 --- a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts +++ b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts @@ -23,16 +23,19 @@ export const meta = { id: { type: 'string', format: 'misskey:id', + optional: false, }, name: { type: 'string', + optional: false, }, callbackUrl: { type: 'string', - nullable: true, + optional: false, nullable: true, }, permission: { type: 'array', + optional: false, uniqueItems: true, items: { type: 'string', @@ -40,6 +43,7 @@ export const meta = { }, isAuthorized: { type: 'boolean', + optional: true, }, }, }, 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 7f311d0f12..dcb2067da7 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 @@ -22,6 +22,15 @@ export const meta = { res: { type: 'object', + properties: { + updatedAt: { + type: 'string', + optional: false, + }, + value: { + optional: false, + }, + }, }, } as const; @@ -50,7 +59,7 @@ export default class extends Endpoint { // eslint- } return { - updatedAt: item.updatedAt, + updatedAt: item.updatedAt.toISOString(), value: item.value, }; }); 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 5732f391f9..870a3735d1 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 @@ -13,6 +13,9 @@ export const meta = { res: { type: 'object', + additionalProperties: { + type: 'string', + }, }, } as const; 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 cd0d9bde7e..0a5ce6d04b 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys.ts @@ -10,6 +10,13 @@ import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, kind: 'read:account', + + res: { + type: 'array', + items: { + type: 'string', + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index c742a459a5..77cb9fbe4d 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -33,6 +33,7 @@ import { HttpRequestService } from '@/core/HttpRequestService.js'; import type { Config } from '@/config.js'; import { safeForSql } from '@/misc/safe-for-sql.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; +import { notificationRecieveConfig } from '@/models/json-schema/user.js'; import { ApiLoggerService } from '../../ApiLoggerService.js'; import { ApiError } from '../../error.js'; @@ -186,7 +187,26 @@ export const paramDef = { mutedInstances: { type: 'array', items: { type: 'string', } }, - notificationRecieveConfig: { type: 'object' }, + notificationRecieveConfig: { + type: 'object', + nullable: false, + properties: { + note: notificationRecieveConfig, + follow: notificationRecieveConfig, + mention: notificationRecieveConfig, + reply: notificationRecieveConfig, + renote: notificationRecieveConfig, + quote: notificationRecieveConfig, + reaction: notificationRecieveConfig, + pollEnded: notificationRecieveConfig, + receiveFollowRequest: notificationRecieveConfig, + followRequestAccepted: notificationRecieveConfig, + roleAssigned: notificationRecieveConfig, + achievementEarned: notificationRecieveConfig, + app: notificationRecieveConfig, + test: notificationRecieveConfig, + }, + }, emailNotificationTypes: { type: 'array', items: { type: 'string', } }, 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 47e2a0a1d6..84eecb3aa9 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -108,7 +108,7 @@ export default class extends Endpoint { // eslint- url: webhook.url, secret: webhook.secret, active: webhook.active, - latestSentAt: webhook.latestSentAt?.toISOString(), + latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null, latestStatus: webhook.latestStatus, }; }); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts index 0772145447..01b43a1838 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts @@ -73,7 +73,7 @@ export default class extends Endpoint { // eslint- url: webhook.url, secret: webhook.secret, active: webhook.active, - latestSentAt: webhook.latestSentAt?.toISOString(), + latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null, latestStatus: webhook.latestStatus, } )); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts index a81212faa6..42455b528b 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts @@ -85,7 +85,7 @@ export default class extends Endpoint { // eslint- url: webhook.url, secret: webhook.secret, active: webhook.active, - latestSentAt: webhook.latestSentAt?.toISOString(), + latestSentAt: webhook.latestSentAt ? webhook.latestSentAt.toISOString() : null, latestStatus: webhook.latestStatus, }; }); diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index a2ffc2dd33..d9530966a9 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { IsNull, LessThanOrEqual, MoreThan, Brackets } from 'typeorm'; +import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import JSON5 from 'json5'; import type { AdsRepository } from '@/models/_.js'; @@ -41,6 +41,10 @@ export const meta = { type: 'string', optional: false, nullable: false, }, + providesTarball: { + type: 'boolean', + optional: false, nullable: false, + }, name: { type: 'string', optional: false, nullable: false, @@ -73,12 +77,12 @@ export const meta = { }, repositoryUrl: { type: 'string', - optional: false, nullable: false, + optional: false, nullable: true, default: 'https://github.com/LycheeBridge/LycheeBridge', }, feedbackUrl: { type: 'string', - optional: false, nullable: false, + optional: false, nullable: true, default: 'https://github.com/LycheeBridge/LycheeBridge/issues/new', }, defaultDarkTheme: { @@ -373,6 +377,7 @@ export default class extends Endpoint { // eslint- version: this.config.version, basedMisskeyVersion: this.config.basedMisskeyVersion, + providesTarball: this.config.publishTarballInsteadOfProvideRepositoryUrl, name: instance.name, shortName: instance.shortName, diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 16a10406e2..93a6fd87fa 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -112,6 +112,12 @@ export const meta = { code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL', id: '33510210-8452-094c-6227-4a6c05d99f00', }, + + containsProhibitedWords: { + message: 'Cannot post because it contains prohibited words.', + code: 'CONTAINS_PROHIBITED_WORDS', + id: 'aa6e01d3-a85c-669d-758a-76aab43af334', + }, }, } as const; @@ -362,38 +368,47 @@ export default class extends Endpoint { // eslint- } // 投稿を作成 - const note = await this.noteCreateService.create(me, { - createdAt: new Date(), - files: files, - poll: ps.poll ? { - choices: ps.poll.choices, - multiple: ps.poll.multiple ?? false, - expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, - } : undefined, - text: ps.text ?? undefined, - reply, - renote, - event: ps.event ? { - start: new Date(ps.event.start!), - end: ps.event.end ? new Date(ps.event.end) : null, - title: ps.event.title!, - metadata: ps.event.metadata ?? {}, - } : undefined, - cw: ps.cw, - localOnly: instance.disableFederation ? true : ps.localOnly, - reactionAcceptance: ps.reactionAcceptance, - disableRightClick: ps.disableRightClick, - visibility: ps.visibility, - visibleUsers, - channel, - apMentions: ps.noExtractMentions ? [] : undefined, - apHashtags: ps.noExtractHashtags ? [] : undefined, - apEmojis: ps.noExtractEmojis ? [] : undefined, - }); + try { + const note = await this.noteCreateService.create(me, { + createdAt: new Date(), + files: files, + poll: ps.poll ? { + choices: ps.poll.choices, + multiple: ps.poll.multiple ?? false, + expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, + } : undefined, + text: ps.text ?? undefined, + reply, + renote, + event: ps.event ? { + start: new Date(ps.event.start!), + end: ps.event.end ? new Date(ps.event.end) : null, + title: ps.event.title!, + metadata: ps.event.metadata ?? {}, + } : undefined, + cw: ps.cw, + localOnly: instance.disableFederation ? true : ps.localOnly, + reactionAcceptance: ps.reactionAcceptance, + disableRightClick: ps.disableRightClick, + visibility: ps.visibility, + visibleUsers, + channel, + apMentions: ps.noExtractMentions ? [] : undefined, + apHashtags: ps.noExtractHashtags ? [] : undefined, + apEmojis: ps.noExtractEmojis ? [] : undefined, + }); - return { - createdNote: await this.noteEntityService.pack(note, me), - }; + return { + createdNote: await this.noteEntityService.pack(note, me), + }; + } catch (e) { + // TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい + if (e instanceof NoteCreateService.ContainsProhibitedWordsError) { + throw new ApiError(meta.errors.containsProhibitedWords); + } + + throw e; + } }); } } diff --git a/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts b/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts index 0851561629..b31ab339c8 100644 --- a/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts +++ b/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts @@ -14,9 +14,6 @@ export const meta = { errors: { }, - - res: { - }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reversi/match.ts b/packages/backend/src/server/api/endpoints/reversi/match.ts index 04543d6288..c287357daa 100644 --- a/packages/backend/src/server/api/endpoints/reversi/match.ts +++ b/packages/backend/src/server/api/endpoints/reversi/match.ts @@ -30,6 +30,9 @@ export const meta = { }, res: { + type: 'object', + optional: true, + ref: 'ReversiGameDetailed', }, } as const; diff --git a/packages/backend/src/server/api/endpoints/test.ts b/packages/backend/src/server/api/endpoints/test.ts index 1ec8d00481..fd078d040d 100644 --- a/packages/backend/src/server/api/endpoints/test.ts +++ b/packages/backend/src/server/api/endpoints/test.ts @@ -19,20 +19,24 @@ export const meta = { id: { type: 'string', format: 'misskey:id', + optional: true, nullable: false, }, required: { type: 'boolean', + optional: false, nullable: false, }, string: { type: 'string', + optional: true, nullable: false, }, default: { type: 'string', + optional: true, nullable: false, }, nullableDefault: { type: 'string', default: 'hello', - nullable: true, + optional: true, nullable: true, }, }, }, diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 238a00bd76..cde8b8d35c 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -34,6 +34,7 @@ import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { deepClone } from '@/misc/clone.js'; +import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; import { bindThis } from '@/decorators.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { RoleService } from '@/core/RoleService.js'; @@ -51,6 +52,7 @@ const clientAssets = `${_dirname}/../../../../frontend/assets/`; const assets = `${_dirname}/../../../../../built/_frontend_dist_/`; const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`; const viteOut = `${_dirname}/../../../../../built/_vite_/`; +const tarball = `${_dirname}/../../../../../built/tarball/`; @Injectable() export class ClientServerService { @@ -253,11 +255,16 @@ export class ClientServerService { //#region vite assets if (this.config.clientManifestExists) { - fastify.register(fastifyStatic, { - root: viteOut, - prefix: '/vite/', - maxAge: ms('30 days'), - decorateReply: false, + fastify.register((fastify, options, done) => { + fastify.register(fastifyStatic, { + root: viteOut, + prefix: '/vite/', + maxAge: ms('30 days'), + immutable: true, + decorateReply: false, + }); + fastify.addHook('onRequest', handleRequestRedirectToOmitSearch); + done(); }); } else { const port = (process.env.VITE_PORT ?? '5173'); @@ -292,6 +299,18 @@ export class ClientServerService { decorateReply: false, }); + fastify.register((fastify, options, done) => { + fastify.register(fastifyStatic, { + root: tarball, + prefix: '/tarball/', + maxAge: ms('30 days'), + immutable: true, + decorateReply: false, + }); + fastify.addHook('onRequest', handleRequestRedirectToOmitSearch); + done(); + }); + fastify.get('/favicon.ico', async (request, reply) => { return reply.sendFile('/favicon.ico', staticAssets); }); diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts index e7e89002d0..82a5ca2f18 100644 --- a/packages/backend/test/e2e/2fa.ts +++ b/packages/backend/test/e2e/2fa.ts @@ -18,7 +18,7 @@ import type { PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON, -} from '@simplewebauthn/typescript-types'; +} from '@simplewebauthn/types'; import type * as misskey from 'cherrypick-js'; describe('2要素認証', () => { diff --git a/packages/backend/test/e2e/fetch-validate-ap-deny.ts b/packages/backend/test/e2e/fetch-validate-ap-deny.ts new file mode 100644 index 0000000000..434a9fe209 --- /dev/null +++ b/packages/backend/test/e2e/fetch-validate-ap-deny.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; + +import { validateContentTypeSetAsActivityPub, validateContentTypeSetAsJsonLD } from '@/core/activitypub/misc/validator.js'; +import { signup, uploadFile, relativeFetch } from '../utils.js'; +import type * as misskey from 'misskey-js'; + +describe('validateContentTypeSetAsActivityPub/JsonLD (deny case)', () => { + let alice: misskey.entities.SignupResponse; + let aliceUploadedFile: any; + + beforeAll(async () => { + alice = await signup({ username: 'alice' }); + aliceUploadedFile = await uploadFile(alice); + }, 1000 * 60 * 2); + + test('ActivityStreams: ファイルはエラーになる', async () => { + const res = await relativeFetch(aliceUploadedFile.webpublicUrl); + + function doValidate() { + validateContentTypeSetAsActivityPub(res); + } + + expect(doValidate).toThrow('Content type is not'); + }); + + test('JSON-LD: ファイルはエラーになる', async () => { + const res = await relativeFetch(aliceUploadedFile.webpublicUrl); + + function doValidate() { + validateContentTypeSetAsJsonLD(res); + } + + expect(doValidate).toThrow('Content type is not'); + }); +}); diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index 5b53028053..92193ada6c 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -16,12 +16,14 @@ describe('Note', () => { let alice: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse; + let tom: misskey.entities.SignupResponse; beforeAll(async () => { const connection = await initTestDb(true); Notes = connection.getRepository(MiNote); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); + tom = await signup({ username: 'tom', host: 'example.com' }); }, 1000 * 60 * 2); test('投稿できる', async () => { @@ -607,6 +609,77 @@ describe('Note', () => { assert.strictEqual(note2.status, 200); assert.strictEqual(note2.body.createdNote.visibility, 'home'); }); + + test('禁止ワードを含む投稿はエラーになる (単語指定)', async () => { + const prohibited = await api('admin/update-meta', { + prohibitedWords: [ + 'test', + ], + }, alice); + + assert.strictEqual(prohibited.status, 204); + + await new Promise(x => setTimeout(x, 2)); + + const note1 = await api('/notes/create', { + text: 'hogetesthuge', + }, alice); + + assert.strictEqual(note1.status, 400); + assert.strictEqual(note1.body.error.code, 'CONTAINS_PROHIBITED_WORDS'); + }); + + test('禁止ワードを含む投稿はエラーになる (正規表現)', async () => { + const prohibited = await api('admin/update-meta', { + prohibitedWords: [ + '/Test/i', + ], + }, alice); + + assert.strictEqual(prohibited.status, 204); + + const note2 = await api('/notes/create', { + text: 'hogetesthuge', + }, alice); + + assert.strictEqual(note2.status, 400); + assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS'); + }); + + test('禁止ワードを含む投稿はエラーになる (スペースアンド)', async () => { + const prohibited = await api('admin/update-meta', { + prohibitedWords: [ + 'Test hoge', + ], + }, alice); + + assert.strictEqual(prohibited.status, 204); + + const note2 = await api('/notes/create', { + text: 'hogeTesthuge', + }, alice); + + assert.strictEqual(note2.status, 400); + assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS'); + }); + + test('禁止ワードを含んでるリモートノートもエラーになる', async () => { + const prohibited = await api('admin/update-meta', { + prohibitedWords: [ + 'test', + ], + }, alice); + + assert.strictEqual(prohibited.status, 204); + + await new Promise(x => setTimeout(x, 2)); + + const note1 = await api('/notes/create', { + text: 'hogetesthuge', + }, tom); + + assert.strictEqual(note1.status, 400); + }); }); describe('notes/delete', () => { diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index fb403755f2..b5a2ad08c2 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -100,6 +100,7 @@ describe('ActivityPub', () => { perRemoteUserUserTimelineCacheMax: 100, blockedHosts: [] as string[], sensitiveWords: [] as string[], + prohibitedWords: [] as string[], } as MiMeta; let meta = metaInitial; @@ -202,7 +203,7 @@ describe('ActivityPub', () => { describe('Renderer', () => { test('Render an announce with visibility: followers', () => { - rendererService.renderAnnounce(null, { + rendererService.renderAnnounce('https://example.com/notes/00example', { id: genAidx(Date.now()), visibility: 'followers', } as MiNote); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 981b76ece5..582f79e389 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -14,6 +14,7 @@ import { DataSource } from 'typeorm'; import { JSDOM } from 'jsdom'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { Packed } from '@/misc/json-schema.js'; +import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; import { entities } from '@/postgres.js'; import { loadConfig } from '@/config.js'; import type * as misskey from 'cherrypick-js'; @@ -327,7 +328,6 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO }); const body = res.status !== 204 ? await res.json() as misskey.Endpoints['drive/files/create']['res'] : null; - return { status: res.status, headers: res.headers, @@ -476,6 +476,14 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde 'text/html; charset=utf-8', ]; + if (res.ok && ( + accept.startsWith('application/activity+json') || + (accept.startsWith('application/ld+json') && accept.includes('https://www.w3.org/ns/activitystreams')) + )) { + // validateContentTypeSetAsActivityPubのテストを兼ねる + validateContentTypeSetAsActivityPub(res); + } + const body = jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() : htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) : diff --git a/packages/cherrypick-js/etc/cherrypick-js.api.md b/packages/cherrypick-js/etc/cherrypick-js.api.md index 5e60a6b6f3..fc4ba946a9 100644 --- a/packages/cherrypick-js/etc/cherrypick-js.api.md +++ b/packages/cherrypick-js/etc/cherrypick-js.api.md @@ -121,9 +121,6 @@ type AdminAvatarDecorationsUpdateRequest = operations['admin/avatar-decorations/ // @public (undocumented) type AdminDeleteAccountRequest = operations['admin/delete-account']['requestBody']['content']['application/json']; -// @public (undocumented) -type AdminDeleteAccountResponse = operations['admin/delete-account']['responses']['200']['content']['application/json']; - // @public (undocumented) type AdminDeleteAllFilesOfAUserRequest = operations['admin/delete-all-files-of-a-user']['requestBody']['content']['application/json']; @@ -509,9 +506,6 @@ type BubbleGameRankingResponse = operations['bubble-game/ranking']['responses'][ // @public (undocumented) type BubbleGameRegisterRequest = operations['bubble-game/register']['requestBody']['content']['application/json']; -// @public (undocumented) -type BubbleGameRegisterResponse = operations['bubble-game/register']['responses']['200']['content']['application/json']; - // @public (undocumented) type Channel = components['schemas']['Channel']; @@ -1271,7 +1265,6 @@ declare namespace entities { AdminUnsuspendUserRequest, AdminUpdateMetaRequest, AdminDeleteAccountRequest, - AdminDeleteAccountResponse, AdminUpdateUserNoteRequest, AdminRolesCreateRequest, AdminRolesCreateResponse, @@ -1469,6 +1462,7 @@ declare namespace entities { HashtagsUsersResponse, IResponse, I2faDoneRequest, + I2faDoneResponse, I2faKeyDoneRequest, I2faKeyDoneResponse, I2faPasswordLessRequest, @@ -1520,6 +1514,7 @@ declare namespace entities { IRegistryKeysWithTypeRequest, IRegistryKeysWithTypeResponse, IRegistryKeysRequest, + IRegistryKeysResponse, IRegistryRemoveRequest, IRegistryScopesWithDomainResponse, IRegistrySetRequest, @@ -1758,11 +1753,9 @@ declare namespace entities { FetchExternalResourcesResponse, RetentionResponse, BubbleGameRegisterRequest, - BubbleGameRegisterResponse, BubbleGameRankingRequest, BubbleGameRankingResponse, ReversiCancelMatchRequest, - ReversiCancelMatchResponse, ReversiGamesRequest, ReversiGamesResponse, ReversiMatchRequest, @@ -1811,6 +1804,12 @@ declare namespace entities { EmojiDetailed, Flash, Signin, + RoleCondFormulaLogics, + RoleCondFormulaValueNot, + RoleCondFormulaValueIsLocalOrRemote, + RoleCondFormulaValueCreated, + RoleCondFormulaFollowersOrFollowingOrNotes, + RoleCondFormulaValue, RoleLite, Role, RolePolicies, @@ -2077,6 +2076,9 @@ type HashtagsUsersResponse = operations['hashtags/users']['responses']['200']['c // @public (undocumented) type I2faDoneRequest = operations['i/2fa/done']['requestBody']['content']['application/json']; +// @public (undocumented) +type I2faDoneResponse = operations['i/2fa/done']['responses']['200']['content']['application/json']; + // @public (undocumented) type I2faKeyDoneRequest = operations['i/2fa/key-done']['requestBody']['content']['application/json']; @@ -2248,6 +2250,9 @@ type IRegistryGetResponse = operations['i/registry/get']['responses']['200']['co // @public (undocumented) type IRegistryKeysRequest = operations['i/registry/keys']['requestBody']['content']['application/json']; +// @public (undocumented) +type IRegistryKeysResponse = operations['i/registry/keys']['responses']['200']['content']['application/json']; + // @public (undocumented) type IRegistryKeysWithTypeRequest = operations['i/registry/keys-with-type']['requestBody']['content']['application/json']; @@ -2270,7 +2275,7 @@ type IResponse = operations['i']['responses']['200']['content']['application/jso type IRevokeTokenRequest = operations['i/revoke-token']['requestBody']['content']['application/json']; // @public (undocumented) -function isAPIError(reason: any): reason is APIError; +function isAPIError(reason: Record): reason is APIError; // @public (undocumented) type ISigninHistoryRequest = operations['i/signin-history']['requestBody']['content']['application/json']; @@ -2809,9 +2814,6 @@ type RetentionResponse = operations['retention']['responses']['200']['content'][ // @public (undocumented) type ReversiCancelMatchRequest = operations['reversi/cancel-match']['requestBody']['content']['application/json']; -// @public (undocumented) -type ReversiCancelMatchResponse = operations['reversi/cancel-match']['responses']['200']['content']['application/json']; - // @public (undocumented) type ReversiGameDetailed = components['schemas']['ReversiGameDetailed']; @@ -2851,6 +2853,24 @@ type ReversiVerifyResponse = operations['reversi/verify']['responses']['200']['c // @public (undocumented) type Role = components['schemas']['Role']; +// @public (undocumented) +type RoleCondFormulaFollowersOrFollowingOrNotes = components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes']; + +// @public (undocumented) +type RoleCondFormulaLogics = components['schemas']['RoleCondFormulaLogics']; + +// @public (undocumented) +type RoleCondFormulaValue = components['schemas']['RoleCondFormulaValue']; + +// @public (undocumented) +type RoleCondFormulaValueCreated = components['schemas']['RoleCondFormulaValueCreated']; + +// @public (undocumented) +type RoleCondFormulaValueIsLocalOrRemote = components['schemas']['RoleCondFormulaValueIsLocalOrRemote']; + +// @public (undocumented) +type RoleCondFormulaValueNot = components['schemas']['RoleCondFormulaValueNot']; + // @public (undocumented) type RoleLite = components['schemas']['RoleLite']; diff --git a/packages/cherrypick-js/package.json b/packages/cherrypick-js/package.json index 873b64f7be..166ccfdbd3 100644 --- a/packages/cherrypick-js/package.json +++ b/packages/cherrypick-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "cherrypick-js", "version": "4.7.0-beta.2", - "basedMisskeyVersion": "2024.2.0-beta.10", + "basedMisskeyVersion": "2024.2.0", "description": "CherryPick SDK for JavaScript", "types": "./built/dts/index.d.ts", "exports": { @@ -40,7 +40,7 @@ "@misskey-dev/eslint-plugin": "1.0.0", "@swc/jest": "0.2.31", "@types/jest": "29.5.11", - "@types/node": "20.11.10", + "@types/node": "20.11.17", "@typescript-eslint/eslint-plugin": "6.18.1", "@typescript-eslint/parser": "6.18.1", "eslint": "8.56.0", diff --git a/packages/cherrypick-js/src/api.ts b/packages/cherrypick-js/src/api.ts index 2e85fc2939..134ead0d79 100644 --- a/packages/cherrypick-js/src/api.ts +++ b/packages/cherrypick-js/src/api.ts @@ -17,7 +17,7 @@ export type APIError = { info: Record; }; -export function isAPIError(reason: any): reason is APIError { +export function isAPIError(reason: Record): reason is APIError { return reason[MK_API_ERROR] === true; } diff --git a/packages/cherrypick-js/src/api.types.ts b/packages/cherrypick-js/src/api.types.ts index 6320081928..af0bade5b3 100644 --- a/packages/cherrypick-js/src/api.types.ts +++ b/packages/cherrypick-js/src/api.types.ts @@ -15,10 +15,10 @@ type Overwrite = Omit< keyof U > & U; -type SwitchCase = { +type SwitchCase = { $switch: { - $cases: [any, any][], - $default: any; + $cases: [Condition, Result][], + $default: Result; }; }; diff --git a/packages/cherrypick-js/src/autogen/endpoint.ts b/packages/cherrypick-js/src/autogen/endpoint.ts index 6decc5c7be..c8d85fc7ec 100644 --- a/packages/cherrypick-js/src/autogen/endpoint.ts +++ b/packages/cherrypick-js/src/autogen/endpoint.ts @@ -94,7 +94,6 @@ import type { AdminUnsuspendUserRequest, AdminUpdateMetaRequest, AdminDeleteAccountRequest, - AdminDeleteAccountResponse, AdminUpdateUserNoteRequest, AdminRolesCreateRequest, AdminRolesCreateResponse, @@ -292,6 +291,7 @@ import type { HashtagsUsersResponse, IResponse, I2faDoneRequest, + I2faDoneResponse, I2faKeyDoneRequest, I2faKeyDoneResponse, I2faPasswordLessRequest, @@ -343,6 +343,7 @@ import type { IRegistryKeysWithTypeRequest, IRegistryKeysWithTypeResponse, IRegistryKeysRequest, + IRegistryKeysResponse, IRegistryRemoveRequest, IRegistryScopesWithDomainResponse, IRegistrySetRequest, @@ -581,11 +582,9 @@ import type { FetchExternalResourcesResponse, RetentionResponse, BubbleGameRegisterRequest, - BubbleGameRegisterResponse, BubbleGameRankingRequest, BubbleGameRankingResponse, ReversiCancelMatchRequest, - ReversiCancelMatchResponse, ReversiGamesRequest, ReversiGamesResponse, ReversiMatchRequest, @@ -671,7 +670,7 @@ export type Endpoints = { 'admin/suspend-user': { req: AdminSuspendUserRequest; res: EmptyResponse }; 'admin/unsuspend-user': { req: AdminUnsuspendUserRequest; res: EmptyResponse }; 'admin/update-meta': { req: AdminUpdateMetaRequest; res: EmptyResponse }; - 'admin/delete-account': { req: AdminDeleteAccountRequest; res: AdminDeleteAccountResponse }; + 'admin/delete-account': { req: AdminDeleteAccountRequest; res: EmptyResponse }; 'admin/update-user-note': { req: AdminUpdateUserNoteRequest; res: EmptyResponse }; 'admin/roles/create': { req: AdminRolesCreateRequest; res: AdminRolesCreateResponse }; 'admin/roles/delete': { req: AdminRolesDeleteRequest; res: EmptyResponse }; @@ -791,7 +790,7 @@ export type Endpoints = { 'hashtags/trend': { req: EmptyRequest; res: HashtagsTrendResponse }; 'hashtags/users': { req: HashtagsUsersRequest; res: HashtagsUsersResponse }; 'i': { req: EmptyRequest; res: IResponse }; - 'i/2fa/done': { req: I2faDoneRequest; res: EmptyResponse }; + 'i/2fa/done': { req: I2faDoneRequest; res: I2faDoneResponse }; 'i/2fa/key-done': { req: I2faKeyDoneRequest; res: I2faKeyDoneResponse }; 'i/2fa/password-less': { req: I2faPasswordLessRequest; res: EmptyResponse }; 'i/2fa/register-key': { req: I2faRegisterKeyRequest; res: I2faRegisterKeyResponse }; @@ -834,7 +833,7 @@ export type Endpoints = { 'i/registry/get-detail': { req: IRegistryGetDetailRequest; res: IRegistryGetDetailResponse }; 'i/registry/get': { req: IRegistryGetRequest; res: IRegistryGetResponse }; 'i/registry/keys-with-type': { req: IRegistryKeysWithTypeRequest; res: IRegistryKeysWithTypeResponse }; - 'i/registry/keys': { req: IRegistryKeysRequest; res: EmptyResponse }; + 'i/registry/keys': { req: IRegistryKeysRequest; res: IRegistryKeysResponse }; 'i/registry/remove': { req: IRegistryRemoveRequest; res: EmptyResponse }; 'i/registry/scopes-with-domain': { req: EmptyRequest; res: IRegistryScopesWithDomainResponse }; 'i/registry/set': { req: IRegistrySetRequest; res: EmptyResponse }; @@ -989,9 +988,9 @@ export type Endpoints = { 'fetch-rss': { req: FetchRssRequest; res: FetchRssResponse }; 'fetch-external-resources': { req: FetchExternalResourcesRequest; res: FetchExternalResourcesResponse }; 'retention': { req: EmptyRequest; res: RetentionResponse }; - 'bubble-game/register': { req: BubbleGameRegisterRequest; res: BubbleGameRegisterResponse }; + 'bubble-game/register': { req: BubbleGameRegisterRequest; res: EmptyResponse }; 'bubble-game/ranking': { req: BubbleGameRankingRequest; res: BubbleGameRankingResponse }; - 'reversi/cancel-match': { req: ReversiCancelMatchRequest; res: ReversiCancelMatchResponse }; + 'reversi/cancel-match': { req: ReversiCancelMatchRequest; res: EmptyResponse }; 'reversi/games': { req: ReversiGamesRequest; res: ReversiGamesResponse }; 'reversi/match': { req: ReversiMatchRequest; res: ReversiMatchResponse }; 'reversi/invitations': { req: EmptyRequest; res: ReversiInvitationsResponse }; diff --git a/packages/cherrypick-js/src/autogen/entities.ts b/packages/cherrypick-js/src/autogen/entities.ts index a84d450d86..3e4c5f0d60 100644 --- a/packages/cherrypick-js/src/autogen/entities.ts +++ b/packages/cherrypick-js/src/autogen/entities.ts @@ -96,7 +96,6 @@ export type AdminSuspendUserRequest = operations['admin/suspend-user']['requestB export type AdminUnsuspendUserRequest = operations['admin/unsuspend-user']['requestBody']['content']['application/json']; export type AdminUpdateMetaRequest = operations['admin/update-meta']['requestBody']['content']['application/json']; export type AdminDeleteAccountRequest = operations['admin/delete-account']['requestBody']['content']['application/json']; -export type AdminDeleteAccountResponse = operations['admin/delete-account']['responses']['200']['content']['application/json']; export type AdminUpdateUserNoteRequest = operations['admin/update-user-note']['requestBody']['content']['application/json']; export type AdminRolesCreateRequest = operations['admin/roles/create']['requestBody']['content']['application/json']; export type AdminRolesCreateResponse = operations['admin/roles/create']['responses']['200']['content']['application/json']; @@ -294,6 +293,7 @@ export type HashtagsUsersRequest = operations['hashtags/users']['requestBody'][' export type HashtagsUsersResponse = operations['hashtags/users']['responses']['200']['content']['application/json']; export type IResponse = operations['i']['responses']['200']['content']['application/json']; export type I2faDoneRequest = operations['i/2fa/done']['requestBody']['content']['application/json']; +export type I2faDoneResponse = operations['i/2fa/done']['responses']['200']['content']['application/json']; export type I2faKeyDoneRequest = operations['i/2fa/key-done']['requestBody']['content']['application/json']; export type I2faKeyDoneResponse = operations['i/2fa/key-done']['responses']['200']['content']['application/json']; export type I2faPasswordLessRequest = operations['i/2fa/password-less']['requestBody']['content']['application/json']; @@ -345,6 +345,7 @@ export type IRegistryGetResponse = operations['i/registry/get']['responses']['20 export type IRegistryKeysWithTypeRequest = operations['i/registry/keys-with-type']['requestBody']['content']['application/json']; export type IRegistryKeysWithTypeResponse = operations['i/registry/keys-with-type']['responses']['200']['content']['application/json']; export type IRegistryKeysRequest = operations['i/registry/keys']['requestBody']['content']['application/json']; +export type IRegistryKeysResponse = operations['i/registry/keys']['responses']['200']['content']['application/json']; export type IRegistryRemoveRequest = operations['i/registry/remove']['requestBody']['content']['application/json']; export type IRegistryScopesWithDomainResponse = operations['i/registry/scopes-with-domain']['responses']['200']['content']['application/json']; export type IRegistrySetRequest = operations['i/registry/set']['requestBody']['content']['application/json']; @@ -583,11 +584,9 @@ export type FetchExternalResourcesRequest = operations['fetch-external-resources export type FetchExternalResourcesResponse = operations['fetch-external-resources']['responses']['200']['content']['application/json']; export type RetentionResponse = operations['retention']['responses']['200']['content']['application/json']; export type BubbleGameRegisterRequest = operations['bubble-game/register']['requestBody']['content']['application/json']; -export type BubbleGameRegisterResponse = operations['bubble-game/register']['responses']['200']['content']['application/json']; export type BubbleGameRankingRequest = operations['bubble-game/ranking']['requestBody']['content']['application/json']; export type BubbleGameRankingResponse = operations['bubble-game/ranking']['responses']['200']['content']['application/json']; export type ReversiCancelMatchRequest = operations['reversi/cancel-match']['requestBody']['content']['application/json']; -export type ReversiCancelMatchResponse = operations['reversi/cancel-match']['responses']['200']['content']['application/json']; export type ReversiGamesRequest = operations['reversi/games']['requestBody']['content']['application/json']; export type ReversiGamesResponse = operations['reversi/games']['responses']['200']['content']['application/json']; export type ReversiMatchRequest = operations['reversi/match']['requestBody']['content']['application/json']; diff --git a/packages/cherrypick-js/src/autogen/models.ts b/packages/cherrypick-js/src/autogen/models.ts index 2d39bda2cd..8737a33b77 100644 --- a/packages/cherrypick-js/src/autogen/models.ts +++ b/packages/cherrypick-js/src/autogen/models.ts @@ -37,6 +37,12 @@ export type EmojiSimple = components['schemas']['EmojiSimple']; export type EmojiDetailed = components['schemas']['EmojiDetailed']; export type Flash = components['schemas']['Flash']; export type Signin = components['schemas']['Signin']; +export type RoleCondFormulaLogics = components['schemas']['RoleCondFormulaLogics']; +export type RoleCondFormulaValueNot = components['schemas']['RoleCondFormulaValueNot']; +export type RoleCondFormulaValueIsLocalOrRemote = components['schemas']['RoleCondFormulaValueIsLocalOrRemote']; +export type RoleCondFormulaValueCreated = components['schemas']['RoleCondFormulaValueCreated']; +export type RoleCondFormulaFollowersOrFollowingOrNotes = components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes']; +export type RoleCondFormulaValue = components['schemas']['RoleCondFormulaValue']; export type RoleLite = components['schemas']['RoleLite']; export type Role = components['schemas']['Role']; export type RolePolicies = components['schemas']['RolePolicies']; diff --git a/packages/cherrypick-js/src/autogen/types.ts b/packages/cherrypick-js/src/autogen/types.ts index 9b961fdac1..77747a2d31 100644 --- a/packages/cherrypick-js/src/autogen/types.ts +++ b/packages/cherrypick-js/src/autogen/types.ts @@ -3989,46 +3989,136 @@ export type components = { hardMutedWords: string[][]; mutedInstances: string[] | null; notificationRecieveConfig: { - app?: { + note?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; - }; - quote?: { + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; - }; - reply?: { + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + follow?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; - }; - follow?: { + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; - }; - renote?: { + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + mention?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; - }; - mention?: { + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; - }; - reaction?: { + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + reply?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; - }; - pollEnded?: { + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; - }; - receiveFollowRequest?: { + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + renote?: OneOf<[{ /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; - }; - groupInvited?: { + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { /** @enum {string} */ - type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; - }; + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + groupInvited?: { + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'list' | 'never'; + }; + quote?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + reaction?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + pollEnded?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + receiveFollowRequest?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + followRequestAccepted?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + roleAssigned?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + achievementEarned?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + app?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + test?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; }; emailNotificationTypes: string[]; achievements: { @@ -4805,6 +4895,36 @@ export type components = { headers: Record; success: boolean; }; + RoleCondFormulaLogics: { + id: string; + /** @enum {string} */ + type: 'and' | 'or'; + values: components['schemas']['RoleCondFormulaValue'][]; + }; + RoleCondFormulaValueNot: { + id: string; + /** @enum {string} */ + type: 'not'; + value: components['schemas']['RoleCondFormulaValue']; + }; + RoleCondFormulaValueIsLocalOrRemote: { + id: string; + /** @enum {string} */ + type: 'isLocal' | 'isRemote'; + }; + RoleCondFormulaValueCreated: { + id: string; + /** @enum {string} */ + type: 'createdLessThan' | 'createdMoreThan'; + sec: number; + }; + RoleCondFormulaFollowersOrFollowingOrNotes: { + id: string; + /** @enum {string} */ + type: 'followersLessThanOrEq' | 'followersMoreThanOrEq' | 'followingLessThanOrEq' | 'followingMoreThanOrEq' | 'notesLessThanOrEq' | 'notesMoreThanOrEq'; + value: number; + }; + RoleCondFormulaValue: components['schemas']['RoleCondFormulaLogics'] | components['schemas']['RoleCondFormulaValueNot'] | components['schemas']['RoleCondFormulaValueIsLocalOrRemote'] | components['schemas']['RoleCondFormulaValueCreated'] | components['schemas']['RoleCondFormulaFollowersOrFollowingOrNotes']; RoleLite: { /** * Format: id @@ -4831,7 +4951,7 @@ export type components = { updatedAt: string; /** @enum {string} */ target: 'manual' | 'conditional'; - condFormula: Record; + condFormula: components['schemas']['RoleCondFormulaValue']; /** @example false */ isPublic: boolean; /** @example false */ @@ -4890,11 +5010,11 @@ export type components = { user1Id: string; /** Format: id */ user2Id: string; - user1: components['schemas']['User']; - user2: components['schemas']['User']; + user1: components['schemas']['UserLite']; + user2: components['schemas']['UserLite']; /** Format: id */ winnerId: string | null; - winner: components['schemas']['User'] | null; + winner: components['schemas']['UserLite'] | null; /** Format: id */ surrenderedUserId: string | null; /** Format: id */ @@ -4926,11 +5046,11 @@ export type components = { user1Id: string; /** Format: id */ user2Id: string; - user1: components['schemas']['User']; - user2: components['schemas']['User']; + user1: components['schemas']['UserLite']; + user2: components['schemas']['UserLite']; /** Format: id */ winnerId: string | null; - winner: components['schemas']['User'] | null; + winner: components['schemas']['UserLite'] | null; /** Format: id */ surrenderedUserId: string | null; /** Format: id */ @@ -4942,7 +5062,7 @@ export type components = { canPutEverywhere: boolean; loopedBoard: boolean; timeLimitForEachTurn: number; - logs: unknown[][]; + logs: number[][]; map: string[]; }; }; @@ -5002,6 +5122,7 @@ export type operations = { hiddenTags: string[]; blockedHosts: string[]; sensitiveWords: string[]; + prohibitedWords: string[]; bannedEmailDomains?: string[]; preservedUsernames: string[]; hcaptchaSecretKey: string | null; @@ -5080,7 +5201,7 @@ export type operations = { objectStorageS3ForcePathStyle: boolean; objectStorageRemoteS3ForcePathStyle: boolean; privacyPolicyUrl: string | null; - repositoryUrl: string; + repositoryUrl: string | null; summalyProxy: string | null; themeColor: string | null; tosUrl: string | null; @@ -6734,7 +6855,12 @@ export type operations = { size: number; comment: string | null; blurhash: string | null; - properties: Record; + properties: { + width?: number; + height?: number; + orientation?: number; + avgColor?: string; + }; /** @example true */ storedInternal: boolean | null; /** Format: url */ @@ -7912,7 +8038,12 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': Record; + 'application/json': { + [key: string]: { + count: number; + size: number; + }; + }; }; }; /** @description Client error */ @@ -8952,7 +9083,162 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': Record; + 'application/json': { + email: string | null; + emailVerified: boolean; + autoAcceptFollowed: boolean; + noCrawle: boolean; + preventAiLearning: boolean; + alwaysMarkNsfw: boolean; + autoSensitive: boolean; + carefulBot: boolean; + injectFeaturedNote: boolean; + receiveAnnouncementEmail: boolean; + mutedWords: (string | string[])[]; + mutedInstances: string[]; + notificationRecieveConfig: { + note?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + follow?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + mention?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + reply?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + renote?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + quote?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + reaction?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + pollEnded?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + receiveFollowRequest?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + followRequestAccepted?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + roleAssigned?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + achievementEarned?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + app?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + test?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + }; + isModerator: boolean; + isSilenced: boolean; + isSuspended: boolean; + isHibernated: boolean; + lastActiveDate: string | null; + moderationNote: string; + signins: components['schemas']['Signin'][]; + policies: components['schemas']['RolePolicies']; + roles: components['schemas']['Role'][]; + roleAssigns: ({ + createdAt: string; + expiresAt: string | null; + roleId: string; + })[]; + }; }; }; /** @description Client error */ @@ -9181,6 +9467,7 @@ export type operations = { hiddenTags?: string[] | null; blockedHosts?: string[] | null; sensitiveWords?: string[] | null; + prohibitedWords?: string[] | null; themeColor?: string | null; mascotImageUrl?: string | null; bannerUrl?: string | null; @@ -9244,8 +9531,8 @@ export type operations = { swPublicKey?: string | null; swPrivateKey?: string | null; tosUrl?: string | null; - repositoryUrl?: string; - feedbackUrl?: string; + repositoryUrl?: string | null; + feedbackUrl?: string | null; impressumUrl?: string | null; privacyPolicyUrl?: string | null; useObjectStorage?: boolean; @@ -9361,11 +9648,9 @@ export type operations = { }; }; responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': unknown; - }; + /** @description OK (without any results) */ + 204: { + content: never; }; /** @description Client error */ 400: { @@ -16364,9 +16649,13 @@ export type operations = { }; }; responses: { - /** @description OK (without any results) */ - 204: { - content: never; + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + backupCodes: string[]; + }; + }; }; /** @description Client error */ 400: { @@ -16534,7 +16823,7 @@ export type operations = { content: { 'application/json': { rp: { - id: string | null; + id?: string; }; user: { id: string; @@ -16845,11 +17134,11 @@ export type operations = { 'application/json': { /** Format: misskey:id */ id: string; - name: string; + name?: string; /** Format: date-time */ createdAt: string; /** Format: date-time */ - lastUsedAt: string; + lastUsedAt?: string; permission: string[]; }[]; }; @@ -16919,7 +17208,7 @@ export type operations = { name: string; callbackUrl: string | null; permission: string[]; - isAuthorized: boolean; + isAuthorized?: boolean; })[]; }; }; @@ -18638,7 +18927,10 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': Record; + 'application/json': { + updatedAt: string; + value: unknown; + }; }; }; /** @description Client error */ @@ -18749,7 +19041,9 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': Record; + 'application/json': { + [key: string]: string; + }; }; }; /** @description Client error */ @@ -18801,9 +19095,11 @@ export type operations = { }; }; responses: { - /** @description OK (without any results) */ - 204: { - content: never; + /** @description OK (with results) */ + 200: { + content: { + 'application/json': string[]; + }; }; /** @description Client error */ 400: { @@ -19282,7 +19578,134 @@ export type operations = { mutedWords?: (string[] | string)[]; hardMutedWords?: (string[] | string)[]; mutedInstances?: string[]; - notificationRecieveConfig?: Record; + notificationRecieveConfig?: { + note?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + follow?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + mention?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + reply?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + renote?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + quote?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + reaction?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + pollEnded?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + receiveFollowRequest?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + followRequestAccepted?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + roleAssigned?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + achievementEarned?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + app?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + test?: OneOf<[{ + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'never'; + }, { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }]>; + }; emailNotificationTypes?: string[]; alsoKnownAs?: string[]; }; @@ -20284,6 +20707,7 @@ export type operations = { maintainerEmail: string | null; version: string; basedMisskeyVersion: string; + providesTarball: boolean; name: string; shortName: string | null; /** @@ -20295,9 +20719,9 @@ export type operations = { langs: string[]; tosUrl: string | null; /** @default https://github.com/LycheeBridge/LycheeBridge */ - repositoryUrl: string; + repositoryUrl: string | null; /** @default https://github.com/LycheeBridge/LycheeBridge/issues/new */ - feedbackUrl: string; + feedbackUrl: string | null; defaultDarkTheme: string | null; defaultLightTheme: string | null; disableRegistration: boolean; @@ -25074,12 +25498,12 @@ export type operations = { content: { 'application/json': { /** Format: misskey:id */ - id: string; + id?: string; required: boolean; - string: string; - default: string; + string?: string; + default?: string; /** @default hello */ - nullableDefault: string | null; + nullableDefault?: string | null; }; }; }; @@ -27958,18 +28382,16 @@ export type operations = { 'application/json': { score: number; seed: string; - logs: unknown[]; + logs: number[][]; gameMode: string; gameVersion: number; }; }; }; responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': unknown; - }; + /** @description OK (without any results) */ + 204: { + content: never; }; /** @description Client error */ 400: { @@ -28031,7 +28453,7 @@ export type operations = { /** Format: misskey:id */ id: string; score: number; - user: components['schemas']['UserLite']; + user?: components['schemas']['UserLite']; }[]; }; }; @@ -28083,11 +28505,9 @@ export type operations = { }; }; responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': unknown; - }; + /** @description OK (without any results) */ + 204: { + content: never; }; /** @description Client error */ 400: { @@ -28204,9 +28624,13 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': unknown; + 'application/json': components['schemas']['ReversiGameDetailed']; }; }; + /** @description OK (without any results) */ + 204: { + content: never; + }; /** @description Client error */ 400: { content: { diff --git a/packages/frontend/.storybook/main.ts b/packages/frontend/.storybook/main.ts index eb57078b39..14b0581bfe 100644 --- a/packages/frontend/.storybook/main.ts +++ b/packages/frontend/.storybook/main.ts @@ -3,25 +3,28 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { resolve } from 'node:path'; +import { createRequire } from 'node:module'; +import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import 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 require = createRequire(import.meta.url); +const _dirname = fileURLToPath(new URL('.', import.meta.url)); const config = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], addons: [ - '@storybook/addon-essentials', - '@storybook/addon-interactions', - '@storybook/addon-links', - '@storybook/addon-storysource', - resolve(dirname, '../node_modules/storybook-addon-misskey-theme'), + getAbsolutePath('@storybook/addon-essentials'), + getAbsolutePath('@storybook/addon-interactions'), + getAbsolutePath('@storybook/addon-links'), + getAbsolutePath('@storybook/addon-storysource'), + getAbsolutePath('@storybook/addon-mdx-gfm'), + resolve(_dirname, '../node_modules/storybook-addon-misskey-theme'), ], framework: { - name: '@storybook/vue3-vite', + name: getAbsolutePath('@storybook/vue3-vite') as '@storybook/vue3-vite', options: {}, }, docs: { @@ -37,10 +40,13 @@ const config = { } return mergeConfig(config, { plugins: [ - // XXX: https://github.com/IanVS/vite-plugin-turbosnap/issues/8 - (turbosnap as any as typeof turbosnap['default'])({ - rootDir: config.root ?? process.cwd(), - }), + { + // XXX: https://github.com/IanVS/vite-plugin-turbosnap/issues/8 + ...(turbosnap as any as typeof turbosnap['default'])({ + rootDir: config.root ?? process.cwd(), + }), + name: 'fake-turbosnap', + }, ], build: { target: [ @@ -53,3 +59,7 @@ const config = { }, } satisfies StorybookConfig; export default config; + +function getAbsolutePath(value: string): string { + return dirname(require.resolve(join(value, 'package.json'))); +} diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts index 0fce45248d..ec96a74b03 100644 --- a/packages/frontend/.storybook/preview.ts +++ b/packages/frontend/.storybook/preview.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { addons } from '@storybook/addons'; import { FORCE_REMOUNT } from '@storybook/core-events'; +import { addons } from '@storybook/preview-api'; import { type Preview, setup } from '@storybook/vue3'; import isChromatic from 'chromatic/isChromatic'; import { initialize, mswDecorator } from 'msw-storybook-addon'; diff --git a/packages/frontend/package.json b/packages/frontend/package.json index ecae96ba77..a065dff27b 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -8,7 +8,7 @@ "build": "vite build", "storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"", "build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js", - "build-storybook": "pnpm build-storybook-pre && storybook build", + "build-storybook": "pnpm build-storybook-pre && storybook build --webpack-stats-json storybook-static", "chromatic": "chromatic", "test": "vitest --run --globals", "test-and-coverage": "vitest --run --coverage --globals", @@ -29,9 +29,9 @@ "@tabler/icons-webfont": "2.44.0", "@twemoji/parser": "15.0.0", "@vitejs/plugin-vue": "5.0.3", - "@vue/compiler-sfc": "3.4.15", + "@vue/compiler-sfc": "3.4.18", "autosize": "6.0.1", - "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6", + "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.2", "astring": "1.8.6", "broadcast-channel": "7.0.0", "buraha": "0.0.1", @@ -78,39 +78,42 @@ "typescript": "5.3.3", "uuid": "9.0.1", "v-code-diff": "1.7.2", - "vite": "5.0.12", - "vue": "3.4.15", + "vite": "5.1.0", + "vue": "3.4.18", "vue-prism-editor": "2.0.0-alpha.2", "vuedraggable": "next" }, "devDependencies": { "@misskey-dev/eslint-plugin": "1.0.0", "@misskey-dev/summaly": "5.0.3", - "@storybook/addon-actions": "7.6.10", - "@storybook/addon-essentials": "7.6.10", - "@storybook/addon-interactions": "7.6.10", - "@storybook/addon-links": "7.6.10", - "@storybook/addon-storysource": "7.6.10", - "@storybook/addons": "7.6.10", - "@storybook/blocks": "7.6.10", - "@storybook/core-events": "7.6.10", + "@storybook/addon-actions": "8.0.0-beta.2", + "@storybook/addon-essentials": "8.0.0-beta.2", + "@storybook/addon-interactions": "8.0.0-beta.2", + "@storybook/addon-links": "8.0.0-beta.2", + "@storybook/addon-mdx-gfm": "8.0.0-beta.2", + "@storybook/addons": "8.0.0-beta.2", + "@storybook/addon-storysource": "8.0.0-beta.2", + "@storybook/blocks": "8.0.0-beta.2", + "@storybook/components": "8.0.0-beta.2", "@storybook/jest": "0.2.3", - "@storybook/manager-api": "7.6.10", - "@storybook/preview-api": "7.6.10", - "@storybook/react": "7.6.10", - "@storybook/react-vite": "7.6.10", "@storybook/testing-library": "0.2.2", - "@storybook/theming": "7.6.10", - "@storybook/types": "7.6.10", - "@storybook/vue3": "7.6.10", - "@storybook/vue3-vite": "7.6.10", - "@testing-library/vue": "8.0.1", + "@storybook/core-events": "8.0.0-beta.2", + "@storybook/manager-api": "8.0.0-beta.2", + "@storybook/preview-api": "8.0.0-beta.2", + "@storybook/react": "8.0.0-beta.2", + "@storybook/react-vite": "8.0.0-beta.2", + "@storybook/test": "8.0.0-beta.2", + "@storybook/theming": "8.0.0-beta.2", + "@storybook/types": "8.0.0-beta.2", + "@storybook/vue3": "8.0.0-beta.2", + "@storybook/vue3-vite": "8.0.0-beta.2", + "@testing-library/vue": "8.0.2", "@types/autosize": "^4.0.1", "@types/escape-regexp": "0.0.3", "@types/estree": "1.0.5", "@types/matter-js": "0.19.6", "@types/micromatch": "4.0.6", - "@types/node": "20.11.10", + "@types/node": "20.11.17", "@types/prismjs": "^1.26.0", "@types/punycode": "2.1.3", "@types/sanitize-html": "2.9.5", @@ -121,10 +124,10 @@ "@typescript-eslint/eslint-plugin": "6.18.1", "@typescript-eslint/parser": "6.18.1", "@vitest/coverage-v8": "0.34.6", - "@vue/runtime-core": "3.4.15", + "@vue/runtime-core": "3.4.18", "acorn": "8.11.3", "cross-env": "7.0.3", - "cypress": "13.6.3", + "cypress": "13.6.4", "eslint": "8.56.0", "eslint-plugin-import": "2.29.1", "eslint-plugin-storybook": "^0.6.13", @@ -136,16 +139,16 @@ "msw": "2.1.7", "msw-storybook-addon": "2.0.0-beta.1", "nodemon": "3.0.3", - "prettier": "3.2.4", + "prettier": "3.2.5", "react": "18.2.0", "react-dom": "18.2.0", "start-server-and-test": "2.0.3", - "storybook": "7.6.10", + "storybook": "8.0.0-beta.2", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "vite-plugin-turbosnap": "1.0.3", "vitest": "0.34.6", "vitest-fetch-mock": "0.2.2", - "vue-component-type-helpers": "^1.8.27", + "vue-component-type-helpers": "1.8.27", "vue-eslint-parser": "9.4.2", "vue-tsc": "1.8.27" } diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index cef55ba0c8..20cddcc817 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -61,12 +61,6 @@ export async function common(createVue: () => App) { }); } - const splash = document.getElementById('splash'); - // 念のためnullチェック(HTMLが古い場合があるため(そのうち消す)) - if (splash) splash.addEventListener('transitionend', () => { - splash.remove(); - }); - let isClientUpdated = false; let isClientMigrated = false; const showPushNotificationDialog = miLocalStorage.getItem('showPushNotificationDialog'); @@ -301,5 +295,10 @@ function removeSplash() { if (splash) { splash.style.opacity = '0'; splash.style.pointerEvents = 'none'; + + // transitionendイベントが発火しない場合があるため + window.setTimeout(() => { + splash.remove(); + }, 1000); } } diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index e47abfc983..fe745c4955 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -11,6 +11,7 @@ import { alert, confirm, popup, post, welcomeToast } from '@/os.js'; import { useStream } from '@/stream.js'; import * as sound from '@/scripts/sound.js'; import { $i, signout, updateAccount } from '@/account.js'; +import { fetchInstance, instance } from '@/instance.js'; import { ColdDeviceStorage, defaultStore } from '@/store.js'; import { makeHotkey } from '@/scripts/hotkey.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; @@ -241,6 +242,13 @@ export async function mainBoot() { } } + fetchInstance().then(() => { + const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read'); + if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') { + popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed'); + } + }); + if ('Notification' in window) { // 許可を得ていなかったらリクエスト if (Notification.permission === 'default') { diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue index 9bac941f6f..2c5ce5b0d6 100644 --- a/packages/frontend/src/components/MkAbuseReport.vue +++ b/packages/frontend/src/components/MkAbuseReport.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only