diff --git a/.config/example.yml b/.config/example.yml
index 489cceec34..1a6b0f5b4b 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -110,10 +110,10 @@ port: 3000
# Changes how the server interpret the origin IP of the request.
#
# Any format supported by Fastify is accepted.
-# Default: trust all proxies (i.e. trustProxy: true)
+# Default: do not trust any proxies (i.e. trustProxy: false)
# See: https://fastify.dev/docs/latest/reference/server/#trustproxy
#
-# trustProxy: 1
+# trustProxy: false
# ┌──────────────────────────┐
#───┘ PostgreSQL configuration └────────────────────────────────
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index e800743469..514abdfb20 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -5,7 +5,7 @@
"workspaceFolder": "/workspace",
"features": {
"ghcr.io/devcontainers/features/node:1": {
- "version": "24.10.0"
+ "version": "22.15.0"
},
"ghcr.io/devcontainers-extra/features/pnpm:2": {
"version": "10.10.0"
diff --git a/.github/workflows/get-backend-memory.yml b/.github/workflows/get-backend-memory.yml
new file mode 100644
index 0000000000..458f303f0f
--- /dev/null
+++ b/.github/workflows/get-backend-memory.yml
@@ -0,0 +1,87 @@
+# this name is used in report-backend-memory.yml so be careful when change name
+name: Get backend memory usage
+
+on:
+ pull_request:
+ branches:
+ - master
+ - develop
+ paths:
+ - packages/backend/**
+ - packages/misskey-js/**
+ - .github/workflows/get-backend-memory.yml
+
+jobs:
+ get-memory-usage:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+
+ strategy:
+ matrix:
+ memory-json-name: [memory-base.json, memory-head.json]
+ include:
+ - memory-json-name: memory-base.json
+ ref: ${{ github.base_ref }}
+ - memory-json-name: memory-head.json
+ ref: refs/pull/${{ github.event.number }}/merge
+
+ services:
+ postgres:
+ image: postgres:18
+ ports:
+ - 54312:5432
+ env:
+ POSTGRES_DB: test-misskey
+ POSTGRES_HOST_AUTH_METHOD: trust
+ redis:
+ image: redis:7
+ ports:
+ - 56312:6379
+
+ steps:
+ - uses: actions/checkout@v4.3.0
+ with:
+ ref: ${{ matrix.ref }}
+ submodules: true
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4.2.0
+ - name: Use Node.js
+ uses: actions/setup-node@v4.4.0
+ with:
+ node-version-file: '.node-version'
+ cache: 'pnpm'
+ - run: pnpm i --frozen-lockfile
+ - name: Check pnpm-lock.yaml
+ run: git diff --exit-code pnpm-lock.yaml
+ - name: Copy Configure
+ run: cp .github/misskey/test.yml .config/default.yml
+ - name: Compile Configure
+ run: pnpm compile-config
+ - name: Build
+ run: pnpm build
+ - name: Run migrations
+ run: pnpm --filter backend migrate
+ - name: Measure memory usage
+ run: |
+ # Start the server and measure memory usage
+ node packages/backend/scripts/measure-memory.mjs > ${{ matrix.memory-json-name }}
+ - name: Upload Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: memory-artifact-${{ matrix.memory-json-name }}
+ path: ${{ matrix.memory-json-name }}
+
+ save-pr-number:
+ runs-on: ubuntu-latest
+ permissions: {}
+ steps:
+ - name: Save PR number
+ env:
+ PR_NUMBER: ${{ github.event.number }}
+ run: |
+ echo "$PR_NUMBER" > ./pr_number
+ - uses: actions/upload-artifact@v4
+ with:
+ name: memory-artifact-pr-number
+ path: pr_number
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index d80c56f032..33a1ccbc76 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -111,10 +111,5 @@ jobs:
node-version-file: '.node-version'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- - run: pnpm --filter misskey-js run build
- if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'frontend' || matrix.workspace == 'sw' }}
- - run: pnpm --filter misskey-reversi run build
- if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'frontend' }}
- - run: pnpm --filter misskey-bubble-game run build
- if: ${{ matrix.workspace == 'frontend' }}
+ - run: pnpm --filter "${{ matrix.workspace }}^..." run build
- run: pnpm --filter ${{ matrix.workspace }} run typecheck
diff --git a/.github/workflows/locale.yml b/.github/workflows/locale.yml
index e63d83997b..d75335f38f 100644
--- a/.github/workflows/locale.yml
+++ b/.github/workflows/locale.yml
@@ -3,10 +3,12 @@ name: Lint
on:
push:
paths:
+ - packages/i18n/**
- locales/**
- .github/workflows/locale.yml
pull_request:
paths:
+ - packages/i18n/**
- locales/**
- .github/workflows/locale.yml
jobs:
@@ -14,15 +16,18 @@ jobs:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- - uses: actions/checkout@v4.3.0
- with:
- fetch-depth: 0
- submodules: true
- - name: Setup pnpm
- uses: pnpm/action-setup@v4.2.0
- - uses: actions/setup-node@v4.4.0
- with:
- node-version-file: '.node-version'
- cache: 'pnpm'
- - run: pnpm i --frozen-lockfile
- - run: cd locales && node verify.js
+ - uses: actions/checkout@v4.3.0
+ with:
+ fetch-depth: 0
+ submodules: true
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4.2.0
+ - uses: actions/setup-node@v4.4.0
+ with:
+ node-version-file: ".node-version"
+ cache: "pnpm"
+ - run: pnpm i --frozen-lockfile
+ - run: pnpm --filter i18n build
+ - name: Verify Locales
+ working-directory: ./packages/i18n
+ run: pnpm run verify
diff --git a/.github/workflows/report-backend-memory.yml b/.github/workflows/report-backend-memory.yml
new file mode 100644
index 0000000000..ede43cc645
--- /dev/null
+++ b/.github/workflows/report-backend-memory.yml
@@ -0,0 +1,122 @@
+name: Report backend memory
+
+on:
+ workflow_run:
+ types: [completed]
+ workflows:
+ - Get backend memory usage # get-backend-memory.yml
+
+jobs:
+ compare-memory:
+ runs-on: ubuntu-latest
+ if: ${{ github.event.workflow_run.conclusion == 'success' }}
+ permissions:
+ pull-requests: write
+
+ steps:
+ - name: Download artifact
+ uses: actions/github-script@v7.1.0
+ with:
+ script: |
+ const fs = require('fs');
+ let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: context.payload.workflow_run.id,
+ });
+ let matchArtifacts = allArtifacts.data.artifacts.filter((artifact) => {
+ return artifact.name.startsWith("memory-artifact-") || artifact.name == "memory-artifact"
+ });
+ await Promise.all(matchArtifacts.map(async (artifact) => {
+ let download = await github.rest.actions.downloadArtifact({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ artifact_id: artifact.id,
+ archive_format: 'zip',
+ });
+ await fs.promises.writeFile(`${process.env.GITHUB_WORKSPACE}/${artifact.name}.zip`, Buffer.from(download.data));
+ }));
+ - name: Extract all artifacts
+ run: |
+ find . -mindepth 1 -maxdepth 1 -type f -name '*.zip' -exec unzip {} -d artifacts ';'
+ ls -la artifacts/
+ - name: Load PR Number
+ id: load-pr-num
+ run: echo "pr-number=$(cat artifacts/pr_number)" >> "$GITHUB_OUTPUT"
+
+ - name: Output base
+ run: cat ./artifacts/memory-base.json
+ - name: Output head
+ run: cat ./artifacts/memory-head.json
+ - name: Compare memory usage
+ id: compare
+ run: |
+ BASE_MEMORY=$(cat ./artifacts/memory-base.json)
+ HEAD_MEMORY=$(cat ./artifacts/memory-head.json)
+
+ BASE_RSS=$(echo "$BASE_MEMORY" | jq -r '.memory.rss // 0')
+ HEAD_RSS=$(echo "$HEAD_MEMORY" | jq -r '.memory.rss // 0')
+
+ # Calculate difference
+ if [ "$BASE_RSS" -gt 0 ] && [ "$HEAD_RSS" -gt 0 ]; then
+ DIFF=$((HEAD_RSS - BASE_RSS))
+ DIFF_PERCENT=$(echo "scale=2; ($DIFF * 100) / $BASE_RSS" | bc)
+
+ # Convert to MB for readability
+ BASE_MB=$(echo "scale=2; $BASE_RSS / 1048576" | bc)
+ HEAD_MB=$(echo "scale=2; $HEAD_RSS / 1048576" | bc)
+ DIFF_MB=$(echo "scale=2; $DIFF / 1048576" | bc)
+
+ echo "base_mb=$BASE_MB" >> "$GITHUB_OUTPUT"
+ echo "head_mb=$HEAD_MB" >> "$GITHUB_OUTPUT"
+ echo "diff_mb=$DIFF_MB" >> "$GITHUB_OUTPUT"
+ echo "diff_percent=$DIFF_PERCENT" >> "$GITHUB_OUTPUT"
+ echo "has_data=true" >> "$GITHUB_OUTPUT"
+
+ # Determine if this is a significant change (more than 5% increase)
+ if [ "$(echo "$DIFF_PERCENT > 5" | bc)" -eq 1 ]; then
+ echo "significant_increase=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "significant_increase=false" >> "$GITHUB_OUTPUT"
+ fi
+ else
+ echo "has_data=false" >> "$GITHUB_OUTPUT"
+ fi
+ - id: build-comment
+ name: Build memory comment
+ run: |
+ HEADER="## Backend Memory Usage Comparison"
+ FOOTER="[See workflow logs for details](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
+
+ echo "$HEADER" > ./output.md
+ echo >> ./output.md
+
+ if [ "${{ steps.compare.outputs.has_data }}" == "true" ]; then
+ echo "| Metric | base | head | Diff |" >> ./output.md
+ echo "|--------|------|------|------|" >> ./output.md
+ echo "| RSS | ${{ steps.compare.outputs.base_mb }} MB | ${{ steps.compare.outputs.head_mb }} MB | ${{ steps.compare.outputs.diff_mb }} MB (${{ steps.compare.outputs.diff_percent }}%) |" >> ./output.md
+ echo >> ./output.md
+
+ if [ "${{ steps.compare.outputs.significant_increase }}" == "true" ]; then
+ echo "⚠️ **Warning**: Memory usage has increased by more than 5%. Please verify this is not an unintended change." >> ./output.md
+ echo >> ./output.md
+ fi
+ else
+ echo "Could not retrieve memory usage data." >> ./output.md
+ echo >> ./output.md
+ fi
+
+ echo "$FOOTER" >> ./output.md
+ - uses: thollander/actions-comment-pull-request@v2
+ with:
+ pr_number: ${{ steps.load-pr-num.outputs.pr-number }}
+ comment_tag: show_memory_diff
+ filePath: ./output.md
+ - name: Tell error to PR
+ uses: thollander/actions-comment-pull-request@v2
+ if: failure() && steps.load-pr-num.outputs.pr-number
+ with:
+ pr_number: ${{ steps.load-pr-num.outputs.pr-number }}
+ comment_tag: show_memory_diff_error
+ message: |
+ An error occurred while comparing backend memory usage. See [workflow logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.
diff --git a/.node-version b/.node-version
index 21651351e2..b8ffd70759 100644
--- a/.node-version
+++ b/.node-version
@@ -1 +1 @@
-24.10.0
+22.15.0
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 5f36a32af4..2d11d24db2 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -3,6 +3,7 @@
"**/node_modules": true
},
"typescript.tsdk": "node_modules/typescript/lib",
+ "typescript.enablePromptUseWorkspaceTsdk": true,
"files.associations": {
"*.test.ts": "typescript"
},
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb0f115058..383af2bf02 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,17 @@
+## 2025.12.0
+
+### Note
+- configの`trustProxy`のデフォルト値を`false`に変更しました。アップデート前に現在のconfigをご確認の上、必要に応じて値を変更してください。
+
+### Client
+- Fix: stacking router viewで連続して戻る操作を行うと何も表示されなくなる問題を修正
+
+### Server
+- Enhance: メモリ使用量を削減しました
+- Enhance: ActivityPubアクティビティを送信する際のパフォーマンス向上
+- Enhance: 依存関係の更新
+- Fix: セキュリティに関する修正
+
## 2025.11.1
### Client
diff --git a/Dockerfile b/Dockerfile
index 20e24d1dc2..02739d9ca2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.4
-ARG NODE_VERSION=24.10.0-bookworm
+ARG NODE_VERSION=22.15.0-bookworm
# build assets & compile TypeScript
@@ -24,6 +24,7 @@ COPY --link ["packages/frontend-shared/package.json", "./packages/frontend-share
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
COPY --link ["packages/frontend-embed/package.json", "./packages/frontend-embed/"]
COPY --link ["packages/frontend-builder/package.json", "./packages/frontend-builder/"]
+COPY --link ["packages/i18n/package.json", "./packages/i18n/"]
COPY --link ["packages/icons-subsetter/package.json", "./packages/icons-subsetter/"]
COPY --link ["packages/sw/package.json", "./packages/sw/"]
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
@@ -101,6 +102,7 @@ COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
+COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built
COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis
COPY --chown=misskey:misskey . ./
diff --git a/README.md b/README.md
index 92e8fef639..a73102d713 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,8 @@
+[](https://deepwiki.com/misskey-dev/misskey)
+
## Thanks
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index 147c60b0be..259dcadd2c 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -128,7 +128,7 @@ pinnedNote: "Nota fijada"
pinned: "Fijar al perfil"
you: "Tú"
clickToShow: "Haz clic para verlo"
-sensitive: "Marcado como sensible"
+sensitive: "Marcado como sensible (NSFW)"
add: "Agregar"
reaction: "Reacción"
reactions: "Reacciones"
@@ -143,7 +143,7 @@ rememberNoteVisibility: "Recordar visibilidad"
attachCancel: "Quitar adjunto"
deleteFile: "Eliminar archivo"
markAsSensitive: "Marcar como sensible"
-unmarkAsSensitive: "Desmarcar como sensible"
+unmarkAsSensitive: "No marcar como sensible"
enterFileName: "Introduce el nombre del archivo"
mute: "Silenciar"
unmute: "Dejar de silenciar"
@@ -319,10 +319,10 @@ remoteUserCaution: "Para el usuario remoto, la información está incompleta"
activity: "Actividad"
images: "Imágenes"
image: "Imágenes"
-birthday: "Fecha de nacimiento"
+birthday: "Cumpleaños"
yearsOld: "{age} años"
registeredDate: "Fecha de registro"
-location: "Lugar"
+location: "Ubicación"
theme: "Tema"
themeForLightMode: "Tema para usar en Modo Linterna"
themeForDarkMode: "Tema para usar en Modo Oscuro"
@@ -353,7 +353,7 @@ emptyFolder: "La carpeta está vacía"
dropHereToUpload: "Arrastra los archivos aquí para subirlos."
unableToDelete: "No se puede borrar"
inputNewFileName: "Ingrese un nuevo nombre de archivo"
-inputNewDescription: "Ingrese nueva descripción"
+inputNewDescription: "Introducir un nuevo texto alternativo"
inputNewFolderName: "Ingrese un nuevo nombre de la carpeta"
circularReferenceFolder: "La carpeta de destino es una sub-carpeta de la carpeta que quieres mover."
hasChildFilesOrFolders: "No se puede borrar esta carpeta. No está vacía."
@@ -579,7 +579,7 @@ objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir "
s3ForcePathStyleDesc: "Si s3ForcePathStyle esta habilitado el nombre del bucket debe ser especificado como parte de la URL en lugar del nombre de host en la URL. Puede ser necesario activar esta opción cuando se utilice, por ejemplo, Minio en un servidor propio."
serverLogs: "Registros del servidor"
deleteAll: "Eliminar todos"
-showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo"
+showFixedPostForm: "Visualizar la ventana de publicación en la parte superior de la línea de tiempo."
showFixedPostFormInChannel: "Mostrar el formulario de publicación por encima de la cronología (Canales)"
withRepliesByDefaultForNewlyFollowed: "Incluir por defecto respuestas de usuarios recién seguidos en la línea de tiempo"
newNoteRecived: "Tienes una nota nueva"
@@ -706,7 +706,7 @@ userSaysSomethingAbout: "{name} dijo algo sobre {word}"
makeActive: "Activar"
display: "Apariencia"
copy: "Copiar"
-copiedToClipboard: "Texto copiado al portapapeles"
+copiedToClipboard: "Copiado al portapapeles"
metrics: "Métricas"
overview: "Resumen"
logs: "Registros"
@@ -715,7 +715,7 @@ database: "Base de datos"
channel: "Canal"
create: "Crear"
notificationSetting: "Ajustes de Notificaciones"
-notificationSettingDesc: "Por favor elija el tipo de notificación a mostrar"
+notificationSettingDesc: "Por favor elige el tipo de notificación a mostrar"
useGlobalSetting: "Usar ajustes globales"
useGlobalSettingDesc: "Al activarse, se usará la configuración de notificaciones de la cuenta, al desactivarse se pueden hacer configuraciones particulares."
other: "Otro"
@@ -747,7 +747,7 @@ system: "Sistema"
switchUi: "Cambiar interfaz de usuario"
desktop: "Escritorio"
clip: "Clip"
-createNew: "Crear"
+createNew: "Crear Nuevo"
optional: "Opcional"
createNewClip: "Crear clip nuevo"
unclip: "Quitar clip"
@@ -844,7 +844,7 @@ jumpToSpecifiedDate: "Saltar a una fecha específica"
showingPastTimeline: "Mostrar líneas de tiempo antiguas"
clear: "Limpiar"
markAllAsRead: "Marcar todo como leído"
-goBack: "Deseleccionar"
+goBack: "Anterior"
unlikeConfirm: "¿Quitar como favorito?"
fullView: "Vista completa"
quitFullView: "quitar vista completa"
@@ -1203,8 +1203,8 @@ iHaveReadXCarefullyAndAgree: "He leído el texto {x} y estoy de acuerdo"
dialog: "Diálogo"
icon: "Avatar"
forYou: "Para ti"
-currentAnnouncements: "Anuncios actuales"
-pastAnnouncements: "Anuncios anteriores"
+currentAnnouncements: "Avisos actuales"
+pastAnnouncements: "Avisos anteriores"
youHaveUnreadAnnouncements: "Hay anuncios sin leer"
useSecurityKey: "Por favor, sigue las instrucciones de tu dispositivo o navegador para usar tu clave de seguridad o tu clave de paso."
replies: "Responder"
@@ -1412,8 +1412,8 @@ _imageEditing:
filename: "Nombre de archivo"
filename_without_ext: "Nombre del archivo sin la extensión"
year: "Año de rodaje"
- month: "Mes de rodaje"
- day: "Día de rodaje"
+ month: "Mes de la fotografía"
+ day: "Día de la fotografía"
hour: "Hora"
minute: "Minuto"
second: "Segundo"
@@ -1427,9 +1427,9 @@ _imageEditing:
gps_lat: "Latitud"
gps_long: "Longitud"
_imageFrameEditor:
- title: "Edición de Fotograma"
+ title: "Edición de Fotos"
tip: "Decora tus imágenes con marcos y etiquetas que contengan metadatos."
- header: "Cabezal"
+ header: "Título"
footer: "Pie de página"
borderThickness: "Ancho del borde"
labelThickness: "Ancho de la etiqueta"
@@ -1456,8 +1456,8 @@ _compression:
medium: "Tamaño mediano"
small: "Tamaño pequeño"
_order:
- newest: "Los más recientes primero"
- oldest: "Los más antiguos primero"
+ newest: "Más reciente primero"
+ oldest: "Más antiguos primero"
_chat:
messages: "Mensajes"
noMessagesYet: "Aún no hay mensajes"
@@ -1511,7 +1511,7 @@ _emojiPalette:
palettes: "Paleta\n"
enableSyncBetweenDevicesForPalettes: "Activar la sincronización de paletas entre dispositivos"
paletteForMain: "Paleta principal"
- paletteForReaction: "Paleta de reacción"
+ paletteForReaction: "Paleta utilizada para las reacciones"
_settings:
driveBanner: "Puedes gestionar y configurar la unidad, comprobar su uso y configurar los ajustes de carga de archivos."
pluginBanner: "Puedes ampliar las funciones del cliente con plugins. Puedes instalar plugins, configurarlos y gestionarlos individualmente."
@@ -1523,7 +1523,7 @@ _settings:
accountData: "Datos de la cuenta"
accountDataBanner: "Exportación e importación para gestionar los datos de la cuenta."
muteAndBlockBanner: "Puedes configurar y gestionar ajustes para ocultar contenidos y restringir acciones a usuarios específicos."
- accessibilityBanner: "Puedes personalizar los visuales y el comportamiento del cliente, y configurar los ajustes para optimizar el uso."
+ accessibilityBanner: "Puedes personalizar el aspecto y el comportamiento del cliente y configurar los ajustes para optimizar su uso."
privacyBanner: "Puedes configurar opciones relacionadas con la privacidad de la cuenta, como la visibilidad del contenido, la posibilidad de descubrir la cuenta y la aprobación de seguimiento."
securityBanner: "Puedes configurar opciones relacionadas con la seguridad de la cuenta, como la contraseña, los métodos de inicio de sesión, las aplicaciones de autenticación y Passkeys."
preferencesBanner: "Puedes configurar el comportamiento general del cliente según tus preferencias."
@@ -1540,7 +1540,7 @@ _settings:
ifOff: "Si está desactivado"
enableSyncThemesBetweenDevices: "Sincronizar los temas instalados entre dispositivos."
enablePullToRefresh: "Tirar para actualizar"
- enablePullToRefresh_description: "Si utiliza un ratón, arrastre mientras pulsa la rueda de desplazamiento."
+ enablePullToRefresh_description: "Si utilizas un ratón, arrastra mientras pulsas la rueda de desplazamiento."
realtimeMode_description: "Establece una conexión con el servidor y actualiza el contenido en tiempo real. Esto puede aumentar el tráfico y el consumo de memoria."
contentsUpdateFrequency: "Frecuencia de adquisición del contenido."
contentsUpdateFrequency_description: "Cuanto mayor sea el valor, más se actualiza el contenido, pero disminuye el rendimiento y aumenta el tráfico y el consumo de memoria."
@@ -1685,7 +1685,7 @@ _initialTutorial:
followers: "Visible solo para seguidores. Sólo tus seguidores podrán ver la nota, y no podrá ser renotada por otras personas."
direct: "Visible sólo para usuarios específicos, y el destinatario será notificado. Puede usarse como alternativa a la mensajería directa."
doNotSendConfidencialOnDirect1: "¡Ten cuidado cuando vayas a enviar información sensible!"
- doNotSendConfidencialOnDirect2: "Los administradores del servidor pueden leer lo que escribes. Ten cuidado cuando envíes información sensible en notas directas en servidores no confiables."
+ doNotSendConfidencialOnDirect2: "Los administradores del servidor, también llamado instancia, pueden leer lo que escribes. Ten cuidado cuando envíes información sensible en notas directas en servidores o instancias no confiables."
localOnly: "Publicando con esta opción seleccionada, la nota no se federará hacia otros servidores. Los usuarios de otros servidores no podrán ver estas notas directamente, sin importar los ajustes seleccionados más arriba."
_cw:
title: "Alerta de contenido (CW)"
@@ -2156,7 +2156,7 @@ _accountDelete:
started: "El proceso de eliminación ha comenzado."
inProgress: "La eliminación está en proceso."
_ad:
- back: "Deseleccionar"
+ back: "Anterior"
reduceFrequencyOfThisAd: "Mostrar menos este anuncio."
hide: "No mostrar"
timezoneinfo: "El día de la semana está determidado por la zona horaria del servidor."
@@ -2299,7 +2299,7 @@ _theme:
indicator: "Indicador"
panel: "Panel"
shadow: "Sombra"
- header: "Cabezal"
+ header: "Título"
navBg: "Fondo de la barra lateral"
navFg: "Texto de la barra lateral"
navActive: "Texto de la barra lateral (activo)"
@@ -2610,10 +2610,10 @@ _profile:
name: "Nombre"
username: "Nombre de usuario"
description: "Descripción"
- youCanIncludeHashtags: "Puedes añadir hashtags"
+ youCanIncludeHashtags: "También puedes incluir hashtags en tu biografía"
metadata: "información adicional"
metadataEdit: "Editar información adicional"
- metadataDescription: "Muestra la información adicional en el perfil"
+ metadataDescription: "Usando esto puedes mostrar campos de información adicionales en tu perfil."
metadataLabel: "Etiqueta"
metadataContent: "Contenido"
changeAvatar: "Cambiar avatar"
@@ -2771,7 +2771,7 @@ _notification:
follow: "Siguiendo"
mention: "Menciones"
reply: "Respuestas"
- renote: "Renotar"
+ renote: "Renotas"
quote: "Citar"
reaction: "Reacción"
pollEnded: "La encuesta terminó"
diff --git a/locales/generateDTS.js b/locales/generateDTS.js
deleted file mode 100644
index ab0613cc82..0000000000
--- a/locales/generateDTS.js
+++ /dev/null
@@ -1,232 +0,0 @@
-import * as fs from 'node:fs';
-import { fileURLToPath } from 'node:url';
-import { dirname } from 'node:path';
-import * as yaml from 'js-yaml';
-import ts from 'typescript';
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = dirname(__filename);
-const parameterRegExp = /\{(\w+)\}/g;
-
-function createMemberType(item) {
- if (typeof item !== 'string') {
- return ts.factory.createTypeLiteralNode(createMembers(item));
- }
- const parameters = Array.from(
- item.matchAll(parameterRegExp),
- ([, parameter]) => parameter,
- );
- return parameters.length
- ? ts.factory.createTypeReferenceNode(
- ts.factory.createIdentifier('ParameterizedString'),
- [
- ts.factory.createUnionTypeNode(
- parameters.map((parameter) =>
- ts.factory.createStringLiteral(parameter),
- ),
- ),
- ],
- )
- : ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
-}
-
-function createMembers(record) {
- return Object.entries(record).map(([k, v]) => {
- const node = ts.factory.createPropertySignature(
- undefined,
- ts.factory.createStringLiteral(k),
- undefined,
- createMemberType(v),
- );
- if (typeof v === 'string') {
- ts.addSyntheticLeadingComment(
- node,
- ts.SyntaxKind.MultiLineCommentTrivia,
- `*
- * ${v.replace(/\n/g, '\n * ')}
- `,
- true,
- );
- }
- return node;
- });
-}
-
-export default function generateDTS() {
- const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8'));
- const members = createMembers(locale);
- const elements = [
- ts.factory.createVariableStatement(
- [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)],
- ts.factory.createVariableDeclarationList(
- [
- ts.factory.createVariableDeclaration(
- ts.factory.createIdentifier('kParameters'),
- undefined,
- ts.factory.createTypeOperatorNode(
- ts.SyntaxKind.UniqueKeyword,
- ts.factory.createKeywordTypeNode(ts.SyntaxKind.SymbolKeyword),
- ),
- undefined,
- ),
- ],
- ts.NodeFlags.Const,
- ),
- ),
- ts.factory.createTypeAliasDeclaration(
- [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
- ts.factory.createIdentifier('ParameterizedString'),
- [
- ts.factory.createTypeParameterDeclaration(
- undefined,
- ts.factory.createIdentifier('T'),
- ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
- ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
- ),
- ],
- ts.factory.createIntersectionTypeNode([
- ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
- ts.factory.createTypeLiteralNode([
- ts.factory.createPropertySignature(
- undefined,
- ts.factory.createComputedPropertyName(
- ts.factory.createIdentifier('kParameters'),
- ),
- undefined,
- ts.factory.createTypeReferenceNode(
- ts.factory.createIdentifier('T'),
- undefined,
- ),
- ),
- ])
- ]),
- ),
- ts.factory.createInterfaceDeclaration(
- [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
- ts.factory.createIdentifier('ILocale'),
- undefined,
- undefined,
- [
- ts.factory.createIndexSignature(
- undefined,
- [
- ts.factory.createParameterDeclaration(
- undefined,
- undefined,
- ts.factory.createIdentifier('_'),
- undefined,
- ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
- undefined,
- ),
- ],
- ts.factory.createUnionTypeNode([
- ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
- ts.factory.createTypeReferenceNode(
- ts.factory.createIdentifier('ParameterizedString'),
- ),
- ts.factory.createTypeReferenceNode(
- ts.factory.createIdentifier('ILocale'),
- undefined,
- ),
- ]),
- ),
- ],
- ),
- ts.factory.createInterfaceDeclaration(
- [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
- ts.factory.createIdentifier('Locale'),
- undefined,
- [
- ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [
- ts.factory.createExpressionWithTypeArguments(
- ts.factory.createIdentifier('ILocale'),
- undefined,
- ),
- ]),
- ],
- members,
- ),
- ts.factory.createVariableStatement(
- [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)],
- ts.factory.createVariableDeclarationList(
- [
- ts.factory.createVariableDeclaration(
- ts.factory.createIdentifier('locales'),
- undefined,
- ts.factory.createTypeLiteralNode([
- ts.factory.createIndexSignature(
- undefined,
- [
- ts.factory.createParameterDeclaration(
- undefined,
- undefined,
- ts.factory.createIdentifier('lang'),
- undefined,
- ts.factory.createKeywordTypeNode(
- ts.SyntaxKind.StringKeyword,
- ),
- undefined,
- ),
- ],
- ts.factory.createTypeReferenceNode(
- ts.factory.createIdentifier('Locale'),
- undefined,
- ),
- ),
- ]),
- undefined,
- ),
- ],
- ts.NodeFlags.Const,
- ),
- ),
- ts.factory.createFunctionDeclaration(
- [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
- undefined,
- ts.factory.createIdentifier('build'),
- undefined,
- [],
- ts.factory.createTypeReferenceNode(
- ts.factory.createIdentifier('Locale'),
- undefined,
- ),
- undefined,
- ),
- ts.factory.createExportDefault(ts.factory.createIdentifier('locales')),
- ];
- ts.addSyntheticLeadingComment(
- elements[0],
- ts.SyntaxKind.MultiLineCommentTrivia,
- ' eslint-disable ',
- true,
- );
- ts.addSyntheticLeadingComment(
- elements[0],
- ts.SyntaxKind.SingleLineCommentTrivia,
- ' This file is generated by locales/generateDTS.js',
- true,
- );
- ts.addSyntheticLeadingComment(
- elements[0],
- ts.SyntaxKind.SingleLineCommentTrivia,
- ' Do not edit this file directly.',
- true,
- );
- const printed = ts
- .createPrinter({
- newLine: ts.NewLineKind.LineFeed,
- })
- .printList(
- ts.ListFormat.MultiLine,
- ts.factory.createNodeArray(elements),
- ts.createSourceFile(
- 'index.d.ts',
- '',
- ts.ScriptTarget.ESNext,
- true,
- ts.ScriptKind.TS,
- ),
- );
-
- fs.writeFileSync(`${__dirname}/index.d.ts`, printed, 'utf-8');
-}
diff --git a/locales/index.js b/locales/index.js
deleted file mode 100644
index 6d9cf4796b..0000000000
--- a/locales/index.js
+++ /dev/null
@@ -1,93 +0,0 @@
-/**
- * Languages Loader
- */
-
-import * as fs from 'node:fs';
-import * as yaml from 'js-yaml';
-
-const merge = (...args) => args.reduce((a, c) => ({
- ...a,
- ...c,
- ...Object.entries(a)
- .filter(([k]) => c && typeof c[k] === 'object')
- .reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {})
-}), {});
-
-const languages = [
- 'ar-SA',
- 'ca-ES',
- 'cs-CZ',
- 'da-DK',
- 'de-DE',
- 'en-US',
- 'es-ES',
- 'fr-FR',
- 'id-ID',
- 'it-IT',
- 'ja-JP',
- 'ja-KS',
- 'kab-KAB',
- 'kn-IN',
- 'ko-KR',
- 'nl-NL',
- 'no-NO',
- 'pl-PL',
- 'pt-PT',
- 'ru-RU',
- 'sk-SK',
- 'th-TH',
- 'tr-TR',
- 'ug-CN',
- 'uk-UA',
- 'vi-VN',
- 'zh-CN',
- 'zh-TW',
-];
-
-const primaries = {
- 'en': 'US',
- 'ja': 'JP',
- 'zh': 'CN',
-};
-
-// 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く
-const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
-
-export function build() {
- // vitestの挙動を調整するため、一度ローカル変数化する必要がある
- // https://github.com/vitest-dev/vitest/issues/3988#issuecomment-1686599577
- // https://github.com/misskey-dev/misskey/pull/14057#issuecomment-2192833785
- const metaUrl = import.meta.url;
- const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {});
-
- // 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
- const removeEmpty = (obj) => {
- for (const [k, v] of Object.entries(obj)) {
- if (v === '') {
- delete obj[k];
- } else if (typeof v === 'object') {
- removeEmpty(v);
- }
- }
- return obj;
- };
- removeEmpty(locales);
-
- return Object.entries(locales)
- .reduce((a, [k, v]) => (a[k] = (() => {
- const [lang] = k.split('-');
- switch (k) {
- case 'ja-JP': return v;
- case 'ja-KS':
- case 'en-US': return merge(locales['ja-JP'], v);
- default: return merge(
- locales['ja-JP'],
- locales['en-US'],
- locales[`${lang}-${primaries[lang]}`] ?? {},
- v
- );
- }
- })(), a), {});
-}
-
-export default build();
diff --git a/locales/it-IT.yml b/locales/it-IT.yml
index e3cffed3a1..3b918e9c9f 100644
--- a/locales/it-IT.yml
+++ b/locales/it-IT.yml
@@ -83,6 +83,8 @@ files: "Allegati"
download: "Scarica"
driveFileDeleteConfirm: "Vuoi davvero eliminare il file \"{name}\", e le Note a cui è stato allegato?"
unfollowConfirm: "Vuoi davvero togliere il Following a {name}?"
+cancelFollowRequestConfirm: "Vuoi annullare la tua richiesta di follow inviata a {name}?"
+rejectFollowRequestConfirm: "Vuoi rifiutare la richiesta di follow ricevuta da {name}?"
exportRequested: "Hai richiesto un'esportazione, e potrebbe volerci tempo. Quando sarà compiuta, il file verrà aggiunto direttamente al Drive."
importRequested: "Hai richiesto un'importazione. Potrebbe richiedere un po' di tempo."
lists: "Liste"
@@ -2350,13 +2352,13 @@ _ago:
yearsAgo: "{n} anni fa"
invalid: "Niente da visualizzare"
_timeIn:
- seconds: "Dopo {n} secondi"
- minutes: "Dopo {n} minuti"
- hours: "Dopo {n} ore"
- days: "Dopo {n} giorni"
- weeks: "Dopo {n} settimane"
- months: "Dopo {n} mesi"
- years: "Dopo {n} anni"
+ seconds: "Tra {n} secondi"
+ minutes: "Tra {n} minuti"
+ hours: "Tra {n} ore"
+ days: "Tra {n} giorni"
+ weeks: "Tra {n} settimane"
+ months: "Tra {n} mesi"
+ years: "Tra {n} anni"
_time:
second: "s"
minute: "min"
diff --git a/locales/package.json b/locales/package.json
deleted file mode 100644
index bedb411a91..0000000000
--- a/locales/package.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "type": "module"
-}
diff --git a/locales/verify.js b/locales/verify.js
deleted file mode 100644
index a8e9875d6e..0000000000
--- a/locales/verify.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import locales from './index.js';
-
-let valid = true;
-
-function writeError(type, lang, tree, data) {
- process.stderr.write(JSON.stringify({ type, lang, tree, data }));
- process.stderr.write('\n');
- valid = false;
-}
-
-function verify(expected, actual, lang, trace) {
- for (let key in expected) {
- if (!Object.prototype.hasOwnProperty.call(actual, key)) {
- continue;
- }
- if (typeof expected[key] === 'object') {
- if (typeof actual[key] !== 'object') {
- writeError('mismatched_type', lang, trace ? `${trace}.${key}` : key, { expected: 'object', actual: typeof actual[key] });
- continue;
- }
- verify(expected[key], actual[key], lang, trace ? `${trace}.${key}` : key);
- } else if (typeof expected[key] === 'string') {
- switch (typeof actual[key]) {
- case 'object':
- writeError('mismatched_type', lang, trace ? `${trace}.${key}` : key, { expected: 'string', actual: 'object' });
- break;
- case 'undefined':
- continue;
- case 'string':
- const expectedParameters = new Set(expected[key].match(/\{[^}]+\}/g)?.map((s) => s.slice(1, -1)));
- const actualParameters = new Set(actual[key].match(/\{[^}]+\}/g)?.map((s) => s.slice(1, -1)));
- for (let parameter of expectedParameters) {
- if (!actualParameters.has(parameter)) {
- writeError('missing_parameter', lang, trace ? `${trace}.${key}` : key, { parameter });
- }
- }
- }
- }
- }
-}
-
-const { ['ja-JP']: original, ...verifiees } = locales;
-
-for (let lang in verifiees) {
- if (!Object.prototype.hasOwnProperty.call(locales, lang)) {
- continue;
- }
- verify(original, verifiees[lang], lang);
-}
-
-if (!valid) {
- process.exit(1);
-}
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index fcad02f241..c5eb96fbb3 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -877,7 +877,7 @@ noInquiryUrlWarning: "尚未设置联络地址。"
noBotProtectionWarning: "尚未设置 Bot 防御。"
configure: "设置"
postToGallery: "创建新图集"
-postToHashtag: "投稿到这个标签"
+postToHashtag: "发布至该话题"
gallery: "图集"
recentPosts: "最新发布"
popularPosts: "热门投稿"
@@ -3146,7 +3146,7 @@ _selfXssPrevention:
description3: "详情请看这里。{link}"
_followRequest:
recieved: "收到的请求"
- sent: "已发送的请求"
+ sent: "发送的请求"
_remoteLookupErrors:
_federationNotAllowed:
title: "无法与此服务器通信"
diff --git a/package.json b/package.json
index bcbd1315f9..40542046eb 100644
--- a/package.json
+++ b/package.json
@@ -1,33 +1,36 @@
{
"name": "misskey",
- "version": "2025.11.1",
+ "version": "2025.12.0",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
- "packageManager": "pnpm@10.22.0",
+ "packageManager": "pnpm@10.24.0",
"workspaces": [
- "packages/frontend-shared",
- "packages/frontend",
- "packages/frontend-embed",
- "packages/icons-subsetter",
- "packages/backend",
- "packages/sw",
"packages/misskey-js",
+ "packages/i18n",
"packages/misskey-reversi",
- "packages/misskey-bubble-game"
+ "packages/misskey-bubble-game",
+ "packages/icons-subsetter",
+ "packages/frontend-shared",
+ "packages/frontend-builder",
+ "packages/sw",
+ "packages/backend",
+ "packages/frontend",
+ "packages/frontend-embed"
],
"private": true,
"scripts": {
+ "compile-config": "cd packages/backend && pnpm compile-config",
"build-pre": "node ./scripts/build-pre.js",
"build-assets": "node ./scripts/build-assets.mjs",
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
"build-storybook": "pnpm --filter frontend build-storybook",
"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
- "start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
- "start:inspect": "cd packages/backend && node --inspect ./built/boot/entry.js",
- "start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
+ "start": "pnpm check:connect && cd packages/backend && pnpm compile-config && node ./built/boot/entry.js",
+ "start:inspect": "cd packages/backend && pnpm compile-config && node --inspect ./built/boot/entry.js",
+ "start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/boot/entry.js",
"cli": "cd packages/backend && pnpm cli",
"init": "pnpm migrate",
"migrate": "cd packages/backend && pnpm migrate",
@@ -74,12 +77,12 @@
"@typescript-eslint/eslint-plugin": "8.47.0",
"@typescript-eslint/parser": "8.47.0",
"cross-env": "10.1.0",
- "cypress": "15.6.0",
+ "cypress": "15.7.0",
"eslint": "9.39.1",
"globals": "16.5.0",
"ncp": "2.0.0",
- "pnpm": "10.22.0",
- "start-server-and-test": "2.1.2"
+ "pnpm": "10.24.0",
+ "start-server-and-test": "2.1.3"
},
"optionalDependencies": {
"@tensorflow/tfjs-core": "4.22.0"
diff --git a/packages/backend/.swcrc b/packages/backend/.swcrc
index f4bf7a4d2a..7e1767a67a 100644
--- a/packages/backend/.swcrc
+++ b/packages/backend/.swcrc
@@ -3,12 +3,17 @@
"jsc": {
"parser": {
"syntax": "typescript",
+ "jsx": true,
"dynamicImport": true,
"decorators": true
},
"transform": {
"legacyDecorator": true,
- "decoratorMetadata": true
+ "decoratorMetadata": true,
+ "react": {
+ "runtime": "automatic",
+ "importSource": "@kitajs/html"
+ }
},
"experimental": {
"keepImportAssertions": true
diff --git a/packages/backend/src/server/web/bios.css b/packages/backend/assets/misc/bios.css
similarity index 100%
rename from packages/backend/src/server/web/bios.css
rename to packages/backend/assets/misc/bios.css
diff --git a/packages/backend/src/server/web/bios.js b/packages/backend/assets/misc/bios.js
similarity index 100%
rename from packages/backend/src/server/web/bios.js
rename to packages/backend/assets/misc/bios.js
diff --git a/packages/backend/src/server/web/cli.css b/packages/backend/assets/misc/cli.css
similarity index 100%
rename from packages/backend/src/server/web/cli.css
rename to packages/backend/assets/misc/cli.css
diff --git a/packages/backend/src/server/web/cli.js b/packages/backend/assets/misc/cli.js
similarity index 100%
rename from packages/backend/src/server/web/cli.js
rename to packages/backend/assets/misc/cli.js
diff --git a/packages/backend/src/server/web/error.css b/packages/backend/assets/misc/error.css
similarity index 100%
rename from packages/backend/src/server/web/error.css
rename to packages/backend/assets/misc/error.css
diff --git a/packages/backend/src/server/web/error.js b/packages/backend/assets/misc/error.js
similarity index 100%
rename from packages/backend/src/server/web/error.js
rename to packages/backend/assets/misc/error.js
diff --git a/packages/backend/assets/misc/flush.js b/packages/backend/assets/misc/flush.js
new file mode 100644
index 0000000000..991b8ea808
--- /dev/null
+++ b/packages/backend/assets/misc/flush.js
@@ -0,0 +1,46 @@
+(async () => {
+ const msg = document.getElementById('msg');
+ const successText = `\nSuccess Flush! Back to Misskey\n成功しました。Misskeyを開き直してください。`;
+
+ if (!document.cookie) {
+ message('Your site data is fully cleared by your browser.');
+ message(successText);
+ } else {
+ message('Your browser does not support Clear-Site-Data header. Start opportunistic flushing.');
+ try {
+ localStorage.clear();
+ message('localStorage cleared.');
+
+ const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => {
+ const delidb = indexedDB.deleteDatabase(name);
+ delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`));
+ delidb.onerror = e => rej(e)
+ }));
+
+ await Promise.all(idbPromises);
+
+ if (navigator.serviceWorker.controller) {
+ navigator.serviceWorker.controller.postMessage('clear');
+ await navigator.serviceWorker.getRegistrations()
+ .then(registrations => {
+ return Promise.all(registrations.map(registration => registration.unregister()));
+ })
+ .catch(e => { throw new Error(e) });
+ }
+
+ message(successText);
+ } catch (e) {
+ message(`\n${e}\n\nFlush Failed. Please retry.\n失敗しました。もう一度試してみてください。`);
+ message(`\nIf you retry more than 3 times, try manually clearing the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを手動で消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`)
+
+ console.error(e);
+ setTimeout(() => {
+ location = '/';
+ }, 10000)
+ }
+ }
+
+ function message(text) {
+ msg.insertAdjacentHTML('beforeend', `
[${(new Date()).toString()}] ${text.replace(/\n/g,'
')}
)
- case 'pre': {
- if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
+ case 'PRE': {
+ if (node.childNodes.length === 1 && (node.childNodes[0] instanceof htmlParser.HTMLElement) && node.childNodes[0].tagName === 'CODE') {
text += '\n```\n';
text += getText(node.childNodes[0]);
text += '\n```\n';
+ } else if (node.childNodes.length === 1 && (node.childNodes[0] instanceof htmlParser.TextNode) && node.childNodes[0].textContent.startsWith('') && node.childNodes[0].textContent.endsWith('')) {
+ text += '\n```\n';
+ text += node.childNodes[0].textContent.slice(6, -7);
+ text += '\n```\n';
} else {
- appendChildren(node.childNodes);
+ analyzeChildren(node.childNodes);
}
break;
}
// inline code ()
- case 'code': {
+ case 'CODE': {
text += '`';
- appendChildren(node.childNodes);
+ analyzeChildren(node.childNodes);
text += '`';
break;
}
- case 'blockquote': {
+ case 'BLOCKQUOTE': {
const t = getText(node);
if (t) {
text += '\n> ';
@@ -235,33 +236,33 @@ export class MfmService {
break;
}
- case 'p':
- case 'h2':
- case 'h3':
- case 'h4':
- case 'h5':
- case 'h6': {
+ case 'P':
+ case 'H2':
+ case 'H3':
+ case 'H4':
+ case 'H5':
+ case 'H6': {
text += '\n\n';
- appendChildren(node.childNodes);
+ analyzeChildren(node.childNodes);
break;
}
// other block elements
- case 'div':
- case 'header':
- case 'footer':
- case 'article':
- case 'li':
- case 'dt':
- case 'dd': {
+ case 'DIV':
+ case 'HEADER':
+ case 'FOOTER':
+ case 'ARTICLE':
+ case 'LI':
+ case 'DT':
+ case 'DD': {
text += '\n';
- appendChildren(node.childNodes);
+ analyzeChildren(node.childNodes);
break;
}
default: // includes inline elements
{
- appendChildren(node.childNodes);
+ analyzeChildren(node.childNodes);
break;
}
}
@@ -269,52 +270,35 @@ export class MfmService {
}
@bindThis
- public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], additionalAppenders: Appender[] = []) {
+ public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], extraHtml: string | null = null) {
if (nodes == null) {
return null;
}
- const { happyDOM, window } = new Window();
-
- const doc = window.document;
-
- const body = doc.createElement('p');
-
- function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
- if (children) {
- for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
- }
+ function toHtml(children?: mfm.MfmNode[]): string {
+ if (children == null) return '';
+ return children.map(x => handlers[x.type](x)).join('');
}
function fnDefault(node: mfm.MfmFn) {
- const el = doc.createElement('i');
- appendChildren(node.children, el);
- return el;
+ return `${toHtml(node.children)}`;
}
- const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => any } = {
+ const handlers = {
bold: (node) => {
- const el = doc.createElement('b');
- appendChildren(node.children, el);
- return el;
+ return `${toHtml(node.children)}`;
},
small: (node) => {
- const el = doc.createElement('small');
- appendChildren(node.children, el);
- return el;
+ return `${toHtml(node.children)}`;
},
strike: (node) => {
- const el = doc.createElement('del');
- appendChildren(node.children, el);
- return el;
+ return `${toHtml(node.children)}`;
},
italic: (node) => {
- const el = doc.createElement('i');
- appendChildren(node.children, el);
- return el;
+ return `${toHtml(node.children)}`;
},
fn: (node) => {
@@ -323,10 +307,7 @@ export class MfmService {
const text = node.children[0].type === 'text' ? node.children[0].props.text : '';
try {
const date = new Date(parseInt(text, 10) * 1000);
- const el = doc.createElement('time');
- el.setAttribute('datetime', date.toISOString());
- el.textContent = date.toISOString();
- return el;
+ return ``;
} catch (err) {
return fnDefault(node);
}
@@ -336,21 +317,9 @@ export class MfmService {
if (node.children.length === 1) {
const child = node.children[0];
const text = child.type === 'text' ? child.props.text : '';
- const rubyEl = doc.createElement('ruby');
- const rtEl = doc.createElement('rt');
- // ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
- const rpStartEl = doc.createElement('rp');
- rpStartEl.appendChild(doc.createTextNode('('));
- const rpEndEl = doc.createElement('rp');
- rpEndEl.appendChild(doc.createTextNode(')'));
-
- rubyEl.appendChild(doc.createTextNode(text.split(' ')[0]));
- rtEl.appendChild(doc.createTextNode(text.split(' ')[1]));
- rubyEl.appendChild(rpStartEl);
- rubyEl.appendChild(rtEl);
- rubyEl.appendChild(rpEndEl);
- return rubyEl;
+ // ruby未対応のHTMLサニタイザーを通したときにルビが「対象テキスト(ルビテキスト)」にフォールバックするようにする
+ return `${escapeHtml(text.split(' ')[0])}`;
} else {
const rt = node.children.at(-1);
@@ -359,21 +328,9 @@ export class MfmService {
}
const text = rt.type === 'text' ? rt.props.text : '';
- const rubyEl = doc.createElement('ruby');
- const rtEl = doc.createElement('rt');
- // ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
- const rpStartEl = doc.createElement('rp');
- rpStartEl.appendChild(doc.createTextNode('('));
- const rpEndEl = doc.createElement('rp');
- rpEndEl.appendChild(doc.createTextNode(')'));
-
- appendChildren(node.children.slice(0, node.children.length - 1), rubyEl);
- rtEl.appendChild(doc.createTextNode(text.trim()));
- rubyEl.appendChild(rpStartEl);
- rubyEl.appendChild(rtEl);
- rubyEl.appendChild(rpEndEl);
- return rubyEl;
+ // ruby未対応のHTMLサニタイザーを通したときにルビが「対象テキスト(ルビテキスト)」にフォールバックするようにする
+ return `${toHtml(node.children.slice(0, node.children.length - 1))}`;
}
}
@@ -384,125 +341,98 @@ export class MfmService {
},
blockCode: (node) => {
- const pre = doc.createElement('pre');
- const inner = doc.createElement('code');
- inner.textContent = node.props.code;
- pre.appendChild(inner);
- return pre;
+ return `${escapeHtml(node.props.code)}
`;
},
center: (node) => {
- const el = doc.createElement('div');
- appendChildren(node.children, el);
- return el;
+ return `${toHtml(node.children)}`;
},
emojiCode: (node) => {
- return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
+ return `\u200B:${escapeHtml(node.props.name)}:\u200B`;
},
unicodeEmoji: (node) => {
- return doc.createTextNode(node.props.emoji);
+ return node.props.emoji;
},
hashtag: (node) => {
- const a = doc.createElement('a');
- a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
- a.textContent = `#${node.props.hashtag}`;
- a.setAttribute('rel', 'tag');
- return a;
+ return `#${escapeHtml(node.props.hashtag)}`;
},
inlineCode: (node) => {
- const el = doc.createElement('code');
- el.textContent = node.props.code;
- return el;
+ return `${escapeHtml(node.props.code)}`;
},
mathInline: (node) => {
- const el = doc.createElement('code');
- el.textContent = node.props.formula;
- return el;
+ return `${escapeHtml(node.props.formula)}`;
},
mathBlock: (node) => {
- const el = doc.createElement('code');
- el.textContent = node.props.formula;
- return el;
+ return `${escapeHtml(node.props.formula)}
`;
},
link: (node) => {
- const a = doc.createElement('a');
- a.setAttribute('href', node.props.url);
- appendChildren(node.children, a);
- return a;
+ try {
+ const url = new URL(node.props.url);
+ return `${toHtml(node.children)}`;
+ } catch (err) {
+ return `[${toHtml(node.children)}](${escapeHtml(node.props.url)})`;
+ }
},
mention: (node) => {
- const a = doc.createElement('a');
const { username, host, acct } = node.props;
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase());
- a.setAttribute('href', remoteUserInfo
+ const href = remoteUserInfo
? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri)
- : `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`);
- a.className = 'u-url mention';
- a.textContent = acct;
- return a;
+ : `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`;
+ try {
+ const url = new URL(href);
+ return `${escapeHtml(acct)}`;
+ } catch (err) {
+ return escapeHtml(acct);
+ }
},
quote: (node) => {
- const el = doc.createElement('blockquote');
- appendChildren(node.children, el);
- return el;
+ return `${toHtml(node.children)}
`;
},
text: (node) => {
if (!node.props.text.match(/[\r\n]/)) {
- return doc.createTextNode(node.props.text);
+ return escapeHtml(node.props.text);
}
- const el = doc.createElement('span');
- const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
+ let html = '';
- for (const x of intersperse('br', nodes)) {
- el.appendChild(x === 'br' ? doc.createElement('br') : x);
+ const lines = node.props.text.split(/\r\n|\r|\n/).map(x => escapeHtml(x));
+
+ for (const x of intersperse('br', lines)) {
+ html += x === 'br' ? '
' : x;
}
- return el;
+ return html;
},
url: (node) => {
- const a = doc.createElement('a');
- a.setAttribute('href', node.props.url);
- a.textContent = node.props.url;
- return a;
+ try {
+ const url = new URL(node.props.url);
+ return `${escapeHtml(node.props.url)}`;
+ } catch (err) {
+ return escapeHtml(node.props.url);
+ }
},
search: (node) => {
- const a = doc.createElement('a');
- a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
- a.textContent = node.props.content;
- return a;
+ return `${escapeHtml(node.props.content)}`;
},
plain: (node) => {
- const el = doc.createElement('span');
- appendChildren(node.children, el);
- return el;
+ return `${toHtml(node.children)}`;
},
- };
+ } satisfies { [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => string } as { [K in mfm.MfmNode['type']]: (node: mfm.MfmNode) => string };
- appendChildren(nodes, body);
-
- for (const additionalAppender of additionalAppenders) {
- additionalAppender(doc, body);
- }
-
- // Remove the unnecessary namespace
- const serialized = new XMLSerializer().serializeToString(body).replace(/^\s*/, '
');
-
- happyDOM.close().catch(err => {});
-
- return serialized;
+ return `${toHtml(nodes)}${extraHtml ?? ''}`;
}
}
diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts
index eeade4569b..310ffec7ce 100644
--- a/packages/backend/src/core/NotificationService.ts
+++ b/packages/backend/src/core/NotificationService.ts
@@ -202,7 +202,7 @@ export class NotificationService implements OnApplicationShutdown {
}
// TODO
- //const locales = await import('../../../../locales/index.js');
+ //const locales = await import('i18n');
// TODO: locale ファイルをクライアント用とサーバー用で分けたい
@@ -271,7 +271,7 @@ export class NotificationService implements OnApplicationShutdown {
let untilTime = untilId ? this.toXListId(untilId) : null;
let notifications: MiNotification[];
- for (;;) {
+ for (; ;) {
let notificationsRes: [id: string, fields: string[]][];
// sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照
diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts
index 372e1e2ab7..31c8d67c60 100644
--- a/packages/backend/src/core/WebAuthnService.ts
+++ b/packages/backend/src/core/WebAuthnService.ts
@@ -66,7 +66,6 @@ export class WebAuthnService {
userID: isoUint8Array.fromUTF8String(userId),
userName: userName,
userDisplayName: userDisplayName,
- attestationType: 'indirect',
excludeCredentials: keys.map(key => (<{ id: string; transports?: AuthenticatorTransportFuture[]; }>{
id: key.id,
transports: key.transports ?? undefined,
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index e88f60b806..81637580e3 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
@@ -14,8 +15,8 @@ import { NotePiningService } from '@/core/NotePiningService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
+import { acquireApObjectLock } from '@/misc/distributed-lock.js';
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
-import { AppLockService } from '@/core/AppLockService.js';
import type Logger from '@/logger.js';
import { IdService } from '@/core/IdService.js';
import { StatusError } from '@/misc/status-error.js';
@@ -48,8 +49,8 @@ export class ApInboxService {
@Inject(DI.config)
private config: Config,
- @Inject(DI.meta)
- private meta: MiMeta,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -76,7 +77,6 @@ export class ApInboxService {
private userBlockingService: UserBlockingService,
private noteCreateService: NoteCreateService,
private noteDeleteService: NoteDeleteService,
- private appLockService: AppLockService,
private apResolverService: ApResolverService,
private apDbResolverService: ApDbResolverService,
private apLoggerService: ApLoggerService,
@@ -311,7 +311,7 @@ export class ApInboxService {
// アナウンス先が許可されているかチェック
if (!this.utilityService.isFederationAllowedUri(uri)) return;
- const unlock = await this.appLockService.getApLock(uri);
+ const unlock = await acquireApObjectLock(this.redisClient, uri);
try {
// 既に同じURIを持つものが登録されていないかチェック
@@ -438,7 +438,7 @@ export class ApInboxService {
}
}
- const unlock = await this.appLockService.getApLock(uri);
+ const unlock = await acquireApObjectLock(this.redisClient, uri);
try {
const exist = await this.apNoteService.fetchNote(note);
@@ -522,7 +522,7 @@ export class ApInboxService {
private async deleteNote(actor: MiRemoteUser, uri: string): Promise {
this.logger.info(`Deleting the Note: ${uri}`);
- const unlock = await this.appLockService.getApLock(uri);
+ const unlock = await acquireApObjectLock(this.redisClient, uri);
try {
const note = await this.apDbResolverService.getNoteFromApId(uri);
diff --git a/packages/backend/src/core/activitypub/ApMfmService.ts b/packages/backend/src/core/activitypub/ApMfmService.ts
index f4c07e472c..a928ed5ccf 100644
--- a/packages/backend/src/core/activitypub/ApMfmService.ts
+++ b/packages/backend/src/core/activitypub/ApMfmService.ts
@@ -5,7 +5,7 @@
import { Injectable } from '@nestjs/common';
import * as mfm from 'mfm-js';
-import { MfmService, Appender } from '@/core/MfmService.js';
+import { MfmService } from '@/core/MfmService.js';
import type { MiNote } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
import { extractApHashtagObjects } from './models/tag.js';
@@ -25,17 +25,17 @@ export class ApMfmService {
}
@bindThis
- public getNoteHtml(note: Pick, additionalAppender: Appender[] = []) {
+ public getNoteHtml(note: Pick, extraHtml: string | null = null) {
let noMisskeyContent = false;
const srcMfm = (note.text ?? '');
const parsed = mfm.parse(srcMfm);
- if (!additionalAppender.length && parsed.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
+ if (extraHtml == null && parsed.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
noMisskeyContent = true;
}
- const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers), additionalAppender);
+ const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers), extraHtml);
return {
content,
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index 55521d6e3a..4570977c5d 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -19,7 +19,7 @@ import type { MiEmoji } from '@/models/Emoji.js';
import type { MiPoll } from '@/models/Poll.js';
import type { MiPollVote } from '@/models/PollVote.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
-import { MfmService, type Appender } from '@/core/MfmService.js';
+import { MfmService } from '@/core/MfmService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { MiUserKeypair } from '@/models/UserKeypair.js';
@@ -28,6 +28,7 @@ import { bindThis } from '@/decorators.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { IdService } from '@/core/IdService.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { escapeHtml } from '@/misc/escape-html.js';
import { JsonLdService } from './JsonLdService.js';
import { ApMfmService } from './ApMfmService.js';
import { CONTEXT } from './misc/contexts.js';
@@ -384,7 +385,7 @@ export class ApRendererService {
inReplyTo = null;
}
- let quote;
+ let quote: string | undefined;
if (note.renoteId) {
const renote = await this.notesRepository.findOneBy({ id: note.renoteId });
@@ -430,29 +431,18 @@ export class ApRendererService {
poll = await this.pollsRepository.findOneBy({ noteId: note.id });
}
- const apAppend: Appender[] = [];
+ let extraHtml: string | null = null;
- if (quote) {
+ if (quote != null) {
// Append quote link as `
RE: ...`
- // the claas name `quote-inline` is used in non-misskey clients for styling quote notes.
+ // the class name `quote-inline` is used in non-misskey clients for styling quote notes.
// For compatibility, the span part should be kept as possible.
- apAppend.push((doc, body) => {
- body.appendChild(doc.createElement('br'));
- body.appendChild(doc.createElement('br'));
- const span = doc.createElement('span');
- span.className = 'quote-inline';
- span.appendChild(doc.createTextNode('RE: '));
- const link = doc.createElement('a');
- link.setAttribute('href', quote);
- link.textContent = quote;
- span.appendChild(link);
- body.appendChild(span);
- });
+ extraHtml = `
RE: ${escapeHtml(quote)}`;
}
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
- const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, apAppend);
+ const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, extraHtml);
const emojis = await this.getEmojis(note.emojis);
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts
index 61d328ccac..49298a1d22 100644
--- a/packages/backend/src/core/activitypub/ApRequestService.ts
+++ b/packages/backend/src/core/activitypub/ApRequestService.ts
@@ -6,7 +6,7 @@
import * as crypto from 'node:crypto';
import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
-import { Window } from 'happy-dom';
+import * as htmlParser from 'node-html-parser';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { MiUser } from '@/models/User.js';
@@ -215,29 +215,9 @@ export class ApRequestService {
_followAlternate === true
) {
const html = await res.text();
- const { window, happyDOM } = new Window({
- settings: {
- disableJavaScriptEvaluation: true,
- disableJavaScriptFileLoading: true,
- disableCSSFileLoading: true,
- disableComputedStyleRendering: true,
- handleDisabledFileLoadingAsSuccess: true,
- navigation: {
- disableMainFrameNavigation: true,
- disableChildFrameNavigation: true,
- disableChildPageNavigation: true,
- disableFallbackToSetURL: true,
- },
- timer: {
- maxTimeout: 0,
- maxIntervalTime: 0,
- maxIntervalIterations: 0,
- },
- },
- });
- const document = window.document;
+
try {
- document.documentElement.innerHTML = html;
+ const document = htmlParser.parse(html);
const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
if (alternate) {
@@ -248,8 +228,6 @@ export class ApRequestService {
}
} catch (e) {
// something went wrong parsing the HTML, ignore the whole thing
- } finally {
- happyDOM.close().catch(err => {});
}
}
//#endregion
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 8abacd293f..214d32f67f 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -5,14 +5,15 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import type { PollsRepository, EmojisRepository, MiMeta } from '@/models/_.js';
import type { Config } from '@/config.js';
import type { MiRemoteUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js';
+import { acquireApObjectLock } from '@/misc/distributed-lock.js';
import { toArray, toSingle, unique } from '@/misc/prelude/array.js';
import type { MiEmoji } from '@/models/Emoji.js';
-import { AppLockService } from '@/core/AppLockService.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import type Logger from '@/logger.js';
@@ -48,6 +49,9 @@ export class ApNoteService {
@Inject(DI.meta)
private meta: MiMeta,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
@@ -67,7 +71,6 @@ export class ApNoteService {
private apMentionService: ApMentionService,
private apImageService: ApImageService,
private apQuestionService: ApQuestionService,
- private appLockService: AppLockService,
private pollService: PollService,
private noteCreateService: NoteCreateService,
private apDbResolverService: ApDbResolverService,
@@ -354,7 +357,7 @@ export class ApNoteService {
throw new StatusError('blocked host', 451);
}
- const unlock = await this.appLockService.getApLock(uri);
+ const unlock = await acquireApObjectLock(this.redisClient, uri);
try {
//#region このサーバーに既に登録されていたらそれを返す
diff --git a/packages/backend/src/core/chart/charts/active-users.ts b/packages/backend/src/core/chart/charts/active-users.ts
index 05905f3782..7b9840af87 100644
--- a/packages/backend/src/core/chart/charts/active-users.ts
+++ b/packages/backend/src/core/chart/charts/active-users.ts
@@ -5,11 +5,12 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import { AppLockService } from '@/core/AppLockService.js';
+import * as Redis from 'ioredis';
import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/active-users.js';
@@ -28,11 +29,13 @@ export default class ActiveUsersChart extends Chart { // eslint-d
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
private chartLoggerService: ChartLoggerService,
private idService: IdService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/ap-request.ts b/packages/backend/src/core/chart/charts/ap-request.ts
index 04e771a95b..ed790de7b5 100644
--- a/packages/backend/src/core/chart/charts/ap-request.ts
+++ b/packages/backend/src/core/chart/charts/ap-request.ts
@@ -5,9 +5,10 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import { AppLockService } from '@/core/AppLockService.js';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/ap-request.js';
@@ -22,10 +23,12 @@ export default class ApRequestChart extends Chart { // eslint-dis
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/drive.ts b/packages/backend/src/core/chart/charts/drive.ts
index 613e074a9f..782873809a 100644
--- a/packages/backend/src/core/chart/charts/drive.ts
+++ b/packages/backend/src/core/chart/charts/drive.ts
@@ -5,10 +5,11 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { MiDriveFile } from '@/models/DriveFile.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/drive.js';
@@ -23,10 +24,12 @@ export default class DriveChart extends Chart { // eslint-disable
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts
index c9b43cc66d..b7a7f640b8 100644
--- a/packages/backend/src/core/chart/charts/federation.ts
+++ b/packages/backend/src/core/chart/charts/federation.ts
@@ -5,10 +5,11 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { FollowingsRepository, InstancesRepository, MiMeta } from '@/models/_.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/federation.js';
@@ -26,16 +27,18 @@ export default class FederationChart extends Chart { // eslint-di
@Inject(DI.meta)
private meta: MiMeta,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
- private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/instance.ts b/packages/backend/src/core/chart/charts/instance.ts
index 97f3bc6f2b..b1657e0a0b 100644
--- a/packages/backend/src/core/chart/charts/instance.ts
+++ b/packages/backend/src/core/chart/charts/instance.ts
@@ -5,13 +5,14 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { DriveFilesRepository, FollowingsRepository, UsersRepository, NotesRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiNote } from '@/models/Note.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/instance.js';
@@ -26,6 +27,9 @@ export default class InstanceChart extends Chart { // eslint-disa
@Inject(DI.db)
private db: DataSource,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -39,10 +43,9 @@ export default class InstanceChart extends Chart { // eslint-disa
private followingsRepository: FollowingsRepository,
private utilityService: UtilityService,
- private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/notes.ts b/packages/backend/src/core/chart/charts/notes.ts
index f763b5fffa..aa64e2329a 100644
--- a/packages/backend/src/core/chart/charts/notes.ts
+++ b/packages/backend/src/core/chart/charts/notes.ts
@@ -5,11 +5,12 @@
import { Injectable, Inject } from '@nestjs/common';
import { Not, IsNull, DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { NotesRepository } from '@/models/_.js';
import type { MiNote } from '@/models/Note.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/notes.js';
@@ -24,13 +25,15 @@ export default class NotesChart extends Chart { // eslint-disable
@Inject(DI.db)
private db: DataSource,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
- private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/per-user-drive.ts b/packages/backend/src/core/chart/charts/per-user-drive.ts
index 404964d8b7..f7e92aecea 100644
--- a/packages/backend/src/core/chart/charts/per-user-drive.ts
+++ b/packages/backend/src/core/chart/charts/per-user-drive.ts
@@ -5,12 +5,13 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { DriveFilesRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/per-user-drive.js';
@@ -25,14 +26,16 @@ export default class PerUserDriveChart extends Chart { // eslint-
@Inject(DI.db)
private db: DataSource,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
- private appLockService: AppLockService,
private driveFileEntityService: DriveFileEntityService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/per-user-following.ts b/packages/backend/src/core/chart/charts/per-user-following.ts
index 588ac638de..ea431a5131 100644
--- a/packages/backend/src/core/chart/charts/per-user-following.ts
+++ b/packages/backend/src/core/chart/charts/per-user-following.ts
@@ -5,12 +5,13 @@
import { Injectable, Inject } from '@nestjs/common';
import { Not, IsNull, DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { MiUser } from '@/models/User.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { FollowingsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/per-user-following.js';
@@ -25,14 +26,16 @@ export default class PerUserFollowingChart extends Chart { // esl
@Inject(DI.db)
private db: DataSource,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
- private appLockService: AppLockService,
private userEntityService: UserEntityService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/per-user-notes.ts b/packages/backend/src/core/chart/charts/per-user-notes.ts
index e4900772bb..824d60042d 100644
--- a/packages/backend/src/core/chart/charts/per-user-notes.ts
+++ b/packages/backend/src/core/chart/charts/per-user-notes.ts
@@ -5,12 +5,13 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { MiUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import type { NotesRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/per-user-notes.js';
@@ -25,13 +26,15 @@ export default class PerUserNotesChart extends Chart { // eslint-
@Inject(DI.db)
private db: DataSource,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
- private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/per-user-pv.ts b/packages/backend/src/core/chart/charts/per-user-pv.ts
index 31708fefa8..b3e1b2cea1 100644
--- a/packages/backend/src/core/chart/charts/per-user-pv.ts
+++ b/packages/backend/src/core/chart/charts/per-user-pv.ts
@@ -5,10 +5,11 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { MiUser } from '@/models/User.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/per-user-pv.js';
@@ -23,10 +24,12 @@ export default class PerUserPvChart extends Chart { // eslint-dis
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/per-user-reactions.ts b/packages/backend/src/core/chart/charts/per-user-reactions.ts
index c29c4d2870..7bc1d9e7fa 100644
--- a/packages/backend/src/core/chart/charts/per-user-reactions.ts
+++ b/packages/backend/src/core/chart/charts/per-user-reactions.ts
@@ -5,12 +5,13 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { MiUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/per-user-reactions.js';
@@ -25,11 +26,13 @@ export default class PerUserReactionsChart extends Chart { // esl
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
private userEntityService: UserEntityService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/test-grouped.ts b/packages/backend/src/core/chart/charts/test-grouped.ts
index 7a2844f4ed..8dd1a5d996 100644
--- a/packages/backend/src/core/chart/charts/test-grouped.ts
+++ b/packages/backend/src/core/chart/charts/test-grouped.ts
@@ -5,10 +5,11 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import { AppLockService } from '@/core/AppLockService.js';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { name, schema } from './entities/test-grouped.js';
import type { KVs } from '../core.js';
@@ -24,10 +25,12 @@ export default class TestGroupedChart extends Chart { // eslint-d
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
logger: Logger,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema, true);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/test-intersection.ts b/packages/backend/src/core/chart/charts/test-intersection.ts
index b8d0556c9f..23b8649cce 100644
--- a/packages/backend/src/core/chart/charts/test-intersection.ts
+++ b/packages/backend/src/core/chart/charts/test-intersection.ts
@@ -5,10 +5,11 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import { AppLockService } from '@/core/AppLockService.js';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { name, schema } from './entities/test-intersection.js';
import type { KVs } from '../core.js';
@@ -22,10 +23,12 @@ export default class TestIntersectionChart extends Chart { // esl
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
logger: Logger,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/test-unique.ts b/packages/backend/src/core/chart/charts/test-unique.ts
index f94e008059..b84dd419ba 100644
--- a/packages/backend/src/core/chart/charts/test-unique.ts
+++ b/packages/backend/src/core/chart/charts/test-unique.ts
@@ -5,10 +5,11 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import { AppLockService } from '@/core/AppLockService.js';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { name, schema } from './entities/test-unique.js';
import type { KVs } from '../core.js';
@@ -22,10 +23,12 @@ export default class TestUniqueChart extends Chart { // eslint-di
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
logger: Logger,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/test.ts b/packages/backend/src/core/chart/charts/test.ts
index a90dc8f99b..0e95ce9239 100644
--- a/packages/backend/src/core/chart/charts/test.ts
+++ b/packages/backend/src/core/chart/charts/test.ts
@@ -5,10 +5,11 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import { AppLockService } from '@/core/AppLockService.js';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { name, schema } from './entities/test.js';
import type { KVs } from '../core.js';
@@ -24,10 +25,12 @@ export default class TestChart extends Chart { // eslint-disable-
@Inject(DI.db)
private db: DataSource,
- private appLockService: AppLockService,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
logger: Logger,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/users.ts b/packages/backend/src/core/chart/charts/users.ts
index d148fc629b..4471c1df23 100644
--- a/packages/backend/src/core/chart/charts/users.ts
+++ b/packages/backend/src/core/chart/charts/users.ts
@@ -5,12 +5,13 @@
import { Injectable, Inject } from '@nestjs/common';
import { Not, IsNull, DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import type { MiUser } from '@/models/User.js';
-import { AppLockService } from '@/core/AppLockService.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UsersRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
+import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
import Chart from '../core.js';
import { ChartLoggerService } from '../ChartLoggerService.js';
import { name, schema } from './entities/users.js';
@@ -25,14 +26,16 @@ export default class UsersChart extends Chart { // eslint-disable
@Inject(DI.db)
private db: DataSource,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
- private appLockService: AppLockService,
private userEntityService: UserEntityService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
+ super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index 6871ba2c72..e7847ba74e 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -15,6 +15,7 @@ import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepos
import { bindThis } from '@/decorators.js';
import { DebounceLoader } from '@/misc/loader.js';
import { IdService } from '@/core/IdService.js';
+import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js';
@@ -116,12 +117,7 @@ export class NoteEntityService implements OnModuleInit {
private treatVisibility(packedNote: Packed<'Note'>): Packed<'Note'>['visibility'] {
if (packedNote.visibility === 'public' || packedNote.visibility === 'home') {
const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore;
- if ((followersOnlyBefore != null)
- && (
- (followersOnlyBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (followersOnlyBefore * 1000)))
- || (followersOnlyBefore > 0 && (new Date(packedNote.createdAt).getTime() < followersOnlyBefore * 1000))
- )
- ) {
+ if (shouldHideNoteByTime(followersOnlyBefore, packedNote.createdAt)) {
packedNote.visibility = 'followers';
}
}
@@ -141,12 +137,7 @@ export class NoteEntityService implements OnModuleInit {
if (!hide) {
const hiddenBefore = packedNote.user.makeNotesHiddenBefore;
- if ((hiddenBefore != null)
- && (
- (hiddenBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (hiddenBefore * 1000)))
- || (hiddenBefore > 0 && (new Date(packedNote.createdAt).getTime() < hiddenBefore * 1000))
- )
- ) {
+ if (shouldHideNoteByTime(hiddenBefore, packedNote.createdAt)) {
hide = true;
}
}
diff --git a/packages/backend/src/misc/distributed-lock.ts b/packages/backend/src/misc/distributed-lock.ts
new file mode 100644
index 0000000000..93bd741f62
--- /dev/null
+++ b/packages/backend/src/misc/distributed-lock.ts
@@ -0,0 +1,49 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Redis from 'ioredis';
+
+export async function acquireDistributedLock(
+ redis: Redis.Redis,
+ name: string,
+ timeout: number,
+ maxRetries: number,
+ retryInterval: number,
+): Promise<() => Promise> {
+ const lockKey = `lock:${name}`;
+ const identifier = Math.random().toString(36).slice(2);
+
+ let retries = 0;
+ while (retries < maxRetries) {
+ const result = await redis.set(lockKey, identifier, 'PX', timeout, 'NX');
+ if (result === 'OK') {
+ return async () => {
+ const currentIdentifier = await redis.get(lockKey);
+ if (currentIdentifier === identifier) {
+ await redis.del(lockKey);
+ }
+ };
+ }
+
+ await new Promise(resolve => setTimeout(resolve, retryInterval));
+ retries++;
+ }
+
+ throw new Error(`Failed to acquire lock ${name}`);
+}
+
+export function acquireApObjectLock(
+ redis: Redis.Redis,
+ uri: string,
+): Promise<() => Promise> {
+ return acquireDistributedLock(redis, `ap-object:${uri}`, 30 * 1000, 50, 100);
+}
+
+export function acquireChartInsertLock(
+ redis: Redis.Redis,
+ name: string,
+): Promise<() => Promise> {
+ return acquireDistributedLock(redis, `chart-insert:${name}`, 30 * 1000, 50, 500);
+}
diff --git a/packages/backend/src/misc/escape-html.ts b/packages/backend/src/misc/escape-html.ts
new file mode 100644
index 0000000000..819aeeed52
--- /dev/null
+++ b/packages/backend/src/misc/escape-html.ts
@@ -0,0 +1,13 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function escapeHtml(text: string): string {
+ return text
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
diff --git a/packages/backend/src/misc/json-stringify-html-safe.ts b/packages/backend/src/misc/json-stringify-html-safe.ts
new file mode 100644
index 0000000000..aac12d57db
--- /dev/null
+++ b/packages/backend/src/misc/json-stringify-html-safe.ts
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+const ESCAPE_LOOKUP = {
+ '&': '\\u0026',
+ '>': '\\u003e',
+ '<': '\\u003c',
+ '\u2028': '\\u2028',
+ '\u2029': '\\u2029',
+} as Record;
+
+const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
+
+export function htmlSafeJsonStringify(obj: any): string {
+ return JSON.stringify(obj).replace(ESCAPE_REGEX, x => ESCAPE_LOOKUP[x]);
+}
diff --git a/packages/backend/src/misc/should-hide-note-by-time.ts b/packages/backend/src/misc/should-hide-note-by-time.ts
new file mode 100644
index 0000000000..ea1951e66c
--- /dev/null
+++ b/packages/backend/src/misc/should-hide-note-by-time.ts
@@ -0,0 +1,29 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/**
+ * ノートが指定された時間条件に基づいて非表示対象かどうかを判定する
+ * @param hiddenBefore 非表示条件(負の値: 作成からの経過秒数、正の値: UNIXタイムスタンプ秒、null: 判定しない)
+ * @param createdAt ノートの作成日時(ISO 8601形式の文字列 または Date オブジェクト)
+ * @returns 非表示にすべき場合は true
+ */
+export function shouldHideNoteByTime(hiddenBefore: number | null | undefined, createdAt: string | Date): boolean {
+ if (hiddenBefore == null) {
+ return false;
+ }
+
+ const createdAtTime = typeof createdAt === 'string' ? new Date(createdAt).getTime() : createdAt.getTime();
+
+ if (hiddenBefore <= 0) {
+ // 負の値: 作成からの経過時間(秒)で判定
+ const elapsedSeconds = (Date.now() - createdAtTime) / 1000;
+ const hideAfterSeconds = Math.abs(hiddenBefore);
+ return elapsedSeconds >= hideAfterSeconds;
+ } else {
+ // 正の値: 絶対的なタイムスタンプ(秒)で判定
+ const createdAtSeconds = createdAtTime / 1000;
+ return createdAtSeconds <= hiddenBefore;
+ }
+}
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index 642d3fc8ad..306fdb41f6 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -5,7 +5,6 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as Bull from 'bullmq';
-import * as Sentry from '@sentry/node';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
@@ -157,6 +156,13 @@ export class QueueProcessorService implements OnApplicationShutdown {
};
}
+ let Sentry: typeof import('@sentry/node') | undefined;
+ if (Sentry != null) {
+ import('@sentry/node').then((mod) => {
+ Sentry = mod;
+ });
+ }
+
//#region system
{
const processer = (job: Bull.Job) => {
@@ -175,7 +181,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
};
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: System: ' + job.name }, () => processer(job));
} else {
return processer(job);
@@ -192,7 +198,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err: Error) => {
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
- if (config.sentryForBackend) {
+ if (Sentry != null) {
Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -232,7 +238,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
};
this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: DB: ' + job.name }, () => processer(job));
} else {
return processer(job);
@@ -249,7 +255,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
- if (config.sentryForBackend) {
+ if (Sentry != null) {
Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -264,7 +270,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region deliver
{
this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: Deliver' }, () => this.deliverProcessorService.process(job));
} else {
return this.deliverProcessorService.process(job);
@@ -289,7 +295,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
- if (config.sentryForBackend) {
+ if (Sentry != null) {
Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -304,7 +310,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region inbox
{
this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: Inbox' }, () => this.inboxProcessorService.process(job));
} else {
return this.inboxProcessorService.process(job);
@@ -329,7 +335,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job: renderJob(job), e: renderError(err) });
- if (config.sentryForBackend) {
+ if (Sentry != null) {
Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -344,7 +350,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region user-webhook deliver
{
this.userWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.USER_WEBHOOK_DELIVER, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: UserWebhookDeliver' }, () => this.userWebhookDeliverProcessorService.process(job));
} else {
return this.userWebhookDeliverProcessorService.process(job);
@@ -369,7 +375,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
- if (config.sentryForBackend) {
+ if (Sentry != null) {
Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -384,7 +390,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region system-webhook deliver
{
this.systemWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.SYSTEM_WEBHOOK_DELIVER, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: SystemWebhookDeliver' }, () => this.systemWebhookDeliverProcessorService.process(job));
} else {
return this.systemWebhookDeliverProcessorService.process(job);
@@ -409,7 +415,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
- if (config.sentryForBackend) {
+ if (Sentry != null) {
Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -434,7 +440,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
};
this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: Relationship: ' + job.name }, () => processer(job));
} else {
return processer(job);
@@ -456,7 +462,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
- if (config.sentryForBackend) {
+ if (Sentry != null) {
Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -479,7 +485,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
};
this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: ObjectStorage: ' + job.name }, () => processer(job));
} else {
return processer(job);
@@ -497,7 +503,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
- if (config.sentryForBackend) {
+ if (Sentry != null) {
Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -512,7 +518,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region ended poll notification
{
this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: EndedPollNotification' }, () => this.endedPollNotificationProcessorService.process(job));
} else {
return this.endedPollNotificationProcessorService.process(job);
@@ -527,7 +533,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region post scheduled note
{
this.postScheduledNoteQueueWorker = new Bull.Worker(QUEUE.POST_SCHEDULED_NOTE, async (job) => {
- if (this.config.sentryForBackend) {
+ if (Sentry != null) {
return Sentry.startSpan({ name: 'Queue: PostScheduledNote' }, () => this.postScheduledNoteProcessorService.process(job));
} else {
return this.postScheduledNoteProcessorService.process(job);
diff --git a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts
index 486dc4c01f..be7d4e9e21 100644
--- a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts
@@ -5,21 +5,20 @@
import * as fs from 'node:fs';
import { Writable } from 'node:stream';
-import { Inject, Injectable, StreamableFile } from '@nestjs/common';
-import { MoreThan } from 'typeorm';
+import { Inject, Injectable } from '@nestjs/common';
import { format as dateFormat } from 'date-fns';
import { DI } from '@/di-symbols.js';
-import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js';
+import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, PollsRepository, UsersRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js';
import type { MiPoll } from '@/models/Poll.js';
import type { MiNote } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
-import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
-import { Packed } from '@/misc/json-schema.js';
import { IdService } from '@/core/IdService.js';
import { NotificationService } from '@/core/NotificationService.js';
+import { QueryService } from '@/core/QueryService.js';
+import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js';
@@ -43,6 +42,7 @@ export class ExportClipsProcessorService {
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
+ private queryService: QueryService,
private idService: IdService,
private notificationService: NotificationService,
) {
@@ -100,16 +100,16 @@ export class ExportClipsProcessorService {
});
while (true) {
- const clips = await this.clipsRepository.find({
- where: {
- userId: user.id,
- ...(cursor ? { id: MoreThan(cursor) } : {}),
- },
- take: 100,
- order: {
- id: 1,
- },
- });
+ const query = this.clipsRepository.createQueryBuilder('clip')
+ .where('clip.userId = :userId', { userId: user.id })
+ .orderBy('clip.id', 'ASC')
+ .take(100);
+
+ if (cursor) {
+ query.andWhere('clip.id > :cursor', { cursor });
+ }
+
+ const clips = await query.getMany();
if (clips.length === 0) {
job.updateProgress(100);
@@ -124,7 +124,7 @@ export class ExportClipsProcessorService {
const isFirst = exportedClipsCount === 0;
await writer.write(isFirst ? content : ',\n' + content);
- await this.processClipNotes(writer, clip.id);
+ await this.processClipNotes(writer, clip.id, user.id);
await writer.write(']}');
exportedClipsCount++;
@@ -134,22 +134,25 @@ export class ExportClipsProcessorService {
}
}
- async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string): Promise {
+ async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string, userId: string): Promise {
let exportedClipNotesCount = 0;
let cursor: MiClipNote['id'] | null = null;
while (true) {
- const clipNotes = await this.clipNotesRepository.find({
- where: {
- clipId,
- ...(cursor ? { id: MoreThan(cursor) } : {}),
- },
- take: 100,
- order: {
- id: 1,
- },
- relations: ['note', 'note.user'],
- }) as (MiClipNote & { note: MiNote & { user: MiUser } })[];
+ const query = this.clipNotesRepository.createQueryBuilder('clipNote')
+ .leftJoinAndSelect('clipNote.note', 'note')
+ .leftJoinAndSelect('note.user', 'user')
+ .where('clipNote.clipId = :clipId', { clipId })
+ .orderBy('clipNote.id', 'ASC')
+ .take(100);
+
+ if (cursor) {
+ query.andWhere('clipNote.id > :cursor', { cursor });
+ }
+
+ this.queryService.generateVisibilityQuery(query, { id: userId });
+
+ const clipNotes = await query.getMany() as (MiClipNote & { note: MiNote & { user: MiUser } })[];
if (clipNotes.length === 0) {
break;
@@ -158,6 +161,11 @@ export class ExportClipsProcessorService {
cursor = clipNotes.at(-1)?.id ?? null;
for (const clipNote of clipNotes) {
+ const noteCreatedAt = this.idService.parse(clipNote.note.id).date;
+ if (shouldHideNoteByTime(clipNote.note.user.makeNotesHiddenBefore, noteCreatedAt)) {
+ continue;
+ }
+
let poll: MiPoll | undefined;
if (clipNote.note.hasPoll) {
poll = await this.pollsRepository.findOneByOrFail({ noteId: clipNote.note.id });
diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
index 7918c8ccb5..87a8ded307 100644
--- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
@@ -5,7 +5,6 @@
import * as fs from 'node:fs';
import { Inject, Injectable } from '@nestjs/common';
-import { MoreThan } from 'typeorm';
import { format as dateFormat } from 'date-fns';
import { DI } from '@/di-symbols.js';
import type { MiNoteFavorite, NoteFavoritesRepository, PollsRepository, MiUser, UsersRepository } from '@/models/_.js';
@@ -17,6 +16,8 @@ import type { MiNote } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import { NotificationService } from '@/core/NotificationService.js';
+import { QueryService } from '@/core/QueryService.js';
+import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js';
@@ -37,6 +38,7 @@ export class ExportFavoritesProcessorService {
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
+ private queryService: QueryService,
private idService: IdService,
private notificationService: NotificationService,
) {
@@ -83,17 +85,20 @@ export class ExportFavoritesProcessorService {
});
while (true) {
- const favorites = await this.noteFavoritesRepository.find({
- where: {
- userId: user.id,
- ...(cursor ? { id: MoreThan(cursor) } : {}),
- },
- take: 100,
- order: {
- id: 1,
- },
- relations: ['note', 'note.user'],
- }) as (MiNoteFavorite & { note: MiNote & { user: MiUser } })[];
+ const query = this.noteFavoritesRepository.createQueryBuilder('favorite')
+ .leftJoinAndSelect('favorite.note', 'note')
+ .leftJoinAndSelect('note.user', 'user')
+ .where('favorite.userId = :userId', { userId: user.id })
+ .orderBy('favorite.id', 'ASC')
+ .take(100);
+
+ if (cursor) {
+ query.andWhere('favorite.id > :cursor', { cursor });
+ }
+
+ this.queryService.generateVisibilityQuery(query, { id: user.id });
+
+ const favorites = await query.getMany() as (MiNoteFavorite & { note: MiNote & { user: MiUser } })[];
if (favorites.length === 0) {
job.updateProgress(100);
@@ -103,6 +108,11 @@ export class ExportFavoritesProcessorService {
cursor = favorites.at(-1)?.id ?? null;
for (const favorite of favorites) {
+ const noteCreatedAt = this.idService.parse(favorite.note.id).date;
+ if (shouldHideNoteByTime(favorite.note.user.makeNotesHiddenBefore, noteCreatedAt)) {
+ continue;
+ }
+
let poll: MiPoll | undefined;
if (favorite.note.hasPoll) {
poll = await this.pollsRepository.findOneByOrFail({ noteId: favorite.note.id });
diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts
index 0223650329..111421472d 100644
--- a/packages/backend/src/server/ServerModule.ts
+++ b/packages/backend/src/server/ServerModule.ts
@@ -25,6 +25,7 @@ import { SignupApiService } from './api/SignupApiService.js';
import { StreamingApiServerService } from './api/StreamingApiServerService.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { ClientServerService } from './web/ClientServerService.js';
+import { HtmlTemplateService } from './web/HtmlTemplateService.js';
import { FeedService } from './web/FeedService.js';
import { UrlPreviewService } from './web/UrlPreviewService.js';
import { ClientLoggerService } from './web/ClientLoggerService.js';
@@ -58,6 +59,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
providers: [
ClientServerService,
ClientLoggerService,
+ HtmlTemplateService,
FeedService,
HealthServerService,
UrlPreviewService,
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 1286b4dad6..4e05322b12 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown {
@bindThis
public async launch(): Promise {
const fastify = Fastify({
- trustProxy: this.config.trustProxy ?? true,
+ trustProxy: this.config.trustProxy ?? false,
logger: false,
});
this.#fastify = fastify;
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 7a4af407a3..27c79ab438 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -7,7 +7,6 @@ import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs';
import * as stream from 'node:stream/promises';
import { Inject, Injectable } from '@nestjs/common';
-import * as Sentry from '@sentry/node';
import { DI } from '@/di-symbols.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
@@ -37,6 +36,7 @@ export class ApiCallService implements OnApplicationShutdown {
private logger: Logger;
private userIpHistories: Map>;
private userIpHistoriesClearIntervalId: NodeJS.Timeout;
+ private Sentry: typeof import('@sentry/node') | null = null;
constructor(
@Inject(DI.meta)
@@ -59,6 +59,12 @@ export class ApiCallService implements OnApplicationShutdown {
this.userIpHistoriesClearIntervalId = setInterval(() => {
this.userIpHistories.clear();
}, 1000 * 60 * 60);
+
+ if (this.config.sentryForBackend) {
+ import('@sentry/node').then((Sentry) => {
+ this.Sentry = Sentry;
+ });
+ }
}
#sendApiError(reply: FastifyReply, err: ApiError): void {
@@ -120,8 +126,8 @@ export class ApiCallService implements OnApplicationShutdown {
},
});
- if (this.config.sentryForBackend) {
- Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
+ if (this.Sentry != null) {
+ this.Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
level: 'error',
user: {
id: userId,
@@ -432,8 +438,8 @@ export class ApiCallService implements OnApplicationShutdown {
}
// API invoking
- if (this.config.sentryForBackend) {
- return await Sentry.startSpan({
+ if (this.Sentry != null) {
+ return await this.Sentry.startSpan({
name: 'API: ' + ep.name,
}, () => ep.exec(data, user, token, file, request.ip, request.headers)
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 113a09cb14..9971a1ea4d 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -7,7 +7,7 @@ import RE2 from 're2';
import * as mfm from 'mfm-js';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
-import { JSDOM } from 'jsdom';
+import * as htmlParser from 'node-html-parser';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
import * as Acct from '@/misc/acct.js';
@@ -569,16 +569,15 @@ export default class extends Endpoint { // eslint-
try {
const html = await this.httpRequestService.getHtml(url);
- const { window } = new JSDOM(html);
- const doc: Document = window.document;
+ const doc = htmlParser.parse(html);
const myLink = `${this.config.url}/@${user.username}`;
const aEls = Array.from(doc.getElementsByTagName('a'));
const linkEls = Array.from(doc.getElementsByTagName('link'));
- const includesMyLink = aEls.some(a => a.href === myLink);
- const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink);
+ const includesMyLink = aEls.some(a => a.attributes.href === myLink);
+ const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.attributes.rel?.split(/\s+/).includes('me') && link.attributes.href === myLink);
if (includesMyLink || includesRelMeLinks) {
await this.userProfilesRepository.createQueryBuilder('profile').update()
@@ -588,8 +587,6 @@ export default class extends Endpoint { // eslint-
})
.execute();
}
-
- window.close();
} catch (err) {
// なにもしない
}
diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts
index cdd7102666..d2391c43ab 100644
--- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts
+++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts
@@ -6,18 +6,15 @@
import dns from 'node:dns/promises';
import { fileURLToPath } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
-import { JSDOM } from 'jsdom';
+import * as htmlParser from 'node-html-parser';
import httpLinkHeader from 'http-link-header';
import ipaddr from 'ipaddr.js';
import oauth2orize, { type OAuth2, AuthorizationError, ValidateFunctionArity2, OAuth2Req, MiddlewareRequest } from 'oauth2orize';
import oauth2Pkce from 'oauth2orize-pkce';
import fastifyCors from '@fastify/cors';
-import fastifyView from '@fastify/view';
-import pug from 'pug';
import bodyParser from 'body-parser';
import fastifyExpress from '@fastify/express';
import { verifyChallenge } from 'pkce-challenge';
-import { mf2 } from 'microformats-parser';
import { permissions as kinds } from 'misskey-js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
@@ -32,6 +29,8 @@ import { MemoryKVCache } from '@/misc/cache.js';
import { LoggerService } from '@/core/LoggerService.js';
import Logger from '@/logger.js';
import { StatusError } from '@/misc/status-error.js';
+import { HtmlTemplateService } from '@/server/web/HtmlTemplateService.js';
+import { OAuthPage } from '@/server/web/views/oauth.js';
import type { ServerResponse } from 'node:http';
import type { FastifyInstance } from 'fastify';
@@ -98,6 +97,32 @@ interface ClientInformation {
logo: string | null;
}
+function parseMicroformats(doc: htmlParser.HTMLElement, baseUrl: string, id: string): { name: string | null; logo: string | null; } {
+ let name: string | null = null;
+ let logo: string | null = null;
+
+ const hApp = doc.querySelector('.h-app');
+ if (hApp == null) return { name, logo };
+
+ const nameEl = hApp.querySelector('.p-name');
+ if (nameEl != null) {
+ const href = nameEl.attributes.href || nameEl.attributes.src;
+ if (href != null && new URL(href, baseUrl).toString() === new URL(id).toString()) {
+ name = nameEl.textContent.trim();
+ }
+ }
+
+ const logoEl = hApp.querySelector('.u-logo');
+ if (logoEl != null) {
+ const href = logoEl.attributes.href || logoEl.attributes.src;
+ if (href != null) {
+ logo = new URL(href, baseUrl).toString();
+ }
+ }
+
+ return { name, logo };
+}
+
// https://indieauth.spec.indieweb.org/#client-information-discovery
// "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id,
// and if there is an [h-app] with a url property matching the client_id URL,
@@ -120,24 +145,19 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
}
const text = await res.text();
- const fragment = JSDOM.fragment(text);
+ const doc = htmlParser.parse(`${text}`);
- redirectUris.push(...[...fragment.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.href));
+ redirectUris.push(...[...doc.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href));
let name = id;
let logo: string | null = null;
if (text) {
- const microformats = mf2(text, { baseUrl: res.url });
- const correspondingProperties = microformats.items.find(item => item.type?.includes('h-app') && item.properties.url.includes(id));
- if (correspondingProperties) {
- const nameProperty = correspondingProperties.properties.name?.[0];
- if (typeof nameProperty === 'string') {
- name = nameProperty;
- }
- const logoProperty = correspondingProperties.properties.logo?.[0];
- if (typeof logoProperty === 'string') {
- logo = logoProperty;
- }
+ const microformats = parseMicroformats(doc, res.url, id);
+ if (typeof microformats.name === 'string') {
+ name = microformats.name;
+ }
+ if (typeof microformats.logo === 'string') {
+ logo = microformats.logo;
}
}
@@ -253,6 +273,7 @@ export class OAuth2ProviderService {
private usersRepository: UsersRepository,
private cacheService: CacheService,
loggerService: LoggerService,
+ private htmlTemplateService: HtmlTemplateService,
) {
this.#logger = loggerService.getLogger('oauth');
@@ -386,24 +407,16 @@ export class OAuth2ProviderService {
this.#logger.info(`Rendering authorization page for "${oauth2.client.name}"`);
reply.header('Cache-Control', 'no-store');
- return await reply.view('oauth', {
+ return await HtmlTemplateService.replyHtml(reply, OAuthPage({
+ ...await this.htmlTemplateService.getCommonData(),
transactionId: oauth2.transactionID,
clientName: oauth2.client.name,
- clientLogo: oauth2.client.logo,
- scope: oauth2.req.scope.join(' '),
- });
+ clientLogo: oauth2.client.logo ?? undefined,
+ scope: oauth2.req.scope,
+ }));
});
fastify.post('/decision', async () => { });
- fastify.register(fastifyView, {
- root: fileURLToPath(new URL('../web/views', import.meta.url)),
- engine: { pug },
- defaultContext: {
- version: this.config.version,
- config: this.config,
- },
- });
-
await fastify.register(fastifyExpress);
fastify.use('/authorize', this.#server.authorize(((areq, done) => {
(async (): Promise> => {
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index f9d904f3cd..bcea935409 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -9,21 +9,16 @@ import { fileURLToPath } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import sharp from 'sharp';
-import pug from 'pug';
import { In, IsNull } from 'typeorm';
import fastifyStatic from '@fastify/static';
-import fastifyView from '@fastify/view';
import fastifyProxy from '@fastify/http-proxy';
import vary from 'vary';
-import htmlSafeJsonStringify from 'htmlescape';
import type { Config } from '@/config.js';
-import { getNoteSummary } from '@/misc/get-note-summary.js';
import { DI } from '@/di-symbols.js';
import * as Acct from '@/misc/acct.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js';
-import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
@@ -42,14 +37,33 @@ import type {
} from '@/models/_.js';
import type Logger from '@/logger.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
+import { htmlSafeJsonStringify } from '@/misc/json-stringify-html-safe.js';
import { bindThis } from '@/decorators.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
-import { RoleService } from '@/core/RoleService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { FeedService } from './FeedService.js';
import { UrlPreviewService } from './UrlPreviewService.js';
import { ClientLoggerService } from './ClientLoggerService.js';
+import { HtmlTemplateService } from './HtmlTemplateService.js';
+
+import { BasePage } from './views/base.js';
+import { UserPage } from './views/user.js';
+import { NotePage } from './views/note.js';
+import { PagePage } from './views/page.js';
+import { ClipPage } from './views/clip.js';
+import { FlashPage } from './views/flash.js';
+import { GalleryPostPage } from './views/gallery-post.js';
+import { ChannelPage } from './views/channel.js';
+import { ReversiGamePage } from './views/reversi-game.js';
+import { AnnouncementPage } from './views/announcement.js';
+import { BaseEmbed } from './views/base-embed.js';
+import { InfoCardPage } from './views/info-card.js';
+import { BiosPage } from './views/bios.js';
+import { CliPage } from './views/cli.js';
+import { FlushPage } from './views/flush.js';
+import { ErrorPage } from './views/error.js';
+
import type { FastifyError, FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify';
const _filename = fileURLToPath(import.meta.url);
@@ -108,7 +122,6 @@ export class ClientServerService {
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private pageEntityService: PageEntityService,
- private metaEntityService: MetaEntityService,
private galleryPostEntityService: GalleryPostEntityService,
private clipEntityService: ClipEntityService,
private channelEntityService: ChannelEntityService,
@@ -116,7 +129,7 @@ export class ClientServerService {
private announcementEntityService: AnnouncementEntityService,
private urlPreviewService: UrlPreviewService,
private feedService: FeedService,
- private roleService: RoleService,
+ private htmlTemplateService: HtmlTemplateService,
private clientLoggerService: ClientLoggerService,
) {
//this.createServer = this.createServer.bind(this);
@@ -182,38 +195,10 @@ export class ClientServerService {
return (manifest);
}
- @bindThis
- private async generateCommonPugData(meta: MiMeta) {
- return {
- instanceName: meta.name ?? 'Misskey',
- icon: meta.iconUrl,
- appleTouchIcon: meta.app512IconUrl,
- themeColor: meta.themeColor,
- serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg',
- infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',
- notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg',
- instanceUrl: this.config.url,
- metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)),
- now: Date.now(),
- federationEnabled: this.meta.federation !== 'none',
- };
- }
-
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
const configUrl = new URL(this.config.url);
- fastify.register(fastifyView, {
- root: _dirname + '/views',
- engine: {
- pug: pug,
- },
- defaultContext: {
- version: this.config.version,
- config: this.config,
- },
- });
-
fastify.addHook('onRequest', (request, reply, done) => {
// クリックジャッキング防止のためiFrameの中に入れられないようにする
reply.header('X-Frame-Options', 'DENY');
@@ -414,16 +399,15 @@ export class ClientServerService {
//#endregion
- const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => {
+ const renderBase = async (reply: FastifyReply, data: Partial[0]> = {}) => {
reply.header('Cache-Control', 'public, max-age=30');
- return await reply.view('base', {
- img: this.meta.bannerUrl,
- url: this.config.url,
+ return await HtmlTemplateService.replyHtml(reply, BasePage({
+ img: this.meta.bannerUrl ?? undefined,
title: this.meta.name ?? 'Misskey',
- desc: this.meta.description,
- ...await this.generateCommonPugData(this.meta),
+ desc: this.meta.description ?? undefined,
+ ...await this.htmlTemplateService.getCommonData(),
...data,
- });
+ }));
};
// URL preview endpoint
@@ -505,11 +489,6 @@ export class ClientServerService {
)
) {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
- const me = profile.fields
- ? profile.fields
- .filter(filed => filed.value != null && filed.value.match(/^https?:/))
- .map(field => field.value)
- : [];
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
@@ -522,15 +501,15 @@ export class ClientServerService {
userProfile: profile,
});
- return await reply.view('user', {
- user, profile, me,
- avatarUrl: _user.avatarUrl,
+ return await HtmlTemplateService.replyHtml(reply, UserPage({
+ user: _user,
+ profile,
sub: request.params.sub,
- ...await this.generateCommonPugData(this.meta),
- clientCtx: htmlSafeJsonStringify({
+ ...await this.htmlTemplateService.getCommonData(),
+ clientCtxJson: htmlSafeJsonStringify({
user: _user,
}),
- });
+ }));
} else {
// リモートユーザーなので
// モデレータがAPI経由で参照可能にするために404にはしない
@@ -581,17 +560,14 @@ export class ClientServerService {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
- return await reply.view('note', {
+ return await HtmlTemplateService.replyHtml(reply, NotePage({
note: _note,
profile,
- avatarUrl: _note.user.avatarUrl,
- // TODO: Let locale changeable by instance setting
- summary: getNoteSummary(_note),
- ...await this.generateCommonPugData(this.meta),
- clientCtx: htmlSafeJsonStringify({
+ ...await this.htmlTemplateService.getCommonData(),
+ clientCtxJson: htmlSafeJsonStringify({
note: _note,
}),
- });
+ }));
} else {
return await renderBase(reply);
}
@@ -624,12 +600,11 @@ export class ClientServerService {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
- return await reply.view('page', {
+ return await HtmlTemplateService.replyHtml(reply, PagePage({
page: _page,
profile,
- avatarUrl: _page.user.avatarUrl,
- ...await this.generateCommonPugData(this.meta),
- });
+ ...await this.htmlTemplateService.getCommonData(),
+ }));
} else {
return await renderBase(reply);
}
@@ -649,12 +624,11 @@ export class ClientServerService {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
- return await reply.view('flash', {
+ return await HtmlTemplateService.replyHtml(reply, FlashPage({
flash: _flash,
profile,
- avatarUrl: _flash.user.avatarUrl,
- ...await this.generateCommonPugData(this.meta),
- });
+ ...await this.htmlTemplateService.getCommonData(),
+ }));
} else {
return await renderBase(reply);
}
@@ -674,15 +648,14 @@ export class ClientServerService {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
- return await reply.view('clip', {
+ return await HtmlTemplateService.replyHtml(reply, ClipPage({
clip: _clip,
profile,
- avatarUrl: _clip.user.avatarUrl,
- ...await this.generateCommonPugData(this.meta),
- clientCtx: htmlSafeJsonStringify({
+ ...await this.htmlTemplateService.getCommonData(),
+ clientCtxJson: htmlSafeJsonStringify({
clip: _clip,
}),
- });
+ }));
} else {
return await renderBase(reply);
}
@@ -700,12 +673,11 @@ export class ClientServerService {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
- return await reply.view('gallery-post', {
- post: _post,
+ return await HtmlTemplateService.replyHtml(reply, GalleryPostPage({
+ galleryPost: _post,
profile,
- avatarUrl: _post.user.avatarUrl,
- ...await this.generateCommonPugData(this.meta),
- });
+ ...await this.htmlTemplateService.getCommonData(),
+ }));
} else {
return await renderBase(reply);
}
@@ -720,10 +692,10 @@ export class ClientServerService {
if (channel) {
const _channel = await this.channelEntityService.pack(channel);
reply.header('Cache-Control', 'public, max-age=15');
- return await reply.view('channel', {
+ return await HtmlTemplateService.replyHtml(reply, ChannelPage({
channel: _channel,
- ...await this.generateCommonPugData(this.meta),
- });
+ ...await this.htmlTemplateService.getCommonData(),
+ }));
} else {
return await renderBase(reply);
}
@@ -738,10 +710,10 @@ export class ClientServerService {
if (game) {
const _game = await this.reversiGameEntityService.packDetail(game);
reply.header('Cache-Control', 'public, max-age=3600');
- return await reply.view('reversi-game', {
- game: _game,
- ...await this.generateCommonPugData(this.meta),
- });
+ return await HtmlTemplateService.replyHtml(reply, ReversiGamePage({
+ reversiGame: _game,
+ ...await this.htmlTemplateService.getCommonData(),
+ }));
} else {
return await renderBase(reply);
}
@@ -757,10 +729,10 @@ export class ClientServerService {
if (announcement) {
const _announcement = await this.announcementEntityService.pack(announcement);
reply.header('Cache-Control', 'public, max-age=3600');
- return await reply.view('announcement', {
+ return await HtmlTemplateService.replyHtml(reply, AnnouncementPage({
announcement: _announcement,
- ...await this.generateCommonPugData(this.meta),
- });
+ ...await this.htmlTemplateService.getCommonData(),
+ }));
} else {
return await renderBase(reply);
}
@@ -793,13 +765,13 @@ export class ClientServerService {
const _user = await this.userEntityService.pack(user);
reply.header('Cache-Control', 'public, max-age=3600');
- return await reply.view('base-embed', {
+ return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
title: this.meta.name ?? 'Misskey',
- ...await this.generateCommonPugData(this.meta),
- embedCtx: htmlSafeJsonStringify({
+ ...await this.htmlTemplateService.getCommonData(),
+ embedCtxJson: htmlSafeJsonStringify({
user: _user,
}),
- });
+ }));
});
fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => {
@@ -819,13 +791,13 @@ export class ClientServerService {
const _note = await this.noteEntityService.pack(note, null, { detail: true });
reply.header('Cache-Control', 'public, max-age=3600');
- return await reply.view('base-embed', {
+ return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
title: this.meta.name ?? 'Misskey',
- ...await this.generateCommonPugData(this.meta),
- embedCtx: htmlSafeJsonStringify({
+ ...await this.htmlTemplateService.getCommonData(),
+ embedCtxJson: htmlSafeJsonStringify({
note: _note,
}),
- });
+ }));
});
fastify.get<{ Params: { clip: string; } }>('/embed/clips/:clip', async (request, reply) => {
@@ -840,48 +812,46 @@ export class ClientServerService {
const _clip = await this.clipEntityService.pack(clip);
reply.header('Cache-Control', 'public, max-age=3600');
- return await reply.view('base-embed', {
+ return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
title: this.meta.name ?? 'Misskey',
- ...await this.generateCommonPugData(this.meta),
- embedCtx: htmlSafeJsonStringify({
+ ...await this.htmlTemplateService.getCommonData(),
+ embedCtxJson: htmlSafeJsonStringify({
clip: _clip,
}),
- });
+ }));
});
fastify.get('/embed/*', async (request, reply) => {
reply.removeHeader('X-Frame-Options');
reply.header('Cache-Control', 'public, max-age=3600');
- return await reply.view('base-embed', {
+ return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
title: this.meta.name ?? 'Misskey',
- ...await this.generateCommonPugData(this.meta),
- });
+ ...await this.htmlTemplateService.getCommonData(),
+ }));
});
fastify.get('/_info_card_', async (request, reply) => {
reply.removeHeader('X-Frame-Options');
- return await reply.view('info-card', {
+ return await HtmlTemplateService.replyHtml(reply, InfoCardPage({
version: this.config.version,
- host: this.config.host,
+ config: this.config,
meta: this.meta,
- originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }),
- originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }),
- });
+ }));
});
//#endregion
fastify.get('/bios', async (request, reply) => {
- return await reply.view('bios', {
+ return await HtmlTemplateService.replyHtml(reply, BiosPage({
version: this.config.version,
- });
+ }));
});
fastify.get('/cli', async (request, reply) => {
- return await reply.view('cli', {
+ return await HtmlTemplateService.replyHtml(reply, CliPage({
version: this.config.version,
- });
+ }));
});
const override = (source: string, target: string, depth = 0) =>
@@ -904,7 +874,7 @@ export class ClientServerService {
reply.header('Clear-Site-Data', '"*"');
}
reply.header('Set-Cookie', 'http-flush-failed=1; Path=/flush; Max-Age=60');
- return await reply.view('flush');
+ return await HtmlTemplateService.replyHtml(reply, FlushPage());
});
// streamingに非WebSocketリクエストが来た場合にbase htmlをキャシュ付きで返すと、Proxy等でそのパスがキャッシュされておかしくなる
@@ -930,10 +900,10 @@ export class ClientServerService {
});
reply.code(500);
reply.header('Cache-Control', 'max-age=10, must-revalidate');
- return await reply.view('error', {
+ return await HtmlTemplateService.replyHtml(reply, ErrorPage({
code: error.code,
id: errId,
- });
+ }));
});
done();
diff --git a/packages/backend/src/server/web/HtmlTemplateService.ts b/packages/backend/src/server/web/HtmlTemplateService.ts
new file mode 100644
index 0000000000..8ff985530d
--- /dev/null
+++ b/packages/backend/src/server/web/HtmlTemplateService.ts
@@ -0,0 +1,105 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { promises as fsp } from 'node:fs';
+import { languages } from 'i18n/const';
+import { Injectable, Inject } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { htmlSafeJsonStringify } from '@/misc/json-stringify-html-safe.js';
+import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
+import type { FastifyReply } from 'fastify';
+import type { Config } from '@/config.js';
+import type { MiMeta } from '@/models/Meta.js';
+import type { CommonData } from './views/_.js';
+
+const _filename = fileURLToPath(import.meta.url);
+const _dirname = dirname(_filename);
+
+const frontendVitePublic = `${_dirname}/../../../../frontend/public/`;
+const frontendEmbedVitePublic = `${_dirname}/../../../../frontend-embed/public/`;
+
+@Injectable()
+export class HtmlTemplateService {
+ private frontendBootloadersFetched = false;
+ public frontendBootloaderJs: string | null = null;
+ public frontendBootloaderCss: string | null = null;
+ public frontendEmbedBootloaderJs: string | null = null;
+ public frontendEmbedBootloaderCss: string | null = null;
+
+ constructor(
+ @Inject(DI.config)
+ private config: Config,
+
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
+ private metaEntityService: MetaEntityService,
+ ) {
+ }
+
+ @bindThis
+ private async prepareFrontendBootloaders() {
+ if (this.frontendBootloadersFetched) return;
+ this.frontendBootloadersFetched = true;
+
+ const [bootJs, bootCss, embedBootJs, embedBootCss] = await Promise.all([
+ fsp.readFile(`${frontendVitePublic}loader/boot.js`, 'utf-8').catch(() => null),
+ fsp.readFile(`${frontendVitePublic}loader/style.css`, 'utf-8').catch(() => null),
+ fsp.readFile(`${frontendEmbedVitePublic}loader/boot.js`, 'utf-8').catch(() => null),
+ fsp.readFile(`${frontendEmbedVitePublic}loader/style.css`, 'utf-8').catch(() => null),
+ ]);
+
+ if (bootJs != null) {
+ this.frontendBootloaderJs = bootJs;
+ }
+
+ if (bootCss != null) {
+ this.frontendBootloaderCss = bootCss;
+ }
+
+ if (embedBootJs != null) {
+ this.frontendEmbedBootloaderJs = embedBootJs;
+ }
+
+ if (embedBootCss != null) {
+ this.frontendEmbedBootloaderCss = embedBootCss;
+ }
+ }
+
+ @bindThis
+ public async getCommonData(): Promise {
+ await this.prepareFrontendBootloaders();
+
+ return {
+ version: this.config.version,
+ config: this.config,
+ langs: [...languages],
+ instanceName: this.meta.name ?? 'Misskey',
+ icon: this.meta.iconUrl,
+ appleTouchIcon: this.meta.app512IconUrl,
+ themeColor: this.meta.themeColor,
+ serverErrorImageUrl: this.meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg',
+ infoImageUrl: this.meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',
+ notFoundImageUrl: this.meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg',
+ instanceUrl: this.config.url,
+ metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(this.meta)),
+ now: Date.now(),
+ federationEnabled: this.meta.federation !== 'none',
+ frontendBootloaderJs: this.frontendBootloaderJs,
+ frontendBootloaderCss: this.frontendBootloaderCss,
+ frontendEmbedBootloaderJs: this.frontendEmbedBootloaderJs,
+ frontendEmbedBootloaderCss: this.frontendEmbedBootloaderCss,
+ };
+ }
+
+ public static async replyHtml(reply: FastifyReply, html: string | Promise) {
+ reply.header('Content-Type', 'text/html; charset=utf-8');
+ const _html = await html;
+ return reply.send(_html);
+ }
+}
diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts
index b9a4015031..bd1dbb430c 100644
--- a/packages/backend/src/server/web/UrlPreviewService.ts
+++ b/packages/backend/src/server/web/UrlPreviewService.ts
@@ -4,8 +4,7 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import { summaly } from '@misskey-dev/summaly';
-import { SummalyResult } from '@misskey-dev/summaly/built/summary.js';
+import type { SummalyResult } from '@misskey-dev/summaly/built/summary.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
@@ -113,7 +112,7 @@ export class UrlPreviewService {
}
}
- private fetchSummary(url: string, meta: MiMeta, lang?: string): Promise {
+ private async fetchSummary(url: string, meta: MiMeta, lang?: string): Promise {
const agent = this.config.proxy
? {
http: this.httpRequestService.httpAgent,
@@ -121,6 +120,8 @@ export class UrlPreviewService {
}
: undefined;
+ const { summaly } = await import('@misskey-dev/summaly');
+
return summaly(url, {
followRedirects: this.meta.urlPreviewAllowRedirect,
lang: lang ?? 'ja-JP',
diff --git a/packages/backend/src/server/web/views/_.ts b/packages/backend/src/server/web/views/_.ts
new file mode 100644
index 0000000000..ac7418f362
--- /dev/null
+++ b/packages/backend/src/server/web/views/_.ts
@@ -0,0 +1,49 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { Config } from '@/config.js';
+
+export const comment = ``;
+
+export const defaultDescription = '✨🌎✨ A interplanetary communication platform ✨🚀✨';
+
+export type MinimumCommonData = {
+ version: string;
+ config: Config;
+};
+
+export type CommonData = MinimumCommonData & {
+ langs: string[];
+ instanceName: string;
+ icon: string | null;
+ appleTouchIcon: string | null;
+ themeColor: string | null;
+ serverErrorImageUrl: string;
+ infoImageUrl: string;
+ notFoundImageUrl: string;
+ instanceUrl: string;
+ now: number;
+ federationEnabled: boolean;
+ frontendBootloaderJs: string | null;
+ frontendBootloaderCss: string | null;
+ frontendEmbedBootloaderJs: string | null;
+ frontendEmbedBootloaderCss: string | null;
+ metaJson?: string;
+ clientCtxJson?: string;
+};
+
+export type CommonPropsMinimum> = MinimumCommonData & T;
+
+export type CommonProps> = CommonData & T;
diff --git a/packages/backend/src/server/web/views/_splash.tsx b/packages/backend/src/server/web/views/_splash.tsx
new file mode 100644
index 0000000000..ea79b8d61d
--- /dev/null
+++ b/packages/backend/src/server/web/views/_splash.tsx
@@ -0,0 +1,26 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function Splash(props: {
+ icon?: string | null;
+}) {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/backend/src/server/web/views/announcement.pug b/packages/backend/src/server/web/views/announcement.pug
deleted file mode 100644
index 7a4052e8a4..0000000000
--- a/packages/backend/src/server/web/views/announcement.pug
+++ /dev/null
@@ -1,21 +0,0 @@
-extends ./base
-
-block vars
- - const title = announcement.title;
- - const description = announcement.text.length > 100 ? announcement.text.slice(0, 100) + '…' : announcement.text;
- - const url = `${config.url}/announcements/${announcement.id}`;
-
-block title
- = `${title} | ${instanceName}`
-
-block desc
- meta(name='description' content=description)
-
-block og
- meta(property='og:type' content='article')
- meta(property='og:title' content= title)
- meta(property='og:description' content= description)
- meta(property='og:url' content= url)
- if announcement.imageUrl
- meta(property='og:image' content=announcement.imageUrl)
- meta(property='twitter:card' content='summary_large_image')
diff --git a/packages/backend/src/server/web/views/announcement.tsx b/packages/backend/src/server/web/views/announcement.tsx
new file mode 100644
index 0000000000..bc1c808177
--- /dev/null
+++ b/packages/backend/src/server/web/views/announcement.tsx
@@ -0,0 +1,41 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { Packed } from '@/misc/json-schema.js';
+import type { CommonProps } from '@/server/web/views/_.js';
+import { Layout } from '@/server/web/views/base.js';
+
+export function AnnouncementPage(props: CommonProps<{
+ announcement: Packed<'Announcement'>;
+}>) {
+ const description = props.announcement.text.length > 100 ? props.announcement.text.slice(0, 100) + '…' : props.announcement.text;
+
+ function ogBlock() {
+ return (
+ <>
+
+
+
+
+ {props.announcement.imageUrl ? (
+ <>
+
+
+ >
+ ) : null}
+ >
+ );
+ }
+
+ return (
+
+
+ );
+}
diff --git a/packages/backend/src/server/web/views/base-embed.pug b/packages/backend/src/server/web/views/base-embed.pug
deleted file mode 100644
index 29de86b8b6..0000000000
--- a/packages/backend/src/server/web/views/base-embed.pug
+++ /dev/null
@@ -1,71 +0,0 @@
-block vars
-
-block loadClientEntry
- - const entry = config.frontendEmbedEntry;
-
-doctype html
-
-html(class='embed')
-
- head
- meta(charset='utf-8')
- meta(name='application-name' content='Misskey')
- meta(name='referrer' content='origin')
- meta(name='theme-color' content= themeColor || '#86b300')
- meta(name='theme-color-orig' content= themeColor || '#86b300')
- meta(property='og:site_name' content= instanceName || 'Misskey')
- meta(property='instance_url' content= instanceUrl)
- meta(name='viewport' content='width=device-width, initial-scale=1')
- meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no')
- link(rel='icon' href= icon || '/favicon.ico')
- link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
-
- if !config.frontendEmbedManifestExists
- script(type="module" src="/embed_vite/@vite/client")
-
- if Array.isArray(entry.css)
- each href in entry.css
- link(rel='stylesheet' href=`/embed_vite/${href}`)
-
- title
- block title
- = title || 'Misskey'
-
- block meta
- meta(name='robots' content='noindex')
-
- style
- include ../style.embed.css
-
- script.
- var VERSION = "#{version}";
- var CLIENT_ENTRY = !{JSON.stringify(entry.file)};
-
- script(type='application/json' id='misskey_meta' data-generated-at=now)
- != metaJson
-
- script(type='application/json' id='misskey_embedCtx' data-generated-at=now)
- != embedCtx
-
- script
- include ../boot.embed.js
-
- body
- noscript: p
- | JavaScriptを有効にしてください
- br
- | Please turn on your JavaScript
- div#splash
- img#splashIcon(src= icon || '/static-assets/splash.png')
- div#splashSpinner
-
-
- block content
diff --git a/packages/backend/src/server/web/views/base-embed.tsx b/packages/backend/src/server/web/views/base-embed.tsx
new file mode 100644
index 0000000000..011b66592e
--- /dev/null
+++ b/packages/backend/src/server/web/views/base-embed.tsx
@@ -0,0 +1,88 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { comment } from '@/server/web/views/_.js';
+import type { CommonProps } from '@/server/web/views/_.js';
+import { Splash } from '@/server/web/views/_splash.js';
+import type { PropsWithChildren, Children } from '@kitajs/html';
+
+export function BaseEmbed(props: PropsWithChildren>) {
+ const now = Date.now();
+
+ // 変数名をsafeで始めることでエラーをスキップ
+ const safeMetaJson = props.metaJson;
+ const safeEmbedCtxJson = props.embedCtxJson;
+
+ return (
+ <>
+ {''}
+ {comment}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!props.config.frontendEmbedManifestExists ? : null}
+
+ {props.config.frontendEmbedEntry.css != null ? props.config.frontendEmbedEntry.css.map((href) => (
+
+ )) : null}
+
+ {props.titleSlot ?? {props.title || 'Misskey'} }
+
+ {props.metaSlot}
+
+
+
+ {props.frontendEmbedBootloaderCss != null ? : }
+
+
+
+ {safeMetaJson != null ? : null}
+ {safeEmbedCtxJson != null ? : null}
+
+ {props.frontendEmbedBootloaderJs != null ? : }
+
+
+
+
+ {props.children}
+
+
+ >
+ );
+}
+
diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug
deleted file mode 100644
index 46b365a9c7..0000000000
--- a/packages/backend/src/server/web/views/base.pug
+++ /dev/null
@@ -1,100 +0,0 @@
-block vars
-
-block loadClientEntry
- - const entry = config.frontendEntry;
- - const baseUrl = config.url;
-
-doctype html
-
-//
- -
- _____ _ _
- | |_|___ ___| |_ ___ _ _
- | | | | |_ -|_ -| '_| -_| | |
- |_|_|_|_|___|___|_,_|___|_ |
- |___|
- Thank you for using Misskey!
- If you are reading this message... how about joining the development?
- https://github.com/misskey-dev/misskey
-
-
-html
-
- head
- meta(charset='utf-8')
- meta(name='application-name' content='Misskey')
- meta(name='referrer' content='origin')
- meta(name='theme-color' content= themeColor || '#86b300')
- meta(name='theme-color-orig' content= themeColor || '#86b300')
- meta(property='og:site_name' content= instanceName || 'Misskey')
- meta(property='instance_url' content= instanceUrl)
- meta(name='viewport' content='width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover')
- meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no')
- link(rel='icon' href= icon || '/favicon.ico')
- link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
- link(rel='manifest' href='/manifest.json')
- link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${baseUrl}/opensearch.xml`)
- link(rel='prefetch' href=serverErrorImageUrl)
- link(rel='prefetch' href=infoImageUrl)
- link(rel='prefetch' href=notFoundImageUrl)
-
- if !config.frontendManifestExists
- script(type="module" src="/vite/@vite/client")
-
- if Array.isArray(entry.css)
- each href in entry.css
- link(rel='stylesheet' href=`/vite/${href}`)
-
- title
- block title
- = title || 'Misskey'
-
- if noindex
- meta(name='robots' content='noindex')
-
- block desc
- meta(name='description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
-
- block meta
-
- block og
- meta(property='og:title' content= title || 'Misskey')
- meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
- meta(property='og:image' content= img)
- meta(property='twitter:card' content='summary')
-
- style
- include ../style.css
-
- script.
- var VERSION = "#{version}";
- var CLIENT_ENTRY = !{JSON.stringify(entry.file)};
-
- script(type='application/json' id='misskey_meta' data-generated-at=now)
- != metaJson
-
- script(type='application/json' id='misskey_clientCtx' data-generated-at=now)
- != clientCtx
-
- script
- include ../boot.js
-
- body
- noscript: p
- | JavaScriptを有効にしてください
- br
- | Please turn on your JavaScript
- div#splash
- img#splashIcon(src= icon || '/static-assets/splash.png')
- div#splashSpinner
-
-
- block content
diff --git a/packages/backend/src/server/web/views/base.tsx b/packages/backend/src/server/web/views/base.tsx
new file mode 100644
index 0000000000..6fa3395fb8
--- /dev/null
+++ b/packages/backend/src/server/web/views/base.tsx
@@ -0,0 +1,108 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { comment, defaultDescription } from '@/server/web/views/_.js';
+import { Splash } from '@/server/web/views/_splash.js';
+import type { CommonProps } from '@/server/web/views/_.js';
+import type { PropsWithChildren, Children } from '@kitajs/html';
+
+export function Layout(props: PropsWithChildren>) {
+ const now = Date.now();
+
+ // 変数名をsafeで始めることでエラーをスキップ
+ const safeMetaJson = props.metaJson;
+ const safeClientCtxJson = props.clientCtxJson;
+
+ return (
+ <>
+ {''}
+ {comment}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {props.serverErrorImageUrl != null ? : null}
+ {props.infoImageUrl != null ? : null}
+ {props.notFoundImageUrl != null ? : null}
+
+ {!props.config.frontendManifestExists ? : null}
+
+ {props.config.frontendEntry.css != null ? props.config.frontendEntry.css.map((href) => (
+
+ )) : null}
+
+ {props.titleSlot ?? {props.title || 'Misskey'} }
+
+ {props.noindex ? : null}
+
+ {props.descSlot ?? (props.desc != null ? : null)}
+
+ {props.metaSlot}
+
+ {props.ogSlot ?? (
+ <>
+
+
+ {props.img != null ? : null}
+
+ >
+ )}
+
+ {props.frontendBootloaderCss != null ? : }
+
+
+
+ {safeMetaJson != null ? : null}
+ {safeClientCtxJson != null ? : null}
+
+ {props.frontendBootloaderJs != null ? : }
+
+
+
+
+ {props.children}
+
+
+ >
+ );
+}
+
+export { Layout as BasePage };
+
diff --git a/packages/backend/src/server/web/views/bios.pug b/packages/backend/src/server/web/views/bios.pug
deleted file mode 100644
index 39a151a29b..0000000000
--- a/packages/backend/src/server/web/views/bios.pug
+++ /dev/null
@@ -1,20 +0,0 @@
-doctype html
-
-html
-
- head
- meta(charset='utf-8')
- meta(name='application-name' content='Misskey')
- title Misskey Repair Tool
- style
- include ../bios.css
- script
- include ../bios.js
-
- body
- header
- h1 Misskey Repair Tool #{version}
- main
- div.tabs
- button#ls edit local storage
- div#content
diff --git a/packages/backend/src/server/web/views/bios.tsx b/packages/backend/src/server/web/views/bios.tsx
new file mode 100644
index 0000000000..9010de8d75
--- /dev/null
+++ b/packages/backend/src/server/web/views/bios.tsx
@@ -0,0 +1,35 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function BiosPage(props: {
+ version: string;
+}) {
+ return (
+ <>
+ {''}
+
+
+
+
+ Misskey Repair Tool
+
+
+
+
+
+ Misskey Repair Tool {props.version}
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/packages/backend/src/server/web/views/channel.pug b/packages/backend/src/server/web/views/channel.pug
deleted file mode 100644
index c514025e0b..0000000000
--- a/packages/backend/src/server/web/views/channel.pug
+++ /dev/null
@@ -1,19 +0,0 @@
-extends ./base
-
-block vars
- - const title = channel.name;
- - const url = `${config.url}/channels/${channel.id}`;
-
-block title
- = `${title} | ${instanceName}`
-
-block desc
- meta(name='description' content= channel.description)
-
-block og
- meta(property='og:type' content='article')
- meta(property='og:title' content= title)
- meta(property='og:description' content= channel.description)
- meta(property='og:url' content= url)
- meta(property='og:image' content= channel.bannerUrl)
- meta(property='twitter:card' content='summary')
diff --git a/packages/backend/src/server/web/views/channel.tsx b/packages/backend/src/server/web/views/channel.tsx
new file mode 100644
index 0000000000..7d8123ea85
--- /dev/null
+++ b/packages/backend/src/server/web/views/channel.tsx
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { Packed } from '@/misc/json-schema.js';
+import type { CommonProps } from '@/server/web/views/_.js';
+import { Layout } from '@/server/web/views/base.js';
+
+export function ChannelPage(props: CommonProps<{
+ channel: Packed<'Channel'>;
+}>) {
+
+ function ogBlock() {
+ return (
+ <>
+
+
+ {props.channel.description != null ? : null}
+
+ {props.channel.bannerUrl ? (
+ <>
+
+
+ >
+ ) : null}
+ >
+ );
+ }
+
+ return (
+
+
+ );
+}
diff --git a/packages/backend/src/server/web/views/cli.pug b/packages/backend/src/server/web/views/cli.pug
deleted file mode 100644
index d2cf7c4335..0000000000
--- a/packages/backend/src/server/web/views/cli.pug
+++ /dev/null
@@ -1,21 +0,0 @@
-doctype html
-
-html
-
- head
- meta(charset='utf-8')
- meta(name='application-name' content='Misskey')
- title Misskey Cli
- style
- include ../cli.css
- script
- include ../cli.js
-
- body
- header
- h1 Misskey Cli #{version}
- main
- div#form
- textarea#text
- button#submit submit
- div#tl
diff --git a/packages/backend/src/server/web/views/cli.tsx b/packages/backend/src/server/web/views/cli.tsx
new file mode 100644
index 0000000000..009d982b35
--- /dev/null
+++ b/packages/backend/src/server/web/views/cli.tsx
@@ -0,0 +1,37 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function CliPage(props: {
+ version: string;
+}) {
+ return (
+ <>
+ {''}
+
+
+
+
+ Misskey CLI Tool
+
+
+
+
+
+
+ Misskey CLI {props.version}
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/packages/backend/src/server/web/views/clip.pug b/packages/backend/src/server/web/views/clip.pug
deleted file mode 100644
index 5a0018803a..0000000000
--- a/packages/backend/src/server/web/views/clip.pug
+++ /dev/null
@@ -1,35 +0,0 @@
-extends ./base
-
-block vars
- - const user = clip.user;
- - const title = clip.name;
- - const url = `${config.url}/clips/${clip.id}`;
-
-block title
- = `${title} | ${instanceName}`
-
-block desc
- meta(name='description' content= clip.description)
-
-block og
- meta(property='og:type' content='article')
- meta(property='og:title' content= title)
- meta(property='og:description' content= clip.description)
- meta(property='og:url' content= url)
- meta(property='og:image' content= avatarUrl)
- meta(property='twitter:card' content='summary')
-
-block meta
- if profile.noCrawle
- meta(name='robots' content='noindex')
- if profile.preventAiLearning
- meta(name='robots' content='noimageai')
- meta(name='robots' content='noai')
-
- meta(name='misskey:user-username' content=user.username)
- meta(name='misskey:user-id' content=user.id)
- meta(name='misskey:clip-id' content=clip.id)
-
- // todo
- if user.twitter
- meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
diff --git a/packages/backend/src/server/web/views/clip.tsx b/packages/backend/src/server/web/views/clip.tsx
new file mode 100644
index 0000000000..c3cc505e35
--- /dev/null
+++ b/packages/backend/src/server/web/views/clip.tsx
@@ -0,0 +1,59 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { Packed } from '@/misc/json-schema.js';
+import type { MiUserProfile } from '@/models/UserProfile.js';
+import type { CommonProps } from '@/server/web/views/_.js';
+import { Layout } from '@/server/web/views/base.js';
+
+export function ClipPage(props: CommonProps<{
+ clip: Packed<'Clip'>;
+ profile: MiUserProfile;
+}>) {
+ function ogBlock() {
+ return (
+ <>
+
+
+ {props.clip.description != null ? : null}
+
+ {props.clip.user.avatarUrl ? (
+ <>
+
+
+ >
+ ) : null}
+ >
+ );
+ }
+
+ function metaBlock() {
+ return (
+ <>
+ {props.profile.noCrawle ? : null}
+ {props.profile.preventAiLearning ? (
+ <>
+
+
+ >
+ ) : null}
+
+
+
+ >
+ );
+ }
+
+ return (
+
+
+ );
+}
diff --git a/packages/backend/src/server/web/views/error.pug b/packages/backend/src/server/web/views/error.pug
deleted file mode 100644
index 6a78d1878c..0000000000
--- a/packages/backend/src/server/web/views/error.pug
+++ /dev/null
@@ -1,71 +0,0 @@
-doctype html
-
-//
- -
- _____ _ _
- | |_|___ ___| |_ ___ _ _
- | | | | |_ -|_ -| '_| -_| | |
- |_|_|_|_|___|___|_,_|___|_ |
- |___|
- Thank you for using Misskey!
- If you are reading this message... how about joining the development?
- https://github.com/misskey-dev/misskey
-
-
-html
-
- head
- meta(charset='utf-8')
- meta(name='viewport' content='width=device-width, initial-scale=1')
- meta(name='application-name' content='Misskey')
- meta(name='referrer' content='origin')
-
- title
- block title
- = 'An error has occurred... | Misskey'
-
- style
- include ../error.css
-
- script
- include ../error.js
-
-body
- svg.icon-warning(xmlns="http://www.w3.org/2000/svg", viewBox="0 0 24 24", stroke-width="2", stroke="currentColor", fill="none", stroke-linecap="round", stroke-linejoin="round")
- path(stroke="none", d="M0 0h24v24H0z", fill="none")
- path(d="M12 9v2m0 4v.01")
- path(d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75")
-
- h1(data-i18n="title") Failed to initialize Misskey
-
- button.button-big(onclick="location.reload();")
- span.button-label-big(data-i18n-reload) Reload
-
- p(data-i18n="serverError") If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID.
-
- div#errors
- code.
- ERROR CODE: #{code}
- ERROR ID: #{id}
-
- p
- b(data-i18n="solution") The following actions may solve the problem.
-
- p(data-i18n="solution1") Update your os and browser
- p(data-i18n="solution2") Disable an adblocker
- p(data-i18n="solution3") Clear your browser cache
- p(data-i18n="solution4") (Tor Browser) Set dom.webaudio.enabled to true
-
- details(style="color: #86b300;")
- summary(data-i18n="otherOption") Other options
- a(href="/flush")
- button.button-small
- span.button-label-small(data-i18n="otherOption1") Clear preferences and cache
- br
- a(href="/cli")
- button.button-small
- span.button-label-small(data-i18n="otherOption2") Start the simple client
- br
- a(href="/bios")
- button.button-small
- span.button-label-small(data-i18n="otherOption3") Start the repair tool
diff --git a/packages/backend/src/server/web/views/error.tsx b/packages/backend/src/server/web/views/error.tsx
new file mode 100644
index 0000000000..9d0e60aa30
--- /dev/null
+++ b/packages/backend/src/server/web/views/error.tsx
@@ -0,0 +1,89 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { comment } from '@/server/web/views/_.js';
+import type { CommonPropsMinimum } from '@/server/web/views/_.js';
+
+export function ErrorPage(props: {
+ title?: string;
+ code: string;
+ id: string;
+}) {
+ return (
+ <>
+ {''}
+ {comment}
+
+
+
+
+
+
+ {props.title ?? 'An error has occurred... | Misskey'}
+
+
+
+
+
+ Failed to initialize Misskey
+
+
+
+
+ If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID.
+
+
+
+
+ ERROR CODE: {props.code}
+ ERROR ID: {props.id}
+
+
+
+ The following actions may solve the problem.
+
+ Update your os and browser
+ Disable an adblocker
+ Clear your browser cache
+ (Tor Browser) Set dom.webaudio.enabled to true
+
+
+
+
+ >
+ );
+}
diff --git a/packages/backend/src/server/web/views/flash.pug b/packages/backend/src/server/web/views/flash.pug
deleted file mode 100644
index 1549aa7906..0000000000
--- a/packages/backend/src/server/web/views/flash.pug
+++ /dev/null
@@ -1,35 +0,0 @@
-extends ./base
-
-block vars
- - const user = flash.user;
- - const title = flash.title;
- - const url = `${config.url}/play/${flash.id}`;
-
-block title
- = `${title} | ${instanceName}`
-
-block desc
- meta(name='description' content= flash.summary)
-
-block og
- meta(property='og:type' content='article')
- meta(property='og:title' content= title)
- meta(property='og:description' content= flash.summary)
- meta(property='og:url' content= url)
- meta(property='og:image' content= avatarUrl)
- meta(property='twitter:card' content='summary')
-
-block meta
- if profile.noCrawle
- meta(name='robots' content='noindex')
- if profile.preventAiLearning
- meta(name='robots' content='noimageai')
- meta(name='robots' content='noai')
-
- meta(name='misskey:user-username' content=user.username)
- meta(name='misskey:user-id' content=user.id)
- meta(name='misskey:flash-id' content=flash.id)
-
- // todo
- if user.twitter
- meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
diff --git a/packages/backend/src/server/web/views/flash.tsx b/packages/backend/src/server/web/views/flash.tsx
new file mode 100644
index 0000000000..25a6b2c0ae
--- /dev/null
+++ b/packages/backend/src/server/web/views/flash.tsx
@@ -0,0 +1,59 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { Packed } from '@/misc/json-schema.js';
+import type { MiUserProfile } from '@/models/UserProfile.js';
+import type { CommonProps } from '@/server/web/views/_.js';
+import { Layout } from '@/server/web/views/base.js';
+
+export function FlashPage(props: CommonProps<{
+ flash: Packed<'Flash'>;
+ profile: MiUserProfile;
+}>) {
+ function ogBlock() {
+ return (
+ <>
+
+
+
+
+ {props.flash.user.avatarUrl ? (
+ <>
+
+
+ >
+ ) : null}
+ >
+ );
+ }
+
+ function metaBlock() {
+ return (
+ <>
+ {props.profile.noCrawle ? : null}
+ {props.profile.preventAiLearning ? (
+ <>
+
+
+ >
+ ) : null}
+
+
+
+ >
+ );
+ }
+
+ return (
+
+
+ );
+}
diff --git a/packages/backend/src/server/web/views/flush.pug b/packages/backend/src/server/web/views/flush.pug
deleted file mode 100644
index 7884495d08..0000000000
--- a/packages/backend/src/server/web/views/flush.pug
+++ /dev/null
@@ -1,51 +0,0 @@
-doctype html
-
-html
- #msg
- script.
- const msg = document.getElementById('msg');
- const successText = `\nSuccess Flush! Back to Misskey\n成功しました。Misskeyを開き直してください。`;
-
- if (!document.cookie) {
- message('Your site data is fully cleared by your browser.');
- message(successText);
- } else {
- message('Your browser does not support Clear-Site-Data header. Start opportunistic flushing.');
- (async function() {
- try {
- localStorage.clear();
- message('localStorage cleared.');
-
- const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => {
- const delidb = indexedDB.deleteDatabase(name);
- delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`));
- delidb.onerror = e => rej(e)
- }));
-
- await Promise.all(idbPromises);
-
- if (navigator.serviceWorker.controller) {
- navigator.serviceWorker.controller.postMessage('clear');
- await navigator.serviceWorker.getRegistrations()
- .then(registrations => {
- return Promise.all(registrations.map(registration => registration.unregister()));
- })
- .catch(e => { throw new Error(e) });
- }
-
- message(successText);
- } catch (e) {
- message(`\n${e}\n\nFlush Failed. Please retry.\n失敗しました。もう一度試してみてください。`);
- message(`\nIf you retry more than 3 times, try manually clearing the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを手動で消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`)
-
- console.error(e);
- setTimeout(() => {
- location = '/';
- }, 10000)
- }
- })();
- }
-
- function message(text) {
- msg.insertAdjacentHTML('beforeend', `[${(new Date()).toString()}] ${text.replace(/\n/g,'
')}
`)
- }
diff --git a/packages/backend/src/server/web/views/flush.tsx b/packages/backend/src/server/web/views/flush.tsx
new file mode 100644
index 0000000000..f3fdc8fcb0
--- /dev/null
+++ b/packages/backend/src/server/web/views/flush.tsx
@@ -0,0 +1,23 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function FlushPage(props?: {}) {
+ return (
+ <>
+ {''}
+
+
+
+
+ Clear preferences and cache
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/packages/backend/src/server/web/views/gallery-post.pug b/packages/backend/src/server/web/views/gallery-post.pug
deleted file mode 100644
index 9ae25d9ac8..0000000000
--- a/packages/backend/src/server/web/views/gallery-post.pug
+++ /dev/null
@@ -1,41 +0,0 @@
-extends ./base
-
-block vars
- - const user = post.user;
- - const title = post.title;
- - const url = `${config.url}/gallery/${post.id}`;
-
-block title
- = `${title} | ${instanceName}`
-
-block desc
- meta(name='description' content= post.description)
-
-block og
- meta(property='og:type' content='article')
- meta(property='og:title' content= title)
- meta(property='og:description' content= post.description)
- meta(property='og:url' content= url)
- if post.isSensitive
- meta(property='og:image' content= avatarUrl)
- meta(property='twitter:card' content='summary')
- else
- meta(property='og:image' content= post.files[0].thumbnailUrl)
- meta(property='twitter:card' content='summary_large_image')
-
-block meta
- if user.host || profile.noCrawle
- meta(name='robots' content='noindex')
- if profile.preventAiLearning
- meta(name='robots' content='noimageai')
- meta(name='robots' content='noai')
-
- meta(name='misskey:user-username' content=user.username)
- meta(name='misskey:user-id' content=user.id)
-
- // todo
- if user.twitter
- meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
-
- if !user.host
- link(rel='alternate' href=url type='application/activity+json')
diff --git a/packages/backend/src/server/web/views/gallery-post.tsx b/packages/backend/src/server/web/views/gallery-post.tsx
new file mode 100644
index 0000000000..2bec2de930
--- /dev/null
+++ b/packages/backend/src/server/web/views/gallery-post.tsx
@@ -0,0 +1,65 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { Packed } from '@/misc/json-schema.js';
+import type { MiUserProfile } from '@/models/UserProfile.js';
+import type { CommonProps } from '@/server/web/views/_.js';
+import { Layout } from '@/server/web/views/base.js';
+
+export function GalleryPostPage(props: CommonProps<{
+ galleryPost: Packed<'GalleryPost'>;
+ profile: MiUserProfile;
+}>) {
+ function ogBlock() {
+ return (
+ <>
+
+
+ {props.galleryPost.description != null ? : null}
+
+ {props.galleryPost.isSensitive && props.galleryPost.user.avatarUrl ? (
+ <>
+
+
+ >
+ ) : null}
+ {!props.galleryPost.isSensitive && props.galleryPost.files != null ? (
+ <>
+
+
+ >
+ ) : null}
+ >
+ );
+ }
+
+ function metaBlock() {
+ return (
+ <>
+ {props.profile.noCrawle ? : null}
+ {props.profile.preventAiLearning ? (
+ <>
+
+
+ >
+ ) : null}
+
+
+
+ >
+ );
+ }
+
+ return (
+
+
+ );
+}
diff --git a/packages/backend/src/server/web/views/info-card.pug b/packages/backend/src/server/web/views/info-card.pug
deleted file mode 100644
index 2a4954ec8b..0000000000
--- a/packages/backend/src/server/web/views/info-card.pug
+++ /dev/null
@@ -1,50 +0,0 @@
-doctype html
-
-html
-
- head
- meta(charset='utf-8')
- meta(name='application-name' content='Misskey')
- title= meta.name || host
- style.
- html, body {
- margin: 0;
- padding: 0;
- min-height: 100vh;
- background: #fff;
- }
-
- #a {
- display: block;
- }
-
- #banner {
- background-size: cover;
- background-position: center center;
- }
-
- #title {
- display: inline-block;
- margin: 24px;
- padding: 0.5em 0.8em;
- color: #fff;
- background: rgba(0, 0, 0, 0.5);
- font-weight: bold;
- font-size: 1.3em;
- }
-
- #content {
- overflow: auto;
- color: #353c3e;
- }
-
- #description {
- margin: 24px;
- }
-
- body
- a#a(href=`https://${host}` target="_blank")
- header#banner(style=`background-image: url(${meta.bannerUrl})`)
- div#title= meta.name || host
- div#content
- div#description!= meta.description
diff --git a/packages/backend/src/server/web/views/info-card.tsx b/packages/backend/src/server/web/views/info-card.tsx
new file mode 100644
index 0000000000..27be4c69e8
--- /dev/null
+++ b/packages/backend/src/server/web/views/info-card.tsx
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { comment, CommonPropsMinimum } from '@/server/web/views/_.js';
+import type { MiMeta } from '@/models/Meta.js';
+
+export function InfoCardPage(props: CommonPropsMinimum<{
+ meta: MiMeta;
+}>) {
+ // 変数名をsafeで始めることでエラーをスキップ
+ const safeDescription = props.meta.description;
+
+ return (
+ <>
+ {''}
+ {comment}
+
+
+
+
+
+ {props.meta.name ?? props.config.url}
+
+
+
+
+
+ {props.meta.name ?? props.config.url}
+
+
+
+ {safeDescription}
+
+
+
+ >
+ );
+}
diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug
deleted file mode 100644
index ea1993aed0..0000000000
--- a/packages/backend/src/server/web/views/note.pug
+++ /dev/null
@@ -1,62 +0,0 @@
-extends ./base
-
-block vars
- - const user = note.user;
- - const title = user.name ? `${user.name} (@${user.username}${user.host ? `@${user.host}` : ''})` : `@${user.username}${user.host ? `@${user.host}` : ''}`;
- - const url = `${config.url}/notes/${note.id}`;
- - const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
- - const images = (note.files || []).filter(file => file.type.startsWith('image/') && !file.isSensitive)
- - const videos = (note.files || []).filter(file => file.type.startsWith('video/') && !file.isSensitive)
-
-block title
- = `${title} | ${instanceName}`
-
-block desc
- meta(name='description' content= summary)
-
-block og
- meta(property='og:type' content='article')
- meta(property='og:title' content= title)
- meta(property='og:description' content= summary)
- meta(property='og:url' content= url)
- if videos.length
- each video in videos
- meta(property='og:video:url' content= video.url)
- meta(property='og:video:secure_url' content= video.url)
- meta(property='og:video:type' content= video.type)
- // FIXME: add width and height
- // FIXME: add embed player for Twitter
- if images.length
- meta(property='twitter:card' content='summary_large_image')
- each image in images
- meta(property='og:image' content= image.url)
- else
- meta(property='twitter:card' content='summary')
- meta(property='og:image' content= avatarUrl)
-
-
-block meta
- if user.host || isRenote || profile.noCrawle
- meta(name='robots' content='noindex')
- if profile.preventAiLearning
- meta(name='robots' content='noimageai')
- meta(name='robots' content='noai')
-
- meta(name='misskey:user-username' content=user.username)
- meta(name='misskey:user-id' content=user.id)
- meta(name='misskey:note-id' content=note.id)
-
- // todo
- if user.twitter
- meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
-
- if note.prev
- link(rel='prev' href=`${config.url}/notes/${note.prev}`)
- if note.next
- link(rel='next' href=`${config.url}/notes/${note.next}`)
-
- if federationEnabled
- if !user.host
- link(rel='alternate' href=url type='application/activity+json')
- if note.uri
- link(rel='alternate' href=note.uri type='application/activity+json')
diff --git a/packages/backend/src/server/web/views/note.tsx b/packages/backend/src/server/web/views/note.tsx
new file mode 100644
index 0000000000..803c3d2537
--- /dev/null
+++ b/packages/backend/src/server/web/views/note.tsx
@@ -0,0 +1,94 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { Packed } from '@/misc/json-schema.js';
+import type { MiUserProfile } from '@/models/UserProfile.js';
+import type { CommonProps } from '@/server/web/views/_.js';
+import { Layout } from '@/server/web/views/base.js';
+import { isRenotePacked } from '@/misc/is-renote.js';
+import { getNoteSummary } from '@/misc/get-note-summary.js';
+
+export function NotePage(props: CommonProps<{
+ note: Packed<'Note'>;
+ profile: MiUserProfile;
+}>) {
+ const title = props.note.user.name ? `${props.note.user.name} (@${props.note.user.username}${props.note.user.host ? `@${props.note.user.host}` : ''})` : `@${props.note.user.username}${props.note.user.host ? `@${props.note.user.host}` : ''}`
+ const isRenote = isRenotePacked(props.note);
+ const images = (props.note.files ?? []).filter(f => f.type.startsWith('image/'));
+ const videos = (props.note.files ?? []).filter(f => f.type.startsWith('video/'));
+ const summary = getNoteSummary(props.note);
+
+ function ogBlock() {
+ return (
+ <>
+
+
+
+
+ {videos.map(video => (
+ <>
+
+
+
+ {video.thumbnailUrl ? : null}
+ {video.properties.width != null ? : null}
+ {video.properties.height != null ? : null}
+ >
+ ))}
+ {images.length > 0 ? (
+ <>
+
+ {images.map(image => (
+ <>
+
+ {image.properties.width != null ? : null}
+ {image.properties.height != null ? : null}
+ >
+ ))}
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+ >
+ );
+ }
+
+ function metaBlock() {
+ return (
+ <>
+ {props.note.user.host != null || isRenote || props.profile.noCrawle ? : null}
+ {props.profile.preventAiLearning ? (
+ <>
+
+
+ >
+ ) : null}
+
+
+
+
+ {props.federationEnabled ? (
+ <>
+ {props.note.user.host == null ? : null}
+ {props.note.uri != null ? : null}
+ >
+ ) : null}
+ >
+ );
+ }
+
+ return (
+
+ )
+}
diff --git a/packages/backend/src/server/web/views/oauth.pug b/packages/backend/src/server/web/views/oauth.pug
deleted file mode 100644
index 4195ccc3a3..0000000000
--- a/packages/backend/src/server/web/views/oauth.pug
+++ /dev/null
@@ -1,11 +0,0 @@
-extends ./base
-
-block meta
- //- Should be removed by the page when it loads, so that it won't needlessly
- //- stay when user navigates away via the navigation bar
- //- XXX: Remove navigation bar in auth page?
- meta(name='misskey:oauth:transaction-id' content=transactionId)
- meta(name='misskey:oauth:client-name' content=clientName)
- if clientLogo
- meta(name='misskey:oauth:client-logo' content=clientLogo)
- meta(name='misskey:oauth:scope' content=scope)
diff --git a/packages/backend/src/server/web/views/oauth.tsx b/packages/backend/src/server/web/views/oauth.tsx
new file mode 100644
index 0000000000..d12b0d15fd
--- /dev/null
+++ b/packages/backend/src/server/web/views/oauth.tsx
@@ -0,0 +1,37 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { CommonProps } from '@/server/web/views/_.js';
+import { Layout } from '@/server/web/views/base.js';
+
+export function OAuthPage(props: CommonProps<{
+ transactionId: string;
+ clientName: string;
+ clientLogo?: string;
+ scope: string[];
+}>) {
+
+ //- Should be removed by the page when it loads, so that it won't needlessly
+ //- stay when user navigates away via the navigation bar
+ //- XXX: Remove navigation bar in auth page?
+ function metaBlock() {
+ return (
+ <>
+
+
+ {props.clientLogo ? : null}
+
+ >
+ );
+ }
+
+ return (
+
+
+ );
+}
diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug
deleted file mode 100644
index 03c50eca8a..0000000000
--- a/packages/backend/src/server/web/views/page.pug
+++ /dev/null
@@ -1,35 +0,0 @@
-extends ./base
-
-block vars
- - const user = page.user;
- - const title = page.title;
- - const url = `${config.url}/@${user.username}/pages/${page.name}`;
-
-block title
- = `${title} | ${instanceName}`
-
-block desc
- meta(name='description' content= page.summary)
-
-block og
- meta(property='og:type' content='article')
- meta(property='og:title' content= title)
- meta(property='og:description' content= page.summary)
- meta(property='og:url' content= url)
- meta(property='og:image' content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : avatarUrl)
- meta(property='twitter:card' content= page.eyeCatchingImage ? 'summary_large_image' : 'summary')
-
-block meta
- if profile.noCrawle
- meta(name='robots' content='noindex')
- if profile.preventAiLearning
- meta(name='robots' content='noimageai')
- meta(name='robots' content='noai')
-
- meta(name='misskey:user-username' content=user.username)
- meta(name='misskey:user-id' content=user.id)
- meta(name='misskey:page-id' content=page.id)
-
- // todo
- if user.twitter
- meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
diff --git a/packages/backend/src/server/web/views/page.tsx b/packages/backend/src/server/web/views/page.tsx
new file mode 100644
index 0000000000..d0484612df
--- /dev/null
+++ b/packages/backend/src/server/web/views/page.tsx
@@ -0,0 +1,64 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { Packed } from '@/misc/json-schema.js';
+import type { MiUserProfile } from '@/models/UserProfile.js';
+import type { CommonProps } from '@/server/web/views/_.js';
+import { Layout } from '@/server/web/views/base.js';
+
+export function PagePage(props: CommonProps<{
+ page: Packed<'Page'>;
+ profile: MiUserProfile;
+}>) {
+ function ogBlock() {
+ return (
+ <>
+
+
+ {props.page.summary != null ? : null}
+
+ {props.page.eyeCatchingImage != null ? (
+ <>
+
+
+ >
+ ) : props.page.user.avatarUrl ? (
+ <>
+
+
+ >
+ ) : null}
+ >
+ );
+ }
+
+ function metaBlock() {
+ return (
+ <>
+ {props.profile.noCrawle ? : null}
+ {props.profile.preventAiLearning ? (
+ <>
+
+
+ >
+ ) : null}
+
+
+
+ >
+ );
+ }
+
+ return (
+
+
+ );
+}
diff --git a/packages/backend/src/server/web/views/reversi-game.pug b/packages/backend/src/server/web/views/reversi-game.pug
deleted file mode 100644
index 0b5ffb2bb0..0000000000
--- a/packages/backend/src/server/web/views/reversi-game.pug
+++ /dev/null
@@ -1,20 +0,0 @@
-extends ./base
-
-block vars
- - const user1 = game.user1;
- - const user2 = game.user2;
- - const title = `${user1.username} vs ${user2.username}`;
- - const url = `${config.url}/reversi/g/${game.id}`;
-
-block title
- = `${title} | ${instanceName}`
-
-block desc
- meta(name='description' content='⚫⚪Misskey Reversi⚪⚫')
-
-block og
- meta(property='og:type' content='article')
- meta(property='og:title' content= title)
- meta(property='og:description' content='⚫⚪Misskey Reversi⚪⚫')
- meta(property='og:url' content= url)
- meta(property='twitter:card' content='summary')
diff --git a/packages/backend/src/server/web/views/reversi-game.tsx b/packages/backend/src/server/web/views/reversi-game.tsx
new file mode 100644
index 0000000000..22609311fd
--- /dev/null
+++ b/packages/backend/src/server/web/views/reversi-game.tsx
@@ -0,0 +1,37 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { Packed } from '@/misc/json-schema.js';
+import type { CommonProps } from '@/server/web/views/_.js';
+import { Layout } from '@/server/web/views/base.js';
+
+export function ReversiGamePage(props: CommonProps<{
+ reversiGame: Packed<'ReversiGameDetailed'>;
+}>) {
+ const title = `${props.reversiGame.user1.username} vs ${props.reversiGame.user2.username}`;
+ const description = `⚫⚪Misskey Reversi⚪⚫`;
+
+ function ogBlock() {
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+ }
+
+ return (
+
+
+ );
+}
diff --git a/packages/backend/src/server/web/views/user.pug b/packages/backend/src/server/web/views/user.pug
deleted file mode 100644
index b9f740f5b6..0000000000
--- a/packages/backend/src/server/web/views/user.pug
+++ /dev/null
@@ -1,44 +0,0 @@
-extends ./base
-
-block vars
- - const title = user.name ? `${user.name} (@${user.username}${user.host ? `@${user.host}` : ''})` : `@${user.username}${user.host ? `@${user.host}` : ''}`;
- - const url = `${config.url}/@${(user.host ? `${user.username}@${user.host}` : user.username)}`;
-
-block title
- = `${title} | ${instanceName}`
-
-block desc
- meta(name='description' content= profile.description)
-
-block og
- meta(property='og:type' content='blog')
- meta(property='og:title' content= title)
- meta(property='og:description' content= profile.description)
- meta(property='og:url' content= url)
- meta(property='og:image' content= avatarUrl)
- meta(property='twitter:card' content='summary')
-
-block meta
- if user.host || profile.noCrawle
- meta(name='robots' content='noindex')
- if profile.preventAiLearning
- meta(name='robots' content='noimageai')
- meta(name='robots' content='noai')
-
- meta(name='misskey:user-username' content=user.username)
- meta(name='misskey:user-id' content=user.id)
-
- if profile.twitter
- meta(name='twitter:creator' content=`@${profile.twitter.screenName}`)
-
- if !sub
- if federationEnabled
- if !user.host
- link(rel='alternate' href=`${config.url}/users/${user.id}` type='application/activity+json')
- if user.uri
- link(rel='alternate' href=user.uri type='application/activity+json')
- if profile.url
- link(rel='alternate' href=profile.url type='text/html')
-
- each m in me
- link(rel='me' href=`${m}`)
diff --git a/packages/backend/src/server/web/views/user.tsx b/packages/backend/src/server/web/views/user.tsx
new file mode 100644
index 0000000000..76c2633ab9
--- /dev/null
+++ b/packages/backend/src/server/web/views/user.tsx
@@ -0,0 +1,74 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { Packed } from '@/misc/json-schema.js';
+import type { MiUserProfile } from '@/models/UserProfile.js';
+import type { CommonProps } from '@/server/web/views/_.js';
+import { Layout } from '@/server/web/views/base.js';
+
+export function UserPage(props: CommonProps<{
+ user: Packed<'UserDetailed'>;
+ profile: MiUserProfile;
+ sub?: string;
+}>) {
+ const title = props.user.name ? `${props.user.name} (@${props.user.username}${props.user.host ? `@${props.user.host}` : ''})` : `@${props.user.username}${props.user.host ? `@${props.user.host}` : ''}`;
+ const me = props.profile.fields
+ ? props.profile.fields
+ .filter(field => field.value != null && field.value.match(/^https?:/))
+ .map(field => field.value)
+ : [];
+
+ function ogBlock() {
+ return (
+ <>
+
+
+ {props.user.description != null ? : null}
+
+
+
+ >
+ );
+ }
+
+ function metaBlock() {
+ return (
+ <>
+ {props.user.host != null || props.profile.noCrawle ? : null}
+ {props.profile.preventAiLearning ? (
+ <>
+
+
+ >
+ ) : null}
+
+
+
+ {props.sub == null && props.federationEnabled ? (
+ <>
+ {props.user.host == null ? : null}
+ {props.user.uri != null ? : null}
+ {props.profile.url != null ? : null}
+ >
+ ) : null}
+
+ {me.map((url) => (
+
+ ))}
+ >
+ );
+ }
+
+ return (
+
+
+ );
+}
diff --git a/packages/backend/test-federation/.config/dummy.yml b/packages/backend/test-federation/.config/dummy.yml
new file mode 100644
index 0000000000..841cab9783
--- /dev/null
+++ b/packages/backend/test-federation/.config/dummy.yml
@@ -0,0 +1,2 @@
+url: https://example.com/
+port: 3000
diff --git a/packages/backend/test-federation/.config/example.config.json b/packages/backend/test-federation/.config/example.config.json
new file mode 100644
index 0000000000..2035d1a200
--- /dev/null
+++ b/packages/backend/test-federation/.config/example.config.json
@@ -0,0 +1,29 @@
+{
+ "url": "https://${HOST}/",
+ "port": 3000,
+ "db": {
+ "host": "db.${HOST}",
+ "port": 5432,
+ "db": "misskey",
+ "user": "postgres",
+ "pass": "postgres"
+ },
+ "dbReplications": false,
+ "trustProxy": true,
+ "redis": {
+ "host": "redis.test",
+ "port": 6379
+ },
+ "id": "aidx",
+ "proxyBypassHosts": [
+ "api.deepl.com",
+ "api-free.deepl.com",
+ "www.recaptcha.net",
+ "hcaptcha.com",
+ "challenges.cloudflare.com"
+ ],
+ "allowedPrivateNetworks": [
+ "127.0.0.1/32",
+ "172.20.0.0/16"
+ ]
+}
diff --git a/packages/backend/test-federation/.config/example.default.yml b/packages/backend/test-federation/.config/example.default.yml
deleted file mode 100644
index fd20613885..0000000000
--- a/packages/backend/test-federation/.config/example.default.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-url: https://${HOST}/
-port: 3000
-db:
- host: db.${HOST}
- port: 5432
- db: misskey
- user: postgres
- pass: postgres
-dbReplications: false
-redis:
- host: redis.test
- port: 6379
-id: 'aidx'
-proxyBypassHosts:
- - api.deepl.com
- - api-free.deepl.com
- - www.recaptcha.net
- - hcaptcha.com
- - challenges.cloudflare.com
-allowedPrivateNetworks:
- - 127.0.0.1/32
- - 172.20.0.0/16
diff --git a/packages/backend/test-federation/compose.a.yml b/packages/backend/test-federation/compose.a.yml
index 6a305b404c..4fd4eb3851 100644
--- a/packages/backend/test-federation/compose.a.yml
+++ b/packages/backend/test-federation/compose.a.yml
@@ -37,8 +37,8 @@ services:
- internal_network_a
volumes:
- type: bind
- source: ./.config/a.test.default.yml
- target: /misskey/.config/default.yml
+ source: ./.config/a.test.config.json
+ target: /misskey/built/._config_.json
read_only: true
db.a.test:
diff --git a/packages/backend/test-federation/compose.b.yml b/packages/backend/test-federation/compose.b.yml
index 1158b53bae..753da22822 100644
--- a/packages/backend/test-federation/compose.b.yml
+++ b/packages/backend/test-federation/compose.b.yml
@@ -37,8 +37,8 @@ services:
- internal_network_b
volumes:
- type: bind
- source: ./.config/b.test.default.yml
- target: /misskey/.config/default.yml
+ source: ./.config/b.test.config.json
+ target: /misskey/built/._config_.json
read_only: true
db.b.test:
diff --git a/packages/backend/test-federation/compose.tpl.yml b/packages/backend/test-federation/compose.tpl.yml
index 92b986736d..1404345e2a 100644
--- a/packages/backend/test-federation/compose.tpl.yml
+++ b/packages/backend/test-federation/compose.tpl.yml
@@ -21,6 +21,10 @@ services:
- type: bind
source: ../../../built
target: /misskey/built
+ read_only: false
+ - type: bind
+ source: ./.config/dummy.yml
+ target: /misskey/.config/default.yml
read_only: true
- type: bind
source: ../assets
@@ -42,6 +46,10 @@ services:
source: ../package.json
target: /misskey/packages/backend/package.json
read_only: true
+ - type: bind
+ source: ../scripts/compile_config.js
+ target: /misskey/packages/backend/scripts/compile_config.js
+ read_only: true
- type: bind
source: ../../misskey-js/built
target: /misskey/packages/misskey-js/built
@@ -50,6 +58,14 @@ services:
source: ../../misskey-js/package.json
target: /misskey/packages/misskey-js/package.json
read_only: true
+ - type: bind
+ source: ../../i18n/built
+ target: /misskey/packages/i18n/built
+ read_only: true
+ - type: bind
+ source: ../../i18n/package.json
+ target: /misskey/packages/i18n/package.json
+ read_only: true
- type: bind
source: ../../misskey-reversi/built
target: /misskey/packages/misskey-reversi/built
diff --git a/packages/backend/test-federation/compose.yml b/packages/backend/test-federation/compose.yml
index 330cc33854..25475a89ab 100644
--- a/packages/backend/test-federation/compose.yml
+++ b/packages/backend/test-federation/compose.yml
@@ -54,6 +54,10 @@ services:
source: ../jest.js
target: /misskey/packages/backend/jest.js
read_only: true
+ - type: bind
+ source: ../scripts/compile_config.js
+ target: /misskey/packages/backend/scripts/compile_config.js
+ read_only: true
- type: bind
source: ../../misskey-js/built
target: /misskey/packages/misskey-js/built
@@ -62,6 +66,14 @@ services:
source: ../../misskey-js/package.json
target: /misskey/packages/misskey-js/package.json
read_only: true
+ - type: bind
+ source: ../../i18n/built
+ target: /misskey/packages/i18n/built
+ read_only: true
+ - type: bind
+ source: ../../i18n/package.json
+ target: /misskey/packages/i18n/package.json
+ read_only: true
- type: bind
source: ../../../package.json
target: /misskey/package.json
diff --git a/packages/backend/test-federation/setup.sh b/packages/backend/test-federation/setup.sh
index 1bc3a2a87c..15aa2eee7f 100644
--- a/packages/backend/test-federation/setup.sh
+++ b/packages/backend/test-federation/setup.sh
@@ -28,7 +28,7 @@ function generate {
-days 500
if [ ! -f .config/docker.env ]; then cp .config/example.docker.env .config/docker.env; fi
if [ ! -f .config/$1.conf ]; then sed "s/\${HOST}/$1/g" .config/example.conf > .config/$1.conf; fi
- if [ ! -f .config/$1.default.yml ]; then sed "s/\${HOST}/$1/g" .config/example.default.yml > .config/$1.default.yml; fi
+ if [ ! -f .config/$1.default.yml ]; then sed "s/\${HOST}/$1/g" .config/example.config.json > .config/$1.config.json; fi
}
generate a.test
diff --git a/packages/backend/test-federation/tsconfig.json b/packages/backend/test-federation/tsconfig.json
index 3a1cb3b9f3..8e74a62e81 100644
--- a/packages/backend/test-federation/tsconfig.json
+++ b/packages/backend/test-federation/tsconfig.json
@@ -13,12 +13,12 @@
/* Language and Environment */
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
- // "jsx": "preserve", /* Specify what JSX code is generated. */
+ "jsx": "react-jsx", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
- // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
+ "jsxImportSource": "@kitajs/html", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
diff --git a/packages/backend/test-server/tsconfig.json b/packages/backend/test-server/tsconfig.json
index 10313699c2..7ed7c10ed7 100644
--- a/packages/backend/test-server/tsconfig.json
+++ b/packages/backend/test-server/tsconfig.json
@@ -23,6 +23,8 @@
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
"isolatedModules": true,
+ "jsx": "react-jsx",
+ "jsxImportSource": "@kitajs/html",
"rootDir": "../src",
"baseUrl": "./",
"paths": {
diff --git a/packages/backend/test/e2e/exports.ts b/packages/backend/test/e2e/exports.ts
index 4bcecc9716..19433f3c88 100644
--- a/packages/backend/test/e2e/exports.ts
+++ b/packages/backend/test/e2e/exports.ts
@@ -16,7 +16,7 @@ describe('export-clips', () => {
let bob: misskey.entities.SignupResponse;
// XXX: Any better way to get the result?
- async function pollFirstDriveFile() {
+ async function pollFirstDriveFile(): Promise {
while (true) {
const files = (await api('drive/files', {}, alice)).body;
if (!files.length) {
@@ -168,7 +168,36 @@ describe('export-clips', () => {
assert.strictEqual(exported[1].clipNotes[0].note.text, 'baz2');
});
- test('Clipping other user\'s note', async () => {
+ test('Clipping other user\'s note (followers only notes are excluded when not following)', async () => {
+ const res = await api('clips/create', {
+ name: 'kawaii',
+ description: 'kawaii',
+ }, alice);
+ assert.strictEqual(res.status, 200);
+ const clip = res.body;
+
+ const note = await post(bob, {
+ text: 'baz',
+ visibility: 'followers',
+ });
+
+ const res2 = await api('clips/add-note', {
+ clipId: clip.id,
+ noteId: note.id,
+ }, alice);
+ assert.strictEqual(res2.status, 204);
+
+ const res3 = await api('i/export-clips', {}, alice);
+ assert.strictEqual(res3.status, 204);
+
+ const exported = await pollFirstDriveFile();
+ assert.strictEqual(exported[0].clipNotes.length, 0);
+ });
+
+ test('Clipping other user\'s note (followers only notes are included when following)', async () => {
+ // Alice follows Bob
+ await api('following/create', { userId: bob.id }, alice);
+
const res = await api('clips/create', {
name: 'kawaii',
description: 'kawaii',
diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts
index bef98893c6..f00843de10 100644
--- a/packages/backend/test/e2e/fetch-resource.ts
+++ b/packages/backend/test/e2e/fetch-resource.ts
@@ -73,7 +73,7 @@ describe('Webリソース', () => {
};
const metaTag = (res: SimpleGetResponse, key: string, superkey = 'name'): string => {
- return res.body.window.document.querySelector('meta[' + superkey + '="' + key + '"]')?.content;
+ return res.body.querySelector('meta[' + superkey + '="' + key + '"]')?.attributes.content;
};
beforeAll(async () => {
diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts
index f639f90ea6..96a6311a5a 100644
--- a/packages/backend/test/e2e/oauth.ts
+++ b/packages/backend/test/e2e/oauth.ts
@@ -19,7 +19,7 @@ import {
ResourceOwnerPassword,
} from 'simple-oauth2';
import pkceChallenge from 'pkce-challenge';
-import { JSDOM } from 'jsdom';
+import * as htmlParser from 'node-html-parser';
import Fastify, { type FastifyInstance, type FastifyReply } from 'fastify';
import { api, port, sendEnvUpdateRequest, signup } from '../utils.js';
import type * as misskey from 'misskey-js';
@@ -73,11 +73,11 @@ const clientConfig: ModuleOptions<'client_id'> = {
};
function getMeta(html: string): { transactionId: string | undefined, clientName: string | undefined, clientLogo: string | undefined } {
- const fragment = JSDOM.fragment(html);
+ const doc = htmlParser.parse(`${html}`);
return {
- transactionId: fragment.querySelector('meta[name="misskey:oauth:transaction-id"]')?.content,
- clientName: fragment.querySelector('meta[name="misskey:oauth:client-name"]')?.content,
- clientLogo: fragment.querySelector('meta[name="misskey:oauth:client-logo"]')?.content,
+ transactionId: doc.querySelector('meta[name="misskey:oauth:transaction-id"]')?.attributes.content,
+ clientName: doc.querySelector('meta[name="misskey:oauth:client-name"]')?.attributes.content,
+ clientLogo: doc.querySelector('meta[name="misskey:oauth:client-logo"]')?.attributes.content,
};
}
@@ -148,7 +148,7 @@ function assertIndirectError(response: Response, error: string): void {
async function assertDirectError(response: Response, status: number, error: string): Promise {
assert.strictEqual(response.status, status);
- const data = await response.json();
+ const data = await response.json() as any;
assert.strictEqual(data.error, error);
}
@@ -704,7 +704,7 @@ describe('OAuth', () => {
const response = await fetch(new URL('.well-known/oauth-authorization-server', host));
assert.strictEqual(response.status, 200);
- const body = await response.json();
+ const body = await response.json() as any;
assert.strictEqual(body.issuer, 'http://misskey.local');
assert.ok(body.scopes_supported.includes('write:notes'));
});
diff --git a/packages/backend/test/jest.setup.ts b/packages/backend/test/jest.setup.ts
index 7c6dd6a55f..9185f58acb 100644
--- a/packages/backend/test/jest.setup.ts
+++ b/packages/backend/test/jest.setup.ts
@@ -9,3 +9,4 @@ beforeAll(async () => {
await initTestDb(false);
await sendEnvResetRequest();
});
+
diff --git a/packages/backend/test/tsconfig.json b/packages/backend/test/tsconfig.json
index 2b562acda8..c6754c4802 100644
--- a/packages/backend/test/tsconfig.json
+++ b/packages/backend/test/tsconfig.json
@@ -23,6 +23,8 @@
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
"isolatedModules": true,
+ "jsx": "react-jsx",
+ "jsxImportSource": "@kitajs/html",
"baseUrl": "./",
"paths": {
"@/*": ["../src/*"]
diff --git a/packages/backend/test/unit/AnnouncementService.ts b/packages/backend/test/unit/AnnouncementService.ts
index 0b24f109f8..b3f7f426fe 100644
--- a/packages/backend/test/unit/AnnouncementService.ts
+++ b/packages/backend/test/unit/AnnouncementService.ts
@@ -26,7 +26,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import type { TestingModule } from '@nestjs/testing';
-import type { MockFunctionMetadata } from 'jest-mock';
+import type { MockMetadata } from 'jest-mock';
const moduleMocker = new ModuleMocker(global);
@@ -84,7 +84,7 @@ describe('AnnouncementService', () => {
log: jest.fn(),
};
} else if (typeof token === 'function') {
- const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata;
+ const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
diff --git a/packages/backend/test/unit/ApMfmService.ts b/packages/backend/test/unit/ApMfmService.ts
index e81a321c9b..93efa5d7d3 100644
--- a/packages/backend/test/unit/ApMfmService.ts
+++ b/packages/backend/test/unit/ApMfmService.ts
@@ -9,7 +9,6 @@ import { Test } from '@nestjs/testing';
import { CoreModule } from '@/core/CoreModule.js';
import { ApMfmService } from '@/core/activitypub/ApMfmService.js';
import { GlobalModule } from '@/GlobalModule.js';
-import { MiNote } from '@/models/Note.js';
describe('ApMfmService', () => {
let apMfmService: ApMfmService;
@@ -31,7 +30,7 @@ describe('ApMfmService', () => {
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
assert.equal(noMisskeyContent, true, 'noMisskeyContent');
- assert.equal(content, 'テキスト #タグ @mention 🍊 :emoji: https://example.com
', 'content');
+ assert.equal(content, 'テキスト #タグ @mention 🍊 :emoji: https://example.com', 'content');
});
test('Provide _misskey_content for MFM', () => {
@@ -43,7 +42,7 @@ describe('ApMfmService', () => {
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
assert.equal(noMisskeyContent, false, 'noMisskeyContent');
- assert.equal(content, 'foo
', 'content');
+ assert.equal(content, 'foo', 'content');
});
});
});
diff --git a/packages/backend/test/unit/CaptchaService.ts b/packages/backend/test/unit/CaptchaService.ts
index 51b70b05a1..24bb81118e 100644
--- a/packages/backend/test/unit/CaptchaService.ts
+++ b/packages/backend/test/unit/CaptchaService.ts
@@ -446,7 +446,7 @@ describe('CaptchaService', () => {
if (!res.success) {
expect(res.error.code).toBe(code);
}
- expect(metaService.update).not.toBeCalled();
+ expect(metaService.update).not.toHaveBeenCalled();
}
describe('invalidParameters', () => {
diff --git a/packages/backend/test/unit/DriveService.ts b/packages/backend/test/unit/DriveService.ts
index 964c65ccaa..48b108fbba 100644
--- a/packages/backend/test/unit/DriveService.ts
+++ b/packages/backend/test/unit/DriveService.ts
@@ -53,7 +53,7 @@ describe('DriveService', () => {
s3Mock.on(DeleteObjectCommand)
.rejects(new InvalidObjectState({ $metadata: {}, message: '' }));
- await expect(driveService.deleteObjectStorageFile('unexpected')).rejects.toThrowError(Error);
+ await expect(driveService.deleteObjectStorageFile('unexpected')).rejects.toThrow(Error);
});
test('delete a file with no valid key', async () => {
diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts
index 29bd03a201..28a2a971f4 100644
--- a/packages/backend/test/unit/FileInfoService.ts
+++ b/packages/backend/test/unit/FileInfoService.ts
@@ -17,7 +17,7 @@ import { FileInfo, FileInfoService } from '@/core/FileInfoService.js';
import { AiService } from '@/core/AiService.js';
import { LoggerService } from '@/core/LoggerService.js';
import type { TestingModule } from '@nestjs/testing';
-import type { MockFunctionMetadata } from 'jest-mock';
+import type { MockMetadata } from 'jest-mock';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
@@ -34,7 +34,7 @@ describe('FileInfoService', () => {
delete fi.sensitive;
delete fi.blurhash;
delete fi.porn;
-
+
return fi;
}
@@ -54,7 +54,7 @@ describe('FileInfoService', () => {
// return { };
//}
if (typeof token === 'function') {
- const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata;
+ const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts
index 7350da3cae..2f5f3745de 100644
--- a/packages/backend/test/unit/MfmService.ts
+++ b/packages/backend/test/unit/MfmService.ts
@@ -24,25 +24,25 @@ describe('MfmService', () => {
describe('toHtml', () => {
test('br', () => {
const input = 'foo\nbar\nbaz';
- const output = 'foo
bar
baz
';
+ const output = 'foo
bar
baz';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
test('br alt', () => {
const input = 'foo\r\nbar\rbaz';
- const output = 'foo
bar
baz
';
+ const output = 'foo
bar
baz';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
test('Do not generate unnecessary span', () => {
const input = 'foo $[tada bar]';
- const output = 'foo bar
';
+ const output = 'foo bar';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
test('escape', () => {
const input = '```\nHello, world!
\n```';
- const output = '<p>Hello, world!</p>
';
+ const output = '<p>Hello, world!</p>
';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
});
@@ -118,7 +118,7 @@ describe('MfmService', () => {
assert.deepStrictEqual(mfmService.fromHtml('a Misskey b c
'), 'a Misskey(ミス キー) b c');
assert.deepStrictEqual(
mfmService.fromHtml('a MisskeyMisskeyMisskey b
'),
- 'a Misskey(ミスキー)Misskey(ミス キー)Misskey(ミスキー) b'
+ 'a Misskey(ミスキー)Misskey(ミス キー)Misskey(ミスキー) b',
);
});
diff --git a/packages/backend/test/unit/RelayService.ts b/packages/backend/test/unit/RelayService.ts
index 074430dd31..bee580d0c7 100644
--- a/packages/backend/test/unit/RelayService.ts
+++ b/packages/backend/test/unit/RelayService.ts
@@ -9,7 +9,7 @@ import { jest } from '@jest/globals';
import { Test } from '@nestjs/testing';
import { ModuleMocker } from 'jest-mock';
import type { TestingModule } from '@nestjs/testing';
-import type { MockFunctionMetadata } from 'jest-mock';
+import type { MockMetadata } from 'jest-mock';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { IdService } from '@/core/IdService.js';
@@ -45,7 +45,7 @@ describe('RelayService', () => {
return { deliver: jest.fn() };
}
if (typeof token === 'function') {
- const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata;
+ const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts
index 71090c8be6..9b17b1fbb9 100644
--- a/packages/backend/test/unit/RoleService.ts
+++ b/packages/backend/test/unit/RoleService.ts
@@ -11,7 +11,7 @@ import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing';
import * as lolex from '@sinonjs/fake-timers';
import type { TestingModule } from '@nestjs/testing';
-import type { MockFunctionMetadata } from 'jest-mock';
+import type { MockMetadata } from 'jest-mock';
import { GlobalModule } from '@/GlobalModule.js';
import { RoleService } from '@/core/RoleService.js';
import {
@@ -104,6 +104,8 @@ describe('RoleService', () => {
beforeEach(async () => {
clock = lolex.install({
+ // https://github.com/sinonjs/sinon/issues/2620
+ toFake: Object.keys(lolex.timers).filter((key) => !['nextTick', 'queueMicrotask'].includes(key)) as lolex.FakeMethod[],
now: new Date(),
shouldClearNativeTimers: true,
});
@@ -135,7 +137,7 @@ describe('RoleService', () => {
return { fetch: jest.fn() };
}
if (typeof token === 'function') {
- const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata;
+ const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
diff --git a/packages/backend/test/unit/S3Service.ts b/packages/backend/test/unit/S3Service.ts
index 151f3b826a..6e7e5a8b59 100644
--- a/packages/backend/test/unit/S3Service.ts
+++ b/packages/backend/test/unit/S3Service.ts
@@ -72,7 +72,7 @@ describe('S3Service', () => {
Bucket: 'fake',
Key: 'fake',
Body: 'x',
- })).rejects.toThrowError(Error);
+ })).rejects.toThrow(Error);
});
test('upload a large file error', async () => {
@@ -82,7 +82,7 @@ describe('S3Service', () => {
Bucket: 'fake',
Key: 'fake',
Body: 'x'.repeat(8 * 1024 * 1024 + 1), // デフォルトpartSizeにしている 8 * 1024 * 1024 を越えるサイズ
- })).rejects.toThrowError(Error);
+ })).rejects.toThrow(Error);
});
});
});
diff --git a/packages/backend/test/unit/SigninWithPasskeyApiService.ts b/packages/backend/test/unit/SigninWithPasskeyApiService.ts
index 0687ed8437..8ef46024ac 100644
--- a/packages/backend/test/unit/SigninWithPasskeyApiService.ts
+++ b/packages/backend/test/unit/SigninWithPasskeyApiService.ts
@@ -9,7 +9,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { FastifyReply, FastifyRequest } from 'fastify';
import { AuthenticationResponseJSON } from '@simplewebauthn/types';
import { HttpHeader } from 'fastify/types/utils.js';
-import { MockFunctionMetadata, ModuleMocker } from 'jest-mock';
+import { MockMetadata, ModuleMocker } from 'jest-mock';
import { MiUser } from '@/models/User.js';
import { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
@@ -95,7 +95,7 @@ describe('SigninWithPasskeyApiService', () => {
],
}).useMocker((token) => {
if (typeof token === 'function') {
- const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata;
+ const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
diff --git a/packages/backend/test/unit/chart.ts b/packages/backend/test/unit/chart.ts
index 9dedd3a79d..364a2c2fbd 100644
--- a/packages/backend/test/unit/chart.ts
+++ b/packages/backend/test/unit/chart.ts
@@ -9,6 +9,7 @@ import * as assert from 'assert';
import { jest } from '@jest/globals';
import * as lolex from '@sinonjs/fake-timers';
import { DataSource } from 'typeorm';
+import * as Redis from 'ioredis';
import TestChart from '@/core/chart/charts/test.js';
import TestGroupedChart from '@/core/chart/charts/test-grouped.js';
import TestUniqueChart from '@/core/chart/charts/test-unique.js';
@@ -18,16 +19,16 @@ import { entity as TestGroupedChartEntity } from '@/core/chart/charts/entities/t
import { entity as TestUniqueChartEntity } from '@/core/chart/charts/entities/test-unique.js';
import { entity as TestIntersectionChartEntity } from '@/core/chart/charts/entities/test-intersection.js';
import { loadConfig } from '@/config.js';
-import type { AppLockService } from '@/core/AppLockService.js';
import Logger from '@/logger.js';
describe('Chart', () => {
const config = loadConfig();
- const appLockService = {
- getChartInsertLock: () => () => Promise.resolve(() => {}),
- } as unknown as jest.Mocked;
let db: DataSource | undefined;
+ let redisClient = {
+ set: () => Promise.resolve('OK'),
+ get: () => Promise.resolve(null),
+ } as unknown as jest.Mocked;
let testChart: TestChart;
let testGroupedChart: TestGroupedChart;
@@ -64,12 +65,14 @@ describe('Chart', () => {
await db.initialize();
const logger = new Logger('chart'); // TODO: モックにする
- testChart = new TestChart(db, appLockService, logger);
- testGroupedChart = new TestGroupedChart(db, appLockService, logger);
- testUniqueChart = new TestUniqueChart(db, appLockService, logger);
- testIntersectionChart = new TestIntersectionChart(db, appLockService, logger);
+ testChart = new TestChart(db, redisClient, logger);
+ testGroupedChart = new TestGroupedChart(db, redisClient, logger);
+ testUniqueChart = new TestUniqueChart(db, redisClient, logger);
+ testIntersectionChart = new TestIntersectionChart(db, redisClient, logger);
clock = lolex.install({
+ // https://github.com/sinonjs/sinon/issues/2620
+ toFake: Object.keys(lolex.timers).filter((key) => !['nextTick', 'queueMicrotask'].includes(key)) as lolex.FakeMethod[],
now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)),
shouldClearNativeTimers: true,
});
diff --git a/packages/backend/test/unit/misc/should-hide-note-by-time.ts b/packages/backend/test/unit/misc/should-hide-note-by-time.ts
new file mode 100644
index 0000000000..1c463c82c6
--- /dev/null
+++ b/packages/backend/test/unit/misc/should-hide-note-by-time.ts
@@ -0,0 +1,136 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { describe, expect, test, beforeEach, afterEach } from '@jest/globals';
+import * as lolex from '@sinonjs/fake-timers';
+import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
+
+describe('misc:should-hide-note-by-time', () => {
+ let clock: lolex.InstalledClock;
+ const epoch = Date.UTC(2000, 0, 1, 0, 0, 0);
+
+ beforeEach(() => {
+ clock = lolex.install({
+ // https://github.com/sinonjs/sinon/issues/2620
+ toFake: Object.keys(lolex.timers).filter((key) => !['nextTick', 'queueMicrotask'].includes(key)) as lolex.FakeMethod[],
+ now: new Date(epoch),
+ shouldClearNativeTimers: true,
+ });
+ });
+
+ afterEach(() => {
+ clock.uninstall();
+ });
+
+ describe('hiddenBefore が null または undefined の場合', () => {
+ test('hiddenBefore が null のときは false を返す(非表示機能が有効でない)', () => {
+ const createdAt = new Date(epoch - 86400000); // 1 day ago
+ expect(shouldHideNoteByTime(null, createdAt)).toBe(false);
+ });
+
+ test('hiddenBefore が undefined のときは false を返す(非表示機能が有効でない)', () => {
+ const createdAt = new Date(epoch - 86400000); // 1 day ago
+ expect(shouldHideNoteByTime(undefined, createdAt)).toBe(false);
+ });
+ });
+
+ describe('相対時間モード (hiddenBefore <= 0)', () => {
+ test('閾値内に作成されたノートは false を返す(作成からの経過時間がまだ短い→表示)', () => {
+ const hiddenBefore = -86400; // 1 day in seconds
+ const createdAt = new Date(epoch - 3600000); // 1 hour ago
+ expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(false);
+ });
+
+ test('閾値を超えて作成されたノートは true を返す(指定期間以上経過している→非表示)', () => {
+ const hiddenBefore = -86400; // 1 day in seconds
+ const createdAt = new Date(epoch - 172800000); // 2 days ago
+ expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
+ });
+
+ test('ちょうど閾値で作成されたノートは true を返す(閾値に達したら非表示)', () => {
+ const hiddenBefore = -86400; // 1 day in seconds
+ const createdAt = new Date(epoch - 86400000); // exactly 1 day ago
+ expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
+ });
+
+ test('異なる相対時間値で判定できる(1時間設定と3時間設定の異なる結果)', () => {
+ const createdAt = new Date(epoch - 7200000); // 2 hours ago
+ expect(shouldHideNoteByTime(-3600, createdAt)).toBe(true); // 1時間経過→非表示
+ expect(shouldHideNoteByTime(-10800, createdAt)).toBe(false); // 3時間未経過→表示
+ });
+
+ test('ISO 8601 形式の文字列の createdAt に対応できる(文字列でも正しく判定)', () => {
+ const createdAtString = new Date(epoch - 86400000).toISOString();
+ const hiddenBefore = -86400; // 1 day in seconds
+ expect(shouldHideNoteByTime(hiddenBefore, createdAtString)).toBe(true);
+ });
+
+ test('hiddenBefore が 0 の場合に対応できる(0秒以上経過で非表示→ほぼ全て非表示)', () => {
+ const hiddenBefore = 0;
+ const createdAt = new Date(epoch - 1); // 1ms ago
+ expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
+ });
+ });
+
+ describe('絶対時間モード (hiddenBefore > 0)', () => {
+ test('閾値タイムスタンプより後に作成されたノートは false を返す(指定日時より後→表示)', () => {
+ const thresholdSeconds = Math.floor(epoch / 1000);
+ const createdAt = new Date(epoch + 3600000); // 1 hour from epoch
+ expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(false);
+ });
+
+ test('閾値タイムスタンプより前に作成されたノートは true を返す(指定日時より前→非表示)', () => {
+ const thresholdSeconds = Math.floor(epoch / 1000);
+ const createdAt = new Date(epoch - 3600000); // 1 hour ago
+ expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(true);
+ });
+
+ test('ちょうど閾値タイムスタンプで作成されたノートは true を返す(指定日時に達したら非表示)', () => {
+ const thresholdSeconds = Math.floor(epoch / 1000);
+ const createdAt = new Date(epoch); // exactly epoch
+ expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(true);
+ });
+
+ test('ISO 8601 形式の文字列の createdAt に対応できる(文字列でも正しく判定)', () => {
+ const thresholdSeconds = Math.floor(epoch / 1000);
+ const createdAtString = new Date(epoch - 3600000).toISOString();
+ expect(shouldHideNoteByTime(thresholdSeconds, createdAtString)).toBe(true);
+ });
+
+ test('異なる閾値タイムスタンプで判定できる(2021年設定と現在より1時間前設定の異なる結果)', () => {
+ const thresholdSeconds = Math.floor((epoch - 86400000) / 1000); // 1 day ago
+ const createdAtBefore = new Date(epoch - 172800000); // 2 days ago
+ const createdAtAfter = new Date(epoch - 3600000); // 1 hour ago
+ expect(shouldHideNoteByTime(thresholdSeconds, createdAtBefore)).toBe(true); // 閾値より前→非表示
+ expect(shouldHideNoteByTime(thresholdSeconds, createdAtAfter)).toBe(false); // 閾値より後→表示
+ });
+ });
+
+ describe('エッジケース', () => {
+ test('相対時間モードで非常に古いノートに対応できる(非常に古い→閾値超→非表示)', () => {
+ const hiddenBefore = -1; // hide notes older than 1 second
+ const createdAt = new Date(epoch - 1000000); // very old
+ expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
+ });
+
+ test('相対時間モードで非常に新しいノートに対応できる(非常に新しい→閾値未満→表示)', () => {
+ const hiddenBefore = -86400; // 1 day
+ const createdAt = new Date(epoch - 1); // 1ms ago
+ expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(false);
+ });
+
+ test('大きなタイムスタンプ値に対応できる(未来の日時を指定→現在のノートは全て非表示)', () => {
+ const thresholdSeconds = Math.floor(epoch / 1000) + 86400; // 1 day from epoch
+ const createdAt = new Date(epoch); // created epoch
+ expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(true);
+ });
+
+ test('小さな相対時間値に対応できる(1秒設定で2秒前→非表示)', () => {
+ const hiddenBefore = -1; // 1 second
+ const createdAt = new Date(epoch - 2000); // 2 seconds ago
+ expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
+ });
+ });
+});
diff --git a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
index 211846eef2..01a36c9fef 100644
--- a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
+++ b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
@@ -141,6 +141,8 @@ describe('CheckModeratorsActivityProcessorService', () => {
beforeEach(async () => {
clock = lolex.install({
+ // https://github.com/sinonjs/sinon/issues/2620
+ toFake: Object.keys(lolex.timers).filter((key) => !['nextTick', 'queueMicrotask'].includes(key)) as lolex.FakeMethod[],
now: new Date(baseDate),
shouldClearNativeTimers: true,
});
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index daae7b9643..ecca28b5af 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -10,8 +10,8 @@ import { randomUUID } from 'node:crypto';
import { inspect } from 'node:util';
import WebSocket, { ClientOptions } from 'ws';
import fetch, { File, RequestInit, type Headers } from 'node-fetch';
+import * as htmlParser from 'node-html-parser';
import { DataSource } from 'typeorm';
-import { JSDOM } from 'jsdom';
import { type Response } from 'node-fetch';
import Fastify from 'fastify';
import { entities } from '../src/postgres.js';
@@ -468,7 +468,7 @@ export function makeStreamCatcher(
export type SimpleGetResponse = {
status: number,
- body: any | JSDOM | null,
+ body: any | null,
type: string | null,
location: string | null
};
@@ -499,7 +499,7 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde
const body =
jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() :
- htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) :
+ htmlTypes.includes(res.headers.get('content-type') ?? '') ? htmlParser.parse(await res.text()) :
await bodyExtractor(res);
return {
diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json
index 2b15a5cc7a..25584e475d 100644
--- a/packages/backend/tsconfig.json
+++ b/packages/backend/tsconfig.json
@@ -23,12 +23,17 @@
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
"isolatedModules": true,
+ "jsx": "react-jsx",
+ "jsxImportSource": "@kitajs/html",
"rootDir": "./src",
"baseUrl": "./",
"paths": {
"@/*": ["./src/*"]
},
"outDir": "./built",
+ "plugins": [
+ {"name": "@kitajs/ts-html-plugin"}
+ ],
"types": [
"node"
],
@@ -43,7 +48,8 @@
},
"compileOnSave": false,
"include": [
- "./src/**/*.ts"
+ "./src/**/*.ts",
+ "./src/**/*.tsx"
],
"exclude": [
"./src/**/*.test.ts"
diff --git a/packages/frontend-builder/locale-inliner.ts b/packages/frontend-builder/locale-inliner.ts
index 9bef465eeb..191d7250a6 100644
--- a/packages/frontend-builder/locale-inliner.ts
+++ b/packages/frontend-builder/locale-inliner.ts
@@ -10,7 +10,7 @@ import { collectModifications } from './locale-inliner/collect-modifications.js'
import { applyWithLocale } from './locale-inliner/apply-with-locale.js';
import { blankLogger } from './logger.js';
import type { Logger } from './logger.js';
-import type { Locale } from '../../locales/index.js';
+import type { Locale } from 'i18n';
import type { Manifest as ViteManifest } from 'vite';
export class LocaleInliner {
diff --git a/packages/frontend-builder/locale-inliner/apply-with-locale.ts b/packages/frontend-builder/locale-inliner/apply-with-locale.ts
index 5e601cdf12..78851d3029 100644
--- a/packages/frontend-builder/locale-inliner/apply-with-locale.ts
+++ b/packages/frontend-builder/locale-inliner/apply-with-locale.ts
@@ -5,7 +5,7 @@
import MagicString from 'magic-string';
import { assertNever } from '../utils.js';
-import type { Locale, ILocale } from '../../../locales/index.js';
+import type { ILocale, Locale } from 'i18n';
import type { TextModification } from '../locale-inliner.js';
import type { Logger } from '../logger.js';
diff --git a/packages/frontend-builder/package.json b/packages/frontend-builder/package.json
index ef5c8e0367..36c32b915d 100644
--- a/packages/frontend-builder/package.json
+++ b/packages/frontend-builder/package.json
@@ -12,14 +12,15 @@
"devDependencies": {
"@types/estree": "1.0.8",
"@types/node": "24.10.1",
- "@typescript-eslint/eslint-plugin": "8.47.0",
- "@typescript-eslint/parser": "8.47.0",
+ "@typescript-eslint/eslint-plugin": "8.48.0",
+ "@typescript-eslint/parser": "8.48.0",
"rollup": "4.53.3",
"typescript": "5.9.3"
},
"dependencies": {
+ "i18n": "workspace:*",
"estree-walker": "3.0.3",
"magic-string": "0.30.21",
- "vite": "7.2.2"
+ "vite": "7.2.4"
}
}
diff --git a/packages/frontend-embed/build.ts b/packages/frontend-embed/build.ts
index 737233a4d0..4e1f588802 100644
--- a/packages/frontend-embed/build.ts
+++ b/packages/frontend-embed/build.ts
@@ -2,7 +2,7 @@ import * as fs from 'fs/promises';
import url from 'node:url';
import path from 'node:path';
import { execa } from 'execa';
-import locales from '../../locales/index.js';
+import locales from 'i18n';
import { LocaleInliner } from '../frontend-builder/locale-inliner.js'
import { createLogger } from '../frontend-builder/logger';
diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json
index 7bfd32686c..6122f2cab6 100644
--- a/packages/frontend-embed/package.json
+++ b/packages/frontend-embed/package.json
@@ -11,12 +11,13 @@
},
"dependencies": {
"@discordapp/twemoji": "16.0.1",
+ "i18n": "workspace:*",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.3",
"@rollup/pluginutils": "5.3.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.2",
- "@vue/compiler-sfc": "3.5.24",
+ "@vue/compiler-sfc": "3.5.25",
"astring": "1.9.0",
"buraha": "0.0.1",
"estree-walker": "3.0.3",
@@ -27,15 +28,15 @@
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.53.3",
- "sass": "1.94.1",
- "shiki": "3.15.0",
+ "sass": "1.94.2",
+ "shiki": "3.17.0",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.9.3",
"uuid": "13.0.0",
- "vite": "7.2.2",
- "vue": "3.5.24"
+ "vite": "7.2.4",
+ "vue": "3.5.25"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.5",
@@ -47,26 +48,26 @@
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
- "@typescript-eslint/eslint-plugin": "8.47.0",
- "@typescript-eslint/parser": "8.47.0",
- "@vitest/coverage-v8": "3.2.4",
- "@vue/runtime-core": "3.5.24",
+ "@typescript-eslint/eslint-plugin": "8.48.0",
+ "@typescript-eslint/parser": "8.48.0",
+ "@vitest/coverage-v8": "4.0.14",
+ "@vue/runtime-core": "3.5.25",
"acorn": "8.15.0",
"cross-env": "10.1.0",
"eslint-plugin-import": "2.32.0",
- "eslint-plugin-vue": "10.5.1",
+ "eslint-plugin-vue": "10.6.2",
"fast-glob": "3.3.3",
- "happy-dom": "20.0.10",
+ "happy-dom": "20.0.11",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
- "msw": "2.12.2",
+ "msw": "2.12.3",
"nodemon": "3.1.11",
- "prettier": "3.6.2",
- "start-server-and-test": "2.1.2",
+ "prettier": "3.7.1",
+ "start-server-and-test": "2.1.3",
"tsx": "4.20.6",
"vite-plugin-turbosnap": "1.0.3",
- "vue-component-type-helpers": "3.1.4",
+ "vue-component-type-helpers": "3.1.5",
"vue-eslint-parser": "10.2.0",
- "vue-tsc": "3.1.4"
+ "vue-tsc": "3.1.5"
}
}
diff --git a/packages/backend/src/server/web/boot.embed.js b/packages/frontend-embed/public/loader/boot.js
similarity index 100%
rename from packages/backend/src/server/web/boot.embed.js
rename to packages/frontend-embed/public/loader/boot.js
diff --git a/packages/backend/src/server/web/style.embed.css b/packages/frontend-embed/public/loader/style.css
similarity index 100%
rename from packages/backend/src/server/web/style.embed.css
rename to packages/frontend-embed/public/loader/style.css
diff --git a/packages/frontend-embed/src/components/I18n.vue b/packages/frontend-embed/src/components/I18n.vue
index b621110ec9..9866e50958 100644
--- a/packages/frontend-embed/src/components/I18n.vue
+++ b/packages/frontend-embed/src/components/I18n.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only