mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-05 14:54:45 +02:00
Compare commits
5 Commits
2025.12.0-
...
copilot/mi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b697c419a7 | ||
|
|
c87fb96195 | ||
|
|
e5f1af9606 | ||
|
|
d5256a290f | ||
|
|
d1d50d19e0 |
@@ -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: do not trust any proxies (i.e. trustProxy: false)
|
||||
# Default: trust all proxies (i.e. trustProxy: true)
|
||||
# See: https://fastify.dev/docs/latest/reference/server/#trustproxy
|
||||
#
|
||||
# trustProxy: false
|
||||
# trustProxy: 1
|
||||
|
||||
# ┌──────────────────────────┐
|
||||
#───┘ PostgreSQL configuration └────────────────────────────────
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"workspaceFolder": "/workspace",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "22.15.0"
|
||||
"version": "24.10.0"
|
||||
},
|
||||
"ghcr.io/devcontainers-extra/features/pnpm:2": {
|
||||
"version": "10.10.0"
|
||||
|
||||
87
.github/workflows/get-backend-memory.yml
vendored
87
.github/workflows/get-backend-memory.yml
vendored
@@ -1,87 +0,0 @@
|
||||
# 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
|
||||
122
.github/workflows/report-backend-memory.yml
vendored
122
.github/workflows/report-backend-memory.yml
vendored
@@ -1,122 +0,0 @@
|
||||
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.
|
||||
@@ -1 +1 @@
|
||||
22.15.0
|
||||
24.10.0
|
||||
|
||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -3,7 +3,6 @@
|
||||
"**/node_modules": true
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"files.associations": {
|
||||
"*.test.ts": "typescript"
|
||||
},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
## 2025.12.0
|
||||
## 2025.11.2
|
||||
|
||||
### General
|
||||
-
|
||||
|
||||
### Client
|
||||
- Fix: stacking router viewで連続して戻る操作を行うと何も表示されなくなる問題を修正
|
||||
-
|
||||
|
||||
### Server
|
||||
- Enhance: メモリ使用量を削減しました
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.4
|
||||
|
||||
ARG NODE_VERSION=22.15.0-bookworm
|
||||
ARG NODE_VERSION=24.10.0-bookworm
|
||||
|
||||
# build assets & compile TypeScript
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@
|
||||
<a href="https://www.patreon.com/syuilo">
|
||||
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a>
|
||||
|
||||
[](https://deepwiki.com/misskey-dev/misskey)
|
||||
|
||||
</div>
|
||||
|
||||
## Thanks
|
||||
|
||||
@@ -319,10 +319,10 @@ remoteUserCaution: "Para el usuario remoto, la información está incompleta"
|
||||
activity: "Actividad"
|
||||
images: "Imágenes"
|
||||
image: "Imágenes"
|
||||
birthday: "Cumpleaños"
|
||||
birthday: "Fecha de nacimiento"
|
||||
yearsOld: "{age} años"
|
||||
registeredDate: "Fecha de registro"
|
||||
location: "Ubicación"
|
||||
location: "Lugar"
|
||||
theme: "Tema"
|
||||
themeForLightMode: "Tema para usar en Modo Linterna"
|
||||
themeForDarkMode: "Tema para usar en Modo Oscuro"
|
||||
@@ -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: "Visualizar la ventana de publicación en la parte superior de la línea de tiempo."
|
||||
showFixedPostForm: "Mostrar el formulario de las entradas encima 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"
|
||||
@@ -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: "Anterior"
|
||||
goBack: "Deseleccionar"
|
||||
unlikeConfirm: "¿Quitar como favorito?"
|
||||
fullView: "Vista completa"
|
||||
quitFullView: "quitar vista completa"
|
||||
@@ -1511,7 +1511,7 @@ _emojiPalette:
|
||||
palettes: "Paleta\n"
|
||||
enableSyncBetweenDevicesForPalettes: "Activar la sincronización de paletas entre dispositivos"
|
||||
paletteForMain: "Paleta principal"
|
||||
paletteForReaction: "Paleta utilizada para las reacciones"
|
||||
paletteForReaction: "Paleta de reacción"
|
||||
_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 el aspecto y el comportamiento del cliente y configurar los ajustes para optimizar su uso."
|
||||
accessibilityBanner: "Puedes personalizar los visuales y el comportamiento del cliente, y configurar los ajustes para optimizar el 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 utilizas un ratón, arrastra mientras pulsas la rueda de desplazamiento."
|
||||
enablePullToRefresh_description: "Si utiliza un ratón, arrastre mientras pulsa 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."
|
||||
@@ -2156,7 +2156,7 @@ _accountDelete:
|
||||
started: "El proceso de eliminación ha comenzado."
|
||||
inProgress: "La eliminación está en proceso."
|
||||
_ad:
|
||||
back: "Anterior"
|
||||
back: "Deseleccionar"
|
||||
reduceFrequencyOfThisAd: "Mostrar menos este anuncio."
|
||||
hide: "No mostrar"
|
||||
timezoneinfo: "El día de la semana está determidado por la zona horaria del servidor."
|
||||
@@ -2610,10 +2610,10 @@ _profile:
|
||||
name: "Nombre"
|
||||
username: "Nombre de usuario"
|
||||
description: "Descripción"
|
||||
youCanIncludeHashtags: "También puedes incluir hashtags en tu biografía"
|
||||
youCanIncludeHashtags: "Puedes añadir hashtags"
|
||||
metadata: "información adicional"
|
||||
metadataEdit: "Editar información adicional"
|
||||
metadataDescription: "Usando esto puedes mostrar campos de información adicionales en tu perfil."
|
||||
metadataDescription: "Muestra la información adicional en el perfil"
|
||||
metadataLabel: "Etiqueta"
|
||||
metadataContent: "Contenido"
|
||||
changeAvatar: "Cambiar avatar"
|
||||
|
||||
@@ -83,8 +83,6 @@ 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"
|
||||
@@ -2352,13 +2350,13 @@ _ago:
|
||||
yearsAgo: "{n} anni fa"
|
||||
invalid: "Niente da visualizzare"
|
||||
_timeIn:
|
||||
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"
|
||||
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"
|
||||
_time:
|
||||
second: "s"
|
||||
minute: "min"
|
||||
|
||||
19
package.json
19
package.json
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.12.0-beta.0",
|
||||
"version": "2025.11.2-alpha.1",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@10.24.0",
|
||||
"packageManager": "pnpm@10.22.0",
|
||||
"workspaces": [
|
||||
"packages/misskey-js",
|
||||
"packages/i18n",
|
||||
@@ -22,15 +22,15 @@
|
||||
],
|
||||
"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",
|
||||
"minify-node-modules": "node ./scripts/minify-node-modules.mjs",
|
||||
"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 && 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",
|
||||
"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",
|
||||
"cli": "cd packages/backend && pnpm cli",
|
||||
"init": "pnpm migrate",
|
||||
"migrate": "cd packages/backend && pnpm migrate",
|
||||
@@ -71,18 +71,19 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.39.1",
|
||||
"i18n": "workspace:*",
|
||||
"@misskey-dev/eslint-plugin": "2.2.0",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/node": "24.10.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.47.0",
|
||||
"@typescript-eslint/parser": "8.47.0",
|
||||
"cross-env": "10.1.0",
|
||||
"cypress": "15.7.0",
|
||||
"cypress": "15.6.0",
|
||||
"eslint": "9.39.1",
|
||||
"globals": "16.5.0",
|
||||
"ncp": "2.0.0",
|
||||
"pnpm": "10.24.0",
|
||||
"start-server-and-test": "2.1.3"
|
||||
"pnpm": "10.22.0",
|
||||
"start-server-and-test": "2.1.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tensorflow/tfjs-core": "4.22.0"
|
||||
|
||||
@@ -3,17 +3,12 @@
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"jsx": true,
|
||||
"dynamicImport": true,
|
||||
"decorators": true
|
||||
},
|
||||
"transform": {
|
||||
"legacyDecorator": true,
|
||||
"decoratorMetadata": true,
|
||||
"react": {
|
||||
"runtime": "automatic",
|
||||
"importSource": "@kitajs/html"
|
||||
}
|
||||
"decoratorMetadata": true
|
||||
},
|
||||
"experimental": {
|
||||
"keepImportAssertions": true
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
(async () => {
|
||||
const msg = document.getElementById('msg');
|
||||
const successText = `\nSuccess Flush! <a href="/">Back to Misskey</a>\n成功しました。<a href="/">Misskeyを開き直してください。</a>`;
|
||||
|
||||
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. <a href="/flush">Please retry.</a>\n失敗しました。<a href="/flush">もう一度試してみてください。</a>`);
|
||||
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', `<p>[${(new Date()).toString()}] ${text.replace(/\n/g,'<br>')}</p>`)
|
||||
}
|
||||
})();
|
||||
@@ -1,35 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -205,7 +205,7 @@ module.exports = {
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
|
||||
testTimeout: 60000,
|
||||
|
||||
|
||||
13
packages/backend/jsconfig.json
Normal file
13
packages/backend/jsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"jspm_packages",
|
||||
"tmp",
|
||||
"temp"
|
||||
]
|
||||
}
|
||||
@@ -3,14 +3,14 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
|
||||
import { isConcurrentIndexMigrationEnabled } from "./js/migration-config.js";
|
||||
|
||||
export class CompositeNoteIndex1745378064470 {
|
||||
name = 'CompositeNoteIndex1745378064470';
|
||||
transaction = isConcurrentIndexMigrationEnabled ? false : undefined;
|
||||
transaction = isConcurrentIndexMigrationEnabled() ? false : undefined;
|
||||
|
||||
async up(queryRunner) {
|
||||
const concurrently = isConcurrentIndexMigrationEnabled;
|
||||
const concurrently = isConcurrentIndexMigrationEnabled();
|
||||
|
||||
if (concurrently) {
|
||||
const hasValidIndex = await queryRunner.query(`SELECT indisvalid FROM pg_index INNER JOIN pg_class ON pg_index.indexrelid = pg_class.oid WHERE pg_class.relname = 'IDX_724b311e6f883751f261ebe378'`);
|
||||
@@ -29,7 +29,7 @@ export class CompositeNoteIndex1745378064470 {
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
const mayConcurrently = isConcurrentIndexMigrationEnabled ? 'CONCURRENTLY' : '';
|
||||
const mayConcurrently = isConcurrentIndexMigrationEnabled() ? 'CONCURRENTLY' : '';
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_724b311e6f883751f261ebe378"`);
|
||||
await queryRunner.query(`CREATE INDEX ${mayConcurrently} "IDX_5b87d9d19127bd5d92026017a7" ON "note" ("userId")`);
|
||||
}
|
||||
|
||||
@@ -3,15 +3,17 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import {loadConfig} from "./js/migration-config.js";
|
||||
|
||||
export class MigrateSomeConfigFileSettingsToMeta1746949539915 {
|
||||
name = 'MigrateSomeConfigFileSettingsToMeta1746949539915'
|
||||
|
||||
async up(queryRunner) {
|
||||
const config = loadConfig();
|
||||
// $1 cannot be used in ALTER TABLE queries
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "proxyRemoteFiles" boolean NOT NULL DEFAULT TRUE`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "signToActivityPubGet" boolean NOT NULL DEFAULT TRUE`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "allowExternalApRedirect" boolean NOT NULL DEFAULT TRUE`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "proxyRemoteFiles" boolean NOT NULL DEFAULT ${config.proxyRemoteFiles}`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "signToActivityPubGet" boolean NOT NULL DEFAULT ${config.signToActivityPubGet}`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "allowExternalApRedirect" boolean NOT NULL DEFAULT ${!config.disallowExternalApRedirect}`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
|
||||
31
packages/backend/migration/js/migration-config.js
Normal file
31
packages/backend/migration/js/migration-config.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { path as configYamlPath } from '../../built/config.js';
|
||||
import * as yaml from 'js-yaml';
|
||||
import fs from "node:fs";
|
||||
|
||||
export function isConcurrentIndexMigrationEnabled() {
|
||||
return process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
|
||||
}
|
||||
|
||||
let loadedConfigCache = undefined;
|
||||
|
||||
function loadConfigInternal() {
|
||||
const config = yaml.load(fs.readFileSync(configYamlPath, 'utf-8'));
|
||||
|
||||
return {
|
||||
disallowExternalApRedirect: Boolean(config.disallowExternalApRedirect ?? false),
|
||||
proxyRemoteFiles: Boolean(config.proxyRemoteFiles ?? false),
|
||||
signToActivityPubGet: Boolean(config.signToActivityPubGet ?? true),
|
||||
}
|
||||
}
|
||||
|
||||
export function loadConfig() {
|
||||
if (loadedConfigCache === undefined) {
|
||||
loadedConfigCache = loadConfigInternal();
|
||||
}
|
||||
return loadedConfigCache;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { loadConfig } from './built/config.js';
|
||||
import { entities } from './built/postgres.js';
|
||||
|
||||
const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
|
||||
import { isConcurrentIndexMigrationEnabled } from "./migration/js/migration-config.js";
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
@@ -16,5 +15,5 @@ export default new DataSource({
|
||||
extra: config.db.extra,
|
||||
entities: entities,
|
||||
migrations: ['migration/*.js'],
|
||||
migrationsTransactionMode: isConcurrentIndexMigrationEnabled ? 'each' : 'all',
|
||||
migrationsTransactionMode: isConcurrentIndexMigrationEnabled() ? 'each' : 'all',
|
||||
});
|
||||
|
||||
@@ -7,37 +7,36 @@
|
||||
"node": "^22.15.0 || ^24.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "pnpm compile-config && node ./built/boot/entry.js",
|
||||
"start:inspect": "pnpm compile-config && node --inspect ./built/boot/entry.js",
|
||||
"start:test": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/boot/entry.js",
|
||||
"migrate": "pnpm compile-config && pnpm typeorm migration:run -d ormconfig.js",
|
||||
"revert": "pnpm compile-config && pnpm typeorm migration:revert -d ormconfig.js",
|
||||
"cli": "pnpm compile-config && node ./built/boot/cli.js",
|
||||
"check:connect": "pnpm compile-config && node ./scripts/check_connect.js",
|
||||
"compile-config": "node ./scripts/compile_config.js",
|
||||
"start": "node ./built/boot/entry.js",
|
||||
"start:inspect": "node --inspect ./built/boot/entry.js",
|
||||
"start:test": "cross-env NODE_ENV=test node ./built/boot/entry.js",
|
||||
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
|
||||
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
|
||||
"cli": "node ./built/boot/cli.js",
|
||||
"check:connect": "node ./scripts/check_connect.js",
|
||||
"build": "swc src -d built -D --strip-leading-paths",
|
||||
"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths",
|
||||
"watch:swc": "swc src -d built -D -w --strip-leading-paths",
|
||||
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
|
||||
"watch": "pnpm compile-config && node ./scripts/watch.mjs",
|
||||
"watch": "node ./scripts/watch.mjs",
|
||||
"restart": "pnpm build && pnpm start",
|
||||
"dev": "pnpm compile-config && node ./scripts/dev.mjs",
|
||||
"dev": "node ./scripts/dev.mjs",
|
||||
"typecheck": "tsc --noEmit && tsc -p test --noEmit && tsc -p test-federation --noEmit",
|
||||
"eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"",
|
||||
"lint": "pnpm typecheck && pnpm eslint",
|
||||
"jest": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.unit.cjs",
|
||||
"jest:e2e": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.e2e.cjs",
|
||||
"jest:fed": "pnpm compile-config && node ./jest.js --forceExit --config jest.config.fed.cjs",
|
||||
"jest-and-coverage": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.unit.cjs",
|
||||
"jest-and-coverage:e2e": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.e2e.cjs",
|
||||
"jest-clear": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --clearCache",
|
||||
"jest": "cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.unit.cjs",
|
||||
"jest:e2e": "cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.e2e.cjs",
|
||||
"jest:fed": "node ./jest.js --forceExit --config jest.config.fed.cjs",
|
||||
"jest-and-coverage": "cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.unit.cjs",
|
||||
"jest-and-coverage:e2e": "cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.e2e.cjs",
|
||||
"jest-clear": "cross-env NODE_ENV=test node ./jest.js --clearCache",
|
||||
"test": "pnpm jest",
|
||||
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
||||
"test:fed": "pnpm jest:fed",
|
||||
"test-and-coverage": "pnpm jest-and-coverage",
|
||||
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
|
||||
"check-migrations": "node scripts/check_migrations_clean.js",
|
||||
"generate-api-json": "pnpm compile-config && node ./scripts/generate_api_json.js"
|
||||
"generate-api-json": "node ./scripts/generate_api_json.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
@@ -71,8 +70,8 @@
|
||||
"utf-8-validate": "6.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.940.0",
|
||||
"@aws-sdk/lib-storage": "3.940.0",
|
||||
"@aws-sdk/client-s3": "3.937.0",
|
||||
"@aws-sdk/lib-storage": "3.937.0",
|
||||
"@discordapp/twemoji": "16.0.1",
|
||||
"@fastify/accepts": "5.0.3",
|
||||
"@fastify/cookie": "11.0.2",
|
||||
@@ -81,16 +80,16 @@
|
||||
"@fastify/http-proxy": "11.3.0",
|
||||
"@fastify/multipart": "9.3.0",
|
||||
"@fastify/static": "8.3.0",
|
||||
"@kitajs/html": "4.2.11",
|
||||
"@fastify/view": "11.1.1",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/summaly": "5.2.5",
|
||||
"@napi-rs/canvas": "0.1.83",
|
||||
"@napi-rs/canvas": "0.1.82",
|
||||
"@nestjs/common": "11.1.9",
|
||||
"@nestjs/core": "11.1.9",
|
||||
"@nestjs/testing": "11.1.9",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sentry/node": "10.27.0",
|
||||
"@sentry/profiling-node": "10.27.0",
|
||||
"@sentry/node": "10.26.0",
|
||||
"@sentry/profiling-node": "10.26.0",
|
||||
"@simplewebauthn/server": "13.2.2",
|
||||
"@sinonjs/fake-timers": "15.0.0",
|
||||
"@smithy/node-http-handler": "4.4.5",
|
||||
@@ -104,8 +103,8 @@
|
||||
"async-mutex": "0.5.0",
|
||||
"bcryptjs": "3.0.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "2.2.1",
|
||||
"bullmq": "5.65.0",
|
||||
"body-parser": "2.2.0",
|
||||
"bullmq": "5.64.1",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "10.0.11",
|
||||
"chalk": "5.6.2",
|
||||
@@ -121,20 +120,21 @@
|
||||
"file-type": "21.1.1",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"form-data": "4.0.5",
|
||||
"got": "14.6.5",
|
||||
"got": "14.6.4",
|
||||
"hpagent": "1.2.0",
|
||||
"http-link-header": "1.1.3",
|
||||
"i18n": "workspace:*",
|
||||
"ioredis": "5.8.2",
|
||||
"ip-cidr": "4.0.2",
|
||||
"ipaddr.js": "2.3.0",
|
||||
"ipaddr.js": "2.2.0",
|
||||
"is-svg": "6.1.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "9.0.0",
|
||||
"jsrsasign": "11.1.0",
|
||||
"juice": "11.0.3",
|
||||
"meilisearch": "0.54.0",
|
||||
"mfm-js": "0.25.0",
|
||||
"microformats-parser": "2.0.4",
|
||||
"mime-types": "3.0.2",
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
@@ -143,7 +143,7 @@
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"node-html-parser": "7.0.1",
|
||||
"nodemailer": "7.0.11",
|
||||
"nodemailer": "7.0.10",
|
||||
"nsfwjs": "4.2.0",
|
||||
"oauth": "0.10.2",
|
||||
"oauth2orize": "1.12.0",
|
||||
@@ -151,9 +151,10 @@
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.4.1",
|
||||
"pg": "8.16.3",
|
||||
"pkce-challenge": "5.0.1",
|
||||
"pkce-challenge": "5.0.0",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
"pug": "3.0.3",
|
||||
"qrcode": "1.5.4",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
@@ -185,9 +186,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@kitajs/ts-html-plugin": "4.1.3",
|
||||
"@nestjs/platform-express": "11.1.9",
|
||||
"@sentry/vue": "10.27.0",
|
||||
"@sentry/vue": "10.26.0",
|
||||
"@simplewebauthn/types": "12.0.0",
|
||||
"@swc/jest": "0.2.39",
|
||||
"@types/accepts": "1.3.7",
|
||||
@@ -198,6 +198,7 @@
|
||||
"@types/fluent-ffmpeg": "2.1.28",
|
||||
"@types/http-link-header": "1.0.7",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsonld": "1.5.15",
|
||||
"@types/jsrsasign": "10.5.15",
|
||||
"@types/mime-types": "3.0.1",
|
||||
@@ -208,6 +209,7 @@
|
||||
"@types/oauth2orize": "1.11.5",
|
||||
"@types/oauth2orize-pkce": "0.1.2",
|
||||
"@types/pg": "8.15.6",
|
||||
"@types/pug": "2.0.10",
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/random-seed": "0.3.5",
|
||||
"@types/ratelimiter": "3.4.6",
|
||||
@@ -222,8 +224,8 @@
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.4",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.48.0",
|
||||
"@typescript-eslint/parser": "8.48.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.47.0",
|
||||
"@typescript-eslint/parser": "8.47.0",
|
||||
"aws-sdk-client-mock": "4.1.0",
|
||||
"cross-env": "10.1.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
@@ -232,11 +234,9 @@
|
||||
"jest": "29.7.0",
|
||||
"jest-mock": "29.7.0",
|
||||
"jest-util": "29.7.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"nodemon": "3.1.11",
|
||||
"pid-port": "2.0.0",
|
||||
"simple-oauth2": "5.1.0",
|
||||
"supertest": "7.1.4",
|
||||
"vite": "7.2.4"
|
||||
"supertest": "7.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* YAMLファイルをJSONファイルに変換するスクリプト
|
||||
* ビルド前に実行し、ランタイムにjs-yamlを含まないようにする
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
||||
const configDir = resolve(_dirname, '../../../.config');
|
||||
const OUTPUT_PATH = resolve(_dirname, '../../../built/.config.json');
|
||||
|
||||
// TODO: yamlのパースに失敗したときのエラーハンドリング
|
||||
|
||||
/**
|
||||
* YAMLファイルをJSONファイルに変換
|
||||
* @param {string} ymlPath - YAMLファイルのパス
|
||||
*/
|
||||
function yamlToJson(ymlPath) {
|
||||
if (!fs.existsSync(ymlPath)) {
|
||||
console.warn(`YAML file not found: ${ymlPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${ymlPath} → ${OUTPUT_PATH}`);
|
||||
|
||||
const yamlContent = fs.readFileSync(ymlPath, 'utf-8');
|
||||
const jsonContent = yaml.load(yamlContent);
|
||||
if (!fs.existsSync(dirname(OUTPUT_PATH))) {
|
||||
fs.mkdirSync(dirname(OUTPUT_PATH), { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(OUTPUT_PATH, JSON.stringify({
|
||||
'_NOTE_': 'This file is auto-generated from YAML file. DO NOT EDIT.',
|
||||
...jsonContent,
|
||||
}), 'utf-8');
|
||||
}
|
||||
|
||||
if (process.env.MISSKEY_CONFIG_YML) {
|
||||
const customYmlPath = resolve(configDir, process.env.MISSKEY_CONFIG_YML);
|
||||
yamlToJson(customYmlPath);
|
||||
} else {
|
||||
yamlToJson(resolve(configDir, process.env.NODE_ENV === 'test' ? 'test.yml' : 'default.yml'));
|
||||
}
|
||||
|
||||
console.log('Configuration compiled ✓');
|
||||
@@ -42,7 +42,7 @@ async function killProc() {
|
||||
'./node_modules/nodemon/bin/nodemon.js',
|
||||
[
|
||||
'-w', 'src',
|
||||
'-e', 'ts,js,mjs,cjs,tsx,json,pug',
|
||||
'-e', 'ts,js,mjs,cjs,json,pug',
|
||||
'--exec', 'pnpm', 'run', 'build',
|
||||
],
|
||||
{
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* This script starts the Misskey backend server, waits for it to be ready,
|
||||
* measures memory usage, and outputs the result as JSON.
|
||||
*
|
||||
* Usage: node scripts/measure-memory.mjs
|
||||
*/
|
||||
|
||||
import { fork } from 'node:child_process';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const STARTUP_TIMEOUT = 120000; // 120 seconds timeout for server startup
|
||||
const MEMORY_SETTLE_TIME = 10000; // Wait 10 seconds after startup for memory to settle
|
||||
|
||||
async function measureMemory() {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Start the Misskey backend server using fork to enable IPC
|
||||
const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), [], {
|
||||
cwd: join(__dirname, '..'),
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'test',
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
||||
});
|
||||
|
||||
let serverReady = false;
|
||||
|
||||
// Listen for the 'ok' message from the server indicating it's ready
|
||||
serverProcess.on('message', (message) => {
|
||||
if (message === 'ok') {
|
||||
serverReady = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle server output
|
||||
serverProcess.stdout?.on('data', (data) => {
|
||||
process.stderr.write(`[server stdout] ${data}`);
|
||||
});
|
||||
|
||||
serverProcess.stderr?.on('data', (data) => {
|
||||
process.stderr.write(`[server stderr] ${data}`);
|
||||
});
|
||||
|
||||
// Handle server error
|
||||
serverProcess.on('error', (err) => {
|
||||
process.stderr.write(`[server error] ${err}\n`);
|
||||
});
|
||||
|
||||
// Wait for server to be ready or timeout
|
||||
const startupStartTime = Date.now();
|
||||
while (!serverReady) {
|
||||
if (Date.now() - startupStartTime > STARTUP_TIMEOUT) {
|
||||
serverProcess.kill('SIGTERM');
|
||||
throw new Error('Server startup timeout');
|
||||
}
|
||||
await setTimeout(100);
|
||||
}
|
||||
|
||||
const startupTime = Date.now() - startupStartTime;
|
||||
process.stderr.write(`Server started in ${startupTime}ms\n`);
|
||||
|
||||
// Wait for memory to settle
|
||||
await setTimeout(MEMORY_SETTLE_TIME);
|
||||
|
||||
// Get memory usage from the server process via /proc
|
||||
const pid = serverProcess.pid;
|
||||
let memoryInfo;
|
||||
|
||||
try {
|
||||
const fs = await import('node:fs/promises');
|
||||
|
||||
// Read /proc/[pid]/status for detailed memory info
|
||||
const status = await fs.readFile(`/proc/${pid}/status`, 'utf-8');
|
||||
const vmRssMatch = status.match(/VmRSS:\s+(\d+)\s+kB/);
|
||||
const vmDataMatch = status.match(/VmData:\s+(\d+)\s+kB/);
|
||||
const vmSizeMatch = status.match(/VmSize:\s+(\d+)\s+kB/);
|
||||
|
||||
memoryInfo = {
|
||||
rss: vmRssMatch ? parseInt(vmRssMatch[1], 10) * 1024 : null,
|
||||
heapUsed: vmDataMatch ? parseInt(vmDataMatch[1], 10) * 1024 : null,
|
||||
vmSize: vmSizeMatch ? parseInt(vmSizeMatch[1], 10) * 1024 : null,
|
||||
};
|
||||
} catch (err) {
|
||||
// Fallback: use ps command
|
||||
process.stderr.write(`Warning: Could not read /proc/${pid}/status: ${err}\n`);
|
||||
|
||||
const { execSync } = await import('node:child_process');
|
||||
try {
|
||||
const ps = execSync(`ps -o rss= -p ${pid}`, { encoding: 'utf-8' });
|
||||
const rssKb = parseInt(ps.trim(), 10);
|
||||
memoryInfo = {
|
||||
rss: rssKb * 1024,
|
||||
heapUsed: null,
|
||||
vmSize: null,
|
||||
};
|
||||
} catch {
|
||||
memoryInfo = {
|
||||
rss: null,
|
||||
heapUsed: null,
|
||||
vmSize: null,
|
||||
error: 'Could not measure memory',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the server
|
||||
serverProcess.kill('SIGTERM');
|
||||
|
||||
// Wait for process to exit
|
||||
let exited = false;
|
||||
await new Promise((resolve) => {
|
||||
serverProcess.on('exit', () => {
|
||||
exited = true;
|
||||
resolve(undefined);
|
||||
});
|
||||
// Force kill after 10 seconds if not exited
|
||||
setTimeout(10000).then(() => {
|
||||
if (!exited) {
|
||||
serverProcess.kill('SIGKILL');
|
||||
}
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
const result = {
|
||||
timestamp: new Date().toISOString(),
|
||||
startupTimeMs: startupTime,
|
||||
memory: memoryInfo,
|
||||
};
|
||||
|
||||
// Output as JSON to stdout
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
measureMemory().catch((err) => {
|
||||
console.error(JSON.stringify({
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
}));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -6,11 +6,11 @@
|
||||
import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { type FastifyServerOptions } from 'fastify';
|
||||
import type * as Sentry from '@sentry/node';
|
||||
import type * as SentryVue from '@sentry/vue';
|
||||
import type { RedisOptions } from 'ioredis';
|
||||
import type { ManifestChunk } from 'vite';
|
||||
|
||||
type RedisOptionsSource = Partial<RedisOptions> & {
|
||||
host: string;
|
||||
@@ -187,9 +187,9 @@ export type Config = {
|
||||
authUrl: string;
|
||||
driveUrl: string;
|
||||
userAgent: string;
|
||||
frontendEntry: ManifestChunk;
|
||||
frontendEntry: { file: string | null };
|
||||
frontendManifestExists: boolean;
|
||||
frontendEmbedEntry: ManifestChunk;
|
||||
frontendEmbedEntry: { file: string | null };
|
||||
frontendEmbedManifestExists: boolean;
|
||||
mediaProxy: string;
|
||||
externalMediaProxyEnabled: boolean;
|
||||
@@ -217,15 +217,21 @@ export type FulltextSearchProvider = 'sqlLike' | 'sqlPgroonga' | 'meilisearch';
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
||||
const compiledConfigFilePathForTest = resolve(_dirname, '../../../built/._config_.json');
|
||||
/**
|
||||
* Path of configuration directory
|
||||
*/
|
||||
const dir = `${_dirname}/../../../.config`;
|
||||
|
||||
export const compiledConfigFilePath = fs.existsSync(compiledConfigFilePathForTest) ? compiledConfigFilePathForTest : resolve(_dirname, '../../../built/.config.json');
|
||||
/**
|
||||
* Path of configuration file
|
||||
*/
|
||||
export const path = process.env.MISSKEY_CONFIG_YML
|
||||
? resolve(dir, process.env.MISSKEY_CONFIG_YML)
|
||||
: process.env.NODE_ENV === 'test'
|
||||
? resolve(dir, 'test.yml')
|
||||
: resolve(dir, 'default.yml');
|
||||
|
||||
export function loadConfig(): Config {
|
||||
if (!fs.existsSync(compiledConfigFilePath)) {
|
||||
throw new Error('Compiled configuration file not found. Try running \'pnpm compile-config\'.');
|
||||
}
|
||||
|
||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
|
||||
|
||||
const frontendManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_vite_/manifest.json');
|
||||
@@ -237,7 +243,7 @@ export function loadConfig(): Config {
|
||||
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
|
||||
: { 'src/boot.ts': { file: null } };
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(compiledConfigFilePath, 'utf-8')) as Source;
|
||||
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
|
||||
|
||||
const url = tryCreateUrl(config.url ?? process.env.MISSKEY_URL ?? '');
|
||||
const version = meta.version;
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
* 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<string, string>;
|
||||
|
||||
const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
|
||||
|
||||
export function htmlSafeJsonStringify(obj: any): string {
|
||||
return JSON.stringify(obj).replace(ESCAPE_REGEX, x => ESCAPE_LOOKUP[x]);
|
||||
}
|
||||
@@ -25,7 +25,6 @@ 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';
|
||||
@@ -59,7 +58,6 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
|
||||
providers: [
|
||||
ClientServerService,
|
||||
ClientLoggerService,
|
||||
HtmlTemplateService,
|
||||
FeedService,
|
||||
HealthServerService,
|
||||
UrlPreviewService,
|
||||
|
||||
@@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown {
|
||||
@bindThis
|
||||
public async launch(): Promise<void> {
|
||||
const fastify = Fastify({
|
||||
trustProxy: this.config.trustProxy ?? false,
|
||||
trustProxy: this.config.trustProxy ?? true,
|
||||
logger: false,
|
||||
});
|
||||
this.#fastify = fastify;
|
||||
|
||||
@@ -12,9 +12,12 @@ 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';
|
||||
@@ -29,8 +32,6 @@ 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';
|
||||
|
||||
@@ -97,32 +98,6 @@ 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,
|
||||
@@ -145,19 +120,24 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
const doc = htmlParser.parse(`<div>${text}</div>`);
|
||||
const fragment = htmlParser.parse(`<div>${text}</div>`);
|
||||
|
||||
redirectUris.push(...[...doc.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href));
|
||||
redirectUris.push(...[...fragment.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href));
|
||||
|
||||
let name = id;
|
||||
let logo: string | null = null;
|
||||
if (text) {
|
||||
const microformats = parseMicroformats(doc, res.url, id);
|
||||
if (typeof microformats.name === 'string') {
|
||||
name = microformats.name;
|
||||
}
|
||||
if (typeof microformats.logo === 'string') {
|
||||
logo = microformats.logo;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +253,6 @@ export class OAuth2ProviderService {
|
||||
private usersRepository: UsersRepository,
|
||||
private cacheService: CacheService,
|
||||
loggerService: LoggerService,
|
||||
private htmlTemplateService: HtmlTemplateService,
|
||||
) {
|
||||
this.#logger = loggerService.getLogger('oauth');
|
||||
|
||||
@@ -407,16 +386,24 @@ export class OAuth2ProviderService {
|
||||
this.#logger.info(`Rendering authorization page for "${oauth2.client.name}"`);
|
||||
|
||||
reply.header('Cache-Control', 'no-store');
|
||||
return await HtmlTemplateService.replyHtml(reply, OAuthPage({
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
return await reply.view('oauth', {
|
||||
transactionId: oauth2.transactionID,
|
||||
clientName: oauth2.client.name,
|
||||
clientLogo: oauth2.client.logo ?? undefined,
|
||||
scope: oauth2.req.scope,
|
||||
}));
|
||||
clientLogo: oauth2.client.logo,
|
||||
scope: oauth2.req.scope.join(' '),
|
||||
});
|
||||
});
|
||||
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<Parameters<typeof done>> => {
|
||||
|
||||
@@ -9,16 +9,20 @@ 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 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';
|
||||
@@ -37,33 +41,14 @@ 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);
|
||||
@@ -77,6 +62,20 @@ const frontendViteOut = `${_dirname}/../../../../../built/_frontend_vite_/`;
|
||||
const frontendEmbedViteOut = `${_dirname}/../../../../../built/_frontend_embed_vite_/`;
|
||||
const tarball = `${_dirname}/../../../../../built/tarball/`;
|
||||
|
||||
const ESCAPE_LOOKUP = {
|
||||
'&': '\\u0026',
|
||||
'>': '\\u003e',
|
||||
'<': '\\u003c',
|
||||
'\u2028': '\\u2028',
|
||||
'\u2029': '\\u2029',
|
||||
} as Record<string, string>;
|
||||
|
||||
const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
|
||||
|
||||
function htmlSafeJsonStringify(obj: any): string {
|
||||
return JSON.stringify(obj).replace(ESCAPE_REGEX, x => ESCAPE_LOOKUP[x]);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ClientServerService {
|
||||
private logger: Logger;
|
||||
@@ -122,6 +121,7 @@ export class ClientServerService {
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private pageEntityService: PageEntityService,
|
||||
private metaEntityService: MetaEntityService,
|
||||
private galleryPostEntityService: GalleryPostEntityService,
|
||||
private clipEntityService: ClipEntityService,
|
||||
private channelEntityService: ChannelEntityService,
|
||||
@@ -129,7 +129,7 @@ export class ClientServerService {
|
||||
private announcementEntityService: AnnouncementEntityService,
|
||||
private urlPreviewService: UrlPreviewService,
|
||||
private feedService: FeedService,
|
||||
private htmlTemplateService: HtmlTemplateService,
|
||||
private roleService: RoleService,
|
||||
private clientLoggerService: ClientLoggerService,
|
||||
) {
|
||||
//this.createServer = this.createServer.bind(this);
|
||||
@@ -195,10 +195,38 @@ 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');
|
||||
@@ -399,15 +427,16 @@ export class ClientServerService {
|
||||
|
||||
//#endregion
|
||||
|
||||
const renderBase = async (reply: FastifyReply, data: Partial<Parameters<typeof BasePage>[0]> = {}) => {
|
||||
const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => {
|
||||
reply.header('Cache-Control', 'public, max-age=30');
|
||||
return await HtmlTemplateService.replyHtml(reply, BasePage({
|
||||
img: this.meta.bannerUrl ?? undefined,
|
||||
return await reply.view('base', {
|
||||
img: this.meta.bannerUrl,
|
||||
url: this.config.url,
|
||||
title: this.meta.name ?? 'Misskey',
|
||||
desc: this.meta.description ?? undefined,
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
desc: this.meta.description,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
...data,
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
// URL preview endpoint
|
||||
@@ -489,6 +518,11 @@ 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) {
|
||||
@@ -501,15 +535,15 @@ export class ClientServerService {
|
||||
userProfile: profile,
|
||||
});
|
||||
|
||||
return await HtmlTemplateService.replyHtml(reply, UserPage({
|
||||
user: _user,
|
||||
profile,
|
||||
return await reply.view('user', {
|
||||
user, profile, me,
|
||||
avatarUrl: _user.avatarUrl,
|
||||
sub: request.params.sub,
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
clientCtxJson: htmlSafeJsonStringify({
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
clientCtx: htmlSafeJsonStringify({
|
||||
user: _user,
|
||||
}),
|
||||
}));
|
||||
});
|
||||
} else {
|
||||
// リモートユーザーなので
|
||||
// モデレータがAPI経由で参照可能にするために404にはしない
|
||||
@@ -560,14 +594,17 @@ export class ClientServerService {
|
||||
reply.header('X-Robots-Tag', 'noimageai');
|
||||
reply.header('X-Robots-Tag', 'noai');
|
||||
}
|
||||
return await HtmlTemplateService.replyHtml(reply, NotePage({
|
||||
return await reply.view('note', {
|
||||
note: _note,
|
||||
profile,
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
clientCtxJson: htmlSafeJsonStringify({
|
||||
avatarUrl: _note.user.avatarUrl,
|
||||
// TODO: Let locale changeable by instance setting
|
||||
summary: getNoteSummary(_note),
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
clientCtx: htmlSafeJsonStringify({
|
||||
note: _note,
|
||||
}),
|
||||
}));
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
@@ -600,11 +637,12 @@ export class ClientServerService {
|
||||
reply.header('X-Robots-Tag', 'noimageai');
|
||||
reply.header('X-Robots-Tag', 'noai');
|
||||
}
|
||||
return await HtmlTemplateService.replyHtml(reply, PagePage({
|
||||
return await reply.view('page', {
|
||||
page: _page,
|
||||
profile,
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
}));
|
||||
avatarUrl: _page.user.avatarUrl,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
@@ -624,11 +662,12 @@ export class ClientServerService {
|
||||
reply.header('X-Robots-Tag', 'noimageai');
|
||||
reply.header('X-Robots-Tag', 'noai');
|
||||
}
|
||||
return await HtmlTemplateService.replyHtml(reply, FlashPage({
|
||||
return await reply.view('flash', {
|
||||
flash: _flash,
|
||||
profile,
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
}));
|
||||
avatarUrl: _flash.user.avatarUrl,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
@@ -648,14 +687,15 @@ export class ClientServerService {
|
||||
reply.header('X-Robots-Tag', 'noimageai');
|
||||
reply.header('X-Robots-Tag', 'noai');
|
||||
}
|
||||
return await HtmlTemplateService.replyHtml(reply, ClipPage({
|
||||
return await reply.view('clip', {
|
||||
clip: _clip,
|
||||
profile,
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
clientCtxJson: htmlSafeJsonStringify({
|
||||
avatarUrl: _clip.user.avatarUrl,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
clientCtx: htmlSafeJsonStringify({
|
||||
clip: _clip,
|
||||
}),
|
||||
}));
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
@@ -673,11 +713,12 @@ export class ClientServerService {
|
||||
reply.header('X-Robots-Tag', 'noimageai');
|
||||
reply.header('X-Robots-Tag', 'noai');
|
||||
}
|
||||
return await HtmlTemplateService.replyHtml(reply, GalleryPostPage({
|
||||
galleryPost: _post,
|
||||
return await reply.view('gallery-post', {
|
||||
post: _post,
|
||||
profile,
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
}));
|
||||
avatarUrl: _post.user.avatarUrl,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
@@ -692,10 +733,10 @@ export class ClientServerService {
|
||||
if (channel) {
|
||||
const _channel = await this.channelEntityService.pack(channel);
|
||||
reply.header('Cache-Control', 'public, max-age=15');
|
||||
return await HtmlTemplateService.replyHtml(reply, ChannelPage({
|
||||
return await reply.view('channel', {
|
||||
channel: _channel,
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
}));
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
@@ -710,10 +751,10 @@ export class ClientServerService {
|
||||
if (game) {
|
||||
const _game = await this.reversiGameEntityService.packDetail(game);
|
||||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return await HtmlTemplateService.replyHtml(reply, ReversiGamePage({
|
||||
reversiGame: _game,
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
}));
|
||||
return await reply.view('reversi-game', {
|
||||
game: _game,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
@@ -729,10 +770,10 @@ export class ClientServerService {
|
||||
if (announcement) {
|
||||
const _announcement = await this.announcementEntityService.pack(announcement);
|
||||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return await HtmlTemplateService.replyHtml(reply, AnnouncementPage({
|
||||
return await reply.view('announcement', {
|
||||
announcement: _announcement,
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
}));
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
@@ -765,13 +806,13 @@ export class ClientServerService {
|
||||
const _user = await this.userEntityService.pack(user);
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
|
||||
return await reply.view('base-embed', {
|
||||
title: this.meta.name ?? 'Misskey',
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
embedCtxJson: htmlSafeJsonStringify({
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
embedCtx: htmlSafeJsonStringify({
|
||||
user: _user,
|
||||
}),
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => {
|
||||
@@ -791,13 +832,13 @@ export class ClientServerService {
|
||||
const _note = await this.noteEntityService.pack(note, null, { detail: true });
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
|
||||
return await reply.view('base-embed', {
|
||||
title: this.meta.name ?? 'Misskey',
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
embedCtxJson: htmlSafeJsonStringify({
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
embedCtx: htmlSafeJsonStringify({
|
||||
note: _note,
|
||||
}),
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { clip: string; } }>('/embed/clips/:clip', async (request, reply) => {
|
||||
@@ -812,46 +853,48 @@ export class ClientServerService {
|
||||
const _clip = await this.clipEntityService.pack(clip);
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
|
||||
return await reply.view('base-embed', {
|
||||
title: this.meta.name ?? 'Misskey',
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
embedCtxJson: htmlSafeJsonStringify({
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
embedCtx: htmlSafeJsonStringify({
|
||||
clip: _clip,
|
||||
}),
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
fastify.get('/embed/*', async (request, reply) => {
|
||||
reply.removeHeader('X-Frame-Options');
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
|
||||
return await reply.view('base-embed', {
|
||||
title: this.meta.name ?? 'Misskey',
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
}));
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
});
|
||||
});
|
||||
|
||||
fastify.get('/_info_card_', async (request, reply) => {
|
||||
reply.removeHeader('X-Frame-Options');
|
||||
|
||||
return await HtmlTemplateService.replyHtml(reply, InfoCardPage({
|
||||
return await reply.view('info-card', {
|
||||
version: this.config.version,
|
||||
config: this.config,
|
||||
host: this.config.host,
|
||||
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 HtmlTemplateService.replyHtml(reply, BiosPage({
|
||||
return await reply.view('bios', {
|
||||
version: this.config.version,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
fastify.get('/cli', async (request, reply) => {
|
||||
return await HtmlTemplateService.replyHtml(reply, CliPage({
|
||||
return await reply.view('cli', {
|
||||
version: this.config.version,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
const override = (source: string, target: string, depth = 0) =>
|
||||
@@ -874,7 +917,7 @@ export class ClientServerService {
|
||||
reply.header('Clear-Site-Data', '"*"');
|
||||
}
|
||||
reply.header('Set-Cookie', 'http-flush-failed=1; Path=/flush; Max-Age=60');
|
||||
return await HtmlTemplateService.replyHtml(reply, FlushPage());
|
||||
return await reply.view('flush');
|
||||
});
|
||||
|
||||
// streamingに非WebSocketリクエストが来た場合にbase htmlをキャシュ付きで返すと、Proxy等でそのパスがキャッシュされておかしくなる
|
||||
@@ -900,10 +943,10 @@ export class ClientServerService {
|
||||
});
|
||||
reply.code(500);
|
||||
reply.header('Cache-Control', 'max-age=10, must-revalidate');
|
||||
return await HtmlTemplateService.replyHtml(reply, ErrorPage({
|
||||
return await reply.view('error', {
|
||||
code: error.code,
|
||||
id: errId,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
done();
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
* 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<CommonData> {
|
||||
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<string>) {
|
||||
reply.header('Content-Type', 'text/html; charset=utf-8');
|
||||
const _html = await html;
|
||||
return reply.send(_html);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { SummalyResult } from '@misskey-dev/summaly/built/summary.js';
|
||||
import { summaly } from '@misskey-dev/summaly';
|
||||
import { 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';
|
||||
@@ -112,7 +113,7 @@ export class UrlPreviewService {
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchSummary(url: string, meta: MiMeta, lang?: string): Promise<SummalyResult> {
|
||||
private fetchSummary(url: string, meta: MiMeta, lang?: string): Promise<SummalyResult> {
|
||||
const agent = this.config.proxy
|
||||
? {
|
||||
http: this.httpRequestService.httpAgent,
|
||||
@@ -120,8 +121,6 @@ export class UrlPreviewService {
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const { summaly } = await import('@misskey-dev/summaly');
|
||||
|
||||
return summaly(url, {
|
||||
followRedirects: this.meta.urlPreviewAllowRedirect,
|
||||
lang: lang ?? 'ja-JP',
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Config } from '@/config.js';
|
||||
|
||||
export const comment = `<!--
|
||||
_____ _ _
|
||||
| |_|___ ___| |_ ___ _ _
|
||||
| | | | |_ -|_ -| '_| -_| | |
|
||||
|_|_|_|_|___|___|_,_|___|_ |
|
||||
|___|
|
||||
Thank you for using Misskey!
|
||||
If you are reading this message... how about joining the development?
|
||||
https://github.com/misskey-dev/misskey
|
||||
|
||||
-->`;
|
||||
|
||||
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<T = Record<string, any>> = MinimumCommonData & T;
|
||||
|
||||
export type CommonProps<T = Record<string, any>> = CommonData & T;
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export function Splash(props: {
|
||||
icon?: string | null;
|
||||
}) {
|
||||
return (
|
||||
<div id="splash">
|
||||
<img id="splashIcon" src={props.icon || '/static-assets/splash.png'} />
|
||||
<div id="splashSpinner">
|
||||
<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1,0,0,1,12,12)">
|
||||
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1,0,0,1,12,12)">
|
||||
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
packages/backend/src/server/web/views/announcement.pug
Normal file
21
packages/backend/src/server/web/views/announcement.pug
Normal file
@@ -0,0 +1,21 @@
|
||||
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')
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* 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 (
|
||||
<>
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:title" content={props.announcement.title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={`${props.config.url}/announcements/${props.announcement.id}`} />
|
||||
{props.announcement.imageUrl ? (
|
||||
<>
|
||||
<meta property="og:image" content={props.announcement.imageUrl} />
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
{...props}
|
||||
title={`${props.announcement.title} | ${props.instanceName}`}
|
||||
desc={description}
|
||||
ogSlot={ogBlock()}
|
||||
>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
71
packages/backend/src/server/web/views/base-embed.pug
Normal file
71
packages/backend/src/server/web/views/base-embed.pug
Normal file
@@ -0,0 +1,71 @@
|
||||
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
|
||||
<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1,0,0,1,12,12)">
|
||||
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1,0,0,1,12,12)">
|
||||
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
||||
</g>
|
||||
</svg>
|
||||
block content
|
||||
@@ -1,88 +0,0 @@
|
||||
/*
|
||||
* 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<CommonProps<{
|
||||
title?: string;
|
||||
noindex?: boolean;
|
||||
desc?: string;
|
||||
img?: string;
|
||||
serverErrorImageUrl?: string;
|
||||
infoImageUrl?: string;
|
||||
notFoundImageUrl?: string;
|
||||
metaJson?: string;
|
||||
embedCtxJson?: string;
|
||||
|
||||
titleSlot?: Children;
|
||||
metaSlot?: Children;
|
||||
}>>) {
|
||||
const now = Date.now();
|
||||
|
||||
// 変数名をsafeで始めることでエラーをスキップ
|
||||
const safeMetaJson = props.metaJson;
|
||||
const safeEmbedCtxJson = props.embedCtxJson;
|
||||
|
||||
return (
|
||||
<>
|
||||
{'<!DOCTYPE html>'}
|
||||
{comment}
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="application-name" content="Misskey" />
|
||||
<meta name="referer" content="origin" />
|
||||
<meta name="theme-color" content={props.themeColor ?? '#86b300'} />
|
||||
<meta name="theme-color-orig" content={props.themeColor ?? '#86b300'} />
|
||||
<meta property="og:site_name" content={props.instanceName || 'Misskey'} />
|
||||
<meta property="instance_url" content={props.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={props.icon ?? '/favicon.ico'} />
|
||||
<link rel="apple-touch-icon" href={props.appleTouchIcon ?? '/apple-touch-icon.png'} />
|
||||
|
||||
{!props.config.frontendEmbedManifestExists ? <script type="module" src="/embed_vite/@vite/client"></script> : null}
|
||||
|
||||
{props.config.frontendEmbedEntry.css != null ? props.config.frontendEmbedEntry.css.map((href) => (
|
||||
<link rel="stylesheet" href={`/embed_vite/${href}`} />
|
||||
)) : null}
|
||||
|
||||
{props.titleSlot ?? <title safe>{props.title || 'Misskey'}</title>}
|
||||
|
||||
{props.metaSlot}
|
||||
|
||||
<meta name="robots" content="noindex" />
|
||||
|
||||
{props.frontendEmbedBootloaderCss != null ? <style safe>{props.frontendEmbedBootloaderCss}</style> : <link rel="stylesheet" href="/embed_vite/loader/style.css" />}
|
||||
|
||||
<script>
|
||||
const VERSION = '{props.version}';
|
||||
const CLIENT_ENTRY = {JSON.stringify(props.config.frontendEmbedEntry.file)};
|
||||
const LANGS = {JSON.stringify(props.langs)};
|
||||
</script>
|
||||
|
||||
{safeMetaJson != null ? <script type="application/json" id="misskey_meta" data-generated-at={now}>{safeMetaJson}</script> : null}
|
||||
{safeEmbedCtxJson != null ? <script type="application/json" id="misskey_embedCtx" data-generated-at={now}>{safeEmbedCtxJson}</script> : null}
|
||||
|
||||
{props.frontendEmbedBootloaderJs != null ? <script>{props.frontendEmbedBootloaderJs}</script> : <script src="/embed_vite/loader/boot.js"></script>}
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<p>
|
||||
JavaScriptを有効にしてください<br />
|
||||
Please turn on your JavaScript
|
||||
</p>
|
||||
</noscript>
|
||||
<Splash icon={props.icon} />
|
||||
{props.children}
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
100
packages/backend/src/server/web/views/base.pug
Normal file
100
packages/backend/src/server/web/views/base.pug
Normal file
@@ -0,0 +1,100 @@
|
||||
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
|
||||
<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1,0,0,1,12,12)">
|
||||
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1,0,0,1,12,12)">
|
||||
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
||||
</g>
|
||||
</svg>
|
||||
block content
|
||||
@@ -1,108 +0,0 @@
|
||||
/*
|
||||
* 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<CommonProps<{
|
||||
title?: string;
|
||||
noindex?: boolean;
|
||||
desc?: string;
|
||||
img?: string;
|
||||
serverErrorImageUrl?: string;
|
||||
infoImageUrl?: string;
|
||||
notFoundImageUrl?: string;
|
||||
metaJson?: string;
|
||||
clientCtxJson?: string;
|
||||
|
||||
titleSlot?: Children;
|
||||
descSlot?: Children;
|
||||
metaSlot?: Children;
|
||||
ogSlot?: Children;
|
||||
}>>) {
|
||||
const now = Date.now();
|
||||
|
||||
// 変数名をsafeで始めることでエラーをスキップ
|
||||
const safeMetaJson = props.metaJson;
|
||||
const safeClientCtxJson = props.clientCtxJson;
|
||||
|
||||
return (
|
||||
<>
|
||||
{'<!DOCTYPE html>'}
|
||||
{comment}
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="application-name" content="Misskey" />
|
||||
<meta name="referer" content="origin" />
|
||||
<meta name="theme-color" content={props.themeColor ?? '#86b300'} />
|
||||
<meta name="theme-color-orig" content={props.themeColor ?? '#86b300'} />
|
||||
<meta property="og:site_name" content={props.instanceName || 'Misskey'} />
|
||||
<meta property="instance_url" content={props.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={props.icon || '/favicon.ico'} />
|
||||
<link rel="apple-touch-icon" href={props.appleTouchIcon || '/apple-touch-icon.png'} />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="search" type="application/opensearchdescription+xml" title={props.title || 'Misskey'} href={`${props.config.url}/opensearch.xml`} />
|
||||
{props.serverErrorImageUrl != null ? <link rel="prefetch" as="image" href={props.serverErrorImageUrl} /> : null}
|
||||
{props.infoImageUrl != null ? <link rel="prefetch" as="image" href={props.infoImageUrl} /> : null}
|
||||
{props.notFoundImageUrl != null ? <link rel="prefetch" as="image" href={props.notFoundImageUrl} /> : null}
|
||||
|
||||
{!props.config.frontendManifestExists ? <script type="module" src="/vite/@vite/client"></script> : null}
|
||||
|
||||
{props.config.frontendEntry.css != null ? props.config.frontendEntry.css.map((href) => (
|
||||
<link rel="stylesheet" href={`/vite/${href}`} />
|
||||
)) : null}
|
||||
|
||||
{props.titleSlot ?? <title safe>{props.title || 'Misskey'}</title>}
|
||||
|
||||
{props.noindex ? <meta name="robots" content="noindex" /> : null}
|
||||
|
||||
{props.descSlot ?? (props.desc != null ? <meta name="description" content={props.desc || defaultDescription} /> : null)}
|
||||
|
||||
{props.metaSlot}
|
||||
|
||||
{props.ogSlot ?? (
|
||||
<>
|
||||
<meta property="og:title" content={props.title || 'Misskey'} />
|
||||
<meta property="og:description" content={props.desc || defaultDescription} />
|
||||
{props.img != null ? <meta property="og:image" content={props.img} /> : null}
|
||||
<meta property="twitter:card" content="summary" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{props.frontendBootloaderCss != null ? <style safe>{props.frontendBootloaderCss}</style> : <link rel="stylesheet" href="/vite/loader/style.css" />}
|
||||
|
||||
<script>
|
||||
const VERSION = '{props.version}';
|
||||
const CLIENT_ENTRY = {JSON.stringify(props.config.frontendEntry.file)};
|
||||
const LANGS = {JSON.stringify(props.langs)};
|
||||
</script>
|
||||
|
||||
{safeMetaJson != null ? <script type="application/json" id="misskey_meta" data-generated-at={now}>{safeMetaJson}</script> : null}
|
||||
{safeClientCtxJson != null ? <script type="application/json" id="misskey_clientCtx" data-generated-at={now}>{safeClientCtxJson}</script> : null}
|
||||
|
||||
{props.frontendBootloaderJs != null ? <script>{props.frontendBootloaderJs}</script> : <script src="/vite/loader/boot.js"></script>}
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<p>
|
||||
JavaScriptを有効にしてください<br />
|
||||
Please turn on your JavaScript
|
||||
</p>
|
||||
</noscript>
|
||||
<Splash icon={props.icon} />
|
||||
{props.children}
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { Layout as BasePage };
|
||||
|
||||
20
packages/backend/src/server/web/views/bios.pug
Normal file
20
packages/backend/src/server/web/views/bios.pug
Normal file
@@ -0,0 +1,20 @@
|
||||
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
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export function BiosPage(props: {
|
||||
version: string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{'<!DOCTYPE html>'}
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="application-name" content="Misskey" />
|
||||
<title>Misskey Repair Tool</title>
|
||||
<link rel="stylesheet" href="/static-assets/misc/bios.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<h1 safe>Misskey Repair Tool {props.version}</h1>
|
||||
</header>
|
||||
<main>
|
||||
<div class="tabs">
|
||||
<button id="ls">edit local storage</button>
|
||||
</div>
|
||||
<div id="content"></div>
|
||||
</main>
|
||||
<script src="/static-assets/misc/bios.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
);
|
||||
}
|
||||
19
packages/backend/src/server/web/views/channel.pug
Normal file
19
packages/backend/src/server/web/views/channel.pug
Normal file
@@ -0,0 +1,19 @@
|
||||
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')
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* 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 (
|
||||
<>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={props.channel.name} />
|
||||
{props.channel.description != null ? <meta property="og:description" content={props.channel.description} /> : null}
|
||||
<meta property="og:url" content={`${props.config.url}/channels/${props.channel.id}`} />
|
||||
{props.channel.bannerUrl ? (
|
||||
<>
|
||||
<meta property="og:image" content={props.channel.bannerUrl} />
|
||||
<meta property="twitter:card" content="summary" />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
{...props}
|
||||
title={`${props.channel.name} | ${props.instanceName}`}
|
||||
desc={props.channel.description ?? undefined}
|
||||
ogSlot={ogBlock()}
|
||||
>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
21
packages/backend/src/server/web/views/cli.pug
Normal file
21
packages/backend/src/server/web/views/cli.pug
Normal file
@@ -0,0 +1,21 @@
|
||||
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
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export function CliPage(props: {
|
||||
version: string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{'<!DOCTYPE html>'}
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="application-name" content="Misskey" />
|
||||
<title>Misskey CLI Tool</title>
|
||||
|
||||
<link rel="stylesheet" href="/static-assets/misc/cli.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<h1 safe>Misskey CLI {props.version}</h1>
|
||||
</header>
|
||||
<main>
|
||||
<div id="form">
|
||||
<textarea id="text"></textarea>
|
||||
<button id="submit">Submit</button>
|
||||
</div>
|
||||
<div id="tl"></div>
|
||||
</main>
|
||||
<script src="/static-assets/misc/cli.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
);
|
||||
}
|
||||
35
packages/backend/src/server/web/views/clip.pug
Normal file
35
packages/backend/src/server/web/views/clip.pug
Normal file
@@ -0,0 +1,35 @@
|
||||
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}`)
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* 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 (
|
||||
<>
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:title" content={props.clip.name} />
|
||||
{props.clip.description != null ? <meta property="og:description" content={props.clip.description} /> : null}
|
||||
<meta property="og:url" content={`${props.config.url}/clips/${props.clip.id}`} />
|
||||
{props.clip.user.avatarUrl ? (
|
||||
<>
|
||||
<meta property="og:image" content={props.clip.user.avatarUrl} />
|
||||
<meta property="twitter:card" content="summary" />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function metaBlock() {
|
||||
return (
|
||||
<>
|
||||
{props.profile.noCrawle ? <meta name="robots" content="noindex" /> : null}
|
||||
{props.profile.preventAiLearning ? (
|
||||
<>
|
||||
<meta name="robots" content="noimageai" />
|
||||
<meta name="robots" content="noai" />
|
||||
</>
|
||||
) : null}
|
||||
<meta name="misskey:user-username" content={props.clip.user.username} />
|
||||
<meta name="misskey:user-id" content={props.clip.user.id} />
|
||||
<meta name="misskey:clip-id" content={props.clip.id} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
{...props}
|
||||
title={`${props.clip.name} | ${props.instanceName}`}
|
||||
desc={props.clip.description ?? ''}
|
||||
metaSlot={metaBlock()}
|
||||
ogSlot={ogBlock()}
|
||||
>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
71
packages/backend/src/server/web/views/error.pug
Normal file
71
packages/backend/src/server/web/views/error.pug
Normal file
@@ -0,0 +1,71 @@
|
||||
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
|
||||
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* 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 (
|
||||
<>
|
||||
{'<!DOCTYPE html>'}
|
||||
{comment}
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="application-name" content="Misskey" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="referrer" content="origin" />
|
||||
<title safe>{props.title ?? 'An error has occurred... | Misskey'}</title>
|
||||
<link rel="stylesheet" href="/static-assets/misc/error.css" />
|
||||
<script src="/static-assets/misc/error.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<svg
|
||||
class="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" />
|
||||
</svg>
|
||||
<h1 data-i18n="title">Failed to initialize Misskey</h1>
|
||||
|
||||
<button class="button-big" onclick="location.reload();">
|
||||
<span class="button-label-big" data-i18n="reload">Reload</span>
|
||||
</button>
|
||||
|
||||
<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.
|
||||
</p>
|
||||
|
||||
<div id="errors">
|
||||
<code safe>
|
||||
ERROR CODE: {props.code}<br />
|
||||
ERROR ID: {props.id}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<p><b data-i18n="solution">The following actions may solve the problem.</b></p>
|
||||
|
||||
<p data-i18n="solution1">Update your os and browser</p>
|
||||
<p data-i18n="solution2">Disable an adblocker</p>
|
||||
<p data-i18n="solution3">Clear your browser cache</p>
|
||||
<p data-i18n="solution4">(Tor Browser) Set dom.webaudio.enabled to true</p>
|
||||
|
||||
<details style="color: #86b300;">
|
||||
<summary data-i18n="otherOption">Other options</summary>
|
||||
<a href="/flush">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small" data-i18n="otherOption1">Clear preferences and cache</span>
|
||||
</button>
|
||||
</a>
|
||||
<a href="/cli">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small" data-i18n="otherOption2">Start the simple client</span>
|
||||
</button>
|
||||
</a>
|
||||
<a href="/bios">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small" data-i18n="otherOption3">Start the repair tool</span>
|
||||
</button>
|
||||
</a>
|
||||
</details>
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
);
|
||||
}
|
||||
35
packages/backend/src/server/web/views/flash.pug
Normal file
35
packages/backend/src/server/web/views/flash.pug
Normal file
@@ -0,0 +1,35 @@
|
||||
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}`)
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* 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 (
|
||||
<>
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:title" content={props.flash.title} />
|
||||
<meta property="og:description" content={props.flash.summary} />
|
||||
<meta property="og:url" content={`${props.config.url}/play/${props.flash.id}`} />
|
||||
{props.flash.user.avatarUrl ? (
|
||||
<>
|
||||
<meta property="og:image" content={props.flash.user.avatarUrl} />
|
||||
<meta property="twitter:card" content="summary" />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function metaBlock() {
|
||||
return (
|
||||
<>
|
||||
{props.profile.noCrawle ? <meta name="robots" content="noindex" /> : null}
|
||||
{props.profile.preventAiLearning ? (
|
||||
<>
|
||||
<meta name="robots" content="noimageai" />
|
||||
<meta name="robots" content="noai" />
|
||||
</>
|
||||
) : null}
|
||||
<meta name="misskey:user-username" content={props.flash.user.username} />
|
||||
<meta name="misskey:user-id" content={props.flash.user.id} />
|
||||
<meta name="misskey:flash-id" content={props.flash.id} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
{...props}
|
||||
title={`${props.flash.title} | ${props.instanceName}`}
|
||||
desc={props.flash.summary}
|
||||
metaSlot={metaBlock()}
|
||||
ogSlot={ogBlock()}
|
||||
>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
51
packages/backend/src/server/web/views/flush.pug
Normal file
51
packages/backend/src/server/web/views/flush.pug
Normal file
@@ -0,0 +1,51 @@
|
||||
doctype html
|
||||
|
||||
html
|
||||
#msg
|
||||
script.
|
||||
const msg = document.getElementById('msg');
|
||||
const successText = `\nSuccess Flush! <a href="/">Back to Misskey</a>\n成功しました。<a href="/">Misskeyを開き直してください。</a>`;
|
||||
|
||||
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. <a href="/flush">Please retry.</a>\n失敗しました。<a href="/flush">もう一度試してみてください。</a>`);
|
||||
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', `<p>[${(new Date()).toString()}] ${text.replace(/\n/g,'<br>')}</p>`)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export function FlushPage(props?: {}) {
|
||||
return (
|
||||
<>
|
||||
{'<!DOCTYPE html>'}
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="application-name" content="Misskey" />
|
||||
<title>Clear preferences and cache</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="msg"></div>
|
||||
<script src="/static-assets/misc/flush.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
);
|
||||
}
|
||||
41
packages/backend/src/server/web/views/gallery-post.pug
Normal file
41
packages/backend/src/server/web/views/gallery-post.pug
Normal file
@@ -0,0 +1,41 @@
|
||||
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')
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* 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 (
|
||||
<>
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:title" content={props.galleryPost.title} />
|
||||
{props.galleryPost.description != null ? <meta property="og:description" content={props.galleryPost.description} /> : null}
|
||||
<meta property="og:url" content={`${props.config.url}/gallery/${props.galleryPost.id}`} />
|
||||
{props.galleryPost.isSensitive && props.galleryPost.user.avatarUrl ? (
|
||||
<>
|
||||
<meta property="og:image" content={props.galleryPost.user.avatarUrl} />
|
||||
<meta property="twitter:card" content="summary" />
|
||||
</>
|
||||
) : null}
|
||||
{!props.galleryPost.isSensitive && props.galleryPost.files != null ? (
|
||||
<>
|
||||
<meta property="og:image" content={props.galleryPost.files[0]!.thumbnailUrl ?? props.galleryPost.files[0]!.url} />
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function metaBlock() {
|
||||
return (
|
||||
<>
|
||||
{props.profile.noCrawle ? <meta name="robots" content="noindex" /> : null}
|
||||
{props.profile.preventAiLearning ? (
|
||||
<>
|
||||
<meta name="robots" content="noimageai" />
|
||||
<meta name="robots" content="noai" />
|
||||
</>
|
||||
) : null}
|
||||
<meta name="misskey:user-username" content={props.galleryPost.user.username} />
|
||||
<meta name="misskey:user-id" content={props.galleryPost.user.id} />
|
||||
<meta name="misskey:gallery-post-id" content={props.galleryPost.id} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
{...props}
|
||||
title={`${props.galleryPost.title} | ${props.instanceName}`}
|
||||
desc={props.galleryPost.description ?? ''}
|
||||
metaSlot={metaBlock()}
|
||||
ogSlot={ogBlock()}
|
||||
>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
50
packages/backend/src/server/web/views/info-card.pug
Normal file
50
packages/backend/src/server/web/views/info-card.pug
Normal file
@@ -0,0 +1,50 @@
|
||||
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
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* 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 (
|
||||
<>
|
||||
{'<!DOCTYPE html>'}
|
||||
{comment}
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="application-name" content="Misskey" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title safe>{props.meta.name ?? props.config.url}</title>
|
||||
<link rel="stylesheet" href="/static-assets/misc/info-card.css" />
|
||||
</head>
|
||||
<body>
|
||||
<a id="a" href={props.config.url} target="_blank" rel="noopener noreferrer">
|
||||
<header id="banner" style={props.meta.bannerUrl != null ? `background-image: url(${props.meta.bannerUrl});` : ''}>
|
||||
<div id="title" safe>{props.meta.name ?? props.config.url}</div>
|
||||
</header>
|
||||
</a>
|
||||
<div id="content">
|
||||
<div id="description">{safeDescription}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
);
|
||||
}
|
||||
62
packages/backend/src/server/web/views/note.pug
Normal file
62
packages/backend/src/server/web/views/note.pug
Normal file
@@ -0,0 +1,62 @@
|
||||
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')
|
||||
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
* 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 (
|
||||
<>
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={summary} />
|
||||
<meta property="og:url" content={`${props.config.url}/notes/${props.note.id}`} />
|
||||
{videos.map(video => (
|
||||
<>
|
||||
<meta property="og:video:url" content={video.url} />
|
||||
<meta property="og:video:secure_url" content={video.url} />
|
||||
<meta property="og:video:type" content={video.type} />
|
||||
{video.thumbnailUrl ? <meta property="og:video:image" content={video.thumbnailUrl} /> : null}
|
||||
{video.properties.width != null ? <meta property="og:video:width" content={video.properties.width.toString()} /> : null}
|
||||
{video.properties.height != null ? <meta property="og:video:height" content={video.properties.height.toString()} /> : null}
|
||||
</>
|
||||
))}
|
||||
{images.length > 0 ? (
|
||||
<>
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
{images.map(image => (
|
||||
<>
|
||||
<meta property="og:image" content={image.url} />
|
||||
{image.properties.width != null ? <meta property="og:image:width" content={image.properties.width.toString()} /> : null}
|
||||
{image.properties.height != null ? <meta property="og:image:height" content={image.properties.height.toString()} /> : null}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<meta property="twitter:card" content="summary" />
|
||||
<meta property="og:image" content={props.note.user.avatarUrl} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function metaBlock() {
|
||||
return (
|
||||
<>
|
||||
{props.note.user.host != null || isRenote || props.profile.noCrawle ? <meta name="robots" content="noindex" /> : null}
|
||||
{props.profile.preventAiLearning ? (
|
||||
<>
|
||||
<meta name="robots" content="noimageai" />
|
||||
<meta name="robots" content="noai" />
|
||||
</>
|
||||
) : null}
|
||||
<meta name="misskey:user-username" content={props.note.user.username} />
|
||||
<meta name="misskey:user-id" content={props.note.user.id} />
|
||||
<meta name="misskey:note-id" content={props.note.id} />
|
||||
|
||||
{props.federationEnabled ? (
|
||||
<>
|
||||
{props.note.user.host == null ? <link rel="alternate" type="application/activity+json" href={`${props.config.url}/notes/${props.note.id}`} /> : null}
|
||||
{props.note.uri != null ? <link rel="alternate" type="application/activity+json" href={props.note.uri} /> : null}
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
{...props}
|
||||
title={`${title} | ${props.instanceName}`}
|
||||
desc={summary}
|
||||
metaSlot={metaBlock()}
|
||||
ogSlot={ogBlock()}
|
||||
></Layout>
|
||||
)
|
||||
}
|
||||
11
packages/backend/src/server/web/views/oauth.pug
Normal file
11
packages/backend/src/server/web/views/oauth.pug
Normal file
@@ -0,0 +1,11 @@
|
||||
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)
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* 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 (
|
||||
<>
|
||||
<meta name="misskey:oauth:transaction-id" content={props.transactionId} />
|
||||
<meta name="misskey:oauth:client-name" content={props.clientName} />
|
||||
{props.clientLogo ? <meta name="misskey:oauth:client-logo" content={props.clientLogo} /> : null}
|
||||
<meta name="misskey:oauth:scope" content={props.scope.join(' ')} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
{...props}
|
||||
metaSlot={metaBlock()}
|
||||
>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
35
packages/backend/src/server/web/views/page.pug
Normal file
35
packages/backend/src/server/web/views/page.pug
Normal file
@@ -0,0 +1,35 @@
|
||||
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}`)
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* 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 (
|
||||
<>
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:title" content={props.page.title} />
|
||||
{props.page.summary != null ? <meta property="og:description" content={props.page.summary} /> : null}
|
||||
<meta property="og:url" content={`${props.config.url}/pages/${props.page.id}`} />
|
||||
{props.page.eyeCatchingImage != null ? (
|
||||
<>
|
||||
<meta property="og:image" content={props.page.eyeCatchingImage.thumbnailUrl ?? props.page.eyeCatchingImage.url} />
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
</>
|
||||
) : props.page.user.avatarUrl ? (
|
||||
<>
|
||||
<meta property="og:image" content={props.page.user.avatarUrl} />
|
||||
<meta property="twitter:card" content="summary" />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function metaBlock() {
|
||||
return (
|
||||
<>
|
||||
{props.profile.noCrawle ? <meta name="robots" content="noindex" /> : null}
|
||||
{props.profile.preventAiLearning ? (
|
||||
<>
|
||||
<meta name="robots" content="noimageai" />
|
||||
<meta name="robots" content="noai" />
|
||||
</>
|
||||
) : null}
|
||||
<meta name="misskey:user-username" content={props.page.user.username} />
|
||||
<meta name="misskey:user-id" content={props.page.user.id} />
|
||||
<meta name="misskey:page-id" content={props.page.id} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
{...props}
|
||||
title={`${props.page.title} | ${props.instanceName}`}
|
||||
desc={props.page.summary ?? ''}
|
||||
metaSlot={metaBlock()}
|
||||
ogSlot={ogBlock()}
|
||||
>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
20
packages/backend/src/server/web/views/reversi-game.pug
Normal file
20
packages/backend/src/server/web/views/reversi-game.pug
Normal file
@@ -0,0 +1,20 @@
|
||||
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')
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* 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 (
|
||||
<>
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={`${props.config.url}/reversi/g/${props.reversiGame.id}`} />
|
||||
<meta property="twitter:card" content="summary" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
{...props}
|
||||
title={`${title} | ${props.instanceName}`}
|
||||
desc={description}
|
||||
ogSlot={ogBlock()}
|
||||
>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
44
packages/backend/src/server/web/views/user.pug
Normal file
44
packages/backend/src/server/web/views/user.pug
Normal file
@@ -0,0 +1,44 @@
|
||||
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}`)
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* 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 (
|
||||
<>
|
||||
<meta property="og:type" content="blog" />
|
||||
<meta property="og:title" content={title} />
|
||||
{props.user.description != null ? <meta property="og:description" content={props.user.description} /> : null}
|
||||
<meta property="og:url" content={`${props.config.url}/@${props.user.username}`} />
|
||||
<meta property="og:image" content={props.user.avatarUrl} />
|
||||
<meta property="twitter:card" content="summary" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function metaBlock() {
|
||||
return (
|
||||
<>
|
||||
{props.user.host != null || props.profile.noCrawle ? <meta name="robots" content="noindex" /> : null}
|
||||
{props.profile.preventAiLearning ? (
|
||||
<>
|
||||
<meta name="robots" content="noimageai" />
|
||||
<meta name="robots" content="noai" />
|
||||
</>
|
||||
) : null}
|
||||
<meta name="misskey:user-username" content={props.user.username} />
|
||||
<meta name="misskey:user-id" content={props.user.id} />
|
||||
|
||||
{props.sub == null && props.federationEnabled ? (
|
||||
<>
|
||||
{props.user.host == null ? <link rel="alternate" type="application/activity+json" href={`${props.config.url}/users/${props.user.id}`} /> : null}
|
||||
{props.user.uri != null ? <link rel="alternate" type="application/activity+json" href={props.user.uri} /> : null}
|
||||
{props.profile.url != null ? <link rel="alternate" type="text/html" href={props.profile.url} /> : null}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{me.map((url) => (
|
||||
<link rel="me" href={url} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
{...props}
|
||||
title={`${props.user.name || props.user.username} (@${props.user.username}) | ${props.instanceName}`}
|
||||
desc={props.user.description ?? ''}
|
||||
metaSlot={metaBlock()}
|
||||
ogSlot={ogBlock()}
|
||||
>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
url: https://example.com/
|
||||
port: 3000
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
22
packages/backend/test-federation/.config/example.default.yml
Normal file
22
packages/backend/test-federation/.config/example.default.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
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
|
||||
@@ -37,8 +37,8 @@ services:
|
||||
- internal_network_a
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./.config/a.test.config.json
|
||||
target: /misskey/built/._config_.json
|
||||
source: ./.config/a.test.default.yml
|
||||
target: /misskey/.config/default.yml
|
||||
read_only: true
|
||||
|
||||
db.a.test:
|
||||
|
||||
@@ -37,8 +37,8 @@ services:
|
||||
- internal_network_b
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./.config/b.test.config.json
|
||||
target: /misskey/built/._config_.json
|
||||
source: ./.config/b.test.default.yml
|
||||
target: /misskey/.config/default.yml
|
||||
read_only: true
|
||||
|
||||
db.b.test:
|
||||
|
||||
@@ -21,10 +21,6 @@ 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
|
||||
@@ -46,10 +42,6 @@ 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
|
||||
@@ -58,14 +50,6 @@ 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
|
||||
|
||||
@@ -54,10 +54,6 @@ 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
|
||||
@@ -66,14 +62,6 @@ 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
|
||||
|
||||
@@ -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.config.json > .config/$1.config.json; fi
|
||||
if [ ! -f .config/$1.default.yml ]; then sed "s/\${HOST}/$1/g" .config/example.default.yml > .config/$1.default.yml; fi
|
||||
}
|
||||
|
||||
generate a.test
|
||||
|
||||
@@ -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": "react-jsx", /* Specify what JSX code is generated. */
|
||||
// "jsx": "preserve", /* 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": "@kitajs/html", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "jsxImportSource": "", /* 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. */
|
||||
|
||||
@@ -23,8 +23,6 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@kitajs/html",
|
||||
"rootDir": "../src",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
|
||||
@@ -23,8 +23,6 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@kitajs/html",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["../src/*"]
|
||||
|
||||
@@ -23,17 +23,12 @@
|
||||
"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"
|
||||
],
|
||||
@@ -48,8 +43,7 @@
|
||||
},
|
||||
"compileOnSave": false,
|
||||
"include": [
|
||||
"./src/**/*.ts",
|
||||
"./src/**/*.tsx"
|
||||
"./src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"./src/**/*.test.ts"
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"devDependencies": {
|
||||
"@types/estree": "1.0.8",
|
||||
"@types/node": "24.10.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.48.0",
|
||||
"@typescript-eslint/parser": "8.48.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.47.0",
|
||||
"@typescript-eslint/parser": "8.47.0",
|
||||
"rollup": "4.53.3",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"@rollup/pluginutils": "5.3.0",
|
||||
"@twemoji/parser": "16.0.0",
|
||||
"@vitejs/plugin-vue": "6.0.2",
|
||||
"@vue/compiler-sfc": "3.5.25",
|
||||
"@vue/compiler-sfc": "3.5.24",
|
||||
"astring": "1.9.0",
|
||||
"buraha": "0.0.1",
|
||||
"estree-walker": "3.0.3",
|
||||
@@ -29,14 +29,14 @@
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.53.3",
|
||||
"sass": "1.94.2",
|
||||
"shiki": "3.17.0",
|
||||
"shiki": "3.15.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.4",
|
||||
"vue": "3.5.25"
|
||||
"vue": "3.5.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/summaly": "5.2.5",
|
||||
@@ -48,21 +48,21 @@
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.18.1",
|
||||
"@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",
|
||||
"@typescript-eslint/eslint-plugin": "8.47.0",
|
||||
"@typescript-eslint/parser": "8.47.0",
|
||||
"@vitest/coverage-v8": "4.0.13",
|
||||
"@vue/runtime-core": "3.5.24",
|
||||
"acorn": "8.15.0",
|
||||
"cross-env": "10.1.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-vue": "10.6.2",
|
||||
"eslint-plugin-vue": "10.6.0",
|
||||
"fast-glob": "3.3.3",
|
||||
"happy-dom": "20.0.11",
|
||||
"happy-dom": "20.0.10",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.8",
|
||||
"msw": "2.12.3",
|
||||
"msw": "2.12.2",
|
||||
"nodemon": "3.1.11",
|
||||
"prettier": "3.7.1",
|
||||
"prettier": "3.6.2",
|
||||
"start-server-and-test": "2.1.3",
|
||||
"tsx": "4.20.6",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "24.10.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.48.0",
|
||||
"@typescript-eslint/parser": "8.48.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.47.0",
|
||||
"@typescript-eslint/parser": "8.47.0",
|
||||
"esbuild": "0.27.0",
|
||||
"eslint-plugin-vue": "10.6.2",
|
||||
"eslint-plugin-vue": "10.6.0",
|
||||
"nodemon": "3.1.11",
|
||||
"typescript": "5.9.3",
|
||||
"vue-eslint-parser": "10.2.0"
|
||||
@@ -36,6 +36,6 @@
|
||||
"dependencies": {
|
||||
"i18n": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
"vue": "3.5.25"
|
||||
"vue": "3.5.24"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,12 @@
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "6.0.3",
|
||||
"@rollup/pluginutils": "5.3.0",
|
||||
"@sentry/vue": "10.27.0",
|
||||
"@sentry/vue": "10.26.0",
|
||||
"@syuilo/aiscript": "1.2.0",
|
||||
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
|
||||
"@twemoji/parser": "16.0.0",
|
||||
"@vitejs/plugin-vue": "6.0.2",
|
||||
"@vue/compiler-sfc": "3.5.25",
|
||||
"@vue/compiler-sfc": "3.5.24",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
|
||||
"analytics": "0.8.19",
|
||||
"astring": "1.9.0",
|
||||
@@ -59,7 +59,7 @@
|
||||
"json5": "2.2.3",
|
||||
"magic-string": "0.30.21",
|
||||
"matter-js": "0.20.0",
|
||||
"mediabunny": "1.25.3",
|
||||
"mediabunny": "1.25.1",
|
||||
"mfm-js": "0.25.0",
|
||||
"misskey-bubble-game": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
@@ -71,7 +71,7 @@
|
||||
"rollup": "4.53.3",
|
||||
"sanitize-html": "2.17.0",
|
||||
"sass": "1.94.2",
|
||||
"shiki": "3.17.0",
|
||||
"shiki": "3.15.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.181.2",
|
||||
@@ -82,7 +82,7 @@
|
||||
"typescript": "5.9.3",
|
||||
"v-code-diff": "1.13.1",
|
||||
"vite": "7.2.4",
|
||||
"vue": "3.5.25",
|
||||
"vue": "3.5.24",
|
||||
"vuedraggable": "next",
|
||||
"wanakana": "5.3.1"
|
||||
},
|
||||
@@ -90,7 +90,7 @@
|
||||
"@misskey-dev/summaly": "5.2.5",
|
||||
"@storybook/addon-essentials": "8.6.14",
|
||||
"@storybook/addon-interactions": "8.6.14",
|
||||
"@storybook/addon-links": "10.1.0",
|
||||
"@storybook/addon-links": "10.0.8",
|
||||
"@storybook/addon-mdx-gfm": "8.6.14",
|
||||
"@storybook/addon-storysource": "8.6.14",
|
||||
"@storybook/blocks": "8.6.14",
|
||||
@@ -98,13 +98,13 @@
|
||||
"@storybook/core-events": "8.6.14",
|
||||
"@storybook/manager-api": "8.6.14",
|
||||
"@storybook/preview-api": "8.6.14",
|
||||
"@storybook/react": "10.1.0",
|
||||
"@storybook/react-vite": "10.1.0",
|
||||
"@storybook/react": "10.0.8",
|
||||
"@storybook/react-vite": "10.0.8",
|
||||
"@storybook/test": "8.6.14",
|
||||
"@storybook/theming": "8.6.14",
|
||||
"@storybook/types": "8.6.14",
|
||||
"@storybook/vue3": "10.1.0",
|
||||
"@storybook/vue3-vite": "10.1.0",
|
||||
"@storybook/vue3": "10.0.8",
|
||||
"@storybook/vue3-vite": "10.0.8",
|
||||
"@tabler/icons-webfont": "3.35.0",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/canvas-confetti": "1.9.0",
|
||||
@@ -118,35 +118,35 @@
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.48.0",
|
||||
"@typescript-eslint/parser": "8.48.0",
|
||||
"@vitest/coverage-v8": "4.0.14",
|
||||
"@vue/compiler-core": "3.5.25",
|
||||
"@vue/runtime-core": "3.5.25",
|
||||
"@typescript-eslint/eslint-plugin": "8.47.0",
|
||||
"@typescript-eslint/parser": "8.47.0",
|
||||
"@vitest/coverage-v8": "4.0.13",
|
||||
"@vue/compiler-core": "3.5.24",
|
||||
"@vue/runtime-core": "3.5.24",
|
||||
"acorn": "8.15.0",
|
||||
"cross-env": "10.1.0",
|
||||
"cypress": "15.7.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-vue": "10.6.2",
|
||||
"eslint-plugin-vue": "10.6.0",
|
||||
"fast-glob": "3.3.3",
|
||||
"happy-dom": "20.0.11",
|
||||
"happy-dom": "20.0.10",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.8",
|
||||
"minimatch": "10.1.1",
|
||||
"msw": "2.12.3",
|
||||
"msw": "2.12.2",
|
||||
"msw-storybook-addon": "2.0.6",
|
||||
"nodemon": "3.1.11",
|
||||
"prettier": "3.7.1",
|
||||
"prettier": "3.6.2",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"start-server-and-test": "2.1.3",
|
||||
"storybook": "10.1.0",
|
||||
"storybook": "10.0.8",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"tsx": "4.20.6",
|
||||
"vite-plugin-glsl": "1.5.4",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "4.0.14",
|
||||
"vitest": "4.0.13",
|
||||
"vitest-fetch-mock": "0.4.5",
|
||||
"vue-component-type-helpers": "3.1.5",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
|
||||
@@ -74,7 +74,6 @@ function mount() {
|
||||
}
|
||||
|
||||
function back() {
|
||||
if (tabs.value.length <= 1) return; // transitionの関係でタブが1つの状態でbackが呼ばれることがある
|
||||
const prev = tabs.value[tabs.value.length - 2];
|
||||
tabs.value = [...tabs.value.slice(0, tabs.value.length - 1)];
|
||||
router?.replaceByPath(prev.fullPath);
|
||||
|
||||
@@ -144,9 +144,7 @@ export function applyTheme(theme: Theme, persist = true) {
|
||||
if (theme.id === currentThemeId && miLocalStorage.getItem('themeCachedVersion') === version) return;
|
||||
currentThemeId = theme.id;
|
||||
|
||||
// visibilityStateがhiddenな状態でstartViewTransitionするとブラウザによってはエラーになる
|
||||
// 通常hiddenな時に呼ばれることはないが、iOSのPWAだとアプリ切り替え時に(何故か)hiddenな状態で(何故か)一瞬デバイスのダークモード判定が変わりapplyThemeが呼ばれる場合がある
|
||||
if (window.document.startViewTransition != null && window.document.visibilityState === 'visible') {
|
||||
if (window.document.startViewTransition != null) {
|
||||
window.document.documentElement.classList.add('_themeChanging_');
|
||||
try {
|
||||
window.document.startViewTransition(async () => {
|
||||
|
||||
@@ -8,10 +8,6 @@
|
||||
".": {
|
||||
"types": "./built/index.d.ts",
|
||||
"import": "./built/index.js"
|
||||
},
|
||||
"./const": {
|
||||
"types": "./built/const.d.ts",
|
||||
"import": "./built/const.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export 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',
|
||||
] as const;
|
||||
|
||||
export const primaries = {
|
||||
'en': 'US',
|
||||
'ja': 'JP',
|
||||
'zh': 'CN',
|
||||
} as const satisfies Record<string, string>;
|
||||
@@ -9,12 +9,48 @@
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { languages, primaries } from './const.js';
|
||||
import type { Locale } from './autogen/locale.js';
|
||||
import type { ILocale, ParameterizedString } from './types.js';
|
||||
|
||||
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',
|
||||
] as const;
|
||||
|
||||
type Language = typeof languages[number];
|
||||
|
||||
const primaries = {
|
||||
'en': 'US',
|
||||
'ja': 'JP',
|
||||
'zh': 'CN',
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
type PrimaryLang = keyof typeof primaries;
|
||||
|
||||
type Locales = Record<Language, ILocale>;
|
||||
@@ -125,6 +161,6 @@ async function writeFrontendLocalesJson(destDir: string, version: string): Promi
|
||||
}
|
||||
}
|
||||
|
||||
export { locales, languages, build, writeFrontendLocalesJson };
|
||||
export { locales, build, writeFrontendLocalesJson };
|
||||
export type { Language, Locale, ILocale, ParameterizedString };
|
||||
export default locales;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user