diff --git a/.config/example.yml b/.config/example.yml index 503f924c8c..dcc7bff11b 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -131,11 +131,20 @@ proxyBypassHosts: # Media Proxy # Reference Implementation: https://github.com/misskey-dev/media-proxy +# * Deliver a common cache between instances +# * Perform image compression (on a different server resource than the main process) #mediaProxy: https://example.com/proxy # Proxy remote files (default: false) +# Proxy remote files by this instance or mediaProxy to prevent remote files from running in remote domains. #proxyRemoteFiles: true +# Movie Thumbnail Generation URL +# There is no reference implementation. +# For example, Misskey will point to the following URL: +# https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 +#videoThumbnailGenerator: https://example.com + # Sign to ActivityPub GET request (default: true) signToActivityPubGet: true diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000..b6ebcf6ad3 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1 @@ +FROM mcr.microsoft.com/devcontainers/javascript-node:0-18 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..fde7ec0f2b --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,11 @@ +{ + "name": "Misskey", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspace", + "features": { + "ghcr.io/devcontainers-contrib/features/pnpm:2": {} + }, + "forwardPorts": [3000], + "postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh" +} diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml new file mode 100644 index 0000000000..8a363a15dc --- /dev/null +++ b/.devcontainer/devcontainer.yml @@ -0,0 +1,146 @@ +#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# Misskey configuration +#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +# ┌─────┐ +#───┘ URL └───────────────────────────────────────────────────── + +# Final accessible URL seen by a user. +url: http://127.0.0.1:3000/ + +# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE +# URL SETTINGS AFTER THAT! + +# ┌───────────────────────┐ +#───┘ Port and TLS settings └─────────────────────────────────── + +# +# Misskey requires a reverse proxy to support HTTPS connections. +# +# +----- https://example.tld/ ------------+ +# +------+ |+-------------+ +----------------+| +# | User | ---> || Proxy (443) | ---> | Misskey (3000) || +# +------+ |+-------------+ +----------------+| +# +---------------------------------------+ +# +# You need to set up a reverse proxy. (e.g. nginx) +# An encrypted connection with HTTPS is highly recommended +# because tokens may be transferred in GET requests. + +# The port that your Misskey server should listen on. +port: 3000 + +# ┌──────────────────────────┐ +#───┘ PostgreSQL configuration └──────────────────────────────── + +db: + host: db + port: 5432 + + # Database name + db: misskey + + # Auth + user: postgres + pass: postgres + + # Whether disable Caching queries + #disableCache: true + + # Extra Connection options + #extra: + # ssl: true + +# ┌─────────────────────┐ +#───┘ Redis configuration └───────────────────────────────────── + +redis: + host: redis + port: 6379 + #family: 0 # 0=Both, 4=IPv4, 6=IPv6 + #pass: example-pass + #prefix: example-prefix + #db: 1 + +# ┌─────────────────────────────┐ +#───┘ Elasticsearch configuration └───────────────────────────── + +#elasticsearch: +# host: localhost +# port: 9200 +# ssl: false +# user: +# pass: + +# ┌───────────────┐ +#───┘ ID generation └─────────────────────────────────────────── + +# You can select the ID generation method. +# You don't usually need to change this setting, but you can +# change it according to your preferences. + +# Available methods: +# aid ... Short, Millisecond accuracy +# meid ... Similar to ObjectID, Millisecond accuracy +# ulid ... Millisecond accuracy +# objectid ... This is left for backward compatibility + +# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE +# ID SETTINGS AFTER THAT! + +id: 'aid' + +# ┌─────────────────────┐ +#───┘ Other configuration └───────────────────────────────────── + +# Whether disable HSTS +#disableHsts: true + +# Number of worker processes +#clusterLimit: 1 + +# Job concurrency per worker +# deliverJobConcurrency: 128 +# inboxJobConcurrency: 16 + +# Job rate limiter +# deliverJobPerSec: 128 +# inboxJobPerSec: 16 + +# Job attempts +# deliverJobMaxAttempts: 12 +# inboxJobMaxAttempts: 8 + +# IP address family used for outgoing request (ipv4, ipv6 or dual) +#outgoingAddressFamily: ipv4 + +# Proxy for HTTP/HTTPS +#proxy: http://127.0.0.1:3128 + +proxyBypassHosts: + - api.deepl.com + - api-free.deepl.com + - www.recaptcha.net + - hcaptcha.com + - challenges.cloudflare.com + +# Proxy for SMTP/SMTPS +#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT +#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4 +#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 + +# Media Proxy +#mediaProxy: https://example.com/proxy + +# Proxy remote files (default: false) +#proxyRemoteFiles: true + +# Sign to ActivityPub GET request (default: true) +signToActivityPubGet: true + +allowedPrivateNetworks: [ + '127.0.0.1/32' +] + +# Upload or download file size limits (bytes) +#maxFileSize: 262144000 diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000000..6ec3c86a4a --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,53 @@ +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + + volumes: + - ../:/workspace:cached + + command: sleep infinity + + networks: + - internal_network + - external_network + + redis: + restart: always + image: redis:7-alpine + networks: + - internal_network + volumes: + - redis-data:/data + healthcheck: + test: "redis-cli ping" + interval: 5s + retries: 20 + + db: + restart: unless-stopped + image: postgres:15-alpine + networks: + - internal_network + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: misskey + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" + interval: 5s + retries: 20 + +volumes: + postgres-data: + redis-data: + +networks: + internal_network: + internal: true + external_network: diff --git a/.devcontainer/init.sh b/.devcontainer/init.sh new file mode 100755 index 0000000000..450c3920c3 --- /dev/null +++ b/.devcontainer/init.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -xe + +sudo chown -R node /workspace +git submodule update --init +pnpm install --frozen-lockfile +cp .devcontainer/devcontainer.yml .config/default.yml +pnpm build +pnpm migrate diff --git a/.editorconfig b/.editorconfig index edccf3a9d5..6db1367645 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,6 +5,7 @@ indent_style = tab indent_size = 2 charset = utf-8 insert_final_newline = true +end_of_line = lf [*.yml] indent_style = space diff --git a/.gitattributes b/.gitattributes index a175917f31..246ecb0a60 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,4 @@ *.glb -diff -text *.blend -diff -text *.afdesign -diff -text +* text=auto eol=lf diff --git a/.github/workflows/check_copyright_year.yml b/.github/workflows/check_copyright_year.yml index 99799672f2..8daea44a83 100644 --- a/.github/workflows/check_copyright_year.yml +++ b/.github/workflows/check_copyright_year.yml @@ -1,18 +1,18 @@ -name: Check copyright year - -on: - push: - branches: - - master - - develop - -jobs: - check_copyright_year: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3.2.0 - - run: | - if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then - echo "Please change copyright year!" - exit 1 - fi +name: Check copyright year + +on: + push: + branches: + - master + - develop + +jobs: + check_copyright_year: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3.2.0 + - run: | + if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then + echo "Please change copyright year!" + exit 1 + fi diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml index 2233a8cc3f..250e93b25d 100644 --- a/.github/workflows/docker-develop.yml +++ b/.github/workflows/docker-develop.yml @@ -15,7 +15,10 @@ jobs: - name: Check out the repo uses: actions/checkout@v3.3.0 - name: Set up Docker Buildx + id: buildx uses: docker/setup-buildx-action@v2.3.0 + with: + platforms: linux/amd64,linux/arm64 - name: Docker meta id: meta uses: docker/metadata-action@v4 @@ -27,10 +30,13 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and Push to Docker Hub - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: + builder: ${{ steps.buildx.outputs.name }} context: . push: true + platforms: ${{ steps.buildx.outputs.platforms }} + provenance: false tags: noridev/cherrypick:develop labels: develop cache-from: type=gha diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index df792f465b..5cc88569b7 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -13,6 +13,11 @@ jobs: steps: - name: Check out the repo uses: actions/checkout@v3.3.0 + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2.3.0 + with: + platforms: linux/amd64,linux/arm64 - name: Docker meta id: meta uses: docker/metadata-action@v4 @@ -31,9 +36,14 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and Push to Docker Hub - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: + builder: ${{ steps.buildx.outputs.name }} context: . push: true + platforms: ${{ steps.buildx.outputs.platforms }} + provenance: false tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b88b97ab0c..6a579bffc8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,54 +1,54 @@ -name: Lint - -on: - push: - branches: - - master - - develop - pull_request: - -jobs: - pnpm_install: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3.3.0 - with: - fetch-depth: 0 - submodules: true - - uses: pnpm/action-setup@v2 - with: - version: 7 - run_install: false - - uses: actions/setup-node@v3.6.0 - with: - node-version: 18.x - cache: 'pnpm' - - run: corepack enable - - run: pnpm i --frozen-lockfile - - lint: - needs: [pnpm_install] - runs-on: ubuntu-latest - continue-on-error: true - strategy: - matrix: - workspace: - - backend - - frontend - - sw - steps: - - uses: actions/checkout@v3.3.0 - with: - fetch-depth: 0 - submodules: true - - uses: pnpm/action-setup@v2 - with: - version: 7 - run_install: false - - uses: actions/setup-node@v3.6.0 - with: - node-version: 18.x - cache: 'pnpm' - - run: corepack enable - - run: pnpm i --frozen-lockfile - - run: pnpm --filter ${{ matrix.workspace }} run lint +name: Lint + +on: + push: + branches: + - master + - develop + pull_request: + +jobs: + pnpm_install: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3.3.0 + with: + fetch-depth: 0 + submodules: true + - uses: pnpm/action-setup@v2 + with: + version: 7 + run_install: false + - uses: actions/setup-node@v3.6.0 + with: + node-version: 18.x + cache: 'pnpm' + - run: corepack enable + - run: pnpm i --frozen-lockfile + + lint: + needs: [pnpm_install] + runs-on: ubuntu-latest + continue-on-error: true + strategy: + matrix: + workspace: + - backend + - frontend + - sw + steps: + - uses: actions/checkout@v3.3.0 + with: + fetch-depth: 0 + submodules: true + - uses: pnpm/action-setup@v2 + with: + version: 7 + run_install: false + - uses: actions/setup-node@v3.6.0 + with: + node-version: 18.x + cache: 'pnpm' + - run: corepack enable + - run: pnpm i --frozen-lockfile + - run: pnpm --filter ${{ matrix.workspace }} run lint diff --git a/.gitignore b/.gitignore index 6ed208f9aa..cee9d5a682 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ coverage !/.config/docker_example.yml !/.config/docker_example.env docker-compose.yml +!/.devcontainer/docker-compose.yml # cherrypick /build diff --git a/CHANGELOG.md b/CHANGELOG.md index 6705535f00..81d81f5de0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,44 @@ You should also include the user name that made the change. --> +## 13.x.x (unreleased) + +### Improvements +- Server: URLプレビュー(summaly)はプロキシを通すように + +### Bugfixes +- + +## 13.6.1 (2023/02/12) + +### Improvements +- アニメーションを少なくする設定の時、MkPageHeaderのタブアニメーションを無効化 +- Backend: activitypub情報がcorsでブロックされないようヘッダーを追加 +- enhance: レートリミットを0%にできるように +- チャンネル内Renoteを行えるように + +### Bugfixes +- Client: ユーザーページでアクティビティを見ることができない問題を修正 + +## 13.6.0 (2023/02/11) + +### Improvements +- MkPageHeaderをごっそり変えた + * モバイルではヘッダーは上下に分割され、下段にタブが表示されるように + * iconOnlyのタブ項目がアクティブな場合にはタブのタイトルを表示するように + * メインタイムラインではタイトルを表示しない + * メインタイムラインかつモバイルで表示される左上のアバターを選択するとアカウントメニューが開くように +- ユーザーページのノート一覧をタブとして分離 +- コンディショナルロールもバッジとして表示可能に +- enhance(client): ロールをより簡単に付与できるように +- enhance(client): 一度見たノートのRenoteは省略して表示するように +- enhance(client): 迷惑になる可能性のある投稿を行う前に警告を表示 +- リアクションの数が多い場合の表示を改善 +- 一部のMFM構文をopt-outに + +### Bugfixes +- Client: ユーザーページでタブがほとんど見れないことがないように + ## 13.5.6 (2023/02/10) ### Improvements diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e539926789..48d8a40dea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -111,6 +111,26 @@ command. - Vite HMR (just the `vite` command) is available. The behavior may be different from production. - Service Worker is watched by esbuild. +### Dev Container +Instead of running `pnpm` locally, you can use Dev Container to set up your development environment. +To use Dev Container, open the project directory on VSCode with Dev Containers installed. +**Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled. + +It will run the following command automatically inside the container. +``` bash +git submodule update --init +pnpm install --frozen-lockfile +cp .devcontainer/devcontainer.yml .config/default.yml +pnpm build +pnpm migrate +``` + +After finishing the migration, run the `pnpm dev` command to start the development server. + +``` bash +pnpm dev +``` + ## Testing - Test codes are located in [`/packages/backend/test`](/packages/backend/test). diff --git a/Dockerfile b/Dockerfile index 0bfd24bd9a..b439716bea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,5 @@ +# syntax = docker/dockerfile:1.4 + ARG NODE_VERSION=18.13.0-bullseye FROM node:${NODE_VERSION} AS builder @@ -14,16 +16,16 @@ RUN corepack enable WORKDIR /misskey -COPY ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] -COPY ["scripts", "./scripts"] -COPY ["packages/backend/package.json", "./packages/backend/"] -COPY ["packages/frontend/package.json", "./packages/frontend/"] -COPY ["packages/sw/package.json", "./packages/sw/"] +COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] +COPY --link ["scripts", "./scripts"] +COPY --link ["packages/backend/package.json", "./packages/backend/"] +COPY --link ["packages/frontend/package.json", "./packages/frontend/"] +COPY --link ["packages/sw/package.json", "./packages/sw/"] RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm i --frozen-lockfile --aggregate-output -COPY . ./ +COPY --link . ./ ARG NODE_ENV=production diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 527d90cbb1..590bb9d941 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -1,3 +1,4 @@ apiVersion: v2 name: cherrypick version: 0.0.0 +description: This chart is created for the purpose of previewing Pull Requests. Do not use this for production use. diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index c5941b774a..998d11e98f 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -886,56 +886,6 @@ _nsfw: respect: "اخف الوسائط ذات المحتوى الحساس" ignore: "اعرض الوسائط ذات المحتوى الحساس" force: "اخف كل الوسائط" -_mfm: - cheatSheet: "مرجع ملخص عن MFM" - intro: "MFM هي لغة ترميزية مخصصة يمكن استخدامها في عدّة أماكن في ميسكي. يمكنك مراجعة كل تعابيرها مع كيفية استخدامها هنا." - mention: "أشر الى" - mentionDescription: "يمكنك الإشارة لمستخدم معيّن من خلال كتابة @ متبوعة باسم مستخدم." - hashtag: "الوسوم" - hashtagDescription: "يمكنك تعيين وسم من خلال كتابة # متبوعة بالنص المطلوب." - url: "الرابط" - urlDescription: "يمكن عرض الروابط" - link: "رابط" - bold: "عريض" - boldDescription: "جعل الحروف أثخن لإبرازها." - small: "صغير" - smallDescription: "يعرض المحتوى صغيرًا ورفيعًا." - center: "وسط" - centerDescription: "يمركز المحتوى في الوَسَط." - quote: "اقتبس" - quoteDescription: "يعرض المحتوى كاقتباس" - emoji: "إيموجي مخصص" - emojiDescription: "إحاطة اسم الإيموجي بنقطتي تفسير سيستبدله بصورة الإيموجي." - search: "البحث" - searchDescription: "يعرض نصًا في صندوق البحث" - flip: "اقلب" - flipDescription: "يقلب المحتوى عموديًا أو أفقيًا" - jelly: "تأثير (هلام)" - jellyDescription: "يمنح المحتوى حركة هلامية." - tada: "تأثير (تادا)" - tadaDescription: "يمنح للمحتوى تأثير تادا" - jump: "تأثير (قفز)" - jumpDescription: "يمنح للمحتوى حركة قفز." - bounce: "تأثير (ارتداد)" - bounceDescription: "يمنح للمحتوى حركة ارتدادية" - shake: "تأثير (اهتزاز)" - shakeDescription: "يمنح المحتوى حركة اهتزازية." - spin: "تأثير (دوران)" - spinDescription: "يمنح المحتوى حركة دورانية." - x2: "كبير" - x2Description: "يُكبر المحتوى" - x3: "كبير جداً" - x3Description: "يُضخم المحتوى" - x4: "هائل" - x4Description: "يُضخم المحتوى أكثر مما سبق." - blur: "طمس" - blurDescription: "يطمس المحتوى، لكن بالتمرير فوقه سيظهر بوضوح." - font: "الخط" - fontDescription: "الخط المستخدم لعرض المحتوى." - rainbow: "قوس قزح" - rainbowDescription: "اجعل المحتوى يظهر بألوان الطيف" - rotate: "تدوير" - rotateDescription: "يُدير المحتوى بزاوية معيّنة." _instanceTicker: none: "لا تظهره بتاتًا" remote: "أظهر للمستخدمين البِعاد" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index da1e16a412..75ebf3a634 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -455,7 +455,6 @@ youHaveNoGroups: "আপনার কোন গ্রুপ নেই " joinOrCreateGroup: "একটি বিদ্যমান গ্রুপের আমন্ত্রণ পান বা একটি নতুন গ্রুপ তৈরি করুন৷" noHistory: "কোনো ইতিহাস নেই" signinHistory: "প্রবেশ করার ইতিহাস" -disableAnimatedMfm: "অ্যানিমেটেড MFM অক্ষম করুন" doing: "প্রক্রিয়া করছে..." category: "বিভাগ" tags: "ট‍্যাগসমূহ" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 8269520e56..0033a872ea 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -375,11 +375,6 @@ file: "Fitxers" _email: _follow: title: "t'ha seguit" -_mfm: - mention: "Menció" - quote: "Citar" - emoji: "Emojis personalitzats" - search: "Cercar" _instanceMute: instanceMuteDescription: "Silencia tots els impulsos dels servidors seleccionats, també els usuaris que responen a altres d'un servidor silenciat." _theme: diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 34a492ca35..2b0dd66c96 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -642,19 +642,6 @@ _registry: _aboutMisskey: allContributors: "Všichni přispěvatelé" source: "Zdrojový kód" -_mfm: - mention: "Zmínění" - hashtag: "Hashtag" - link: "Odkaz" - bold: "Tučně" - quote: "Citovat" - emoji: "Vlastní emoji" - search: "Vyhledávání" - flip: "Otočit" - tada: "Animace (tadá)" - blur: "Rozmazání" - font: "Font" - rainbow: "Duha" _channel: featured: "Trendy" _menuDisplay: diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 97fa79f9e9..07f8571518 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -467,7 +467,6 @@ youHaveNoGroups: "Keine Gruppen vorhanden" joinOrCreateGroup: "Lass dich zu einer Gruppe einladen oder erstelle deine eigene." noHistory: "Kein Verlauf gefunden" signinHistory: "Anmeldungsverlauf" -disableAnimatedMfm: "MFM, die Animationen enthalten, deaktivieren" doing: "In Bearbeitung …" category: "Kategorie" tags: "Schlagwörter" diff --git a/locales/el-GR.yml b/locales/el-GR.yml index c711683ffc..889be05578 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -298,11 +298,6 @@ cannotUploadBecauseNoFreeSpace: "Το ανέβασμα απέτυχε λόγω _email: _follow: title: "Έχετε ένα νέο ακόλουθο" -_mfm: - mention: "Επισήμανση" - quote: "Παράθεση" - emoji: "Επιπλέον emoji" - search: "Αναζήτηση" _channel: featured: "Δημοφιλή" _theme: diff --git a/locales/en-US.yml b/locales/en-US.yml index efced688e0..6d7698ec71 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -480,7 +480,8 @@ youHaveNoGroups: "You have no groups" joinOrCreateGroup: "Get invited to a group or create your own." noHistory: "No history available" signinHistory: "Login history" -disableAnimatedMfm: "Disable MFM with animation" +enableAdvancedMfm: "Enable advanced MFM" +enableAnimatedMfm: "Enable MFM with animation" doing: "Processing..." category: "Category" tags: "Tags" @@ -959,6 +960,10 @@ selectFromPresets: "Choose from presets" achievements: "Achievements" gotInvalidResponseError: "Invalid server response" gotInvalidResponseErrorDescription: "The server may be unreachable or undergoing maintenance. Please try again later." +thisPostMayBeAnnoying: "This note may annoy others." +thisPostMayBeAnnoyingHome: "Post to home timeline" +thisPostMayBeAnnoyingCancel: "Cancel" +thisPostMayBeAnnoyingIgnore: "Post anyway" _achievements: earnedAt: "Unlocked at" _types: diff --git a/locales/es-ES.yml b/locales/es-ES.yml index f777c2213b..ebda6252a8 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -467,7 +467,6 @@ youHaveNoGroups: "Sin grupos" joinOrCreateGroup: "Obtenga una invitación para unirse al grupos o puede crear su propio grupo." noHistory: "No hay datos en el historial" signinHistory: "Historial de ingresos" -disableAnimatedMfm: "Deshabilitar MFM que tiene animaciones" doing: "Voy en camino" category: "Categoría" tags: "Etiqueta" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index edb4e05d80..9e5588de80 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -464,7 +464,6 @@ youHaveNoGroups: "Vous n’avez aucun groupe" joinOrCreateGroup: "Vous pouvez être invité·e à rejoindre des groupes existants ou créer votre propre nouveau groupe." noHistory: "Pas d'historique" signinHistory: "Historique de connexion" -disableAnimatedMfm: "Désactiver MFM ayant des animations" doing: "En cours..." category: "Catégorie" tags: "Étiquettes" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index e3c4386a4f..17d7c472d6 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -464,7 +464,6 @@ youHaveNoGroups: "Kamu tidak memiliki grup" joinOrCreateGroup: "Bergabunglah dengan grup atau kamu dapat membuat grupmu sendiri." noHistory: "Tidak ada riwayat" signinHistory: "Riwayat masuk" -disableAnimatedMfm: "Nonaktifkan MFM dengan animasi" doing: "Sedang berkerja..." category: "Kategori" tags: "Tandai" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 14b68d5fbb..1f1a4a0531 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -464,7 +464,6 @@ youHaveNoGroups: "Nessun gruppo" joinOrCreateGroup: "Puoi creare il tuo gruppo o essere invitat@ a gruppi che già esistono." noHistory: "Nessuna cronologia" signinHistory: "Storico degli accessi al profilo" -disableAnimatedMfm: "Disabilità i MFM animati" doing: "In corso..." category: "Categoria" tags: "Tag" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 7a12ec9df9..6fc45187d0 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -116,6 +116,8 @@ renoted: "Renoteしました。" cantRenote: "この投稿はRenoteできません。" cantReRenote: "RenoteをRenoteすることはできません。" quote: "引用" +inChannelRenote: "チャンネル内Renote" +inChannelQuote: "チャンネル内引用" pinnedNote: "ピン留めされたノート" pinned: "ピン留め" you: "あなた" @@ -480,7 +482,8 @@ youHaveNoGroups: "グループがありません" joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループを作成してください。" noHistory: "履歴はありません" signinHistory: "ログイン履歴" -disableAnimatedMfm: "動きのあるMFMを無効にする" +enableAdvancedMfm: "高度なMFMを有効にする" +enableAnimatedMfm: "動きのあるMFMを有効にする" doing: "やっています" category: "カテゴリ" tags: "タグ" @@ -959,6 +962,14 @@ selectFromPresets: "プリセットから選択" achievements: "実績" gotInvalidResponseError: "サーバーの応答が無効です" gotInvalidResponseErrorDescription: "サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。" +thisPostMayBeAnnoying: "この投稿は迷惑になる可能性があります。" +thisPostMayBeAnnoyingHome: "ホームに投稿" +thisPostMayBeAnnoyingCancel: "やめる" +thisPostMayBeAnnoyingIgnore: "このまま投稿" +collapseRenotes: "見たことのあるRenoteを省略して表示" +internalServerError: "サーバー内部エラー" +internalServerErrorDescription: "サーバー内部で予期しないエラーが発生しました。" +copyErrorInfo: "エラー情報をコピー" _achievements: earnedAt: "獲得日時" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 3ee70966b7..8bb7ae96b7 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -467,7 +467,8 @@ youHaveNoGroups: "グループがあらへんねぇ。" joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループ作ってからやってな" noHistory: "履歴はあらへんねぇ。" signinHistory: "ログイン履歴" -disableAnimatedMfm: "動きがやかましいMFMを止める" +enableAdvancedMfm: "ややこしいMFMもありにする" +enableAnimatedMfm: "動きがやかましいMFMも許したる" doing: "やっとるがな" category: "カテゴリ" tags: "タグ" @@ -946,6 +947,8 @@ selectFromPresets: "プリセットから選ぶ" achievements: "実績" gotInvalidResponseError: "サーバー黙っとるわ、知らんけど" gotInvalidResponseErrorDescription: "サーバーいま日曜日。またきて月曜日。" +thisPostMayBeAnnoying: "この投稿は迷惑かもしらんで。" +collapseRenotes: "見たことあるRenoteは省略やで" _achievements: earnedAt: "貰った日ぃ" _types: diff --git a/locales/kab-KAB.yml b/locales/kab-KAB.yml index 7c2e3a065e..fcd490db12 100644 --- a/locales/kab-KAB.yml +++ b/locales/kab-KAB.yml @@ -61,10 +61,6 @@ account: "Imiḍan" _email: _follow: title: "Yeṭṭafaṛ-ik·em-id" -_mfm: - mention: "Bder" - search: "Nadi" - font: "Tasefsit" _theme: keys: mention: "Bder" diff --git a/locales/kn-IN.yml b/locales/kn-IN.yml index a87cf69b39..0aa165d2e4 100644 --- a/locales/kn-IN.yml +++ b/locales/kn-IN.yml @@ -64,8 +64,6 @@ file: "ಕಡತಗಳು" _email: _follow: title: "ಹಿಂಬಾಲಿಸಿದರು" -_mfm: - search: "ಹುಡುಕು" _sfx: notification: "ಅಧಿಸೂಚನೆಗಳು" _widgets: diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 21275a62b3..4979bcb186 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -142,6 +142,7 @@ unblockConfirm: "이 계정의 차단을 해제할까요? 상대방이 나를 suspendConfirm: "이 계정을 정지할까요?" unsuspendConfirm: "이 계정의 정지를 해제할까요?" selectList: "리스트 선택" +selectChannel: "채널 선택" selectAntenna: "안테나 선택" selectWidget: "위젯 선택" editWidgets: "위젯 편집" @@ -269,6 +270,8 @@ noMoreHistory: "타임머신이 더 이상은 돌아갈 수 없대요!" startMessaging: "대화 시작하기" nUsersRead: "{n}명이 읽음" agreeTo: "{0}에 동의" +agreeBelow: "아래 내용에 동의합니다" +basicNotesBeforeCreateAccount: "기본적인 주의사항" tos: "이용 약관" start: "시작하기" home: "홈" @@ -477,8 +480,9 @@ youHaveNoGroups: "그룹이 없어요" joinOrCreateGroup: "다른 그룹의 초대를 받거나, 직접 새 그룹을 만들 수 있어요!" noHistory: "기록이 없어요" signinHistory: "로그인 기록" -disableAnimatedMfm: "움직임이 있는 MFM 비활성화" -doing: "잠시만요.." +enableAdvancedMfm: "고급 MFM을 활성화" +enableAnimatedMfm: "움직임이 있는 MFM을 활성화" +doing: "잠시만 기다려 주세요" category: "카테고리" tags: "태그" docSource: "이 문서의 소스" @@ -875,6 +879,8 @@ failedToFetchAccountInformation: "계정 정보를 가져오지 못했어요" rateLimitExceeded: "요청 제한 횟수를 초과했어요! 나중에 다시 시도해 주세요." cropImage: "이미지 자르기" cropImageAsk: "이미지를 자르시겠어요?" +cropYes: "잘라내기" +cropNo: "그대로 사용" file: "파일" recentNHours: "최근 {n}시간" recentNDays: "최근 {n}일" @@ -953,6 +959,12 @@ cannotPerformTemporaryDescription: "조작 횟수 제한을 초과하여 일시 preset: "프리셋" selectFromPresets: "프리셋에서 선택" achievements: "도전 과제" +gotInvalidResponseError: "서버의 응답이 올바르지 않아요.." +gotInvalidResponseErrorDescription: "서버가 다운되었거나 점검 중일 가능성이 있어요. 잠시 후에 다시 접속해 주세요." +thisPostMayBeAnnoying: "이 게시물은 다른 유저에게 피해를 줄 가능성이 있어요!" +thisPostMayBeAnnoyingHome: "홈에 게시" +thisPostMayBeAnnoyingCancel: "그만두기" +thisPostMayBeAnnoyingIgnore: "이대로 게시" _achievements: earnedAt: "달성 일시" _types: @@ -1209,6 +1221,9 @@ _role: baseRole: "기본 역할" useBaseValue: "기본값 사용" chooseRoleToAssign: "할당할 역할 선택" + iconUrl: "아이콘 URL" + asBadge: "배지로 표시" + descriptionOfAsBadge: "활성화하면 유저명 옆에 역할의 아이콘이 표시돼요." canEditMembersByModerator: "모더레이터의 역할 수정 허용" descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 사용자를 할당하거나 삭제할 수 있어요. 꺼져 있으면 관리자만 사용자를 할당할 수 있어요." priority: "우선순위" @@ -1618,12 +1633,15 @@ _permissions: "read:gallery-likes": "갤러리의 좋아요를 확인합니다" "write:gallery-likes": "갤러리에 좋아요를 추가하거나 취소합니다" _auth: + shareAccessTitle: "애플리케이션 접근 허가" shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용할까요?" shareAccessAsk: "이 애플리케이션이 계정에 접근하는 것을 허용할까요?" - permissionAsk: "이 앱은 다음 권한을 요청해요" + permission: "{name}에서 다음 권한을 요청했어요" + permissionAsk: "이 앱은 다음 권한을 요청하고 있습니다" pleaseGoBack: "앱으로 돌아가서 계속 진행해 주세요" callback: "앱으로 돌아갈게요!" denied: "앗, 접근이 거부되었어요!" + pleaseLogin: "애플리케이션의 접근을 허가하려면 먼저 로그인해 주세요." _antennaSources: all: "모든 노트" homeTimeline: "팔로우중인 유저의 노트" diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index cfda4e0e9e..263a7eb18c 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -187,11 +187,6 @@ file: "ໄຟລ໌" _email: _follow: title: "ໄດ້ຕິດຕາມທ່ານ" -_mfm: - mention: "ໄດ້ກ່າວມາ" - quote: "ລວມຂໍ້ຄວາມອ້າງອີງ" - emoji: "ອີໂມຈິແບບກຳນົດເອງ" - search: "ຄົ້ນຫາ" _theme: keys: mention: "ໄດ້ກ່າວມາ" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index dfab49025c..4925792e35 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -427,11 +427,6 @@ loggedInAsBot: "Momenteel als bot ingelogd" _email: _follow: title: "volgde jou" -_mfm: - mention: "Vermelding" - quote: "Quote" - emoji: "Maatwerk emoji" - search: "Zoeken" _theme: keys: mention: "Vermelding" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index c10cdfd999..f0ee7913d6 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -461,7 +461,6 @@ youHaveNoGroups: "Nie masz żadnych grup" joinOrCreateGroup: "Uzyskaj zaproszenie do dołączenia do grupy lub utwórz własną grupę." noHistory: "Brak historii" signinHistory: "Historia logowania" -disableAnimatedMfm: "Wyłącz MFM z animacją" doing: "Przetwarzanie..." category: "Kategoria" tags: "Tagi" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index d4f74bdd0f..344a795c5d 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -475,11 +475,6 @@ file: "Ficheiros" _email: _follow: title: "Você tem um novo seguidor" -_mfm: - mention: "Menção" - quote: "Citar" - emoji: "Emoji personalizado" - search: "Buscar" _theme: keys: mention: "Menção" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 1e3bb31b4a..c5ab90245b 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -455,7 +455,6 @@ youHaveNoGroups: "Nu ai niciun grup" joinOrCreateGroup: "Primește o invitație într-un grup sau creează unul nou." noHistory: "Nu există istoric" signinHistory: "Istoric autentificări" -disableAnimatedMfm: "Dezactivează MFM cu animații" doing: "Se procesează..." category: "Categorie" tags: "Etichete" @@ -655,11 +654,6 @@ _role: _email: _follow: title: "te-a urmărit" -_mfm: - mention: "Mențiune" - quote: "Citează" - emoji: "Emoji personalizat" - search: "Caută" _theme: description: "Descriere" keys: diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index c54b6a3dca..aa2d084613 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -462,7 +462,6 @@ youHaveNoGroups: "У вас нет ни одной группы" joinOrCreateGroup: "Получайте приглашения в группы или создавайте свои собственные" noHistory: "История пока пуста" signinHistory: "Журнал посещений" -disableAnimatedMfm: "Отключение анимированной разметки MFM" doing: "В процессе" category: "Категория" tags: "Метки" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 2465ee06c9..a22bcb0c02 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -464,7 +464,6 @@ youHaveNoGroups: "Nemáte žiadne skupiny" joinOrCreateGroup: "Požiadajte o pozvanie do existujúcej skupiny alebo vytvorte novú." noHistory: "Žiadna história" signinHistory: "História prihlásení" -disableAnimatedMfm: "Vypnúť MFM s animáciou" doing: "Pracujem..." category: "Kategórie" tags: "Značky" diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index c5136a2086..8eb6041edc 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -371,11 +371,6 @@ pushNotificationNotSupported: "Din webbläsare eller instans har inte stöd för _email: _follow: title: "följde dig" -_mfm: - mention: "Nämn" - quote: "Citat" - emoji: "Anpassa emoji" - search: "Sök" _channel: setBanner: "Välj banner" removeBanner: "Ta bort banner" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 4cdbe5e869..5c82cebb04 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -465,7 +465,6 @@ youHaveNoGroups: "คุณยังไม่มีกลุ่ม" joinOrCreateGroup: "รับเชิญเข้าร่วมกลุ่มหรือสร้างกลุ่มของคุณเองเลยนะ" noHistory: "ไม่มีรายการ" signinHistory: "ประวัติการเข้าสู่ระบบ" -disableAnimatedMfm: "ปิดการใช้งาน MFM ด้วยแอนิเมชั่น" doing: "กำลังประมวลผล......" category: "หมวดหมู่" tags: "แท็ก" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index 3d16cee7ed..6463056f45 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -48,8 +48,6 @@ smtpUser: "Kullanıcı Adı" smtpPass: "Şifre" user: "Kullanıcı" searchByGoogle: "Arama" -_mfm: - search: "Arama" _sfx: notification: "Bildirim" _widgets: diff --git a/locales/ug-CN.yml b/locales/ug-CN.yml index a7504542d0..65ef841259 100644 --- a/locales/ug-CN.yml +++ b/locales/ug-CN.yml @@ -2,5 +2,3 @@ _lang_: "ياپونچە" search: "ئىزدەش" searchByGoogle: "ئىزدەش" -_mfm: - search: "ئىزدەش" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index cd9dd7e11c..8de529d0f2 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -166,7 +166,7 @@ recipient: "Отримувач" annotation: "Коментарі" federation: "Федіверс" instances: "Інстанс" -registeredAt: "Приєднався(лась)" +registeredAt: "Реєстрація" latestRequestReceivedAt: "Останній запит прийнято" latestStatus: "Останній статус" storageUsage: "Використання простору" @@ -263,7 +263,7 @@ activity: "Активність" images: "Зображення" birthday: "День народження" yearsOld: "{age} років" -registeredDate: "Приєднався(лась)" +registeredDate: "Приєднання" location: "Локація" theme: "Тема" themeForLightMode: "Світла тема" @@ -461,7 +461,6 @@ youHaveNoGroups: "Немає груп" joinOrCreateGroup: "Отримуйте запрошення до груп або створюйте свої власні групи." noHistory: "Історія порожня" signinHistory: "Історія входів" -disableAnimatedMfm: "Відключити анімації MFM" doing: "Виконується" category: "Категорія" tags: "Теги" @@ -1087,6 +1086,9 @@ _achievements: _outputHelloWorldOnScratchpad: title: "Hello, world!" description: "Вивести \"hello world\" у Скретчпаді" + _reactWithoutRead: + title: "Прочитали як слід?" + description: "Реакція на нотатку, що містить понад 100 символів, протягом 3 секунд після її публікації" _clickedClickHere: title: "Натисніть тут" description: "Натиснуто тут" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 6b9141bf5f..11c3986855 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -457,7 +457,6 @@ youHaveNoGroups: "Không có nhóm nào" joinOrCreateGroup: "Tham gia hoặc tạo một nhóm mới." noHistory: "Không có dữ liệu" signinHistory: "Lịch sử đăng nhập" -disableAnimatedMfm: "Tắt MFM với chuyển động" doing: "Đang xử lý..." category: "Phân loại" tags: "Thẻ" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 592a366341..e7b3b94c9d 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -467,7 +467,8 @@ youHaveNoGroups: "没有群组" joinOrCreateGroup: "请加入一个现有的群组,或者创建新群组。" noHistory: "没有历史记录" signinHistory: "登录历史" -disableAnimatedMfm: "禁用MFM动画" +enableAdvancedMfm: "启用扩展MFM" +enableAnimatedMfm: "启用MFM动画" doing: "正在进行" category: "类别" tags: "标签" @@ -946,6 +947,10 @@ selectFromPresets: "從預設值中選擇" achievements: "成就" gotInvalidResponseError: "服务器无应答" gotInvalidResponseErrorDescription: "您的网络连接可能出现了问题, 或是远程服务器暂时不可用. 请稍后重试。" +thisPostMayBeAnnoying: "这个帖子可能会让其他人感到困扰。" +thisPostMayBeAnnoyingHome: "发到首页" +thisPostMayBeAnnoyingCancel: "取消" +thisPostMayBeAnnoyingIgnore: "就这样发布" _achievements: earnedAt: "达成时间" _types: diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index c2f18e7cd5..2502eab68b 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -467,7 +467,8 @@ youHaveNoGroups: "找不到群組" joinOrCreateGroup: "請加入現有群組,或創建新群組。" noHistory: "沒有歷史紀錄" signinHistory: "登入歷史" -disableAnimatedMfm: "禁用MFM動畫" +enableAdvancedMfm: "啟用高級MFM" +enableAnimatedMfm: "啟用MFM動畫" doing: "正在進行" category: "類別" tags: "標籤" @@ -946,6 +947,11 @@ selectFromPresets: "從預設值中選擇" achievements: "成就" gotInvalidResponseError: "伺服器的回應無效" gotInvalidResponseErrorDescription: "伺服器可能已關閉或者在維護中,請稍後再試。" +thisPostMayBeAnnoying: "這篇貼文可能會造成別人的困擾。" +thisPostMayBeAnnoyingHome: "發布到首頁" +thisPostMayBeAnnoyingCancel: "退出" +thisPostMayBeAnnoyingIgnore: "直接發布貼文" +collapseRenotes: "省略顯示已看過的轉發貼文" _achievements: earnedAt: "獲得日期" _types: diff --git a/packages/backend/jest-resolver.cjs b/packages/backend/jest-resolver.cjs deleted file mode 100644 index 4424b800dc..0000000000 --- a/packages/backend/jest-resolver.cjs +++ /dev/null @@ -1,14 +0,0 @@ -// https://github.com/facebook/jest/issues/12270#issuecomment-1194746382 - -const nativeModule = require('node:module'); - -function resolver(module, options) { - const { basedir, defaultResolver } = options; - try { - return defaultResolver(module, options); - } catch (error) { - return nativeModule.createRequire(basedir).resolve(module); - } -} - -module.exports = resolver; diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs index f0a3dc16c2..2f11f6a3e9 100644 --- a/packages/backend/jest.config.cjs +++ b/packages/backend/jest.config.cjs @@ -83,7 +83,14 @@ module.exports = { // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module moduleNameMapper: { - "^@/(.*?).js": "/src/$1.ts", + // Do not resolve .wasm.js to .wasm by the rule below + '^(.+)\\.wasm\\.js$': '$1.wasm.js', + // SWC converts @/foo/bar.js to `../../src/foo/bar.js`, and then this rule + // converts it again to `../../src/foo/bar` which then can be resolved to + // `.ts` files. + // See https://github.com/swc-project/jest/issues/64#issuecomment-1029753225 + // TODO: Use `--allowImportingTsExtensions` on TypeScript 5.0 so that we can + // directly import `.ts` files without this hack. '^(\\.{1,2}/.*)\\.js$': '$1', }, @@ -112,7 +119,7 @@ module.exports = { // resetModules: false, // A path to a custom resolver - resolver: './jest-resolver.cjs', + // resolver: './jest-resolver.cjs', // Automatically restore mock state between every test restoreMocks: true, diff --git a/packages/backend/package.json b/packages/backend/package.json index 6ec2ef4b76..90acaac754 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -109,14 +109,14 @@ "speakeasy": "2.0.0", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "summaly": "2.7.0", + "summaly": "github:misskey-dev/summaly", "systeminformation": "5.17.8", "tinycolor2": "1.6.0", "tmp": "0.2.1", "tsc-alias": "1.8.2", "tsconfig-paths": "4.1.2", "twemoji-parser": "14.0.0", - "typeorm": "0.3.12", + "typeorm": "0.3.11", "typescript": "4.9.5", "ulid": "2.3.0", "unzipper": "0.10.11", diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 460c6615ff..a0c3d27c48 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -67,6 +67,7 @@ export type Source = { mediaProxy?: string; proxyRemoteFiles?: boolean; + videoThumbnailGenerator?: string; signToActivityPubGet?: boolean; }; @@ -89,6 +90,7 @@ export type Mixin = { clientManifestExists: boolean; mediaProxy: string; externalMediaProxyEnabled: boolean; + videoThumbnailGenerator: string | null; }; export type Config = Source & Mixin; @@ -144,6 +146,10 @@ export function loadConfig() { mixin.mediaProxy = externalMediaProxy ?? internalMediaProxy; mixin.externalMediaProxyEnabled = externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy; + mixin.videoThumbnailGenerator = config.videoThumbnailGenerator ? + config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator + : null; + if (!config.redis.prefix) config.redis.prefix = mixin.host; return Object.assign(config, mixin); diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index 5f6dfca0ca..d8ba7b169d 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -32,7 +32,7 @@ export class AccountUpdateService { // フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信 if (this.userEntityService.isLocalUser(user)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user)); + const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user)); this.apDeliverManagerService.deliverToFollowers(user, content); this.relayService.deliverToRelays(user, content); } diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 598a457e83..b15c967c85 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -7,7 +7,7 @@ import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import Logger from '@/logger.js'; -import type { IRemoteUser, User } from '@/models/entities/User.js'; +import type { RemoteUser, User } from '@/models/entities/User.js'; import { MetaService } from '@/core/MetaService.js'; import { DriveFile } from '@/models/entities/DriveFile.js'; import { IdService } from '@/core/IdService.js'; @@ -250,6 +250,14 @@ export class DriveService { @bindThis public async generateAlts(path: string, type: string, generateWeb: boolean) { if (type.startsWith('video/')) { + if (this.config.videoThumbnailGenerator != null) { + // videoThumbnailGeneratorが指定されていたら動画サムネイル生成はスキップ + return { + webpublic: null, + thumbnail: null, + }; + } + try { const thumbnail = await this.videoProcessingService.generateVideoThumbnail(path); return { @@ -391,7 +399,7 @@ export class DriveService { } @bindThis - private async deleteOldFile(user: IRemoteUser) { + private async deleteOldFile(user: RemoteUser) { const q = this.driveFilesRepository.createQueryBuilder('file') .where('file.userId = :userId', { userId: user.id }) .andWhere('file.isLink = FALSE'); @@ -492,7 +500,7 @@ export class DriveService { throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.'); } else { // (アバターまたはバナーを含まず)最も古いファイルを削除する - this.deleteOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as IRemoteUser); + this.deleteOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as RemoteUser); } } } diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index e32026b04f..375aa846cb 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -99,7 +99,6 @@ export class HttpRequestService { const res = await this.send(url, { method: 'GET', headers: Object.assign({ - 'User-Agent': this.config.userAgent, Accept: accept, }, headers ?? {}), timeout: 5000, @@ -114,7 +113,6 @@ export class HttpRequestService { const res = await this.send(url, { method: 'GET', headers: Object.assign({ - 'User-Agent': this.config.userAgent, Accept: accept, }, headers ?? {}), timeout: 5000, @@ -144,7 +142,10 @@ export class HttpRequestService { const res = await fetch(url, { method: args.method ?? 'GET', - headers: args.headers, + headers: { + 'User-Agent': this.config.userAgent, + ...(args.headers ?? {}) + }, body: args.body, size: args.size ?? 10 * 1024 * 1024, agent: (url) => this.getAgentByUrl(url), diff --git a/packages/backend/src/core/InstanceActorService.ts b/packages/backend/src/core/InstanceActorService.ts index 0b4a83c634..ee9ae0733f 100644 --- a/packages/backend/src/core/InstanceActorService.ts +++ b/packages/backend/src/core/InstanceActorService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; -import type { ILocalUser } from '@/models/entities/User.js'; +import type { LocalUser } from '@/models/entities/User.js'; import type { UsersRepository } from '@/models/index.js'; import { Cache } from '@/misc/cache.js'; import { DI } from '@/di-symbols.js'; @@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const; @Injectable() export class InstanceActorService { - private cache: Cache; + private cache: Cache; constructor( @Inject(DI.usersRepository) @@ -19,24 +19,24 @@ export class InstanceActorService { private createSystemUserService: CreateSystemUserService, ) { - this.cache = new Cache(Infinity); + this.cache = new Cache(Infinity); } @bindThis - public async getInstanceActor(): Promise { + public async getInstanceActor(): Promise { const cached = this.cache.get(null); if (cached) return cached; const user = await this.usersRepository.findOneBy({ host: IsNull(), username: ACTOR_USERNAME, - }) as ILocalUser | undefined; + }) as LocalUser | undefined; if (user) { this.cache.set(null, user); return user; } else { - const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as ILocalUser; + const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser; this.cache.set(null, created); return created; } diff --git a/packages/backend/src/core/MessagingService.ts b/packages/backend/src/core/MessagingService.ts index f4a1090658..3a8a25c602 100644 --- a/packages/backend/src/core/MessagingService.ts +++ b/packages/backend/src/core/MessagingService.ts @@ -5,7 +5,7 @@ import type { Config } from '@/config.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; import type { Note } from '@/models/entities/Note.js'; -import type { User, CacheableUser, IRemoteUser } from '@/models/entities/User.js'; +import type { User, RemoteUser } from '@/models/entities/User.js'; import type { UserGroup } from '@/models/entities/UserGroup.js'; import { QueueService } from '@/core/QueueService.js'; import { toArray } from '@/misc/prelude/array.js'; @@ -48,7 +48,7 @@ export class MessagingService { } @bindThis - public async createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: CacheableUser | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) { + public async createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: User | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) { const message = { id: this.idService.genId(), createdAt: new Date(), @@ -135,7 +135,7 @@ export class MessagingService { }))), } as Note; - const activity = this.apRendererService.renderActivity(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false, true), note)); + const activity = this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false, true), note)); this.queueService.deliver(user, activity, recipientUser.inbox); } @@ -158,7 +158,7 @@ export class MessagingService { if (this.userEntityService.isLocalUser(recipient)) this.globalEventService.publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id); if (this.userEntityService.isLocalUser(user) && this.userEntityService.isRemoteUser(recipient)) { - const activity = this.apRendererService.renderActivity(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), user)); + const activity = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), user)); this.queueService.deliver(user, activity, recipient.inbox); } } else if (message.groupId) { @@ -291,16 +291,16 @@ export class MessagingService { } @bindThis - public async deliverReadActivity(user: { id: User['id']; host: null; }, recipient: IRemoteUser, messages: MessagingMessage | MessagingMessage[]) { + public async deliverReadActivity(user: { id: User['id']; host: null; }, recipient: RemoteUser, messages: MessagingMessage | MessagingMessage[]) { messages = toArray(messages).filter(x => x.uri); const contents = messages.map(x => this.apRendererService.renderRead(user, x)); if (contents.length > 1) { const collection = this.apRendererService.renderOrderedCollection(null, contents.length, undefined, undefined, contents); - this.queueService.deliver(user, this.apRendererService.renderActivity(collection), recipient.inbox); + this.queueService.deliver(user, this.apRendererService.addContext(collection), recipient.inbox); } else { for (const content of contents) { - this.queueService.deliver(user, this.apRendererService.renderActivity(content), recipient.inbox); + this.queueService.deliver(user, this.apRendererService.addContext(content), recipient.inbox); } } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index e7265f71d2..6d2995c6e1 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -11,7 +11,7 @@ import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { App } from '@/models/entities/App.js'; import { concat } from '@/misc/prelude/array.js'; import { IdService } from '@/core/IdService.js'; -import type { User, ILocalUser, IRemoteUser } from '@/models/entities/User.js'; +import type { User, LocalUser, RemoteUser } from '@/models/entities/User.js'; import type { IPoll } from '@/models/entities/Poll.js'; import { Poll } from '@/models/entities/Poll.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; @@ -52,7 +52,7 @@ class NotificationManager { private notifier: { id: User['id']; }; private note: Note; private queue: { - target: ILocalUser['id']; + target: LocalUser['id']; reason: NotificationType; }[]; @@ -68,7 +68,7 @@ class NotificationManager { } @bindThis - public push(notifiee: ILocalUser['id'], reason: NotificationType) { + public push(notifiee: LocalUser['id'], reason: NotificationType) { // 自分自身へは通知しない if (this.notifier.id === notifiee) return; @@ -608,7 +608,7 @@ export class NoteCreateService { // メンションされたリモートユーザーに配送 for (const u of mentionedUsers.filter(u => this.userEntityService.isRemoteUser(u))) { - dm.addDirectRecipe(u as IRemoteUser); + dm.addDirectRecipe(u as RemoteUser); } // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 @@ -714,7 +714,7 @@ export class NoteCreateService { ? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note) : this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note); - return this.apRendererService.renderActivity(content); + return this.apRendererService.addContext(content); } @bindThis diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 4dad825097..571b625523 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -1,6 +1,6 @@ import { Brackets, In } from 'typeorm'; import { Injectable, Inject } from '@nestjs/common'; -import type { User, ILocalUser, IRemoteUser } from '@/models/entities/User.js'; +import type { User, LocalUser, RemoteUser } from '@/models/entities/User.js'; import type { Note, IMentionedRemoteUsers } from '@/models/entities/Note.js'; import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/index.js'; import { RelayService } from '@/core/RelayService.js'; @@ -78,7 +78,7 @@ export class NoteDeleteService { }); } - const content = this.apRendererService.renderActivity(renote + const content = this.apRendererService.addContext(renote ? this.apRendererService.renderUndo(this.apRendererService.renderAnnounce(renote.uri ?? `${this.config.url}/notes/${renote.id}`, note), user) : this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${note.id}`), user)); @@ -90,7 +90,7 @@ export class NoteDeleteService { for (const cascadingNote of cascadingNotes) { if (!cascadingNote.user) continue; if (!this.userEntityService.isLocalUser(cascadingNote.user)) continue; - const content = this.apRendererService.renderActivity(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); + const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); this.deliverToConcerned(cascadingNote.user, cascadingNote, content); } //#endregion @@ -159,11 +159,11 @@ export class NoteDeleteService { return await this.usersRepository.find({ where, - }) as IRemoteUser[]; + }) as RemoteUser[]; } @bindThis - private async deliverToConcerned(user: { id: ILocalUser['id']; host: null; }, note: Note, content: any) { + private async deliverToConcerned(user: { id: LocalUser['id']; host: null; }, note: Note, content: any) { this.apDeliverManagerService.deliverToFollowers(user, content); this.relayService.deliverToRelays(user, content); const remoteUsers = await this.getMentionedRemoteUsers(note); diff --git a/packages/backend/src/core/NotePiningService.ts b/packages/backend/src/core/NotePiningService.ts index bb6def1edb..3a9f832ac0 100644 --- a/packages/backend/src/core/NotePiningService.ts +++ b/packages/backend/src/core/NotePiningService.ts @@ -115,7 +115,7 @@ export class NotePiningService { const target = `${this.config.url}/users/${user.id}/collections/featured`; const item = `${this.config.url}/notes/${noteId}`; - const content = this.apRendererService.renderActivity(isAddition ? this.apRendererService.renderAdd(user, target, item) : this.apRendererService.renderRemove(user, target, item)); + const content = this.apRendererService.addContext(isAddition ? this.apRendererService.renderAdd(user, target, item) : this.apRendererService.renderRemove(user, target, item)); this.apDeliverManagerService.deliverToFollowers(user, content); this.relayService.deliverToRelays(user, content); diff --git a/packages/backend/src/core/PollService.ts b/packages/backend/src/core/PollService.ts index 042dcb3e67..94adbf2756 100644 --- a/packages/backend/src/core/PollService.ts +++ b/packages/backend/src/core/PollService.ts @@ -1,10 +1,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; +import type { NotesRepository, UsersRepository, PollsRepository, PollVotesRepository, User } from '@/models/index.js'; import type { Note } from '@/models/entities/Note.js'; import { RelayService } from '@/core/RelayService.js'; -import type { CacheableUser } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; @@ -39,7 +38,7 @@ export class PollService { } @bindThis - public async vote(user: CacheableUser, note: Note, choice: number) { + public async vote(user: User, note: Note, choice: number) { const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); if (poll == null) throw new Error('poll not found'); @@ -97,7 +96,7 @@ export class PollService { if (user == null) throw new Error('note not found'); if (this.userEntityService.isLocalUser(user)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user)); + const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user)); this.apDeliverManagerService.deliverToFollowers(user, content); this.relayService.deliverToRelays(user, content); } diff --git a/packages/backend/src/core/ProxyAccountService.ts b/packages/backend/src/core/ProxyAccountService.ts index 55b70bfc94..7ed322ae65 100644 --- a/packages/backend/src/core/ProxyAccountService.ts +++ b/packages/backend/src/core/ProxyAccountService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository } from '@/models/index.js'; -import type { ILocalUser, User } from '@/models/entities/User.js'; +import type { LocalUser, User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; @@ -16,9 +16,9 @@ export class ProxyAccountService { } @bindThis - public async fetch(): Promise { + public async fetch(): Promise { const meta = await this.metaService.fetch(); if (meta.proxyAccountId == null) return null; - return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as ILocalUser; + return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as LocalUser; } } diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 3806590059..9fccc14ee4 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -3,7 +3,7 @@ import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { EmojisRepository, BlockingsRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import type { IRemoteUser, User } from '@/models/entities/User.js'; +import type { RemoteUser, User } from '@/models/entities/User.js'; import type { Note } from '@/models/entities/Note.js'; import { IdService } from '@/core/IdService.js'; import type { NoteReaction } from '@/models/entities/NoteReaction.js'; @@ -85,7 +85,7 @@ export class ReactionService { } @bindThis - public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string) { + public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string | null) { // Check blocking if (note.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); @@ -177,11 +177,11 @@ export class ReactionService { //#region 配信 if (this.userEntityService.isLocalUser(user) && !note.localOnly) { - const content = this.apRendererService.renderActivity(await this.apRendererService.renderLike(record, note)); + const content = this.apRendererService.addContext(await this.apRendererService.renderLike(record, note)); const dm = this.apDeliverManagerService.createDeliverManager(user, content); if (note.userHost !== null) { const reactee = await this.usersRepository.findOneBy({ id: note.userId }); - dm.addDirectRecipe(reactee as IRemoteUser); + dm.addDirectRecipe(reactee as RemoteUser); } if (['public', 'home', 'followers'].includes(note.visibility)) { @@ -189,7 +189,7 @@ export class ReactionService { } else if (note.visibility === 'specified') { const visibleUsers = await Promise.all(note.visibleUserIds.map(id => this.usersRepository.findOneBy({ id }))); for (const u of visibleUsers.filter(u => u && this.userEntityService.isRemoteUser(u))) { - dm.addDirectRecipe(u as IRemoteUser); + dm.addDirectRecipe(u as RemoteUser); } } @@ -235,11 +235,11 @@ export class ReactionService { //#region 配信 if (this.userEntityService.isLocalUser(user) && !note.localOnly) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(await this.apRendererService.renderLike(exist, note), user)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(await this.apRendererService.renderLike(exist, note), user)); const dm = this.apDeliverManagerService.createDeliverManager(user, content); if (note.userHost !== null) { const reactee = await this.usersRepository.findOneBy({ id: note.userId }); - dm.addDirectRecipe(reactee as IRemoteUser); + dm.addDirectRecipe(reactee as RemoteUser); } dm.addFollowersRecipe(); dm.execute(); diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index a7408649b8..2e07825e9b 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; -import type { ILocalUser, User } from '@/models/entities/User.js'; +import type { LocalUser, User } from '@/models/entities/User.js'; import type { RelaysRepository, UsersRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { Cache } from '@/misc/cache.js'; @@ -34,16 +34,16 @@ export class RelayService { } @bindThis - private async getRelayActor(): Promise { + private async getRelayActor(): Promise { const user = await this.usersRepository.findOneBy({ host: IsNull(), username: ACTOR_USERNAME, }); - if (user) return user as ILocalUser; + if (user) return user as LocalUser; const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME); - return created as ILocalUser; + return created as LocalUser; } @bindThis @@ -56,7 +56,7 @@ export class RelayService { const relayActor = await this.getRelayActor(); const follow = await this.apRendererService.renderFollowRelay(relay, relayActor); - const activity = this.apRendererService.renderActivity(follow); + const activity = this.apRendererService.addContext(follow); this.queueService.deliver(relayActor, activity, relay.inbox); return relay; @@ -75,7 +75,7 @@ export class RelayService { const relayActor = await this.getRelayActor(); const follow = this.apRendererService.renderFollowRelay(relay, relayActor); const undo = this.apRendererService.renderUndo(follow, relayActor); - const activity = this.apRendererService.renderActivity(undo); + const activity = this.apRendererService.addContext(undo); this.queueService.deliver(relayActor, activity, relay.inbox); await this.relaysRepository.delete(relay.id); diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index dde4098624..b72dce5180 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -4,7 +4,7 @@ import chalk from 'chalk'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { UsersRepository } from '@/models/index.js'; -import type { IRemoteUser, User } from '@/models/entities/User.js'; +import type { RemoteUser, User } from '@/models/entities/User.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -60,7 +60,7 @@ export class RemoteUserResolveService { }); } - const user = await this.usersRepository.findOneBy({ usernameLower, host }) as IRemoteUser | null; + const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null; const acctLower = `${usernameLower}@${host}`; @@ -82,7 +82,7 @@ export class RemoteUserResolveService { const self = await this.resolveSelf(acctLower); if (user.uri !== self.href) { - // if uri mismatch, Fix (user@host <=> AP's Person id(IRemoteUser.uri)) mapping. + // if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping. this.logger.info(`uri missmatch: ${acctLower}`); this.logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`); diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index d15d8c0aee..b84d5e7585 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -3,7 +3,7 @@ import Redis from 'ioredis'; import { In } from 'typeorm'; import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; import { Cache } from '@/misc/cache.js'; -import type { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/models/entities/User.js'; +import type { User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { MetaService } from '@/core/MetaService.js'; @@ -211,8 +211,14 @@ export class RoleService implements OnApplicationShutdown { const assignedRoleIds = assigns.map(x => x.roleId); const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id)); - // コンディショナルロールも含めるのは負荷高そうだから一旦無し - return assignedBadgeRoles; + const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional')); + if (badgeCondRoles.length > 0) { + const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null; + const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula)); + return [...assignedBadgeRoles, ...matchedBadgeCondRoles]; + } else { + return assignedBadgeRoles; + } } @bindThis diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index d734328669..be37bad52e 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import Redis from 'ioredis'; import { IdService } from '@/core/IdService.js'; -import type { CacheableUser, User } from '@/models/entities/User.js'; +import type { User } from '@/models/entities/User.js'; import type { Blocking } from '@/models/entities/Blocking.js'; import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -117,7 +117,7 @@ export class UserBlockingService implements OnApplicationShutdown { }); if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderBlock(blocking)); + const content = this.apRendererService.addContext(this.apRendererService.renderBlock(blocking)); this.queueService.deliver(blocker, content, blockee.inbox); } } @@ -162,13 +162,13 @@ export class UserBlockingService implements OnApplicationShutdown { // リモートにフォローリクエストをしていたらUndoFollow送信 if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); this.queueService.deliver(follower, content, followee.inbox); } // リモートからフォローリクエストを受けていたらReject送信 if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee)); this.queueService.deliver(followee, content, follower.inbox); } } @@ -210,13 +210,13 @@ export class UserBlockingService implements OnApplicationShutdown { // リモートにフォローをしていたらUndoFollow送信 if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); this.queueService.deliver(follower, content, followee.inbox); } // リモートからフォローをされていたらRejectFollow送信 if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee)); this.queueService.deliver(followee, content, follower.inbox); } } @@ -236,7 +236,7 @@ export class UserBlockingService implements OnApplicationShutdown { } @bindThis - public async unblock(blocker: CacheableUser, blockee: CacheableUser) { + public async unblock(blocker: User, blockee: User) { const blocking = await this.blockingsRepository.findOneBy({ blockerId: blocker.id, blockeeId: blockee.id, @@ -261,7 +261,7 @@ export class UserBlockingService implements OnApplicationShutdown { // deliver if remote bloking if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker)); this.queueService.deliver(blocker, content, blockee.inbox); } } diff --git a/packages/backend/src/core/UserCacheService.ts b/packages/backend/src/core/UserCacheService.ts index 29a64f5848..fc383d1c08 100644 --- a/packages/backend/src/core/UserCacheService.ts +++ b/packages/backend/src/core/UserCacheService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; import type { UsersRepository } from '@/models/index.js'; import { Cache } from '@/misc/cache.js'; -import type { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/models/entities/User.js'; +import type { LocalUser, User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -11,10 +11,10 @@ import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class UserCacheService implements OnApplicationShutdown { - public userByIdCache: Cache; - public localUserByNativeTokenCache: Cache; - public localUserByIdCache: Cache; - public uriPersonCache: Cache; + public userByIdCache: Cache; + public localUserByNativeTokenCache: Cache; + public localUserByIdCache: Cache; + public uriPersonCache: Cache; constructor( @Inject(DI.redisSubscriber) @@ -27,10 +27,10 @@ export class UserCacheService implements OnApplicationShutdown { ) { //this.onMessage = this.onMessage.bind(this); - this.userByIdCache = new Cache(Infinity); - this.localUserByNativeTokenCache = new Cache(Infinity); - this.localUserByIdCache = new Cache(Infinity); - this.uriPersonCache = new Cache(Infinity); + this.userByIdCache = new Cache(Infinity); + this.localUserByNativeTokenCache = new Cache(Infinity); + this.localUserByIdCache = new Cache(Infinity); + this.uriPersonCache = new Cache(Infinity); this.redisSubscriber.on('message', this.onMessage); } @@ -58,7 +58,7 @@ export class UserCacheService implements OnApplicationShutdown { break; } case 'userTokenRegenerated': { - const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as ILocalUser; + const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as LocalUser; this.localUserByNativeTokenCache.delete(body.oldToken); this.localUserByNativeTokenCache.set(body.newToken, user); break; diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 2214a4862a..d8426512bf 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { CacheableUser, ILocalUser, IRemoteUser, User } from '@/models/entities/User.js'; +import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { QueueService } from '@/core/QueueService.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; @@ -21,16 +21,16 @@ import Logger from '../logger.js'; const logger = new Logger('following/create'); -type Local = ILocalUser | { - id: ILocalUser['id']; - host: ILocalUser['host']; - uri: ILocalUser['uri'] +type Local = LocalUser | { + id: LocalUser['id']; + host: LocalUser['host']; + uri: LocalUser['uri'] }; -type Remote = IRemoteUser | { - id: IRemoteUser['id']; - host: IRemoteUser['host']; - uri: IRemoteUser['uri']; - inbox: IRemoteUser['inbox']; +type Remote = RemoteUser | { + id: RemoteUser['id']; + host: RemoteUser['host']; + uri: RemoteUser['uri']; + inbox: RemoteUser['inbox']; }; type Both = Local | Remote; @@ -81,7 +81,7 @@ export class UserFollowingService { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) { // リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。 - const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee)); this.queueService.deliver(followee, content, follower.inbox); return; } else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) { @@ -130,7 +130,7 @@ export class UserFollowingService { await this.insertFollowingDoc(followee, follower); if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); this.queueService.deliver(followee, content, follower.inbox); } } @@ -293,13 +293,13 @@ export class UserFollowingService { } if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); this.queueService.deliver(follower, content, followee.inbox); } if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) { // local user has null host - const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee)); this.queueService.deliver(followee, content, follower.inbox); } } @@ -388,7 +388,7 @@ export class UserFollowingService { } if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderFollow(follower, followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)); this.queueService.deliver(follower, content, followee.inbox); } } @@ -403,7 +403,7 @@ export class UserFollowingService { }, ): Promise { if (this.userEntityService.isRemoteUser(followee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので this.queueService.deliver(follower, content, followee.inbox); @@ -434,7 +434,7 @@ export class UserFollowingService { followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, - follower: CacheableUser, + follower: User, ): Promise { const request = await this.followRequestsRepository.findOneBy({ followeeId: followee.id, @@ -448,7 +448,7 @@ export class UserFollowingService { await this.insertFollowingDoc(followee, follower); if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - const content = this.apRendererService.renderActivity(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee)); this.queueService.deliver(followee, content, follower.inbox); } @@ -556,7 +556,7 @@ export class UserFollowingService { followerId: follower.id, }); - const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request?.requestId ?? undefined), followee)); + const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, request?.requestId ?? undefined), followee)); this.queueService.deliver(followee, content, follower.inbox); } diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index c174394999..bc726a1feb 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -14,6 +14,8 @@ import { RoleService } from '@/core/RoleService.js'; @Injectable() export class UserListService { + public static TooManyUsersError = class extends Error {}; + constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -36,7 +38,7 @@ export class UserListService { userListId: list.id, }); if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) { - throw new Error('Too many users'); + throw new UserListService.TooManyUsersError(); } await this.userListJoiningsRepository.insert({ diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index df1664942f..02903a0590 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -35,7 +35,7 @@ export class UserSuspendService { if (this.userEntityService.isLocalUser(user)) { // 知り得る全SharedInboxにDelete配信 - const content = this.apRendererService.renderActivity(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user)); + const content = this.apRendererService.addContext(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user)); const queue: string[] = []; @@ -65,7 +65,7 @@ export class UserSuspendService { if (this.userEntityService.isLocalUser(user)) { // 知り得る全SharedInboxにUndo Delete配信 - const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user), user)); + const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user), user)); const queue: string[] = []; diff --git a/packages/backend/src/core/VideoProcessingService.ts b/packages/backend/src/core/VideoProcessingService.ts index ea5701decc..dd6c51c217 100644 --- a/packages/backend/src/core/VideoProcessingService.ts +++ b/packages/backend/src/core/VideoProcessingService.ts @@ -6,6 +6,7 @@ import { ImageProcessingService } from '@/core/ImageProcessingService.js'; import type { IImage } from '@/core/ImageProcessingService.js'; import { createTempDir } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; +import { appendQuery, query } from '@/misc/prelude/url.js'; @Injectable() export class VideoProcessingService { @@ -41,5 +42,18 @@ export class VideoProcessingService { cleanup(); } } + + @bindThis + public getExternalVideoThumbnailUrl(url: string): string | null { + if (this.config.videoThumbnailGenerator == null) return null; + + return appendQuery( + `${this.config.videoThumbnailGenerator}/thumbnail.webp`, + query({ + thumbnail: '1', + url, + }) + ) + } } diff --git a/packages/backend/src/core/activitypub/ApAudienceService.ts b/packages/backend/src/core/activitypub/ApAudienceService.ts index 64f01644a7..4c22297b37 100644 --- a/packages/backend/src/core/activitypub/ApAudienceService.ts +++ b/packages/backend/src/core/activitypub/ApAudienceService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import promiseLimit from 'promise-limit'; import { DI } from '@/di-symbols.js'; -import type { CacheableRemoteUser, CacheableUser } from '@/models/entities/User.js'; +import type { RemoteUser, User } from '@/models/entities/User.js'; import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js'; import { bindThis } from '@/decorators.js'; import { getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isPost, isRead, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; @@ -14,8 +14,8 @@ type Visibility = 'public' | 'home' | 'followers' | 'specified'; type AudienceInfo = { visibility: Visibility, - mentionedUsers: CacheableUser[], - visibleUsers: CacheableUser[], + mentionedUsers: User[], + visibleUsers: User[], }; @Injectable() @@ -26,16 +26,16 @@ export class ApAudienceService { } @bindThis - public async parseAudience(actor: CacheableRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise { + public async parseAudience(actor: RemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise { const toGroups = this.groupingAudience(getApIds(to), actor); const ccGroups = this.groupingAudience(getApIds(cc), actor); const others = unique(concat([toGroups.other, ccGroups.other])); - const limit = promiseLimit(2); + const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))), - )).filter((x): x is CacheableUser => x != null); + )).filter((x): x is User => x != null); if (toGroups.public.length > 0) { return { @@ -69,7 +69,7 @@ export class ApAudienceService { } @bindThis - private groupingAudience(ids: string[], actor: CacheableRemoteUser) { + private groupingAudience(ids: string[], actor: RemoteUser) { const groups = { public: [] as string[], followers: [] as string[], @@ -101,7 +101,7 @@ export class ApAudienceService { } @bindThis - private isFollowers(id: string, actor: CacheableRemoteUser) { + private isFollowers(id: string, actor: RemoteUser) { return ( id === (actor.followersUri ?? `${actor.uri}/followers`) ); diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 1d0c2d5da4..9a894826c8 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -3,13 +3,13 @@ import escapeRegexp from 'escape-regexp'; import { DI } from '@/di-symbols.js'; import type { MessagingMessagesRepository, NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import type { CacheableRemoteUser, CacheableUser } from '@/models/entities/User.js'; import { Cache } from '@/misc/cache.js'; import type { UserPublickey } from '@/models/entities/UserPublickey.js'; import { UserCacheService } from '@/core/UserCacheService.js'; import type { Note } from '@/models/entities/Note.js'; import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; import { bindThis } from '@/decorators.js'; +import { RemoteUser, User } from '@/models/entities/User.js'; import { getApId } from './type.js'; import { ApPersonService } from './models/ApPersonService.js'; import type { IObject } from './type.js'; @@ -122,7 +122,7 @@ export class ApDbResolverService { * AP Person => Misskey User in DB */ @bindThis - public async getUserFromApId(value: string | IObject): Promise { + public async getUserFromApId(value: string | IObject): Promise { const parsed = this.parseUri(value); if (parsed.local) { @@ -143,7 +143,7 @@ export class ApDbResolverService { */ @bindThis public async getAuthUserFromKeyId(keyId: string): Promise<{ - user: CacheableRemoteUser; + user: RemoteUser; key: UserPublickey; } | null> { const key = await this.publicKeyCache.fetch(keyId, async () => { @@ -159,7 +159,7 @@ export class ApDbResolverService { if (key == null) return null; return { - user: await this.userCacheService.findById(key.userId) as CacheableRemoteUser, + user: await this.userCacheService.findById(key.userId) as RemoteUser, key, }; } @@ -169,10 +169,10 @@ export class ApDbResolverService { */ @bindThis public async getAuthUserFromApId(uri: string): Promise<{ - user: CacheableRemoteUser; + user: RemoteUser; key: UserPublickey | null; } | null> { - const user = await this.apPersonService.resolvePerson(uri) as CacheableRemoteUser; + const user = await this.apPersonService.resolvePerson(uri) as RemoteUser; if (user == null) return null; diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 256cf12651..5e6ea69846 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -3,7 +3,7 @@ import { IsNull, Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js'; +import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import { QueueService } from '@/core/QueueService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -18,7 +18,7 @@ interface IFollowersRecipe extends IRecipe { interface IDirectRecipe extends IRecipe { type: 'Direct'; - to: IRemoteUser; + to: RemoteUser; } const isFollowers = (recipe: any): recipe is IFollowersRecipe => @@ -50,7 +50,7 @@ export class ApDeliverManagerService { * @param from Followee */ @bindThis - public async deliverToFollowers(actor: { id: ILocalUser['id']; host: null; }, activity: any) { + public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: any) { const manager = new DeliverManager( this.userEntityService, this.followingsRepository, @@ -68,7 +68,7 @@ export class ApDeliverManagerService { * @param to Target user */ @bindThis - public async deliverToUser(actor: { id: ILocalUser['id']; host: null; }, activity: any, to: IRemoteUser) { + public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: any, to: RemoteUser) { const manager = new DeliverManager( this.userEntityService, this.followingsRepository, @@ -132,7 +132,7 @@ class DeliverManager { * @param to To */ @bindThis - public addDirectRecipe(to: IRemoteUser) { + public addDirectRecipe(to: RemoteUser) { const recipe = { type: 'Direct', to, diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 76c8bf68df..62f3827343 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -2,7 +2,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import type { CacheableRemoteUser } from '@/models/entities/User.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { ReactionService } from '@/core/ReactionService.js'; import { RelayService } from '@/core/RelayService.js'; @@ -22,6 +21,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { QueueService } from '@/core/QueueService.js'; import { MessagingService } from '@/core/MessagingService.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, MessagingMessagesRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js'; +import { bindThis } from '@/decorators.js'; +import type { RemoteUser } from '@/models/entities/User.js'; import { getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isPost, isRead, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { ApNoteService } from './models/ApNoteService.js'; import { ApLoggerService } from './ApLoggerService.js'; @@ -32,7 +33,6 @@ import { ApPersonService } from './models/ApPersonService.js'; import { ApQuestionService } from './models/ApQuestionService.js'; import type { Resolver } from './ApResolverService.js'; import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IRead, IReject, IRemove, IUndo, IUpdate } from './type.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ApInboxService { @@ -87,7 +87,7 @@ export class ApInboxService { } @bindThis - public async performActivity(actor: CacheableRemoteUser, activity: IObject) { + public async performActivity(actor: RemoteUser, activity: IObject) { if (isCollectionOrOrderedCollection(activity)) { const resolver = this.apResolverService.createResolver(); for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { @@ -115,7 +115,7 @@ export class ApInboxService { } @bindThis - public async performOneActivity(actor: CacheableRemoteUser, activity: IObject): Promise { + public async performOneActivity(actor: RemoteUser, activity: IObject): Promise { if (actor.isSuspended) return; if (isCreate(activity)) { @@ -152,7 +152,7 @@ export class ApInboxService { } @bindThis - private async follow(actor: CacheableRemoteUser, activity: IFollow): Promise { + private async follow(actor: RemoteUser, activity: IFollow): Promise { const followee = await this.apDbResolverService.getUserFromApId(activity.object); if (followee == null) { @@ -168,7 +168,7 @@ export class ApInboxService { } @bindThis - private async like(actor: CacheableRemoteUser, activity: ILike): Promise { + private async like(actor: RemoteUser, activity: ILike): Promise { const targetUri = getApId(activity.object); const note = await this.apNoteService.fetchNote(targetUri); @@ -186,7 +186,7 @@ export class ApInboxService { } @bindThis - private async read(actor: CacheableRemoteUser, activity: IRead): Promise { + private async read(actor: RemoteUser, activity: IRead): Promise { const id = await getApId(activity.object); if (!this.utilityService.isSelfHost(this.utilityService.extractDbHost(id))) { @@ -209,7 +209,7 @@ export class ApInboxService { } @bindThis - private async accept(actor: CacheableRemoteUser, activity: IAccept): Promise { + private async accept(actor: RemoteUser, activity: IAccept): Promise { const uri = activity.id ?? activity; this.logger.info(`Accept: ${uri}`); @@ -227,7 +227,7 @@ export class ApInboxService { } @bindThis - private async acceptFollow(actor: CacheableRemoteUser, activity: IFollow): Promise { + private async acceptFollow(actor: RemoteUser, activity: IFollow): Promise { // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある const follower = await this.apDbResolverService.getUserFromApId(activity.actor); @@ -251,7 +251,7 @@ export class ApInboxService { } @bindThis - private async add(actor: CacheableRemoteUser, activity: IAdd): Promise { + private async add(actor: RemoteUser, activity: IAdd): Promise { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -271,7 +271,7 @@ export class ApInboxService { } @bindThis - private async announce(actor: CacheableRemoteUser, activity: IAnnounce): Promise { + private async announce(actor: RemoteUser, activity: IAnnounce): Promise { const uri = getApId(activity); this.logger.info(`Announce: ${uri}`); @@ -282,7 +282,7 @@ export class ApInboxService { } @bindThis - private async announceNote(actor: CacheableRemoteUser, activity: IAnnounce, targetUri: string): Promise { + private async announceNote(actor: RemoteUser, activity: IAnnounce, targetUri: string): Promise { const uri = getApId(activity); if (actor.isSuspended) { @@ -342,7 +342,7 @@ export class ApInboxService { } @bindThis - private async block(actor: CacheableRemoteUser, activity: IBlock): Promise { + private async block(actor: RemoteUser, activity: IBlock): Promise { // ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず const blockee = await this.apDbResolverService.getUserFromApId(activity.object); @@ -360,7 +360,7 @@ export class ApInboxService { } @bindThis - private async create(actor: CacheableRemoteUser, activity: ICreate): Promise { + private async create(actor: RemoteUser, activity: ICreate): Promise { const uri = getApId(activity); this.logger.info(`Create: ${uri}`); @@ -396,7 +396,7 @@ export class ApInboxService { } @bindThis - private async createNote(resolver: Resolver, actor: CacheableRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { + private async createNote(resolver: Resolver, actor: RemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { const uri = getApId(note); if (typeof note === 'object') { @@ -431,7 +431,7 @@ export class ApInboxService { } @bindThis - private async delete(actor: CacheableRemoteUser, activity: IDelete): Promise { + private async delete(actor: RemoteUser, activity: IDelete): Promise { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -473,7 +473,7 @@ export class ApInboxService { } @bindThis - private async deleteActor(actor: CacheableRemoteUser, uri: string): Promise { + private async deleteActor(actor: RemoteUser, uri: string): Promise { this.logger.info(`Deleting the Actor: ${uri}`); if (actor.uri !== uri) { @@ -495,7 +495,7 @@ export class ApInboxService { } @bindThis - private async deleteNote(actor: CacheableRemoteUser, uri: string): Promise { + private async deleteNote(actor: RemoteUser, uri: string): Promise { this.logger.info(`Deleting the Note: ${uri}`); const unlock = await this.appLockService.getApLock(uri); @@ -528,7 +528,7 @@ export class ApInboxService { } @bindThis - private async flag(actor: CacheableRemoteUser, activity: IFlag): Promise { + private async flag(actor: RemoteUser, activity: IFlag): Promise { // objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する const uris = getApIds(activity.object); @@ -553,7 +553,7 @@ export class ApInboxService { } @bindThis - private async reject(actor: CacheableRemoteUser, activity: IReject): Promise { + private async reject(actor: RemoteUser, activity: IReject): Promise { const uri = activity.id ?? activity; this.logger.info(`Reject: ${uri}`); @@ -571,7 +571,7 @@ export class ApInboxService { } @bindThis - private async rejectFollow(actor: CacheableRemoteUser, activity: IFollow): Promise { + private async rejectFollow(actor: RemoteUser, activity: IFollow): Promise { // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある const follower = await this.apDbResolverService.getUserFromApId(activity.actor); @@ -595,7 +595,7 @@ export class ApInboxService { } @bindThis - private async remove(actor: CacheableRemoteUser, activity: IRemove): Promise { + private async remove(actor: RemoteUser, activity: IRemove): Promise { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -615,7 +615,7 @@ export class ApInboxService { } @bindThis - private async undo(actor: CacheableRemoteUser, activity: IUndo): Promise { + private async undo(actor: RemoteUser, activity: IUndo): Promise { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -641,7 +641,7 @@ export class ApInboxService { } @bindThis - private async undoAccept(actor: CacheableRemoteUser, activity: IAccept): Promise { + private async undoAccept(actor: RemoteUser, activity: IAccept): Promise { const follower = await this.apDbResolverService.getUserFromApId(activity.object); if (follower == null) { return 'skip: follower not found'; @@ -661,7 +661,7 @@ export class ApInboxService { } @bindThis - private async undoAnnounce(actor: CacheableRemoteUser, activity: IAnnounce): Promise { + private async undoAnnounce(actor: RemoteUser, activity: IAnnounce): Promise { const uri = getApId(activity); const note = await this.notesRepository.findOneBy({ @@ -676,7 +676,7 @@ export class ApInboxService { } @bindThis - private async undoBlock(actor: CacheableRemoteUser, activity: IBlock): Promise { + private async undoBlock(actor: RemoteUser, activity: IBlock): Promise { const blockee = await this.apDbResolverService.getUserFromApId(activity.object); if (blockee == null) { @@ -692,7 +692,7 @@ export class ApInboxService { } @bindThis - private async undoFollow(actor: CacheableRemoteUser, activity: IFollow): Promise { + private async undoFollow(actor: RemoteUser, activity: IFollow): Promise { const followee = await this.apDbResolverService.getUserFromApId(activity.object); if (followee == null) { return 'skip: followee not found'; @@ -726,7 +726,7 @@ export class ApInboxService { } @bindThis - private async undoLike(actor: CacheableRemoteUser, activity: ILike): Promise { + private async undoLike(actor: RemoteUser, activity: ILike): Promise { const targetUri = getApId(activity.object); const note = await this.apNoteService.fetchNote(targetUri); @@ -741,7 +741,7 @@ export class ApInboxService { } @bindThis - private async update(actor: CacheableRemoteUser, activity: IUpdate): Promise { + private async update(actor: RemoteUser, activity: IUpdate): Promise { if ('actor' in activity && actor.uri !== activity.actor) { return 'skip: invalid actor'; } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 648f30229a..92bc1869e5 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -5,7 +5,7 @@ import { v4 as uuid } from 'uuid'; import * as mfm from 'mfm-js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js'; +import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import type { IMentionedRemoteUsers, Note } from '@/models/entities/Note.js'; import type { Blocking } from '@/models/entities/Blocking.js'; import type { Relay } from '@/models/entities/Relay.js'; @@ -24,7 +24,7 @@ import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFil import { bindThis } from '@/decorators.js'; import { LdSignatureService } from './LdSignatureService.js'; import { ApMfmService } from './ApMfmService.js'; -import type { IActivity, IObject } from './type.js'; +import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IObject, IPost, IQuestion, IRead, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; import type { IIdentifier } from './models/identifier.js'; @Injectable() @@ -61,7 +61,7 @@ export class ApRendererService { } @bindThis - public renderAccept(object: any, user: { id: User['id']; host: null }) { + public renderAccept(object: any, user: { id: User['id']; host: null }): IAccept { return { type: 'Accept', actor: `${this.config.url}/users/${user.id}`, @@ -70,7 +70,7 @@ export class ApRendererService { } @bindThis - public renderAdd(user: ILocalUser, target: any, object: any) { + public renderAdd(user: LocalUser, target: any, object: any): IAdd { return { type: 'Add', actor: `${this.config.url}/users/${user.id}`, @@ -80,7 +80,7 @@ export class ApRendererService { } @bindThis - public renderAnnounce(object: any, note: Note) { + public renderAnnounce(object: any, note: Note): IAnnounce { const attributedTo = `${this.config.url}/users/${note.userId}`; let to: string[] = []; @@ -93,7 +93,7 @@ export class ApRendererService { to = [`${attributedTo}/followers`]; cc = ['https://www.w3.org/ns/activitystreams#Public']; } else { - return null; + throw new Error('renderAnnounce: cannot render non-public note'); } return { @@ -113,7 +113,7 @@ export class ApRendererService { * @param block The block to be rendered. The blockee relation must be loaded. */ @bindThis - public renderBlock(block: Blocking) { + public renderBlock(block: Blocking): IBlock { if (block.blockee?.uri == null) { throw new Error('renderBlock: missing blockee uri'); } @@ -127,14 +127,14 @@ export class ApRendererService { } @bindThis - public renderCreate(object: any, note: Note) { + public renderCreate(object: IObject, note: Note): ICreate { const activity = { id: `${this.config.url}/notes/${note.id}/activity`, actor: `${this.config.url}/users/${note.userId}`, type: 'Create', published: note.createdAt.toISOString(), object, - } as any; + } as ICreate; if (object.to) activity.to = object.to; if (object.cc) activity.cc = object.cc; @@ -143,7 +143,7 @@ export class ApRendererService { } @bindThis - public renderDelete(object: any, user: { id: User['id']; host: null }) { + public renderDelete(object: IObject | string, user: { id: User['id']; host: null }): IDelete { return { type: 'Delete', actor: `${this.config.url}/users/${user.id}`, @@ -153,7 +153,7 @@ export class ApRendererService { } @bindThis - public renderDocument(file: DriveFile) { + public renderDocument(file: DriveFile): IApDocument { return { type: 'Document', mediaType: file.type, @@ -163,12 +163,12 @@ export class ApRendererService { } @bindThis - public renderEmoji(emoji: Emoji) { + public renderEmoji(emoji: Emoji): IApEmoji { return { id: `${this.config.url}/emojis/${emoji.name}`, type: 'Emoji', name: `:${emoji.name}:`, - updated: emoji.updatedAt != null ? emoji.updatedAt.toISOString() : new Date().toISOString, + updated: emoji.updatedAt != null ? emoji.updatedAt.toISOString() : new Date().toISOString(), icon: { type: 'Image', mediaType: emoji.type ?? 'image/png', @@ -181,7 +181,7 @@ export class ApRendererService { // to anonymise reporters, the reporting actor must be a system user // object has to be a uri or array of uris @bindThis - public renderFlag(user: ILocalUser, object: [string], content: string) { + public renderFlag(user: LocalUser, object: IObject | string | string[], content: string): IFlag { return { type: 'Flag', actor: `${this.config.url}/users/${user.id}`, @@ -191,15 +191,13 @@ export class ApRendererService { } @bindThis - public renderFollowRelay(relay: Relay, relayActor: ILocalUser) { - const follow = { + public renderFollowRelay(relay: Relay, relayActor: LocalUser): IFollow { + return { id: `${this.config.url}/activities/follow-relay/${relay.id}`, type: 'Follow', actor: `${this.config.url}/users/${relayActor.id}`, object: 'https://www.w3.org/ns/activitystreams#Public', }; - - return follow; } /** @@ -217,19 +215,17 @@ export class ApRendererService { follower: { id: User['id']; host: User['host']; uri: User['host'] }, followee: { id: User['id']; host: User['host']; uri: User['host'] }, requestId?: string, - ) { - const follow = { + ): IFollow { + return { id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`, type: 'Follow', - actor: this.userEntityService.isLocalUser(follower) ? `${this.config.url}/users/${follower.id}` : follower.uri, - object: this.userEntityService.isLocalUser(followee) ? `${this.config.url}/users/${followee.id}` : followee.uri, - } as any; - - return follow; + actor: this.userEntityService.isLocalUser(follower) ? `${this.config.url}/users/${follower.id}` : follower.uri!, + object: this.userEntityService.isLocalUser(followee) ? `${this.config.url}/users/${followee.id}` : followee.uri!, + }; } @bindThis - public renderHashtag(tag: string) { + public renderHashtag(tag: string): IApHashtag { return { type: 'Hashtag', href: `${this.config.url}/tags/${encodeURIComponent(tag)}`, @@ -238,7 +234,7 @@ export class ApRendererService { } @bindThis - public renderImage(file: DriveFile) { + public renderImage(file: DriveFile): IApImage { return { type: 'Image', url: this.driveFileEntityService.getPublicUrl(file), @@ -248,7 +244,7 @@ export class ApRendererService { } @bindThis - public renderKey(user: ILocalUser, key: UserKeypair, postfix?: string) { + public renderKey(user: LocalUser, key: UserKeypair, postfix?: string): IKey { return { id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`, type: 'Key', @@ -261,7 +257,7 @@ export class ApRendererService { } @bindThis - public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }) { + public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }): Promise { const reaction = noteReaction.reaction; const object = { @@ -271,10 +267,11 @@ export class ApRendererService { object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`, content: reaction, _misskey_reaction: reaction, - } as any; + } as ILike; if (reaction.startsWith(':')) { const name = reaction.replaceAll(':', ''); + // TODO: cache const emoji = await this.emojisRepository.findOneBy({ name, host: IsNull(), @@ -287,16 +284,16 @@ export class ApRendererService { } @bindThis - public renderMention(mention: User) { + public renderMention(mention: User): IApMention { return { type: 'Mention', - href: this.userEntityService.isRemoteUser(mention) ? mention.uri : `${this.config.url}/users/${(mention as ILocalUser).id}`, - name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`, + href: this.userEntityService.isRemoteUser(mention) ? mention.uri! : `${this.config.url}/users/${(mention as LocalUser).id}`, + name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`, }; } @bindThis - public async renderNote(note: Note, dive = true, isTalk = false): Promise { + public async renderNote(note: Note, dive = true, isTalk = false): Promise { const getPromisedFiles = async (ids: string[]) => { if (!ids || ids.length === 0) return []; const items = await this.driveFilesRepository.findBy({ id: In(ids) }); @@ -409,11 +406,11 @@ export class ApRendererService { totalItems: poll!.votes[i], }, })), - } : {}; + } as const : {}; const asTalk = isTalk ? { _misskey_talk: true, - } : {}; + } as const : {}; return { id: `${this.config.url}/notes/${note.id}`, @@ -441,7 +438,7 @@ export class ApRendererService { } @bindThis - public async renderPerson(user: ILocalUser) { + public async renderPerson(user: LocalUser) { const id = `${this.config.url}/users/${user.id}`; const isSystem = !!user.username.match(/\./); @@ -518,8 +515,8 @@ export class ApRendererService { } @bindThis - public async renderQuestion(user: { id: User['id'] }, note: Note, poll: Poll) { - const question = { + public renderQuestion(user: { id: User['id'] }, note: Note, poll: Poll): IQuestion { + return { type: 'Question', id: `${this.config.url}/questions/${note.id}`, actor: `${this.config.url}/users/${user.id}`, @@ -533,21 +530,19 @@ export class ApRendererService { }, })), }; - - return question; } @bindThis - public renderRead(user: { id: User['id'] }, message: MessagingMessage) { + public renderRead(user: { id: User['id'] }, message: MessagingMessage): IRead { return { type: 'Read', actor: `${this.config.url}/users/${user.id}`, - object: message.uri, + object: message.uri!, }; } @bindThis - public renderReject(object: any, user: { id: User['id'] }) { + public renderReject(object: any, user: { id: User['id'] }): IReject { return { type: 'Reject', actor: `${this.config.url}/users/${user.id}`, @@ -556,7 +551,7 @@ export class ApRendererService { } @bindThis - public renderRemove(user: { id: User['id'] }, target: any, object: any) { + public renderRemove(user: { id: User['id'] }, target: any, object: any): IRemove { return { type: 'Remove', actor: `${this.config.url}/users/${user.id}`, @@ -566,7 +561,7 @@ export class ApRendererService { } @bindThis - public renderTombstone(id: string) { + public renderTombstone(id: string): ITombstone { return { id, type: 'Tombstone', @@ -574,8 +569,7 @@ export class ApRendererService { } @bindThis - public renderUndo(object: any, user: { id: User['id'] }) { - if (object == null) return null; + public renderUndo(object: any, user: { id: User['id'] }): IUndo { const id = typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined; return { @@ -588,21 +582,19 @@ export class ApRendererService { } @bindThis - public renderUpdate(object: any, user: { id: User['id'] }) { - const activity = { + public renderUpdate(object: any, user: { id: User['id'] }): IUpdate { + return { id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`, actor: `${this.config.url}/users/${user.id}`, type: 'Update', to: ['https://www.w3.org/ns/activitystreams#Public'], object, published: new Date().toISOString(), - } as any; - - return activity; + }; } @bindThis - public renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: IRemoteUser) { + public renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: RemoteUser): ICreate { return { id: `${this.config.url}/users/${user.id}#votes/${vote.id}/activity`, actor: `${this.config.url}/users/${user.id}`, @@ -621,9 +613,7 @@ export class ApRendererService { } @bindThis - public renderActivity(x: any): IActivity | null { - if (x == null) return null; - + public addContext(x: T): T & { '@context': any; id: string; } { if (typeof x === 'object' && x.id == null) { x.id = `${this.config.url}/${uuid()}`; } @@ -659,7 +649,7 @@ export class ApRendererService { vcard: 'http://www.w3.org/2006/vcard/ns#', }, ], - }, x); + }, x as T & { id: string; }); } @bindThis diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 7e962cb127..df7bb46405 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { ILocalUser } from '@/models/entities/User.js'; +import type { LocalUser } from '@/models/entities/User.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; @@ -18,7 +18,7 @@ import type { IObject, ICollection, IOrderedCollection } from './type.js'; export class Resolver { private history: Set; - private user?: ILocalUser; + private user?: LocalUser; private logger: Logger; constructor( @@ -38,8 +38,7 @@ export class Resolver { private recursionLimit = 100, ) { this.history = new Set(); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.logger = this.loggerService?.getLogger('ap-resolve'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる + this.logger = this.loggerService.getLogger('ap-resolve'); } @bindThis @@ -124,17 +123,17 @@ export class Resolver { switch (parsed.type) { case 'notes': return this.notesRepository.findOneByOrFail({ id: parsed.id }) - .then(note => { + .then(async note => { if (parsed.rest === 'activity') { // this refers to the create activity and not the note itself - return this.apRendererService.renderActivity(this.apRendererService.renderCreate(this.apRendererService.renderNote(note), note)); + return this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note), note)); } else { return this.apRendererService.renderNote(note); } }); case 'users': return this.usersRepository.findOneByOrFail({ id: parsed.id }) - .then(user => this.apRendererService.renderPerson(user as ILocalUser)); + .then(user => this.apRendererService.renderPerson(user as LocalUser)); case 'questions': // Polls are indexed by the note they are attached to. return Promise.all([ @@ -143,8 +142,8 @@ export class Resolver { ]) .then(([note, poll]) => this.apRendererService.renderQuestion({ id: note.userId }, note, poll)); case 'likes': - return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(reaction => - this.apRendererService.renderActivity(this.apRendererService.renderLike(reaction, { uri: null }))!); + return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(async reaction => + this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, { uri: null }))); case 'follows': // rest should be if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) throw new Error('resolveLocal: invalid follow URI'); @@ -152,7 +151,7 @@ export class Resolver { return Promise.all( [parsed.id, parsed.rest].map(id => this.usersRepository.findOneByOrFail({ id })), ) - .then(([follower, followee]) => this.apRendererService.renderActivity(this.apRendererService.renderFollow(follower, followee, url))); + .then(([follower, followee]) => this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee, url))); default: throw new Error(`resolveLocal: type ${parsed.type} unhandled`); } @@ -184,6 +183,7 @@ export class ApResolverService { private httpRequestService: HttpRequestService, private apRendererService: ApRendererService, private apDbResolverService: ApDbResolverService, + private loggerService: LoggerService, ) { } @@ -202,6 +202,7 @@ export class ApResolverService { this.httpRequestService, this.apRendererService, this.apDbResolverService, + this.loggerService, ); } } diff --git a/packages/backend/src/core/activitypub/LdSignatureService.ts b/packages/backend/src/core/activitypub/LdSignatureService.ts index a29e1be564..618ae48b1b 100644 --- a/packages/backend/src/core/activitypub/LdSignatureService.ts +++ b/packages/backend/src/core/activitypub/LdSignatureService.ts @@ -1,6 +1,5 @@ import * as crypto from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; -import jsonld from 'jsonld'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import { CONTEXTS } from './misc/contexts.js'; @@ -85,7 +84,9 @@ class LdSignature { @bindThis public async normalize(data: any) { const customLoader = this.getLoader(); - return await jsonld.normalize(data, { + // XXX: Importing jsonld dynamically since Jest frequently fails to import it statically + // https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595 + return (await import('jsonld')).default.normalize(data, { documentLoader: customLoader, }); } diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index 928ef1ae79..3b671af127 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import type { CacheableRemoteUser } from '@/models/entities/User.js'; +import type { RemoteUser } from '@/models/entities/User.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import { MetaService } from '@/core/MetaService.js'; import { truncate } from '@/misc/truncate.js'; @@ -36,7 +36,7 @@ export class ApImageService { * Imageを作成します。 */ @bindThis - public async createImage(actor: CacheableRemoteUser, value: any): Promise { + public async createImage(actor: RemoteUser, value: any): Promise { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { throw new Error('actor has been suspended'); @@ -88,7 +88,7 @@ export class ApImageService { * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ @bindThis - public async resolveImage(actor: CacheableRemoteUser, value: any): Promise { + public async resolveImage(actor: RemoteUser, value: any): Promise { // TODO // リモートサーバーからフェッチしてきて登録 diff --git a/packages/backend/src/core/activitypub/models/ApMentionService.ts b/packages/backend/src/core/activitypub/models/ApMentionService.ts index 41e6c6b14f..1ed9fb89dd 100644 --- a/packages/backend/src/core/activitypub/models/ApMentionService.ts +++ b/packages/backend/src/core/activitypub/models/ApMentionService.ts @@ -1,15 +1,14 @@ import { Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository } from '@/models/index.js'; +import type { User, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { toArray, unique } from '@/misc/prelude/array.js'; -import type { CacheableUser } from '@/models/entities/User.js'; +import { bindThis } from '@/decorators.js'; import { isMention } from '../type.js'; import { ApResolverService, Resolver } from '../ApResolverService.js'; import { ApPersonService } from './ApPersonService.js'; import type { IObject, IApMention } from '../type.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ApMentionService { @@ -26,10 +25,10 @@ export class ApMentionService { public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver) { const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href as string)); - const limit = promiseLimit(2); + const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))), - )).filter((x): x is CacheableUser => x != null); + )).filter((x): x is User => x != null); return mentionedUsers; } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 0cce64e541..560b19e57d 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -3,7 +3,7 @@ import promiseLimit from 'promise-limit'; import { DI } from '@/di-symbols.js'; import type { MessagingMessagesRepository, PollsRepository, EmojisRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import type { CacheableRemoteUser } from '@/models/entities/User.js'; +import type { RemoteUser } from '@/models/entities/User.js'; import type { Note } from '@/models/entities/Note.js'; import { toArray, toSingle, unique } from '@/misc/prelude/array.js'; import type { Emoji } from '@/models/entities/Emoji.js'; @@ -114,7 +114,7 @@ export class ApNoteService { public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { if (resolver == null) resolver = this.apResolverService.createResolver(); - const object: any = await resolver.resolve(value); + const object = await resolver.resolve(value); const entryUri = getApId(value); const err = this.validateNote(object, entryUri); @@ -129,7 +129,7 @@ export class ApNoteService { throw new Error('invalid note'); } - const note: IPost = object; + const note: IPost = object as any; this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); @@ -146,7 +146,7 @@ export class ApNoteService { this.logger.info(`Creating the Note: ${note.id}`); // 投稿者をフェッチ - const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as CacheableRemoteUser; + const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo!), resolver) as RemoteUser; // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 76f820cda0..35d6985f7a 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -5,7 +5,7 @@ import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import type { CacheableUser, IRemoteUser } from '@/models/entities/User.js'; +import type { RemoteUser } from '@/models/entities/User.js'; import { User } from '@/models/entities/User.js'; import { truncate } from '@/misc/truncate.js'; import type { UserCacheService } from '@/core/UserCacheService.js'; @@ -197,7 +197,7 @@ export class ApPersonService implements OnModuleInit { * Misskeyに対象のPersonが登録されていればそれを返します。 */ @bindThis - public async fetchPerson(uri: string, resolver?: Resolver): Promise { + public async fetchPerson(uri: string, resolver?: Resolver): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); const cached = this.userCacheService.uriPersonCache.get(uri); @@ -259,7 +259,7 @@ export class ApPersonService implements OnModuleInit { } // Create user - let user: IRemoteUser; + let user: RemoteUser; try { // Start transaction await this.db.transaction(async transactionalEntityManager => { @@ -284,7 +284,7 @@ export class ApPersonService implements OnModuleInit { isBot, isCat: (person as any).isCat === true, showTimelineReplies: false, - })) as IRemoteUser; + })) as RemoteUser; await transactionalEntityManager.save(new UserProfile({ userId: user.id, @@ -313,7 +313,7 @@ export class ApPersonService implements OnModuleInit { }); if (u) { - user = u as IRemoteUser; + user = u as RemoteUser; } else { throw new Error('already registered'); } @@ -392,7 +392,7 @@ export class ApPersonService implements OnModuleInit { } //#region このサーバーに既に登録されているか - const exist = await this.usersRepository.findOneBy({ uri }) as IRemoteUser; + const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser; if (exist == null) { return; @@ -500,7 +500,7 @@ export class ApPersonService implements OnModuleInit { * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ @bindThis - public async resolvePerson(uri: string, resolver?: Resolver): Promise { + public async resolvePerson(uri: string, resolver?: Resolver): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); //#region このサーバーに既に登録されていたらそれを返す diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index dcc5110aa5..9dc7ed4e31 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -2,24 +2,24 @@ export type obj = { [x: string]: any }; export type ApObject = IObject | string | (IObject | string)[]; export interface IObject { - '@context': string | string[] | obj | obj[]; + '@context'?: string | string[] | obj | obj[]; type: string | string[]; id?: string; + name?: string | null; summary?: string; published?: string; cc?: ApObject; to?: ApObject; - attributedTo: ApObject; + attributedTo?: ApObject; attachment?: any[]; inReplyTo?: any; replies?: ICollection; - content?: string; - name?: string; + content?: string | null; startTime?: Date; endTime?: Date; icon?: any; image?: any; - url?: ApObject; + url?: ApObject | string; href?: string; tag?: IObject | IObject[]; sensitive?: boolean; @@ -118,6 +118,7 @@ export interface IPost extends IObject { export interface IQuestion extends IObject { type: 'Note' | 'Question'; + actor: string; source?: { content: string; mediaType: string; @@ -200,6 +201,7 @@ export const isPropertyValue = (object: IObject): object is IApPropertyValue => export interface IApMention extends IObject { type: 'Mention'; href: string; + name: string; } export const isMention = (object: IObject): object is IApMention => @@ -217,12 +219,30 @@ export const isHashtag = (object: IObject): object is IApHashtag => export interface IApEmoji extends IObject { type: 'Emoji'; - updated: Date; + name: string; + updated: string; } export const isEmoji = (object: IObject): object is IApEmoji => getApType(object) === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null; +export interface IKey extends IObject { + type: 'Key'; + owner: string; + publicKeyPem: string | Buffer; +} + +export interface IApDocument extends IObject { + type: 'Document'; + name: string | null; + mediaType: string; +} + +export interface IApImage extends IObject { + type: 'Image'; + name: string | null; +} + export interface ICreate extends IActivity { type: 'Create'; } diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index 9dd115d45a..38af51a196 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -11,6 +11,7 @@ import type { DriveFile } from '@/models/entities/DriveFile.js'; import { appendQuery, query } from '@/misc/prelude/url.js'; import { deepClone } from '@/misc/clone.js'; import { UtilityService } from '../UtilityService.js'; +import { VideoProcessingService } from '../VideoProcessingService.js'; import { UserEntityService } from './UserEntityService.js'; import { DriveFolderEntityService } from './DriveFolderEntityService.js'; @@ -43,6 +44,7 @@ export class DriveFileEntityService { private utilityService: UtilityService, private driveFolderEntityService: DriveFolderEntityService, + private videoProcessingService: VideoProcessingService, ) { } @@ -72,40 +74,63 @@ export class DriveFileEntityService { } @bindThis - public getPublicUrl(file: DriveFile, mode? : 'static' | 'avatar'): string | null { // static = thumbnail - const proxiedUrl = (url: string) => appendQuery( + private getProxiedUrl(url: string, mode?: 'static' | 'avatar'): string { + return appendQuery( `${this.config.mediaProxy}/${mode ?? 'image'}.webp`, query({ url, ...(mode ? { [mode]: '1' } : {}), - }) + }), ); + } + @bindThis + public getThumbnailUrl(file: DriveFile): string | null { + if (file.type.startsWith('video')) { + if (file.thumbnailUrl) return file.thumbnailUrl; + + if (this.config.videoThumbnailGenerator == null) { + return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url ?? file.uri); + } + } else if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) { + // 動画ではなくリモートかつメディアプロキシ + return this.getProxiedUrl(file.uri, 'static'); + } + + if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) { + // リモートかつ期限切れはローカルプロキシを試みる + // 従来は/files/${thumbnailAccessKey}にアクセスしていたが、 + // /filesはメディアプロキシにリダイレクトするようにしたため直接メディアプロキシを指定する + return this.getProxiedUrl(file.uri, 'static'); + } + + const url = file.webpublicUrl ?? file.url; + + return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? this.getProxiedUrl(url, 'static') : null); + } + + @bindThis + public getPublicUrl(file: DriveFile, mode?: 'avatar'): string { // static = thumbnail // リモートかつメディアプロキシ if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) { - if (!(mode === 'static' && file.type.startsWith('video'))) { - return proxiedUrl(file.uri); - } + return this.getProxiedUrl(file.uri, mode); } // リモートかつ期限切れはローカルプロキシを試みる if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) { - const key = mode === 'static' ? file.thumbnailAccessKey : file.webpublicAccessKey; + const key = file.webpublicAccessKey; if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外 const url = `${this.config.url}/files/${key}`; - if (mode === 'avatar') return proxiedUrl(file.uri); + if (mode === 'avatar') return this.getProxiedUrl(file.uri, 'avatar'); return url; } } const url = file.webpublicUrl ?? file.url; - if (mode === 'static') { - return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? proxiedUrl(url) : null); - } if (mode === 'avatar') { - return proxiedUrl(url); + return this.getProxiedUrl(url, 'avatar'); } return url; } @@ -183,7 +208,7 @@ export class DriveFileEntityService { blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), url: opts.self ? file.url : this.getPublicUrl(file), - thumbnailUrl: this.getPublicUrl(file, 'static'), + thumbnailUrl: this.getThumbnailUrl(file), comment: file.comment, folderId: file.folderId, folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { @@ -218,7 +243,7 @@ export class DriveFileEntityService { blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), url: opts.self ? file.url : this.getPublicUrl(file), - thumbnailUrl: this.getPublicUrl(file, 'static'), + thumbnailUrl: this.getThumbnailUrl(file), comment: file.comment, folderId: file.folderId, folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index eea9d5567d..fa3337c019 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -10,7 +10,7 @@ import { awaitAll } from '@/misc/prelude/await-all.js'; import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; import { Cache } from '@/misc/cache.js'; import type { Instance } from '@/models/entities/Instance.js'; -import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js'; +import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js'; import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; @@ -32,13 +32,13 @@ type IsMeAndIsUserDetailed(user: T): user is T & { host: null; }; function isLocalUser(user: User | { host: User['host'] }): boolean { return user.host == null; } -function isRemoteUser(user: User): user is IRemoteUser; +function isRemoteUser(user: User): user is RemoteUser; function isRemoteUser(user: T): user is T & { host: string; }; function isRemoteUser(user: User | { host: User['host'] }): boolean { return !isLocalUser(user); diff --git a/packages/backend/src/models/entities/User.ts b/packages/backend/src/models/entities/User.ts index 1cfcc814ea..0a8b89ea06 100644 --- a/packages/backend/src/models/entities/User.ts +++ b/packages/backend/src/models/entities/User.ts @@ -215,20 +215,16 @@ export class User { } } -export interface ILocalUser extends User { +export type LocalUser = User & { host: null; + uri: null; } -export interface IRemoteUser extends User { +export type RemoteUser = User & { host: string; + uri: string; } -export type CacheableLocalUser = ILocalUser; - -export type CacheableRemoteUser = IRemoteUser; - -export type CacheableUser = CacheableLocalUser | CacheableRemoteUser; - export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const; export const passwordSchema = { type: 'string', minLength: 1 } as const; export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index f814368a7a..fb4ab0c62e 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -16,7 +16,7 @@ import InstanceChart from '@/core/chart/charts/instance.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; import FederationChart from '@/core/chart/charts/federation.js'; import { getApId } from '@/core/activitypub/type.js'; -import type { CacheableRemoteUser } from '@/models/entities/User.js'; +import type { RemoteUser } from '@/models/entities/User.js'; import type { UserPublickey } from '@/models/entities/UserPublickey.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { StatusError } from '@/misc/status-error.js'; @@ -87,7 +87,7 @@ export class InboxProcessorService { // HTTP-Signature keyIdを元にDBから取得 let authUser: { - user: CacheableRemoteUser; + user: RemoteUser; key: UserPublickey | null; } | null = await this.apDbResolverService.getAuthUserFromKeyId(signature.keyId); diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 186d3822d8..da8d0114e5 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -11,7 +11,7 @@ import * as url from '@/misc/prelude/url.js'; import type { Config } from '@/config.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { QueueService } from '@/core/QueueService.js'; -import type { ILocalUser, User } from '@/models/entities/User.js'; +import type { LocalUser, User } from '@/models/entities/User.js'; import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js'; import type { Following } from '@/models/entities/Following.js'; import { countIf } from '@/misc/prelude/array.js'; @@ -183,13 +183,13 @@ export class ActivityPubServerService { ); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(rendered)); + return (this.apRendererService.addContext(rendered)); } else { // index page const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`); reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(rendered)); + return (this.apRendererService.addContext(rendered)); } } @@ -271,13 +271,13 @@ export class ActivityPubServerService { ); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(rendered)); + return (this.apRendererService.addContext(rendered)); } else { // index page const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`); reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(rendered)); + return (this.apRendererService.addContext(rendered)); } } @@ -312,7 +312,7 @@ export class ActivityPubServerService { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(rendered)); + return (this.apRendererService.addContext(rendered)); } @bindThis @@ -389,7 +389,7 @@ export class ActivityPubServerService { ); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(rendered)); + return (this.apRendererService.addContext(rendered)); } else { // index page const rendered = this.apRendererService.renderOrderedCollection(partOf, user.notesCount, @@ -398,7 +398,7 @@ export class ActivityPubServerService { ); reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(rendered)); + return (this.apRendererService.addContext(rendered)); } } @@ -411,7 +411,7 @@ export class ActivityPubServerService { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(await this.apRendererService.renderPerson(user as ILocalUser))); + return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as LocalUser))); } @bindThis @@ -441,6 +441,14 @@ export class ActivityPubServerService { fastify.addContentTypeParser('application/activity+json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore')); fastify.addContentTypeParser('application/ld+json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore')); + fastify.addHook('onRequest', (request, reply, done) => { + reply.header('Access-Control-Allow-Headers', 'Accept'); + reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS'); + reply.header('Access-Control-Allow-Origin', '*'); + reply.header('Access-Control-Expose-Headers', 'Vary'); + done(); + }); + //#region Routing // inbox (limit: 64kb) fastify.post('/inbox', { bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply)); @@ -473,7 +481,7 @@ export class ActivityPubServerService { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(await this.apRendererService.renderNote(note, false))); + return this.apRendererService.addContext(await this.apRendererService.renderNote(note, false)); }); // note activity @@ -494,7 +502,7 @@ export class ActivityPubServerService { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(await this.packActivity(note))); + return (this.apRendererService.addContext(await this.packActivity(note))); }); // outbox @@ -537,7 +545,7 @@ export class ActivityPubServerService { if (this.userEntityService.isLocalUser(user)) { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(this.apRendererService.renderKey(user, keypair))); + return (this.apRendererService.addContext(this.apRendererService.renderKey(user, keypair))); } else { reply.code(400); return; @@ -581,7 +589,7 @@ export class ActivityPubServerService { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(await this.apRendererService.renderEmoji(emoji))); + return (this.apRendererService.addContext(await this.apRendererService.renderEmoji(emoji))); }); // like @@ -602,7 +610,7 @@ export class ActivityPubServerService { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(await this.apRendererService.renderLike(reaction, note))); + return (this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, note))); }); // follow @@ -628,7 +636,7 @@ export class ActivityPubServerService { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.renderActivity(this.apRendererService.renderFollow(follower, followee))); + return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee))); }); done(); diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 49ded6c28e..f4bc568fdc 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -150,6 +150,12 @@ export class FileServerService { file.cleanup(); return await reply.redirect(301, url.toString()); } else if (file.mime.startsWith('video/')) { + const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url); + if (externalThumbnail) { + file.cleanup(); + return await reply.redirect(301, externalThumbnail); + } + image = await this.videoProcessingService.generateVideoThumbnail(file.path); } } diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 395a1c468a..4a8b8a1c12 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -5,7 +5,7 @@ import { promisify } from 'node:util'; import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; -import type { CacheableLocalUser, ILocalUser, User } from '@/models/entities/User.js'; +import type { LocalUser, User } from '@/models/entities/User.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; import type Logger from '@/logger.js'; import type { UserIpsRepository } from '@/models/index.js'; @@ -168,7 +168,7 @@ export class ApiCallService implements OnApplicationShutdown { } @bindThis - private async logIp(request: FastifyRequest, user: ILocalUser) { + private async logIp(request: FastifyRequest, user: LocalUser) { const meta = await this.metaService.fetch(); if (!meta.enableIpLogging) return; const ip = request.ip; @@ -194,7 +194,7 @@ export class ApiCallService implements OnApplicationShutdown { @bindThis private async call( ep: IEndpoint & { exec: any }, - user: CacheableLocalUser | null | undefined, + user: LocalUser | null | undefined, token: AccessToken | null | undefined, data: any, file: { @@ -227,15 +227,17 @@ export class ApiCallService implements OnApplicationShutdown { // TODO: 毎リクエスト計算するのもあれだしキャッシュしたい const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1; - // Rate limit - await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor).catch(err => { - throw new ApiError({ - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - httpStatusCode: 429, + if (factor > 0) { + // Rate limit + await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor).catch(err => { + throw new ApiError({ + message: 'Rate limit exceeded. Please try again later.', + code: 'RATE_LIMIT_EXCEEDED', + id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', + httpStatusCode: 429, + }); }); - }); + } } if (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin) { diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index 8b39f6c924..87438c348d 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js'; -import type { CacheableLocalUser, ILocalUser } from '@/models/entities/User.js'; +import type { LocalUser } from '@/models/entities/User.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; import { Cache } from '@/misc/cache.js'; import type { App } from '@/models/entities/App.js'; @@ -36,14 +36,14 @@ export class AuthenticateService { } @bindThis - public async authenticate(token: string | null | undefined): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> { + public async authenticate(token: string | null | undefined): Promise<[LocalUser | null | undefined, AccessToken | null | undefined]> { if (token == null) { return [null, null]; } if (isNativeToken(token)) { const user = await this.userCacheService.localUserByNativeTokenCache.fetch(token, - () => this.usersRepository.findOneBy({ token }) as Promise); + () => this.usersRepository.findOneBy({ token }) as Promise); if (user == null) { throw new AuthenticationError('user not found'); @@ -70,7 +70,7 @@ export class AuthenticateService { const user = await this.userCacheService.localUserByIdCache.fetch(accessToken.userId, () => this.usersRepository.findOneBy({ id: accessToken.userId, - }) as Promise); + }) as Promise); if (accessToken.appId) { const app = await this.appCache.fetch(accessToken.appId, diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index d490097dea..f1164b9957 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -7,7 +7,7 @@ import { DI } from '@/di-symbols.js'; import type { UserSecurityKeysRepository, SigninsRepository, UserProfilesRepository, AttestationChallengesRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; -import type { ILocalUser } from '@/models/entities/User.js'; +import type { LocalUser } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; import { bindThis } from '@/decorators.js'; @@ -105,7 +105,7 @@ export class SigninApiService { const user = await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull(), - }) as ILocalUser; + }) as LocalUser; if (user == null) { return error(404, { diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts index c78d9f85cd..f821dbb500 100644 --- a/packages/backend/src/server/api/SigninService.ts +++ b/packages/backend/src/server/api/SigninService.ts @@ -3,7 +3,7 @@ import { DI } from '@/di-symbols.js'; import type { SigninsRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { IdService } from '@/core/IdService.js'; -import type { ILocalUser } from '@/models/entities/User.js'; +import type { LocalUser } from '@/models/entities/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { SigninEntityService } from '@/core/entities/SigninEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -25,7 +25,7 @@ export class SigninService { } @bindThis - public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser) { + public signin(request: FastifyRequest, reply: FastifyReply, user: LocalUser) { setImmediate(async () => { // Append signin history const record = await this.signinsRepository.insert({ diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index ffd7e203ea..41e8365d08 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -10,7 +10,7 @@ import { IdService } from '@/core/IdService.js'; import { SignupService } from '@/core/SignupService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { EmailService } from '@/core/EmailService.js'; -import { ILocalUser } from '@/models/entities/User.js'; +import { LocalUser } from '@/models/entities/User.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { bindThis } from '@/decorators.js'; import { SigninService } from './SigninService.js'; @@ -194,7 +194,7 @@ export class SignupApiService { emailVerifyCode: null, }); - return this.signinService.signin(request, reply, account as ILocalUser); + return this.signinService.signin(request, reply, account as LocalUser); } catch (err) { throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString()); } diff --git a/packages/backend/src/server/api/endpoint-base.ts b/packages/backend/src/server/api/endpoint-base.ts index b27329b9a9..115526d997 100644 --- a/packages/backend/src/server/api/endpoint-base.ts +++ b/packages/backend/src/server/api/endpoint-base.ts @@ -1,7 +1,7 @@ import * as fs from 'node:fs'; import Ajv from 'ajv'; import type { Schema, SchemaType } from '@/misc/schema.js'; -import type { CacheableLocalUser } from '@/models/entities/User.js'; +import type { LocalUser } from '@/models/entities/User.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; import { ApiError } from './error.js'; import type { IEndpointMeta } from './endpoints.js'; @@ -21,16 +21,16 @@ type File = { // TODO: paramsの型をT['params']のスキーマ定義から推論する type executor = - (params: SchemaType, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record | null) => + (params: SchemaType, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record | null) => Promise>>; export abstract class Endpoint { - public exec: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => Promise; + public exec: (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => Promise; constructor(meta: T, paramDef: Ps, cb: executor) { const validate = ajv.compile(paramDef); - this.exec = (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => { + this.exec = (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record | null) => { let cleanup: undefined | (() => void) = undefined; if (meta.requireFile) { diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts index cdaec13a3f..84fcc05edc 100644 --- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts +++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts @@ -49,7 +49,7 @@ export default class extends Endpoint { const actor = await this.instanceActorService.getInstanceActor(); const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId }); - this.queueService.deliver(actor, this.apRendererService.renderActivity(this.apRendererService.renderFlag(actor, [targetUser.uri!], report.comment)), targetUser.inbox); + this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, [targetUser.uri!], report.comment)), targetUser.inbox); } await this.abuseUserReportsRepository.update(report.id, { diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 9470dd3cbb..61e05531e6 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -3,7 +3,7 @@ import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository, NotesRepository } from '@/models/index.js'; import type { Note } from '@/models/entities/Note.js'; -import type { CacheableLocalUser, User } from '@/models/entities/User.js'; +import type { LocalUser, User } from '@/models/entities/User.js'; import { isActor, isPost, getApId } from '@/core/activitypub/type.js'; import type { SchemaType } from '@/misc/schema.js'; import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; @@ -114,7 +114,7 @@ export default class extends Endpoint { * URIからUserかNoteを解決する */ @bindThis - private async fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise | null> { + private async fetchAny(uri: string, me: LocalUser | null | undefined): Promise | null> { // ブロックしてたら中断 const fetchedMeta = await this.metaService.fetch(); if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, this.utilityService.extractDbHost(uri))) return null; @@ -147,7 +147,7 @@ export default class extends Endpoint { } @bindThis - private async mergePack(me: CacheableLocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise | null> { + private async mergePack(me: LocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise | null> { if (user != null) { return { type: 'User', diff --git a/packages/backend/src/server/api/endpoints/clips/remove-note.ts b/packages/backend/src/server/api/endpoints/clips/remove-note.ts index 55778c7ecb..5d88870ed2 100644 --- a/packages/backend/src/server/api/endpoints/clips/remove-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/remove-note.ts @@ -1,72 +1,72 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; -import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; -import { GetterService } from '@/server/api/GetterService.js'; - -export const meta = { - tags: ['account', 'notes', 'clips'], - - requireCredential: true, - - kind: 'write:account', - - errors: { - noSuchClip: { - message: 'No such clip.', - code: 'NO_SUCH_CLIP', - id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', - }, - - noSuchNote: { - message: 'No such note.', - code: 'NO_SUCH_NOTE', - id: 'aff017de-190e-434b-893e-33a9ff5049d8', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - clipId: { type: 'string', format: 'misskey:id' }, - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['clipId', 'noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - @Inject(DI.clipsRepository) - private clipsRepository: ClipsRepository, - - @Inject(DI.clipNotesRepository) - private clipNotesRepository: ClipNotesRepository, - - private getterService: GetterService, - ) { - super(meta, paramDef, async (ps, me) => { - const clip = await this.clipsRepository.findOneBy({ - id: ps.clipId, - userId: me.id, - }); - - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } - - const note = await this.getterService.getNote(ps.noteId).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - await this.clipNotesRepository.delete({ - noteId: note.id, - clipId: clip.id, - }); - }); - } -} +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; +import { GetterService } from '@/server/api/GetterService.js'; + +export const meta = { + tags: ['account', 'notes', 'clips'], + + requireCredential: true, + + kind: 'write:account', + + errors: { + noSuchClip: { + message: 'No such clip.', + code: 'NO_SUCH_CLIP', + id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', + }, + + noSuchNote: { + message: 'No such note.', + code: 'NO_SUCH_NOTE', + id: 'aff017de-190e-434b-893e-33a9ff5049d8', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + clipId: { type: 'string', format: 'misskey:id' }, + noteId: { type: 'string', format: 'misskey:id' }, + }, + required: ['clipId', 'noteId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.clipsRepository) + private clipsRepository: ClipsRepository, + + @Inject(DI.clipNotesRepository) + private clipNotesRepository: ClipNotesRepository, + + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + const clip = await this.clipsRepository.findOneBy({ + id: ps.clipId, + userId: me.id, + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } + + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + await this.clipNotesRepository.delete({ + noteId: note.id, + clipId: clip.id, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index befaea4664..f734e4e778 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -1,7 +1,7 @@ import { Not } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js'; -import type { IRemoteUser } from '@/models/entities/User.js'; +import type { RemoteUser } from '@/models/entities/User.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; @@ -160,9 +160,9 @@ export default class extends Endpoint { // リモート投票の場合リプライ送信 if (note.userHost != null) { - const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as IRemoteUser; + const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as RemoteUser; - this.queueService.deliver(me, this.apRendererService.renderActivity(await this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox); + this.queueService.deliver(me, this.apRendererService.addContext(await this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox); } // リモートフォロワーにUpdate配信 diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index 3a079ee1ab..1c1fdc23f1 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -45,6 +45,12 @@ export const meta = { code: 'YOU_HAVE_BEEN_BLOCKED', id: '990232c5-3f9d-4d83-9f3f-ef27b6332a4b', }, + + tooManyUsers: { + message: 'You can not push users any more.', + code: 'TOO_MANY_USERS', + id: '2dd9752e-a338-413d-8eec-41814430989b', + }, }, } as const; @@ -110,8 +116,15 @@ export default class extends Endpoint { throw new ApiError(meta.errors.alreadyAdded); } - // Push the user - await this.userListService.push(user, userList, me); + try { + await this.userListService.push(user, userList, me); + } catch (err) { + if (err instanceof UserListService.TooManyUsersError) { + throw new ApiError(meta.errors.tooManyUsers); + } + + throw err; + } }); } } diff --git a/packages/backend/src/server/api/stream/channels/messaging.ts b/packages/backend/src/server/api/stream/channels/messaging.ts index 92af6b591c..b544e297c5 100644 --- a/packages/backend/src/server/api/stream/channels/messaging.ts +++ b/packages/backend/src/server/api/stream/channels/messaging.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import type { UserGroupJoiningsRepository, UsersRepository, MessagingMessagesRepository } from '@/models/index.js'; -import type { User, ILocalUser, IRemoteUser } from '@/models/entities/User.js'; +import type { User, LocalUser, RemoteUser } from '@/models/entities/User.js'; import type { UserGroup } from '@/models/entities/UserGroup.js'; import { MessagingService } from '@/core/MessagingService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -89,7 +89,7 @@ class MessagingChannel extends Channel { // リモートユーザーからのメッセージだったら既読配信 if (this.userEntityService.isLocalUser(this.user!) && this.userEntityService.isRemoteUser(this.otherparty!)) { this.messagingMessagesRepository.findOneBy({ id: body.id }).then(message => { - if (message) this.messagingService.deliverReadActivity(this.user as ILocalUser, this.otherparty as IRemoteUser, message); + if (message) this.messagingService.deliverReadActivity(this.user as LocalUser, this.otherparty as RemoteUser, message); }); } } else if (this.groupId) { diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 57461b7a33..2ce7293a52 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import summaly from 'summaly'; +import { summaly } from 'summaly'; import { DI } from '@/di-symbols.js'; import type { UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; @@ -30,7 +30,7 @@ export class UrlPreviewService { } @bindThis - private wrap(url?: string): string | null { + private wrap(url?: string | null): string | null { return url != null ? url.match(/^https?:\/\//) ? `${this.config.mediaProxy}/preview.webp?${query({ @@ -64,14 +64,21 @@ export class UrlPreviewService { ? `(Proxy) Getting preview of ${url}@${lang} ...` : `Getting preview of ${url}@${lang} ...`); try { - const summary = meta.summalyProxy ? await this.httpRequestService.getJson>(`${meta.summalyProxy}?${query({ - url: url, - lang: lang ?? 'ja-JP', - })}`) : await summaly.default(url, { - followRedirects: false, - lang: lang ?? 'ja-JP', - }); - + const summary = meta.summalyProxy ? + await this.httpRequestService.getJson>(`${meta.summalyProxy}?${query({ + url: url, + lang: lang ?? 'ja-JP', + })}`) + : + await summaly(url, { + followRedirects: false, + lang: lang ?? 'ja-JP', + agent: { + http: this.httpRequestService.httpAgent, + https: this.httpRequestService.httpsAgent, + }, + }); + this.logger.succ(`Got preview of ${url}: ${summary.title}`); if (summary.url && !(summary.url.startsWith('http://') || summary.url.startsWith('https://'))) { diff --git a/packages/backend/src/server/web/cli.js b/packages/backend/src/server/web/cli.js index 3dff1d4860..3467f7ac2a 100644 --- a/packages/backend/src/server/web/cli.js +++ b/packages/backend/src/server/web/cli.js @@ -11,6 +11,9 @@ window.onload = async () => { // Send request fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, { + headers: { + 'Content-Type': 'application/json' + }, method: 'POST', body: JSON.stringify(data), credentials: 'omit', diff --git a/packages/backend/test/_e2e/fetch-resource.ts b/packages/backend/test/_e2e/fetch-resource.ts index 7ae133496a..b8ba3f2477 100644 --- a/packages/backend/test/_e2e/fetch-resource.ts +++ b/packages/backend/test/_e2e/fetch-resource.ts @@ -11,7 +11,7 @@ const PREFER_AP = 'application/activity+json, */*'; const PREFER_HTML = 'text/html, */*'; const UNSPECIFIED = '*/*'; -// Response Contet-Type +// Response Content-Type const AP = 'application/activity+json; charset=utf-8'; const JSON = 'application/json; charset=utf-8'; const HTML = 'text/html; charset=utf-8'; diff --git a/packages/frontend/assets/label-red.svg b/packages/frontend/assets/label-red.svg index 45996aa9ce..c89d3f5f3a 100644 --- a/packages/frontend/assets/label-red.svg +++ b/packages/frontend/assets/label-red.svg @@ -1,6 +1,6 @@ - - - - - + + + + + diff --git a/packages/frontend/assets/label.svg b/packages/frontend/assets/label.svg index b1f85f3c07..997335f505 100644 --- a/packages/frontend/assets/label.svg +++ b/packages/frontend/assets/label.svg @@ -1,6 +1,6 @@ - - - - - + + + + + diff --git a/packages/frontend/assets/unread.svg b/packages/frontend/assets/unread.svg index 8c3cc9f475..8bd4156e51 100644 --- a/packages/frontend/assets/unread.svg +++ b/packages/frontend/assets/unread.svg @@ -1,7 +1,7 @@ - - - - - - + + + + + + diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index da4db63406..9690353432 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -32,7 +32,7 @@ {{ cancelText ?? i18n.ts.cancel }}
- {{ action.text }} + {{ action.text }}
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index 6ac56f3ce0..b777a1329b 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -8,7 +8,7 @@ -
+
props.image, () => { position: relative; //box-shadow: 0 0 0 1px var(--divider) inset; background: var(--bg); - --c: rgb(0 0 0 / 2%); background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%); background-size: 16px 16px; } diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 6a76bdc63d..8a8bd7e0ec 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -5,14 +5,14 @@ ref="el" v-hotkey="keymap" :class="$style.root" - :tabindex="!isDeleted ? '-1' : null" + :tabindex="!isDeleted ? '-1' : undefined" > - +
{{ i18n.ts.pinnedNote }}
- +
- + diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 17dc719858..addfe37a8c 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -161,6 +161,7 @@ import { useNoteCapture } from '@/scripts/use-note-capture'; import { deepClone } from '@/scripts/clone'; import { useTooltip } from '@/scripts/use-tooltip'; import { claimAchievement } from '@/scripts/achievements'; +import { MenuItem } from '@/types/menu'; const props = defineProps<{ note: misskey.entities.Note; @@ -242,7 +243,32 @@ useTooltip(renoteButton, async (showing) => { function renote(viaKeyboard = false) { pleaseLogin(); - os.popupMenu([{ + + let items = [] as MenuItem[]; + + if (appearNote.channel) { + items = items.concat([{ + text: i18n.ts.inChannelRenote, + icon: 'ti ti-repeat', + action: () => { + os.api('notes/create', { + renoteId: appearNote.id, + channelId: appearNote.channelId, + }); + }, + }, { + text: i18n.ts.inChannelQuote, + icon: 'ti ti-quote', + action: () => { + os.post({ + renote: appearNote, + channel: appearNote.channel, + }); + }, + }, null]); + } + + items = items.concat([{ text: i18n.ts.renote, icon: 'ti ti-repeat', action: () => { @@ -258,7 +284,9 @@ function renote(viaKeyboard = false) { renote: appearNote, }); }, - }], renoteButton.value, { + }]); + + os.popupMenu(items, renoteButton.value, { viaKeyboard, }); } diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index 6b43f14665..32998e1a70 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -1,6 +1,6 @@