diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 446db4708..db34a5036 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -72,7 +72,7 @@ dbReplications: false #───┘ Redis configuration └───────────────────────────────────── redis: - host: redis + host: keydb port: 6379 #family: 0 # 0=Both, 4=IPv4, 6=IPv6 #pass: example-pass @@ -80,7 +80,7 @@ redis: #db: 1 #redisForPubsub: -# host: redis +# host: keydb # port: 6379 # #family: 0 # 0=Both, 4=IPv4, 6=IPv6 # #pass: example-pass @@ -88,7 +88,7 @@ redis: # #db: 1 #redisForJobQueue: -# host: redis +# host: keydb # port: 6379 # #family: 0 # 0=Both, 4=IPv4, 6=IPv6 # #pass: example-pass diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 56a7e88dd..a50068f1d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,7 @@ "version": "8.9.2" }, "ghcr.io/devcontainers/features/node:1": { - "version": "18.18.0" + "version": "20" } }, "forwardPorts": [3000], diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml index 3d57d1245..54ef6a3dc 100644 --- a/.devcontainer/devcontainer.yml +++ b/.devcontainer/devcontainer.yml @@ -72,7 +72,7 @@ dbReplications: false #───┘ Redis configuration └───────────────────────────────────── redis: - host: redis + host: keydb port: 6379 #family: 0 # 0=Both, 4=IPv4, 6=IPv6 #pass: example-pass @@ -80,7 +80,7 @@ redis: #db: 1 #redisForPubsub: -# host: redis +# host: keydb # port: 6379 # #family: 0 # 0=Both, 4=IPv4, 6=IPv6 # #pass: example-pass @@ -88,7 +88,7 @@ redis: # #db: 1 #redisForJobQueue: -# host: redis +# host: keydb # port: 6379 # #family: 0 # 0=Both, 4=IPv4, 6=IPv6 # #pass: example-pass diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 2809cd2ca..ece9e8f11 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -15,15 +15,15 @@ services: - internal_network - external_network - redis: + keydb: restart: unless-stopped - image: redis:7-alpine + image: eqalpha/keydb:latest networks: - internal_network volumes: - - redis-data:/data + - keydb-data:/data healthcheck: - test: "redis-cli ping" + test: "keydb-cli ping" interval: 5s retries: 20 @@ -45,7 +45,7 @@ services: volumes: postgres-data: - redis-data: + keydb-data: networks: internal_network: diff --git a/.dockerignore b/.dockerignore index 1de0c7982..8ecffe1c7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,6 +11,7 @@ docker-compose.yml node_modules/ packages/*/node_modules redis/ +keydb/ files/ misskey-assets/ fluent-emojis/ diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml deleted file mode 100644 index a43789b75..000000000 --- a/.github/workflows/docker-develop.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Publish Docker image (develop) - -on: - push: - branches: - - develop - workflow_dispatch: - -jobs: - push_to_registry: - name: Push Docker image to Docker Hub - runs-on: ubuntu-latest - if: github.repository == 'misskey-dev/misskey' - steps: - - name: Check out the repo - uses: actions/checkout@v4.1.1 - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v3.0.0 - with: - platforms: linux/amd64,linux/arm64 - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: misskey/misskey - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build and Push to Docker Hub - uses: docker/build-push-action@v5 - with: - builder: ${{ steps.buildx.outputs.name }} - context: . - push: true - platforms: ${{ steps.buildx.outputs.platforms }} - provenance: false - tags: misskey/misskey:develop - labels: develop - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/docker-io.yml b/.github/workflows/docker-io.yml index 051426a28..29f2687fa 100644 --- a/.github/workflows/docker-io.yml +++ b/.github/workflows/docker-io.yml @@ -43,8 +43,8 @@ jobs: platforms: ${{ steps.buildx.outputs.platforms }} provenance: false labels: ${{ env.FORMATTED_BRANCH_NAME }} - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=registry,ref=ghcr.io/misskeyio/misskey:io-buildcache + cache-to: type=registry,ref=ghcr.io/misskeyio/misskey:io-buildcache,mode=max tags: | ghcr.io/misskeyio/misskey:latest ghcr.io/misskeyio/misskey:${{ env.FORMATTED_BRANCH_NAME }} diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml deleted file mode 100644 index 08cb91c2d..000000000 --- a/.github/workflows/docker.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Publish Docker image - -on: - release: - types: [published] - workflow_dispatch: - -jobs: - push_to_registry: - name: Push Docker image to Docker Hub - runs-on: ubuntu-latest - - steps: - - name: Check out the repo - uses: actions/checkout@v4.1.1 - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v3.0.0 - with: - platforms: linux/amd64,linux/arm64 - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: misskey/misskey - tags: | - type=edge - type=ref,event=pr - type=ref,event=branch - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build and Push to Docker Hub - uses: docker/build-push-action@v5 - with: - builder: ${{ steps.buildx.outputs.name }} - context: . - push: true - platforms: ${{ steps.buildx.outputs.platforms }} - provenance: false - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml index edb18b04d..d9366d8f3 100644 --- a/.github/workflows/dockle.yml +++ b/.github/workflows/dockle.yml @@ -1,4 +1,3 @@ ---- name: Dockle on: @@ -11,20 +10,23 @@ on: jobs: dockle: runs-on: ubuntu-latest - env: - DOCKER_CONTENT_TRUST: 1 steps: - - uses: actions/checkout@v4.1.1 - - run: | - curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb" - sudo dpkg -i dockle.deb - - run: | - cp .config/docker_example.env .config/docker.env - cp ./docker-compose.yml.example ./docker-compose.yml - - run: | - docker compose up -d web - docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest - - run: | - cmd="dockle --exit-code 1 misskey-web:latest ${image_name}" - echo "> ${cmd}" - eval "${cmd}" + - name: Checkout code + uses: actions/checkout@v4 + - name: Build an image from Dockerfile + uses: docker/build-push-action@v5 + with: + context: . + push: false + provenance: false + cache-from: type=registry,ref=ghcr.io/misskeyio/misskey:io-buildcache + tags: | + misskey:scan + - name: Run dockle + uses: goodwithtech/dockle-action@main + with: + image: 'misskey:scan' + format: 'list' + exit-code: '1' + exit-level: 'warn' + ignore: 'CIS-DI-0005,CIS-DI-0010' diff --git a/.github/workflows/ok-to-test.yml b/.github/workflows/ok-to-test.yml deleted file mode 100644 index c02b980e4..000000000 --- a/.github/workflows/ok-to-test.yml +++ /dev/null @@ -1,36 +0,0 @@ -# If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event -name: Ok To Test - -on: - issue_comment: - types: [created] - -jobs: - ok-to-test: - runs-on: ubuntu-latest - # Only run for PRs, not issue comments - if: ${{ github.event.issue.pull_request }} - steps: - # Generate a GitHub App installation access token from an App ID and private key - # To create a new GitHub App: - # https://developer.github.com/apps/building-github-apps/creating-a-github-app/ - # See app.yml for an example app manifest - - name: Generate token - id: generate_token - uses: tibdex/github-app-token@v2 - with: - app_id: ${{ secrets.DEPLOYBOT_APP_ID }} - private_key: ${{ secrets.DEPLOYBOT_PRIVATE_KEY }} - - - name: Slash Command Dispatch - uses: peter-evans/slash-command-dispatch@v3 - env: - TOKEN: ${{ steps.generate_token.outputs.token }} - with: - token: ${{ env.TOKEN }} # GitHub App installation access token - # token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work - reaction-token: ${{ secrets.GITHUB_TOKEN }} - issue-type: pull-request - commands: deploy - named-args: true - permission: write diff --git a/.github/workflows/pr-preview-deploy.yml b/.github/workflows/pr-preview-deploy.yml deleted file mode 100644 index 0e76bdeb2..000000000 --- a/.github/workflows/pr-preview-deploy.yml +++ /dev/null @@ -1,92 +0,0 @@ -# Run secret-dependent integration tests only after /deploy approval -on: - repository_dispatch: - types: [deploy-command] - -name: Deploy preview environment - -jobs: - # Repo owner has commented /deploy on a (fork-based) pull request - deploy-preview-environment: - runs-on: ubuntu-latest - if: - github.event.client_payload.slash_command.sha != '' && - contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha) - steps: - - uses: actions/github-script@v6.3.3 - id: check-id - env: - number: ${{ github.event.client_payload.pull_request.number }} - job: ${{ github.job }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - result-encoding: string - script: | - const { data: pull } = await github.rest.pulls.get({ - ...context.repo, - pull_number: process.env.number - }); - const ref = pull.head.sha; - - const { data: checks } = await github.rest.checks.listForRef({ - ...context.repo, - ref - }); - - const check = checks.check_runs.filter(c => c.name === process.env.job); - - return check[0].id; - - - uses: actions/github-script@v6.3.3 - env: - check_id: ${{ steps.check-id.outputs.result }} - details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - await github.rest.checks.update({ - ...context.repo, - check_run_id: process.env.check_id, - status: 'in_progress', - details_url: process.env.details_url - }); - - # Check out merge commit - - name: Fork based /deploy checkout - uses: actions/checkout@v4.1.1 - with: - ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' - - # - - name: Context - uses: okteto/context@latest - with: - token: ${{ secrets.OKTETO_TOKEN }} - - - name: Deploy preview environment - uses: ikuradon/deploy-preview@latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - name: pr-${{ github.event.client_payload.pull_request.number }}-syuilo - timeout: 15m - - # Update check run called "integration-fork" - - uses: actions/github-script@v6.3.3 - id: update-check-run - if: ${{ always() }} - env: - # Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run - conclusion: ${{ job.status }} - check_id: ${{ steps.check-id.outputs.result }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { data: result } = await github.rest.checks.update({ - ...context.repo, - check_run_id: process.env.check_id, - status: 'completed', - conclusion: process.env.conclusion - }); - - return result; diff --git a/.github/workflows/pr-preview-destroy.yml b/.github/workflows/pr-preview-destroy.yml deleted file mode 100644 index 8adfad9da..000000000 --- a/.github/workflows/pr-preview-destroy.yml +++ /dev/null @@ -1,54 +0,0 @@ -# file: .github/workflows/preview-closed.yaml -on: - pull_request: - types: - - closed - -name: Destroy preview environment - -jobs: - destroy-preview-environment: - runs-on: ubuntu-latest - steps: - - uses: actions/github-script@v6.3.3 - id: check-conclusion - env: - number: ${{ github.event.number }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - result-encoding: string - script: | - const { data: pull } = await github.rest.pulls.get({ - ...context.repo, - pull_number: process.env.number - }); - const ref = pull.head.sha; - - const { data: checks } = await github.rest.checks.listForRef({ - ...context.repo, - ref - }); - - const check = checks.check_runs.filter(c => c.name === 'deploy-preview-environment'); - - if (check.length === 0) { - return; - } - - const { data: result } = await github.rest.checks.get({ - ...context.repo, - check_run_id: check[0].id, - }); - - return result.conclusion; - - name: Context - if: steps.check-conclusion.outputs.result == 'success' - uses: okteto/context@latest - with: - token: ${{ secrets.OKTETO_TOKEN }} - - - name: Destroy preview environment - if: steps.check-conclusion.outputs.result == 'success' - uses: okteto/destroy-preview@latest - with: - name: pr-${{ github.event.number }}-syuilo diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 6fcd42a2c..3628d762d 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -13,18 +13,18 @@ jobs: strategy: matrix: - node-version: [18.x] + node-version: [20.x] services: postgres: - image: postgres:13 + image: postgres:15 ports: - 54312:5432 env: POSTGRES_DB: test-misskey POSTGRES_HOST_AUTH_METHOD: trust - redis: - image: redis:7 + keydb: + image: eqalpha/keydb:latest ports: - 56312:6379 diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index c90f30b65..59e243c80 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [18.x] + node-version: [20.x] steps: - uses: actions/checkout@v4.1.1 @@ -51,19 +51,19 @@ jobs: strategy: fail-fast: false matrix: - node-version: [18.x] + node-version: [20.x] browser: [chrome] services: postgres: - image: postgres:13 + image: postgres:15 ports: - 54312:5432 env: POSTGRES_DB: test-misskey POSTGRES_HOST_AUTH_METHOD: trust - redis: - image: redis:7 + keydb: + image: eqalpha/keydb:latest ports: - 56312:6379 diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml index 39aa78092..3e880b57e 100644 --- a/.github/workflows/test-misskey-js.yml +++ b/.github/workflows/test-misskey-js.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [18.x] + node-version: [20.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml index 8e200c461..46038173c 100644 --- a/.github/workflows/test-production.yml +++ b/.github/workflows/test-production.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [18.x] + node-version: [20.x] steps: - uses: actions/checkout@v4.1.1 diff --git a/.gitignore b/.gitignore index a66e527db..f66d6e134 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ coverage !/.config/docker_example.env docker-compose.yml !/.devcontainer/docker-compose.yml +!/packages/backend/test/docker-compose.yml # misskey /build @@ -51,6 +52,7 @@ run.bat api-docs.json *.log /redis +/keydb *.code-workspace .DS_Store /files diff --git a/.node-version b/.node-version index 02c8b485e..209e3ef4b 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -18.18.0 +20 diff --git a/Dockerfile b/Dockerfile index 316bedd08..2b3ec0da7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.4 -ARG NODE_VERSION=18.18.0-bullseye +ARG NODE_VERSION=20 # build assets & compile TypeScript @@ -18,15 +18,18 @@ RUN corepack enable WORKDIR /misskey -COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] +COPY --link pnpm-lock.yaml ./ +RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ + pnpm fetch + +COPY --link ["pnpm-workspace.yaml", "package.json", "./"] COPY --link ["scripts", "./scripts"] COPY --link ["packages/backend/package.json", "./packages/backend/"] COPY --link ["packages/frontend/package.json", "./packages/frontend/"] COPY --link ["packages/sw/package.json", "./packages/sw/"] COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"] -RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ - pnpm i --frozen-lockfile --aggregate-output +RUN pnpm i --frozen-lockfile --aggregate-output --offline COPY --link . ./ @@ -48,12 +51,15 @@ RUN corepack enable WORKDIR /misskey -COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] +COPY --link pnpm-lock.yaml ./ +RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ + pnpm fetch + +COPY --link ["pnpm-workspace.yaml", "package.json", "./"] COPY --link ["scripts", "./scripts"] COPY --link ["packages/backend/package.json", "./packages/backend/"] -RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ - pnpm i --frozen-lockfile --aggregate-output +RUN pnpm i --frozen-lockfile --aggregate-output --offline FROM --platform=$TARGETPLATFORM node:${NODE_VERSION}-slim AS runner diff --git a/chart/templates/Deployment.yml b/chart/templates/Deployment.yml index d5dd14f59..a6ea8b216 100644 --- a/chart/templates/Deployment.yml +++ b/chart/templates/Deployment.yml @@ -27,7 +27,7 @@ spec: ports: - containerPort: 3000 - name: postgres - image: postgres:14-alpine + image: postgres:15-alpine env: - name: POSTGRES_USER value: "example-misskey-user" @@ -37,8 +37,8 @@ spec: value: "misskey" ports: - containerPort: 5432 - - name: redis - image: redis:alpine + - name: keydb + image: eqalpha/keydb:latest ports: - containerPort: 6379 volumes: diff --git a/docker-compose.yml.example b/docker-compose.yml.example index 60ba4dc8c..830c1a294 100644 --- a/docker-compose.yml.example +++ b/docker-compose.yml.example @@ -6,12 +6,12 @@ services: restart: always links: - db - - redis + - keydb # - meilisearch depends_on: db: condition: service_healthy - redis: + keydb: condition: service_healthy ports: - "3000:3000" @@ -22,15 +22,15 @@ services: - ./files:/misskey/files - ./.config:/misskey/.config:ro - redis: + keydb: restart: always - image: redis:7-alpine + image: eqalpha/keydb:latest networks: - internal_network volumes: - - ./redis:/data + - ./keydb:/data healthcheck: - test: "redis-cli ping" + test: "keydb-cli ping" interval: 5s retries: 20 diff --git a/locales/en-US.yml b/locales/en-US.yml index b8252480d..b30ea50ab 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -312,6 +312,7 @@ folderName: "Folder name" createFolder: "Create a folder" renameFolder: "Rename this folder" deleteFolder: "Delete this folder" +folder: "Folder" addFile: "Add a file" emptyDrive: "Your Drive is empty" emptyFolder: "This folder is empty" @@ -565,6 +566,10 @@ output: "Output" script: "Script" disablePagesScript: "Disable AiScript on Pages" updateRemoteUser: "Update remote user information" +deleteUserAvatar: "Delete user icon" +deleteUserAvatarConfirm: "Are you sure that you want to delete this user's icon?" +deleteUserBanner: "Delete user banner" +deleteUserBannerConfirm: "Are you sure that you want to delete this user's banner?" deleteAllFiles: "Delete all files" deleteAllFilesConfirm: "Are you sure that you want to delete all files?" removeAllFollowing: "Unfollow all followed users" diff --git a/locales/index.d.ts b/locales/index.d.ts index ec62ce8ae..984689ddb 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -315,6 +315,7 @@ export interface Locale { "createFolder": string; "renameFolder": string; "deleteFolder": string; + "folder": string; "addFile": string; "emptyDrive": string; "emptyFolder": string; @@ -568,6 +569,10 @@ export interface Locale { "script": string; "disablePagesScript": string; "updateRemoteUser": string; + "deleteUserAvatar": string; + "deleteUserAvatarConfirm": string; + "deleteUserBanner": string; + "deleteUserBannerConfirm": string; "deleteAllFiles": string; "deleteAllFilesConfirm": string; "removeAllFollowing": string; @@ -1164,6 +1169,8 @@ export interface Locale { "signupPendingError": string; "cwNotationRequired": string; "doReaction": string; + "urlPreviewDenyList": string; + "urlPreviewDenyListDescription": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 213fb36d5..06009d48a 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -312,6 +312,7 @@ folderName: "フォルダー名" createFolder: "フォルダーを作成" renameFolder: "フォルダー名を変更" deleteFolder: "フォルダーを削除" +folder: "フォルダー" addFile: "ファイルを追加" emptyDrive: "ドライブは空です" emptyFolder: "フォルダーは空です" @@ -565,6 +566,10 @@ output: "出力" script: "スクリプト" disablePagesScript: "Pagesのスクリプトを無効にする" updateRemoteUser: "リモートユーザー情報の更新" +deleteUserAvatar: "アイコンを削除" +deleteUserAvatarConfirm: "アイコンを削除しますか?" +deleteUserBanner: "バナーを削除" +deleteUserBannerConfirm: "バナーを削除しますか?" deleteAllFiles: "すべてのファイルを削除" deleteAllFilesConfirm: "すべてのファイルを削除しますか?" removeAllFollowing: "フォローを全解除" @@ -1161,6 +1166,8 @@ useGroupedNotifications: "通知をグルーピングして表示する" signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。" cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。" doReaction: "リアクションする" +urlPreviewDenyList: "サムネイルの表示を制限するURL" +urlPreviewDenyListDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。一致した場合、サムネイルがぼかされて表示されます。" _announcement: forExistingUsers: "既存ユーザーのみ" diff --git a/packages/backend/migration/1699284486293-urlPreviewDenyList.js b/packages/backend/migration/1699284486293-urlPreviewDenyList.js new file mode 100644 index 000000000..4b921ad57 --- /dev/null +++ b/packages/backend/migration/1699284486293-urlPreviewDenyList.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class UrlPreviewDenyList1699284486293 { + name = 'UrlPreviewDenyList1699284486293' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "urlPreviewDenyList" character varying(3072) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "urlPreviewDenyList"`); + } +} diff --git a/packages/backend/src/core/FeaturedService.ts b/packages/backend/src/core/FeaturedService.ts index 9617f8388..078d2f522 100644 --- a/packages/backend/src/core/FeaturedService.ts +++ b/packages/backend/src/core/FeaturedService.ts @@ -38,7 +38,7 @@ export class FeaturedService { redisTransaction.expire( `${name}:${currentWindow}`, (windowRange * 3) / 1000, - 'NX'); // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定 + ); await redisTransaction.exec(); } @@ -48,10 +48,10 @@ export class FeaturedService { const previousWindow = currentWindow - 1; const redisPipeline = this.redisClient.pipeline(); - redisPipeline.zrange( - `${name}:${currentWindow}`, 0, threshold, 'REV', 'WITHSCORES'); - redisPipeline.zrange( - `${name}:${previousWindow}`, 0, threshold, 'REV', 'WITHSCORES'); + redisPipeline.zrevrange( + `${name}:${currentWindow}`, 0, threshold, 'WITHSCORES'); + redisPipeline.zrevrange( + `${name}:${previousWindow}`, 0, threshold, 'WITHSCORES'); const [currentRankingResult, previousRankingResult] = await redisPipeline.exec().then(result => result ? result.map(r => (r[1] ?? []) as string[]) : [[], []]); const ranking = new Map(); diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index 17da885c1..7abf8370d 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -52,20 +52,20 @@ export class FetchInstanceMetadataService { @bindThis public async tryLock(host: string): Promise { - const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'EX', 60 * 5, 'NX', 'GET'); - return mutex !== '1'; + const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, Date.now(), 'EX', 60 * 5, 'NX'); + return mutex !== null; } @bindThis public unlock(host: string): Promise { - return this.redisClient.del(`fetchInstanceMetadata:mutex:${host}`); + return this.redisClient.unlink(`fetchInstanceMetadata:mutex:${host}`); } @bindThis public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise { const host = instance.host; // Acquire mutex to ensure no parallel runs - if (!await this.tryLock(host)) return; + if (!await this.tryLock(host) && !force) return; try { if (!force) { const _instance = await this.federatedInstanceService.fetch(host); diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts index 5945dc291..7b2cf8467 100644 --- a/packages/backend/src/core/WebAuthnService.ts +++ b/packages/backend/src/core/WebAuthnService.ts @@ -7,7 +7,8 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { generateAuthenticationOptions, - generateRegistrationOptions, verifyAuthenticationResponse, + generateRegistrationOptions, + verifyAuthenticationResponse, verifyRegistrationResponse, } from '@simplewebauthn/server'; import { AttestationFormat, isoCBOR } from '@simplewebauthn/server/helpers'; diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 360239f50..00999bbec 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -518,4 +518,9 @@ export class MiMeta { default: 0, }) public notesPerOneAd: number; + + @Column('varchar', { + length: 3072, array: true, default: '{}', + }) + public urlPreviewDenyList: string[]; } diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 17b537c74..aa2e14101 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -27,6 +27,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js'; import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js'; import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; +import * as ep___admin_deleteUserAvatar from './endpoints/admin/delete-user-avatar.js'; +import * as ep___admin_deleteUserBanner from './endpoints/admin/delete-user-banner.js'; import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; @@ -389,6 +391,8 @@ const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-de const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default }; const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default }; const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default }; +const $admin_deleteUserAvatar: Provider = { provide: 'ep:admin/delete-user-avatar', useClass: ep___admin_deleteUserAvatar.default }; +const $admin_deleteUserBanner: Provider = { provide: 'ep:admin/delete-user-banner', useClass: ep___admin_deleteUserBanner.default }; const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default }; const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default }; const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default }; @@ -755,6 +759,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_avatarDecorations_list, $admin_avatarDecorations_update, $admin_deleteAllFilesOfAUser, + $admin_deleteUserAvatar, + $admin_deleteUserBanner, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, $admin_drive_files, @@ -1115,6 +1121,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_avatarDecorations_list, $admin_avatarDecorations_update, $admin_deleteAllFilesOfAUser, + $admin_deleteUserAvatar, + $admin_deleteUserBanner, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, $admin_drive_files, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 91fa98848..6a611c815 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -27,6 +27,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js'; import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js'; import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; +import * as ep___admin_deleteUserAvatar from './endpoints/admin/delete-user-avatar.js'; +import * as ep___admin_deleteUserBanner from './endpoints/admin/delete-user-banner.js'; import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; @@ -387,6 +389,8 @@ const eps = [ ['admin/avatar-decorations/list', ep___admin_avatarDecorations_list], ['admin/avatar-decorations/update', ep___admin_avatarDecorations_update], ['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser], + ['admin/delete-user-avatar', ep___admin_deleteUserAvatar], + ['admin/delete-user-banner', ep___admin_deleteUserBanner], ['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles], ['admin/drive/cleanup', ep___admin_drive_cleanup], ['admin/drive/files', ep___admin_drive_files], diff --git a/packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts b/packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts new file mode 100644 index 000000000..bdc1357d7 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error('user not found'); + } + + await this.usersRepository.update(user.id, { + avatar: null, + avatarId: null, + avatarUrl: null, + avatarBlurhash: null, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts b/packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts new file mode 100644 index 000000000..4d8d1bcc5 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error('user not found'); + } + + await this.usersRepository.update(user.id, { + banner: null, + bannerId: null, + bannerUrl: null, + bannerBlurhash: null, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 73c84a867..400db13fd 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -315,6 +315,14 @@ export const meta = { type: 'number', optional: false, nullable: false, }, + urlPreviewDenyList: { + type: 'array', + optional: true, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, }, }, } as const; @@ -429,6 +437,7 @@ export default class extends Endpoint { // eslint- perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, notesPerOneAd: instance.notesPerOneAd, + urlPreviewDenyList: instance.urlPreviewDenyList, }; }); } 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 c58569a31..5e1e4dd0b 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -133,6 +133,9 @@ export const paramDef = { type: 'string', }, }, + urlPreviewDenyList: { type: 'array', nullable: true, items: { + type: 'string', + } }, }, required: [], } as const; @@ -173,6 +176,11 @@ export default class extends Endpoint { // eslint- return h !== '' && h !== lv && !set.blockedHosts?.includes(h); }); } + + if (Array.isArray(ps.urlPreviewDenyList)) { + set.urlPreviewDenyList = ps.urlPreviewDenyList.filter(Boolean); + } + if (ps.themeColor !== undefined) { set.themeColor = ps.themeColor; } diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index d590244e3..76e847b7c 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { summaly } from 'summaly'; +import RE2 from 're2'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; @@ -94,6 +95,23 @@ export class UrlPreviewService { summary.icon = this.wrap(summary.icon); summary.thumbnail = this.wrap(summary.thumbnail); + const includeDenyList = meta.urlPreviewDenyList.some(filter => { + // represents RegExp + const regexp = /^\/(.+)\/(.*)$/.exec(filter); + // This should never happen due to input sanitisation. + if (!regexp) { + const words = filter.split(' '); + return words.every(keyword => summary.url.includes(keyword)); + } + try { + return new RE2(regexp[1], regexp[2]).test(summary.url); + } catch (err) { + // This should never happen due to input sanitisation. + return false; + } + }); + if (includeDenyList) summary.sensitive = true; + // Cache 7days reply.header('Cache-Control', 'max-age=604800, immutable'); diff --git a/packages/backend/test/docker-compose.yml b/packages/backend/test/docker-compose.yml index da6c01dda..286a6607a 100644 --- a/packages/backend/test/docker-compose.yml +++ b/packages/backend/test/docker-compose.yml @@ -1,13 +1,13 @@ version: "3" services: - redistest: - image: redis:7 + keydbtest: + image: eqalpha/keydb:latest ports: - "127.0.0.1:56312:6379" dbtest: - image: postgres:13 + image: postgres:15 ports: - "127.0.0.1:54312:5432" environment: diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts index 34200899d..3e0b06575 100644 --- a/packages/backend/test/unit/FetchInstanceMetadataService.ts +++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts @@ -21,9 +21,10 @@ import type { TestingModule } from '@nestjs/testing'; function mockRedis() { const hash = {}; const set = jest.fn((key, value) => { - const ret = hash[key]; + // このテストで呼び出すSETにはNXオプションが付いてる + if (hash[key]) return null; hash[key] = value; - return ret; + return 'OK'; }); return set; } diff --git a/packages/frontend/src/components/MkAnimBg.vue b/packages/frontend/src/components/MkAnimBg.vue index 70d101a9d..6bda74a81 100644 --- a/packages/frontend/src/components/MkAnimBg.vue +++ b/packages/frontend/src/components/MkAnimBg.vue @@ -24,8 +24,16 @@ const props = withDefaults(defineProps<{ function loadShader(gl, type, source) { const shader = gl.createShader(type); - gl.shaderSource(shader, source); - gl.compileShader(shader); + try { + gl.shaderSource(shader, source); + gl.compileShader(shader); + } catch (error) { + alert( + `failed to compile shader: ${error} ${gl.getShaderInfoLog(shader)}`, + ); + gl.deleteShader(shader); + return null; + } if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert( diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index 08297ea5b..c745c6b3f 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -5,9 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index e9253eb5f..c8248c210 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -73,18 +73,20 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.customEmojis }}
- {{ category || i18n.ts.other }} + {{ child.category || i18n.ts.other }}
{{ i18n.ts.emoji }}
- {{ category }} + {{ category }}
@@ -100,7 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, shallowRef, computed, watch, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; import XSection from '@/components/MkEmojiPicker.section.vue'; -import { emojilist, emojiCharByCategory, UnicodeEmojiDef, unicodeEmojiCategories as categories, getEmojiName } from '@/scripts/emojilist.js'; +import { emojilist, emojiCharByCategory, UnicodeEmojiDef, unicodeEmojiCategories as categories, getEmojiName, CustomEmojiFolderTree } from '@/scripts/emojilist.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import * as os from '@/os.js'; import { isTouchUsing } from '@/scripts/touch.js'; @@ -144,6 +146,39 @@ const searchResultCustom = ref([]); const searchResultUnicode = ref([]); const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index'); +const customEmojiFolderRoot: CustomEmojiFolderTree = { category: "", children: [] }; + +function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): CustomEmojiFolderTree { + const parts = (input && input !== 'null' ? input : '').split(' / '); + let currentNode: CustomEmojiFolderTree = root; + + for (const part of parts) { + const path = currentNode.category ? `${currentNode.category} / ${part}` : part; + + let existingNode = currentNode.children.find((node) => node.category === path); + if (!existingNode) { + const newNode: CustomEmojiFolderTree = { + category: path, + children: [], + }; + currentNode.children.push(newNode); + existingNode = newNode; + } + + currentNode = existingNode; + } + + return currentNode; +} + +customEmojiCategories.value.forEach(ec => { + if (ec !== null) { + parseAndMergeCategories(ec, customEmojiFolderRoot); + } +}); + +parseAndMergeCategories('', customEmojiFolderRoot); + watch(q, () => { if (emojisEl.value) emojisEl.value.scrollTop = 0; @@ -573,8 +608,7 @@ defineExpose({ position: sticky; top: 0; left: 0; - height: 32px; - line-height: 32px; + line-height: 28px; z-index: 1; padding: 0 8px; font-size: 12px; diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 77e66f016..5386fdf25 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue index 00c1d3808..4db341cd5 100644 --- a/packages/frontend/src/components/MkPullToRefresh.vue +++ b/packages/frontend/src/components/MkPullToRefresh.vue @@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue index 14bc6917a..40e4dcee7 100644 --- a/packages/frontend/src/ui/deck/list-column.vue +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only -->