mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-30 20:05:48 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72cdaff810 | |||
| 7b9e83a6b8 | |||
| 483483bc44 | |||
| f222d7e24d | |||
| e1b6e9d4b6 | |||
| 128fe6d644 | |||
| aa905a74cf | |||
| 5e2a6021ae | |||
| dfd479bec5 | |||
| 0933aa4d92 | |||
| fbd11c1eec | |||
| 768e1dd016 | |||
| d55f51a69b | |||
| fe01a5a28f | |||
| 32b5583432 | |||
| 5fbe801d35 | |||
| 3c11797c6d | |||
| 10242d5f14 | |||
| 0455187a68 |
@@ -111,10 +111,5 @@ jobs:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- run: pnpm --filter misskey-js run build
|
||||
if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'frontend' || matrix.workspace == 'sw' }}
|
||||
- run: pnpm --filter misskey-reversi run build
|
||||
if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'frontend' }}
|
||||
- run: pnpm --filter misskey-bubble-game run build
|
||||
if: ${{ matrix.workspace == 'frontend' }}
|
||||
- run: pnpm --filter "${{ matrix.workspace }}^..." run build
|
||||
- run: pnpm --filter ${{ matrix.workspace }} run typecheck
|
||||
|
||||
@@ -3,10 +3,12 @@ name: Lint
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- packages/i18n/**
|
||||
- locales/**
|
||||
- .github/workflows/locale.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/i18n/**
|
||||
- locales/**
|
||||
- .github/workflows/locale.yml
|
||||
jobs:
|
||||
@@ -14,15 +16,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4.3.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.2.0
|
||||
- uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- run: cd locales && node verify.js
|
||||
- uses: actions/checkout@v4.3.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.2.0
|
||||
- uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: ".node-version"
|
||||
cache: "pnpm"
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- run: pnpm --filter i18n build
|
||||
- name: Verify Locales
|
||||
working-directory: ./packages/i18n
|
||||
run: pnpm run verify
|
||||
|
||||
Vendored
+1
@@ -3,6 +3,7 @@
|
||||
"**/node_modules": true
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"files.associations": {
|
||||
"*.test.ts": "typescript"
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
### Server
|
||||
- Enhance: メモリ使用量を削減しました
|
||||
- Enhance: ActivityPubアクティビティを送信する際のパフォーマンス向上
|
||||
- Enhance: 依存関係の更新
|
||||
|
||||
|
||||
## 2025.11.1
|
||||
|
||||
@@ -24,6 +24,7 @@ COPY --link ["packages/frontend-shared/package.json", "./packages/frontend-share
|
||||
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
|
||||
COPY --link ["packages/frontend-embed/package.json", "./packages/frontend-embed/"]
|
||||
COPY --link ["packages/frontend-builder/package.json", "./packages/frontend-builder/"]
|
||||
COPY --link ["packages/i18n/package.json", "./packages/i18n/"]
|
||||
COPY --link ["packages/icons-subsetter/package.json", "./packages/icons-subsetter/"]
|
||||
COPY --link ["packages/sw/package.json", "./packages/sw/"]
|
||||
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
||||
@@ -101,6 +102,7 @@ COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis
|
||||
COPY --chown=misskey:misskey . ./
|
||||
|
||||
|
||||
+17
-17
@@ -128,7 +128,7 @@ pinnedNote: "Nota fijada"
|
||||
pinned: "Fijar al perfil"
|
||||
you: "Tú"
|
||||
clickToShow: "Haz clic para verlo"
|
||||
sensitive: "Marcado como sensible"
|
||||
sensitive: "Marcado como sensible (NSFW)"
|
||||
add: "Agregar"
|
||||
reaction: "Reacción"
|
||||
reactions: "Reacciones"
|
||||
@@ -143,7 +143,7 @@ rememberNoteVisibility: "Recordar visibilidad"
|
||||
attachCancel: "Quitar adjunto"
|
||||
deleteFile: "Eliminar archivo"
|
||||
markAsSensitive: "Marcar como sensible"
|
||||
unmarkAsSensitive: "Desmarcar como sensible"
|
||||
unmarkAsSensitive: "No marcar como sensible"
|
||||
enterFileName: "Introduce el nombre del archivo"
|
||||
mute: "Silenciar"
|
||||
unmute: "Dejar de silenciar"
|
||||
@@ -353,7 +353,7 @@ emptyFolder: "La carpeta está vacía"
|
||||
dropHereToUpload: "Arrastra los archivos aquí para subirlos."
|
||||
unableToDelete: "No se puede borrar"
|
||||
inputNewFileName: "Ingrese un nuevo nombre de archivo"
|
||||
inputNewDescription: "Ingrese nueva descripción"
|
||||
inputNewDescription: "Introducir un nuevo texto alternativo"
|
||||
inputNewFolderName: "Ingrese un nuevo nombre de la carpeta"
|
||||
circularReferenceFolder: "La carpeta de destino es una sub-carpeta de la carpeta que quieres mover."
|
||||
hasChildFilesOrFolders: "No se puede borrar esta carpeta. No está vacía."
|
||||
@@ -706,7 +706,7 @@ userSaysSomethingAbout: "{name} dijo algo sobre {word}"
|
||||
makeActive: "Activar"
|
||||
display: "Apariencia"
|
||||
copy: "Copiar"
|
||||
copiedToClipboard: "Texto copiado al portapapeles"
|
||||
copiedToClipboard: "Copiado al portapapeles"
|
||||
metrics: "Métricas"
|
||||
overview: "Resumen"
|
||||
logs: "Registros"
|
||||
@@ -715,7 +715,7 @@ database: "Base de datos"
|
||||
channel: "Canal"
|
||||
create: "Crear"
|
||||
notificationSetting: "Ajustes de Notificaciones"
|
||||
notificationSettingDesc: "Por favor elija el tipo de notificación a mostrar"
|
||||
notificationSettingDesc: "Por favor elige el tipo de notificación a mostrar"
|
||||
useGlobalSetting: "Usar ajustes globales"
|
||||
useGlobalSettingDesc: "Al activarse, se usará la configuración de notificaciones de la cuenta, al desactivarse se pueden hacer configuraciones particulares."
|
||||
other: "Otro"
|
||||
@@ -747,7 +747,7 @@ system: "Sistema"
|
||||
switchUi: "Cambiar interfaz de usuario"
|
||||
desktop: "Escritorio"
|
||||
clip: "Clip"
|
||||
createNew: "Crear"
|
||||
createNew: "Crear Nuevo"
|
||||
optional: "Opcional"
|
||||
createNewClip: "Crear clip nuevo"
|
||||
unclip: "Quitar clip"
|
||||
@@ -1203,8 +1203,8 @@ iHaveReadXCarefullyAndAgree: "He leído el texto {x} y estoy de acuerdo"
|
||||
dialog: "Diálogo"
|
||||
icon: "Avatar"
|
||||
forYou: "Para ti"
|
||||
currentAnnouncements: "Anuncios actuales"
|
||||
pastAnnouncements: "Anuncios anteriores"
|
||||
currentAnnouncements: "Avisos actuales"
|
||||
pastAnnouncements: "Avisos anteriores"
|
||||
youHaveUnreadAnnouncements: "Hay anuncios sin leer"
|
||||
useSecurityKey: "Por favor, sigue las instrucciones de tu dispositivo o navegador para usar tu clave de seguridad o tu clave de paso."
|
||||
replies: "Responder"
|
||||
@@ -1412,8 +1412,8 @@ _imageEditing:
|
||||
filename: "Nombre de archivo"
|
||||
filename_without_ext: "Nombre del archivo sin la extensión"
|
||||
year: "Año de rodaje"
|
||||
month: "Mes de rodaje"
|
||||
day: "Día de rodaje"
|
||||
month: "Mes de la fotografía"
|
||||
day: "Día de la fotografía"
|
||||
hour: "Hora"
|
||||
minute: "Minuto"
|
||||
second: "Segundo"
|
||||
@@ -1427,9 +1427,9 @@ _imageEditing:
|
||||
gps_lat: "Latitud"
|
||||
gps_long: "Longitud"
|
||||
_imageFrameEditor:
|
||||
title: "Edición de Fotograma"
|
||||
title: "Edición de Fotos"
|
||||
tip: "Decora tus imágenes con marcos y etiquetas que contengan metadatos."
|
||||
header: "Cabezal"
|
||||
header: "Título"
|
||||
footer: "Pie de página"
|
||||
borderThickness: "Ancho del borde"
|
||||
labelThickness: "Ancho de la etiqueta"
|
||||
@@ -1456,8 +1456,8 @@ _compression:
|
||||
medium: "Tamaño mediano"
|
||||
small: "Tamaño pequeño"
|
||||
_order:
|
||||
newest: "Los más recientes primero"
|
||||
oldest: "Los más antiguos primero"
|
||||
newest: "Más reciente primero"
|
||||
oldest: "Más antiguos primero"
|
||||
_chat:
|
||||
messages: "Mensajes"
|
||||
noMessagesYet: "Aún no hay mensajes"
|
||||
@@ -1685,7 +1685,7 @@ _initialTutorial:
|
||||
followers: "Visible solo para seguidores. Sólo tus seguidores podrán ver la nota, y no podrá ser renotada por otras personas."
|
||||
direct: "Visible sólo para usuarios específicos, y el destinatario será notificado. Puede usarse como alternativa a la mensajería directa."
|
||||
doNotSendConfidencialOnDirect1: "¡Ten cuidado cuando vayas a enviar información sensible!"
|
||||
doNotSendConfidencialOnDirect2: "Los administradores del servidor pueden leer lo que escribes. Ten cuidado cuando envíes información sensible en notas directas en servidores no confiables."
|
||||
doNotSendConfidencialOnDirect2: "Los administradores del servidor, también llamado instancia, pueden leer lo que escribes. Ten cuidado cuando envíes información sensible en notas directas en servidores o instancias no confiables."
|
||||
localOnly: "Publicando con esta opción seleccionada, la nota no se federará hacia otros servidores. Los usuarios de otros servidores no podrán ver estas notas directamente, sin importar los ajustes seleccionados más arriba."
|
||||
_cw:
|
||||
title: "Alerta de contenido (CW)"
|
||||
@@ -2299,7 +2299,7 @@ _theme:
|
||||
indicator: "Indicador"
|
||||
panel: "Panel"
|
||||
shadow: "Sombra"
|
||||
header: "Cabezal"
|
||||
header: "Título"
|
||||
navBg: "Fondo de la barra lateral"
|
||||
navFg: "Texto de la barra lateral"
|
||||
navActive: "Texto de la barra lateral (activo)"
|
||||
@@ -2771,7 +2771,7 @@ _notification:
|
||||
follow: "Siguiendo"
|
||||
mention: "Menciones"
|
||||
reply: "Respuestas"
|
||||
renote: "Renotar"
|
||||
renote: "Renotas"
|
||||
quote: "Citar"
|
||||
reaction: "Reacción"
|
||||
pollEnded: "La encuesta terminó"
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import ts from 'typescript';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const parameterRegExp = /\{(\w+)\}/g;
|
||||
|
||||
function createMemberType(item) {
|
||||
if (typeof item !== 'string') {
|
||||
return ts.factory.createTypeLiteralNode(createMembers(item));
|
||||
}
|
||||
const parameters = Array.from(
|
||||
item.matchAll(parameterRegExp),
|
||||
([, parameter]) => parameter,
|
||||
);
|
||||
return parameters.length
|
||||
? ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('ParameterizedString'),
|
||||
[
|
||||
ts.factory.createUnionTypeNode(
|
||||
parameters.map((parameter) =>
|
||||
ts.factory.createStringLiteral(parameter),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
||||
}
|
||||
|
||||
function createMembers(record) {
|
||||
return Object.entries(record).map(([k, v]) => {
|
||||
const node = ts.factory.createPropertySignature(
|
||||
undefined,
|
||||
ts.factory.createStringLiteral(k),
|
||||
undefined,
|
||||
createMemberType(v),
|
||||
);
|
||||
if (typeof v === 'string') {
|
||||
ts.addSyntheticLeadingComment(
|
||||
node,
|
||||
ts.SyntaxKind.MultiLineCommentTrivia,
|
||||
`*
|
||||
* ${v.replace(/\n/g, '\n * ')}
|
||||
`,
|
||||
true,
|
||||
);
|
||||
}
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
export default function generateDTS() {
|
||||
const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8'));
|
||||
const members = createMembers(locale);
|
||||
const elements = [
|
||||
ts.factory.createVariableStatement(
|
||||
[ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)],
|
||||
ts.factory.createVariableDeclarationList(
|
||||
[
|
||||
ts.factory.createVariableDeclaration(
|
||||
ts.factory.createIdentifier('kParameters'),
|
||||
undefined,
|
||||
ts.factory.createTypeOperatorNode(
|
||||
ts.SyntaxKind.UniqueKeyword,
|
||||
ts.factory.createKeywordTypeNode(ts.SyntaxKind.SymbolKeyword),
|
||||
),
|
||||
undefined,
|
||||
),
|
||||
],
|
||||
ts.NodeFlags.Const,
|
||||
),
|
||||
),
|
||||
ts.factory.createTypeAliasDeclaration(
|
||||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
||||
ts.factory.createIdentifier('ParameterizedString'),
|
||||
[
|
||||
ts.factory.createTypeParameterDeclaration(
|
||||
undefined,
|
||||
ts.factory.createIdentifier('T'),
|
||||
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
||||
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
||||
),
|
||||
],
|
||||
ts.factory.createIntersectionTypeNode([
|
||||
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
||||
ts.factory.createTypeLiteralNode([
|
||||
ts.factory.createPropertySignature(
|
||||
undefined,
|
||||
ts.factory.createComputedPropertyName(
|
||||
ts.factory.createIdentifier('kParameters'),
|
||||
),
|
||||
undefined,
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('T'),
|
||||
undefined,
|
||||
),
|
||||
),
|
||||
])
|
||||
]),
|
||||
),
|
||||
ts.factory.createInterfaceDeclaration(
|
||||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
||||
ts.factory.createIdentifier('ILocale'),
|
||||
undefined,
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createIndexSignature(
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createParameterDeclaration(
|
||||
undefined,
|
||||
undefined,
|
||||
ts.factory.createIdentifier('_'),
|
||||
undefined,
|
||||
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
||||
undefined,
|
||||
),
|
||||
],
|
||||
ts.factory.createUnionTypeNode([
|
||||
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('ParameterizedString'),
|
||||
),
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('ILocale'),
|
||||
undefined,
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
ts.factory.createInterfaceDeclaration(
|
||||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
||||
ts.factory.createIdentifier('Locale'),
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [
|
||||
ts.factory.createExpressionWithTypeArguments(
|
||||
ts.factory.createIdentifier('ILocale'),
|
||||
undefined,
|
||||
),
|
||||
]),
|
||||
],
|
||||
members,
|
||||
),
|
||||
ts.factory.createVariableStatement(
|
||||
[ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)],
|
||||
ts.factory.createVariableDeclarationList(
|
||||
[
|
||||
ts.factory.createVariableDeclaration(
|
||||
ts.factory.createIdentifier('locales'),
|
||||
undefined,
|
||||
ts.factory.createTypeLiteralNode([
|
||||
ts.factory.createIndexSignature(
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createParameterDeclaration(
|
||||
undefined,
|
||||
undefined,
|
||||
ts.factory.createIdentifier('lang'),
|
||||
undefined,
|
||||
ts.factory.createKeywordTypeNode(
|
||||
ts.SyntaxKind.StringKeyword,
|
||||
),
|
||||
undefined,
|
||||
),
|
||||
],
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('Locale'),
|
||||
undefined,
|
||||
),
|
||||
),
|
||||
]),
|
||||
undefined,
|
||||
),
|
||||
],
|
||||
ts.NodeFlags.Const,
|
||||
),
|
||||
),
|
||||
ts.factory.createFunctionDeclaration(
|
||||
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
|
||||
undefined,
|
||||
ts.factory.createIdentifier('build'),
|
||||
undefined,
|
||||
[],
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('Locale'),
|
||||
undefined,
|
||||
),
|
||||
undefined,
|
||||
),
|
||||
ts.factory.createExportDefault(ts.factory.createIdentifier('locales')),
|
||||
];
|
||||
ts.addSyntheticLeadingComment(
|
||||
elements[0],
|
||||
ts.SyntaxKind.MultiLineCommentTrivia,
|
||||
' eslint-disable ',
|
||||
true,
|
||||
);
|
||||
ts.addSyntheticLeadingComment(
|
||||
elements[0],
|
||||
ts.SyntaxKind.SingleLineCommentTrivia,
|
||||
' This file is generated by locales/generateDTS.js',
|
||||
true,
|
||||
);
|
||||
ts.addSyntheticLeadingComment(
|
||||
elements[0],
|
||||
ts.SyntaxKind.SingleLineCommentTrivia,
|
||||
' Do not edit this file directly.',
|
||||
true,
|
||||
);
|
||||
const printed = ts
|
||||
.createPrinter({
|
||||
newLine: ts.NewLineKind.LineFeed,
|
||||
})
|
||||
.printList(
|
||||
ts.ListFormat.MultiLine,
|
||||
ts.factory.createNodeArray(elements),
|
||||
ts.createSourceFile(
|
||||
'index.d.ts',
|
||||
'',
|
||||
ts.ScriptTarget.ESNext,
|
||||
true,
|
||||
ts.ScriptKind.TS,
|
||||
),
|
||||
);
|
||||
|
||||
fs.writeFileSync(`${__dirname}/index.d.ts`, printed, 'utf-8');
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/**
|
||||
* Languages Loader
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as yaml from 'js-yaml';
|
||||
|
||||
const merge = (...args) => args.reduce((a, c) => ({
|
||||
...a,
|
||||
...c,
|
||||
...Object.entries(a)
|
||||
.filter(([k]) => c && typeof c[k] === 'object')
|
||||
.reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {})
|
||||
}), {});
|
||||
|
||||
const languages = [
|
||||
'ar-SA',
|
||||
'ca-ES',
|
||||
'cs-CZ',
|
||||
'da-DK',
|
||||
'de-DE',
|
||||
'en-US',
|
||||
'es-ES',
|
||||
'fr-FR',
|
||||
'id-ID',
|
||||
'it-IT',
|
||||
'ja-JP',
|
||||
'ja-KS',
|
||||
'kab-KAB',
|
||||
'kn-IN',
|
||||
'ko-KR',
|
||||
'nl-NL',
|
||||
'no-NO',
|
||||
'pl-PL',
|
||||
'pt-PT',
|
||||
'ru-RU',
|
||||
'sk-SK',
|
||||
'th-TH',
|
||||
'tr-TR',
|
||||
'ug-CN',
|
||||
'uk-UA',
|
||||
'vi-VN',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
];
|
||||
|
||||
const primaries = {
|
||||
'en': 'US',
|
||||
'ja': 'JP',
|
||||
'zh': 'CN',
|
||||
};
|
||||
|
||||
// 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く
|
||||
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
|
||||
|
||||
export function build() {
|
||||
// vitestの挙動を調整するため、一度ローカル変数化する必要がある
|
||||
// https://github.com/vitest-dev/vitest/issues/3988#issuecomment-1686599577
|
||||
// https://github.com/misskey-dev/misskey/pull/14057#issuecomment-2192833785
|
||||
const metaUrl = import.meta.url;
|
||||
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {});
|
||||
|
||||
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
|
||||
const removeEmpty = (obj) => {
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
if (v === '') {
|
||||
delete obj[k];
|
||||
} else if (typeof v === 'object') {
|
||||
removeEmpty(v);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
removeEmpty(locales);
|
||||
|
||||
return Object.entries(locales)
|
||||
.reduce((a, [k, v]) => (a[k] = (() => {
|
||||
const [lang] = k.split('-');
|
||||
switch (k) {
|
||||
case 'ja-JP': return v;
|
||||
case 'ja-KS':
|
||||
case 'en-US': return merge(locales['ja-JP'], v);
|
||||
default: return merge(
|
||||
locales['ja-JP'],
|
||||
locales['en-US'],
|
||||
locales[`${lang}-${primaries[lang]}`] ?? {},
|
||||
v
|
||||
);
|
||||
}
|
||||
})(), a), {});
|
||||
}
|
||||
|
||||
export default build();
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import locales from './index.js';
|
||||
|
||||
let valid = true;
|
||||
|
||||
function writeError(type, lang, tree, data) {
|
||||
process.stderr.write(JSON.stringify({ type, lang, tree, data }));
|
||||
process.stderr.write('\n');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
function verify(expected, actual, lang, trace) {
|
||||
for (let key in expected) {
|
||||
if (!Object.prototype.hasOwnProperty.call(actual, key)) {
|
||||
continue;
|
||||
}
|
||||
if (typeof expected[key] === 'object') {
|
||||
if (typeof actual[key] !== 'object') {
|
||||
writeError('mismatched_type', lang, trace ? `${trace}.${key}` : key, { expected: 'object', actual: typeof actual[key] });
|
||||
continue;
|
||||
}
|
||||
verify(expected[key], actual[key], lang, trace ? `${trace}.${key}` : key);
|
||||
} else if (typeof expected[key] === 'string') {
|
||||
switch (typeof actual[key]) {
|
||||
case 'object':
|
||||
writeError('mismatched_type', lang, trace ? `${trace}.${key}` : key, { expected: 'string', actual: 'object' });
|
||||
break;
|
||||
case 'undefined':
|
||||
continue;
|
||||
case 'string':
|
||||
const expectedParameters = new Set(expected[key].match(/\{[^}]+\}/g)?.map((s) => s.slice(1, -1)));
|
||||
const actualParameters = new Set(actual[key].match(/\{[^}]+\}/g)?.map((s) => s.slice(1, -1)));
|
||||
for (let parameter of expectedParameters) {
|
||||
if (!actualParameters.has(parameter)) {
|
||||
writeError('missing_parameter', lang, trace ? `${trace}.${key}` : key, { parameter });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { ['ja-JP']: original, ...verifiees } = locales;
|
||||
|
||||
for (let lang in verifiees) {
|
||||
if (!Object.prototype.hasOwnProperty.call(locales, lang)) {
|
||||
continue;
|
||||
}
|
||||
verify(original, verifiees[lang], lang);
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
process.exit(1);
|
||||
}
|
||||
+2
-2
@@ -877,7 +877,7 @@ noInquiryUrlWarning: "尚未设置联络地址。"
|
||||
noBotProtectionWarning: "尚未设置 Bot 防御。"
|
||||
configure: "设置"
|
||||
postToGallery: "创建新图集"
|
||||
postToHashtag: "投稿到这个标签"
|
||||
postToHashtag: "发布至该话题"
|
||||
gallery: "图集"
|
||||
recentPosts: "最新发布"
|
||||
popularPosts: "热门投稿"
|
||||
@@ -3146,7 +3146,7 @@ _selfXssPrevention:
|
||||
description3: "详情请看这里。{link}"
|
||||
_followRequest:
|
||||
recieved: "收到的请求"
|
||||
sent: "已发送的请求"
|
||||
sent: "发送的请求"
|
||||
_remoteLookupErrors:
|
||||
_federationNotAllowed:
|
||||
title: "无法与此服务器通信"
|
||||
|
||||
+14
-12
@@ -1,22 +1,24 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.11.2-alpha.0",
|
||||
"version": "2025.12.0-alpha.0",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@10.22.0",
|
||||
"packageManager": "pnpm@10.23.0",
|
||||
"workspaces": [
|
||||
"packages/frontend-shared",
|
||||
"packages/frontend",
|
||||
"packages/frontend-embed",
|
||||
"packages/icons-subsetter",
|
||||
"packages/backend",
|
||||
"packages/sw",
|
||||
"packages/misskey-js",
|
||||
"packages/i18n",
|
||||
"packages/misskey-reversi",
|
||||
"packages/misskey-bubble-game"
|
||||
"packages/misskey-bubble-game",
|
||||
"packages/icons-subsetter",
|
||||
"packages/frontend-shared",
|
||||
"packages/frontend-builder",
|
||||
"packages/sw",
|
||||
"packages/backend",
|
||||
"packages/frontend",
|
||||
"packages/frontend-embed"
|
||||
],
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -74,12 +76,12 @@
|
||||
"@typescript-eslint/eslint-plugin": "8.47.0",
|
||||
"@typescript-eslint/parser": "8.47.0",
|
||||
"cross-env": "10.1.0",
|
||||
"cypress": "15.6.0",
|
||||
"cypress": "15.7.0",
|
||||
"eslint": "9.39.1",
|
||||
"globals": "16.5.0",
|
||||
"ncp": "2.0.0",
|
||||
"pnpm": "10.22.0",
|
||||
"start-server-and-test": "2.1.2"
|
||||
"pnpm": "10.23.0",
|
||||
"start-server-and-test": "2.1.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tensorflow/tfjs-core": "4.22.0"
|
||||
|
||||
@@ -3,12 +3,17 @@
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"jsx": true,
|
||||
"dynamicImport": true,
|
||||
"decorators": true
|
||||
},
|
||||
"transform": {
|
||||
"legacyDecorator": true,
|
||||
"decoratorMetadata": true
|
||||
"decoratorMetadata": true,
|
||||
"react": {
|
||||
"runtime": "automatic",
|
||||
"importSource": "@kitajs/html"
|
||||
}
|
||||
},
|
||||
"experimental": {
|
||||
"keepImportAssertions": true
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
(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>`)
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,35 @@
|
||||
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'],
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
|
||||
testTimeout: 60000,
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"jspm_packages",
|
||||
"tmp",
|
||||
"temp"
|
||||
]
|
||||
}
|
||||
@@ -40,17 +40,17 @@
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
"@swc/core-darwin-arm64": "1.15.2",
|
||||
"@swc/core-darwin-x64": "1.15.2",
|
||||
"@swc/core-darwin-arm64": "1.15.3",
|
||||
"@swc/core-darwin-x64": "1.15.3",
|
||||
"@swc/core-freebsd-x64": "1.3.11",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.15.2",
|
||||
"@swc/core-linux-arm64-gnu": "1.15.2",
|
||||
"@swc/core-linux-arm64-musl": "1.15.2",
|
||||
"@swc/core-linux-x64-gnu": "1.15.2",
|
||||
"@swc/core-linux-x64-musl": "1.15.2",
|
||||
"@swc/core-win32-arm64-msvc": "1.15.2",
|
||||
"@swc/core-win32-ia32-msvc": "1.15.2",
|
||||
"@swc/core-win32-x64-msvc": "1.15.2",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.15.3",
|
||||
"@swc/core-linux-arm64-gnu": "1.15.3",
|
||||
"@swc/core-linux-arm64-musl": "1.15.3",
|
||||
"@swc/core-linux-x64-gnu": "1.15.3",
|
||||
"@swc/core-linux-x64-musl": "1.15.3",
|
||||
"@swc/core-win32-arm64-msvc": "1.15.3",
|
||||
"@swc/core-win32-ia32-msvc": "1.15.3",
|
||||
"@swc/core-win32-x64-msvc": "1.15.3",
|
||||
"@tensorflow/tfjs": "4.22.0",
|
||||
"@tensorflow/tfjs-node": "4.22.0",
|
||||
"bufferutil": "4.0.9",
|
||||
@@ -70,17 +70,17 @@
|
||||
"utf-8-validate": "6.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.936.0",
|
||||
"@aws-sdk/lib-storage": "3.936.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",
|
||||
"@fastify/cors": "10.1.0",
|
||||
"@fastify/cors": "11.1.0",
|
||||
"@fastify/express": "4.0.2",
|
||||
"@fastify/http-proxy": "10.0.2",
|
||||
"@fastify/http-proxy": "11.3.0",
|
||||
"@fastify/multipart": "9.3.0",
|
||||
"@fastify/static": "8.3.0",
|
||||
"@fastify/view": "10.0.2",
|
||||
"@kitajs/html": "4.2.11",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/summaly": "5.2.5",
|
||||
"@napi-rs/canvas": "0.1.82",
|
||||
@@ -90,52 +90,52 @@
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sentry/node": "10.26.0",
|
||||
"@sentry/profiling-node": "10.26.0",
|
||||
"@simplewebauthn/server": "12.0.0",
|
||||
"@sinonjs/fake-timers": "11.3.1",
|
||||
"@smithy/node-http-handler": "2.5.0",
|
||||
"@simplewebauthn/server": "13.2.2",
|
||||
"@sinonjs/fake-timers": "15.0.0",
|
||||
"@smithy/node-http-handler": "4.4.5",
|
||||
"@swc/cli": "0.7.9",
|
||||
"@swc/core": "1.15.2",
|
||||
"@swc/core": "1.15.3",
|
||||
"@twemoji/parser": "16.0.0",
|
||||
"@types/redis-info": "3.0.3",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.17.1",
|
||||
"archiver": "7.0.1",
|
||||
"async-mutex": "0.5.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bcryptjs": "3.0.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "1.20.3",
|
||||
"bullmq": "5.63.2",
|
||||
"body-parser": "2.2.0",
|
||||
"bullmq": "5.64.1",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.2",
|
||||
"cbor": "10.0.11",
|
||||
"chalk": "5.6.2",
|
||||
"chalk-template": "1.1.2",
|
||||
"chokidar": "4.0.3",
|
||||
"color-convert": "2.0.1",
|
||||
"content-disposition": "0.5.4",
|
||||
"date-fns": "2.30.0",
|
||||
"color-convert": "3.1.3",
|
||||
"content-disposition": "1.0.1",
|
||||
"date-fns": "4.1.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"fastify": "5.6.2",
|
||||
"fastify-raw-body": "5.0.0",
|
||||
"feed": "4.2.2",
|
||||
"feed": "5.1.0",
|
||||
"file-type": "21.1.1",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"form-data": "4.0.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.2.0",
|
||||
"is-svg": "5.1.0",
|
||||
"is-svg": "6.1.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "8.3.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": "2.1.35",
|
||||
"mime-types": "3.0.2",
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"ms": "3.0.0-canary.202508261828",
|
||||
@@ -151,22 +151,20 @@
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.4.1",
|
||||
"pg": "8.16.3",
|
||||
"pkce-challenge": "4.1.0",
|
||||
"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",
|
||||
"re2": "1.22.3",
|
||||
"redis-info": "3.1.0",
|
||||
"redis-lock": "0.1.4",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"rename": "1.0.4",
|
||||
"rss-parser": "3.13.0",
|
||||
"rxjs": "7.8.2",
|
||||
"sanitize-html": "2.17.0",
|
||||
"secure-json-parse": "3.0.2",
|
||||
"secure-json-parse": "4.1.0",
|
||||
"semver": "7.7.3",
|
||||
"sharp": "0.33.5",
|
||||
"slacc": "0.0.10",
|
||||
@@ -179,7 +177,7 @@
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typeorm": "0.3.27",
|
||||
"typescript": "5.9.3",
|
||||
"ulid": "2.4.0",
|
||||
"ulid": "3.0.1",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.6.7",
|
||||
"ws": "8.18.3",
|
||||
@@ -187,13 +185,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@nestjs/platform-express": "10.4.20",
|
||||
"@kitajs/ts-html-plugin": "4.1.3",
|
||||
"@nestjs/platform-express": "11.1.9",
|
||||
"@sentry/vue": "10.26.0",
|
||||
"@simplewebauthn/types": "12.0.0",
|
||||
"@swc/jest": "0.2.39",
|
||||
"@types/accepts": "1.3.7",
|
||||
"@types/archiver": "6.0.4",
|
||||
"@types/bcryptjs": "2.4.6",
|
||||
"@types/archiver": "7.0.0",
|
||||
"@types/body-parser": "1.19.6",
|
||||
"@types/color-convert": "2.0.4",
|
||||
"@types/content-disposition": "0.5.9",
|
||||
@@ -203,15 +201,14 @@
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsonld": "1.5.15",
|
||||
"@types/jsrsasign": "10.5.15",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/ms": "0.7.34",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@types/ms": "2.1.0",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/nodemailer": "6.4.21",
|
||||
"@types/nodemailer": "7.0.4",
|
||||
"@types/oauth": "0.9.6",
|
||||
"@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",
|
||||
@@ -219,7 +216,7 @@
|
||||
"@types/sanitize-html": "2.16.0",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/simple-oauth2": "5.0.7",
|
||||
"@types/sinonjs__fake-timers": "8.1.5",
|
||||
"@types/sinonjs__fake-timers": "15.0.1",
|
||||
"@types/supertest": "6.0.3",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/tmp": "0.2.6",
|
||||
@@ -229,15 +226,17 @@
|
||||
"@typescript-eslint/eslint-plugin": "8.47.0",
|
||||
"@typescript-eslint/parser": "8.47.0",
|
||||
"aws-sdk-client-mock": "4.1.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cross-env": "10.1.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"execa": "8.0.1",
|
||||
"fkill": "9.0.0",
|
||||
"execa": "9.6.0",
|
||||
"fkill": "10.0.1",
|
||||
"jest": "29.7.0",
|
||||
"jest-mock": "29.7.0",
|
||||
"jest-util": "29.7.0",
|
||||
"nodemon": "3.1.11",
|
||||
"pid-port": "1.0.2",
|
||||
"pid-port": "2.0.0",
|
||||
"simple-oauth2": "5.1.0",
|
||||
"supertest": "7.1.4"
|
||||
"supertest": "7.1.4",
|
||||
"vite": "7.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ async function killProc() {
|
||||
'./node_modules/nodemon/bin/nodemon.js',
|
||||
[
|
||||
'-w', 'src',
|
||||
'-e', 'ts,js,mjs,cjs,json,pug',
|
||||
'-e', 'ts,js,mjs,cjs,tsx,json,pug',
|
||||
'--exec', 'pnpm', 'run', 'build',
|
||||
],
|
||||
{
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
declare module 'redis-lock' {
|
||||
import type Redis from 'ioredis';
|
||||
|
||||
type Lock = (lockName: string, timeout?: number, taskToPerform?: () => Promise<void>) => void;
|
||||
function redisLock(client: Redis.Redis, retryDelay: number): Lock;
|
||||
|
||||
export = redisLock;
|
||||
}
|
||||
@@ -10,8 +10,6 @@ import * as os from 'node:os';
|
||||
import cluster from 'node:cluster';
|
||||
import chalk from 'chalk';
|
||||
import chalkTemplate from 'chalk-template';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { nodeProfilingIntegration } from '@sentry/profiling-node';
|
||||
import Logger from '@/logger.js';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -74,6 +72,9 @@ export async function masterMain() {
|
||||
bootLogger.succ('Misskey initialized');
|
||||
|
||||
if (config.sentryForBackend) {
|
||||
const Sentry = await import('@sentry/node');
|
||||
const { nodeProfilingIntegration } = await import('@sentry/profiling-node');
|
||||
|
||||
Sentry.init({
|
||||
integrations: [
|
||||
...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
*/
|
||||
|
||||
import cluster from 'node:cluster';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { nodeProfilingIntegration } from '@sentry/profiling-node';
|
||||
import { envOption } from '@/env.js';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import { jobQueue, server } from './common.js';
|
||||
@@ -17,6 +15,9 @@ export async function workerMain() {
|
||||
const config = loadConfig();
|
||||
|
||||
if (config.sentryForBackend) {
|
||||
const Sentry = await import('@sentry/node');
|
||||
const { nodeProfilingIntegration } = await import('@sentry/profiling-node');
|
||||
|
||||
Sentry.init({
|
||||
integrations: [
|
||||
...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []),
|
||||
|
||||
@@ -11,6 +11,7 @@ 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 +188,9 @@ export type Config = {
|
||||
authUrl: string;
|
||||
driveUrl: string;
|
||||
userAgent: string;
|
||||
frontendEntry: { file: string | null };
|
||||
frontendEntry: ManifestChunk;
|
||||
frontendManifestExists: boolean;
|
||||
frontendEmbedEntry: { file: string | null };
|
||||
frontendEmbedEntry: ManifestChunk;
|
||||
frontendEmbedManifestExists: boolean;
|
||||
mediaProxy: string;
|
||||
externalMediaProxyEnabled: boolean;
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { promisify } from 'node:util';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import redisLock from 'redis-lock';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
/**
|
||||
* Retry delay (ms) for lock acquisition
|
||||
*/
|
||||
const retryDelay = 100;
|
||||
|
||||
@Injectable()
|
||||
export class AppLockService {
|
||||
private lock: (key: string, timeout?: number, _?: (() => Promise<void>) | undefined) => Promise<() => void>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
) {
|
||||
this.lock = promisify(redisLock(this.redisClient, retryDelay));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AP Object lock
|
||||
* @param uri AP object ID
|
||||
* @param timeout Lock timeout (ms), The timeout releases previous lock.
|
||||
* @returns Unlock function
|
||||
*/
|
||||
@bindThis
|
||||
public getApLock(uri: string, timeout = 30 * 1000): Promise<() => void> {
|
||||
return this.lock(`ap-object:${uri}`, timeout);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getChartInsertLock(lockKey: string, timeout = 30 * 1000): Promise<() => void> {
|
||||
return this.lock(`chart-insert:${lockKey}`, timeout);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import { AccountUpdateService } from './AccountUpdateService.js';
|
||||
import { AiService } from './AiService.js';
|
||||
import { AnnouncementService } from './AnnouncementService.js';
|
||||
import { AntennaService } from './AntennaService.js';
|
||||
import { AppLockService } from './AppLockService.js';
|
||||
import { AchievementService } from './AchievementService.js';
|
||||
import { AvatarDecorationService } from './AvatarDecorationService.js';
|
||||
import { CaptchaService } from './CaptchaService.js';
|
||||
@@ -166,7 +165,6 @@ const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useEx
|
||||
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
|
||||
const $AnnouncementService: Provider = { provide: 'AnnouncementService', useExisting: AnnouncementService };
|
||||
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
|
||||
const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService };
|
||||
const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
|
||||
const $AvatarDecorationService: Provider = { provide: 'AvatarDecorationService', useExisting: AvatarDecorationService };
|
||||
const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
|
||||
@@ -320,7 +318,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
AiService,
|
||||
AnnouncementService,
|
||||
AntennaService,
|
||||
AppLockService,
|
||||
AchievementService,
|
||||
AvatarDecorationService,
|
||||
CaptchaService,
|
||||
@@ -470,7 +467,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$AiService,
|
||||
$AnnouncementService,
|
||||
$AntennaService,
|
||||
$AppLockService,
|
||||
$AchievementService,
|
||||
$AvatarDecorationService,
|
||||
$CaptchaService,
|
||||
@@ -621,7 +617,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
AiService,
|
||||
AnnouncementService,
|
||||
AntennaService,
|
||||
AppLockService,
|
||||
AchievementService,
|
||||
AvatarDecorationService,
|
||||
CaptchaService,
|
||||
@@ -770,7 +765,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$AiService,
|
||||
$AnnouncementService,
|
||||
$AntennaService,
|
||||
$AppLockService,
|
||||
$AchievementService,
|
||||
$AvatarDecorationService,
|
||||
$CaptchaService,
|
||||
|
||||
@@ -202,7 +202,7 @@ export class NotificationService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
// TODO
|
||||
//const locales = await import('../../../../locales/index.js');
|
||||
//const locales = await import('i18n');
|
||||
|
||||
// TODO: locale ファイルをクライアント用とサーバー用で分けたい
|
||||
|
||||
@@ -271,7 +271,7 @@ export class NotificationService implements OnApplicationShutdown {
|
||||
let untilTime = untilId ? this.toXListId(untilId) : null;
|
||||
|
||||
let notifications: MiNotification[];
|
||||
for (;;) {
|
||||
for (; ;) {
|
||||
let notificationsRes: [id: string, fields: string[]][];
|
||||
|
||||
// sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照
|
||||
|
||||
@@ -66,7 +66,6 @@ export class WebAuthnService {
|
||||
userID: isoUint8Array.fromUTF8String(userId),
|
||||
userName: userName,
|
||||
userDisplayName: userDisplayName,
|
||||
attestationType: 'indirect',
|
||||
excludeCredentials: keys.map(key => (<{ id: string; transports?: AuthenticatorTransportFuture[]; }>{
|
||||
id: key.id,
|
||||
transports: key.transports ?? undefined,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
@@ -14,8 +15,8 @@ import { NotePiningService } from '@/core/NotePiningService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
|
||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||
import { acquireApObjectLock } from '@/misc/distributed-lock.js';
|
||||
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
@@ -48,8 +49,8 @@ export class ApInboxService {
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
@@ -76,7 +77,6 @@ export class ApInboxService {
|
||||
private userBlockingService: UserBlockingService,
|
||||
private noteCreateService: NoteCreateService,
|
||||
private noteDeleteService: NoteDeleteService,
|
||||
private appLockService: AppLockService,
|
||||
private apResolverService: ApResolverService,
|
||||
private apDbResolverService: ApDbResolverService,
|
||||
private apLoggerService: ApLoggerService,
|
||||
@@ -311,7 +311,7 @@ export class ApInboxService {
|
||||
// アナウンス先が許可されているかチェック
|
||||
if (!this.utilityService.isFederationAllowedUri(uri)) return;
|
||||
|
||||
const unlock = await this.appLockService.getApLock(uri);
|
||||
const unlock = await acquireApObjectLock(this.redisClient, uri);
|
||||
|
||||
try {
|
||||
// 既に同じURIを持つものが登録されていないかチェック
|
||||
@@ -438,7 +438,7 @@ export class ApInboxService {
|
||||
}
|
||||
}
|
||||
|
||||
const unlock = await this.appLockService.getApLock(uri);
|
||||
const unlock = await acquireApObjectLock(this.redisClient, uri);
|
||||
|
||||
try {
|
||||
const exist = await this.apNoteService.fetchNote(note);
|
||||
@@ -522,7 +522,7 @@ export class ApInboxService {
|
||||
private async deleteNote(actor: MiRemoteUser, uri: string): Promise<string> {
|
||||
this.logger.info(`Deleting the Note: ${uri}`);
|
||||
|
||||
const unlock = await this.appLockService.getApLock(uri);
|
||||
const unlock = await acquireApObjectLock(this.redisClient, uri);
|
||||
|
||||
try {
|
||||
const note = await this.apDbResolverService.getNoteFromApId(uri);
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
|
||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { PollsRepository, EmojisRepository, MiMeta } from '@/models/_.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { MiRemoteUser } from '@/models/User.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { acquireApObjectLock } from '@/misc/distributed-lock.js';
|
||||
import { toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
||||
import type { MiEmoji } from '@/models/Emoji.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
@@ -48,6 +49,9 @@ export class ApNoteService {
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.pollsRepository)
|
||||
private pollsRepository: PollsRepository,
|
||||
|
||||
@@ -67,7 +71,6 @@ export class ApNoteService {
|
||||
private apMentionService: ApMentionService,
|
||||
private apImageService: ApImageService,
|
||||
private apQuestionService: ApQuestionService,
|
||||
private appLockService: AppLockService,
|
||||
private pollService: PollService,
|
||||
private noteCreateService: NoteCreateService,
|
||||
private apDbResolverService: ApDbResolverService,
|
||||
@@ -354,7 +357,7 @@ export class ApNoteService {
|
||||
throw new StatusError('blocked host', 451);
|
||||
}
|
||||
|
||||
const unlock = await this.appLockService.getApLock(uri);
|
||||
const unlock = await acquireApObjectLock(this.redisClient, uri);
|
||||
|
||||
try {
|
||||
//#region このサーバーに既に登録されていたらそれを返す
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||
import Chart from '../core.js';
|
||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||
import { name, schema } from './entities/active-users.js';
|
||||
@@ -28,11 +29,13 @@ export default class ActiveUsersChart extends Chart<typeof schema> { // eslint-d
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
private appLockService: AppLockService,
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
private chartLoggerService: ChartLoggerService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
|
||||
super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
|
||||
}
|
||||
|
||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||
import Chart from '../core.js';
|
||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||
import { name, schema } from './entities/ap-request.js';
|
||||
@@ -22,10 +23,12 @@ export default class ApRequestChart extends Chart<typeof schema> { // eslint-dis
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
private appLockService: AppLockService,
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
private chartLoggerService: ChartLoggerService,
|
||||
) {
|
||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
|
||||
super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
|
||||
}
|
||||
|
||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||
import Chart from '../core.js';
|
||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||
import { name, schema } from './entities/drive.js';
|
||||
@@ -23,10 +24,12 @@ export default class DriveChart extends Chart<typeof schema> { // eslint-disable
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
private appLockService: AppLockService,
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
private chartLoggerService: ChartLoggerService,
|
||||
) {
|
||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
|
||||
super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
|
||||
}
|
||||
|
||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { FollowingsRepository, InstancesRepository, MiMeta } from '@/models/_.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||
import Chart from '../core.js';
|
||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||
import { name, schema } from './entities/federation.js';
|
||||
@@ -26,16 +27,18 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(DI.instancesRepository)
|
||||
private instancesRepository: InstancesRepository,
|
||||
|
||||
private appLockService: AppLockService,
|
||||
private chartLoggerService: ChartLoggerService,
|
||||
) {
|
||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
|
||||
super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
|
||||
}
|
||||
|
||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { DriveFilesRepository, FollowingsRepository, UsersRepository, NotesRepository } from '@/models/_.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||
import Chart from '../core.js';
|
||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||
import { name, schema } from './entities/instance.js';
|
||||
@@ -26,6 +27,9 @@ export default class InstanceChart extends Chart<typeof schema> { // eslint-disa
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -39,10 +43,9 @@ export default class InstanceChart extends Chart<typeof schema> { // eslint-disa
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private appLockService: AppLockService,
|
||||
private chartLoggerService: ChartLoggerService,
|
||||
) {
|
||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
|
||||
super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
|
||||
}
|
||||
|
||||
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { Not, IsNull, DataSource } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { NotesRepository } from '@/models/_.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||
import Chart from '../core.js';
|
||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||
import { name, schema } from './entities/notes.js';
|
||||
@@ -24,13 +25,15 @@ export default class NotesChart extends Chart<typeof schema> { // eslint-disable
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
private appLockService: AppLockService,
|
||||
private chartLoggerService: ChartLoggerService,
|
||||
) {
|
||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
|
||||
super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
|
||||
}
|
||||
|
||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { DriveFilesRepository } from '@/models/_.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||
import Chart from '../core.js';
|
||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||
import { name, schema } from './entities/per-user-drive.js';
|
||||
@@ -25,14 +26,16 @@ export default class PerUserDriveChart extends Chart<typeof schema> { // eslint-
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private appLockService: AppLockService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private chartLoggerService: ChartLoggerService,
|
||||
) {
|
||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
|
||||
super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
|
||||
}
|
||||
|
||||
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { Not, IsNull, DataSource } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { FollowingsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||
import Chart from '../core.js';
|
||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||
import { name, schema } from './entities/per-user-following.js';
|
||||
@@ -25,14 +26,16 @@ export default class PerUserFollowingChart extends Chart<typeof schema> { // esl
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
private appLockService: AppLockService,
|
||||
private userEntityService: UserEntityService,
|
||||
private chartLoggerService: ChartLoggerService,
|
||||
) {
|
||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
|
||||
super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
|
||||
}
|
||||
|
||||
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { NotesRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||
import Chart from '../core.js';
|
||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||
import { name, schema } from './entities/per-user-notes.js';
|
||||
@@ -25,13 +26,15 @@ export default class PerUserNotesChart extends Chart<typeof schema> { // eslint-
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
private appLockService: AppLockService,
|
||||
private chartLoggerService: ChartLoggerService,
|
||||
) {
|
||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
|
||||
super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
|
||||
}
|
||||
|
||||
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||
import Chart from '../core.js';
|
||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||
import { name, schema } from './entities/per-user-pv.js';
|
||||
@@ -23,10 +24,12 @@ export default class PerUserPvChart extends Chart<typeof schema> { // eslint-dis
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
private appLockService: AppLockService,
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
private chartLoggerService: ChartLoggerService,
|
||||
) {
|
||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
|
||||
super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
|
||||
}
|
||||
|
||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||
import Chart from '../core.js';
|
||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||
import { name, schema } from './entities/per-user-reactions.js';
|
||||
@@ -25,11 +26,13 @@ export default class PerUserReactionsChart extends Chart<typeof schema> { // esl
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
private appLockService: AppLockService,
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private chartLoggerService: ChartLoggerService,
|
||||
) {
|
||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
|
||||
super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
|
||||
}
|
||||
|
||||
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||
import Chart from '../core.js';
|
||||
import { name, schema } from './entities/test-grouped.js';
|
||||
import type { KVs } from '../core.js';
|
||||
@@ -24,10 +25,12 @@ export default class TestGroupedChart extends Chart<typeof schema> { // eslint-d
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
private appLockService: AppLockService,
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
logger: Logger,
|
||||
) {
|
||||
super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema, true);
|
||||
super(db, (k) => acquireChartInsertLock(redisClient, k), logger, name, schema, true);
|
||||
}
|
||||
|
||||
protected async tickMajor(group: string): Promise<Partial<KVs<typeof schema>>> {
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||
import Chart from '../core.js';
|
||||
import { name, schema } from './entities/test-intersection.js';
|
||||
import type { KVs } from '../core.js';
|
||||
@@ -22,10 +23,12 @@ export default class TestIntersectionChart extends Chart<typeof schema> { // esl
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
private appLockService: AppLockService,
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
logger: Logger,
|
||||
) {
|
||||
super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema);
|
||||
super(db, (k) => acquireChartInsertLock(redisClient, k), logger, name, schema);
|
||||
}
|
||||
|
||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||
import Chart from '../core.js';
|
||||
import { name, schema } from './entities/test-unique.js';
|
||||
import type { KVs } from '../core.js';
|
||||
@@ -22,10 +23,12 @@ export default class TestUniqueChart extends Chart<typeof schema> { // eslint-di
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
private appLockService: AppLockService,
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
logger: Logger,
|
||||
) {
|
||||
super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema);
|
||||
super(db, (k) => acquireChartInsertLock(redisClient, k), logger, name, schema);
|
||||
}
|
||||
|
||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||
import Chart from '../core.js';
|
||||
import { name, schema } from './entities/test.js';
|
||||
import type { KVs } from '../core.js';
|
||||
@@ -24,10 +25,12 @@ export default class TestChart extends Chart<typeof schema> { // eslint-disable-
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
private appLockService: AppLockService,
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
logger: Logger,
|
||||
) {
|
||||
super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema);
|
||||
super(db, (k) => acquireChartInsertLock(redisClient, k), logger, name, schema);
|
||||
}
|
||||
|
||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { Not, IsNull, DataSource } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { acquireChartInsertLock } from '@/misc/distributed-lock.js';
|
||||
import Chart from '../core.js';
|
||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||
import { name, schema } from './entities/users.js';
|
||||
@@ -25,14 +26,16 @@ export default class UsersChart extends Chart<typeof schema> { // eslint-disable
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private appLockService: AppLockService,
|
||||
private userEntityService: UserEntityService,
|
||||
private chartLoggerService: ChartLoggerService,
|
||||
) {
|
||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
|
||||
super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
|
||||
}
|
||||
|
||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as Redis from 'ioredis';
|
||||
|
||||
export async function acquireDistributedLock(
|
||||
redis: Redis.Redis,
|
||||
name: string,
|
||||
timeout: number,
|
||||
maxRetries: number,
|
||||
retryInterval: number,
|
||||
): Promise<() => Promise<void>> {
|
||||
const lockKey = `lock:${name}`;
|
||||
const identifier = Math.random().toString(36).slice(2);
|
||||
|
||||
let retries = 0;
|
||||
while (retries < maxRetries) {
|
||||
const result = await redis.set(lockKey, identifier, 'PX', timeout, 'NX');
|
||||
if (result === 'OK') {
|
||||
return async () => {
|
||||
const currentIdentifier = await redis.get(lockKey);
|
||||
if (currentIdentifier === identifier) {
|
||||
await redis.del(lockKey);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, retryInterval));
|
||||
retries++;
|
||||
}
|
||||
|
||||
throw new Error(`Failed to acquire lock ${name}`);
|
||||
}
|
||||
|
||||
export function acquireApObjectLock(
|
||||
redis: Redis.Redis,
|
||||
uri: string,
|
||||
): Promise<() => Promise<void>> {
|
||||
return acquireDistributedLock(redis, `ap-object:${uri}`, 30 * 1000, 50, 100);
|
||||
}
|
||||
|
||||
export function acquireChartInsertLock(
|
||||
redis: Redis.Redis,
|
||||
name: string,
|
||||
): Promise<() => Promise<void>> {
|
||||
return acquireDistributedLock(redis, `chart-insert:${name}`, 30 * 1000, 50, 500);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
const ESCAPE_LOOKUP = {
|
||||
'&': '\\u0026',
|
||||
'>': '\\u003e',
|
||||
'<': '\\u003c',
|
||||
'\u2028': '\\u2028',
|
||||
'\u2029': '\\u2029',
|
||||
} as Record<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]);
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import * as Bull from 'bullmq';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type Logger from '@/logger.js';
|
||||
@@ -157,6 +156,13 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
};
|
||||
}
|
||||
|
||||
let Sentry: typeof import('@sentry/node') | undefined;
|
||||
if (Sentry != null) {
|
||||
import('@sentry/node').then((mod) => {
|
||||
Sentry = mod;
|
||||
});
|
||||
}
|
||||
|
||||
//#region system
|
||||
{
|
||||
const processer = (job: Bull.Job) => {
|
||||
@@ -175,7 +181,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
};
|
||||
|
||||
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
|
||||
if (this.config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: System: ' + job.name }, () => processer(job));
|
||||
} else {
|
||||
return processer(job);
|
||||
@@ -192,7 +198,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err: Error) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
@@ -232,7 +238,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
};
|
||||
|
||||
this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
|
||||
if (this.config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: DB: ' + job.name }, () => processer(job));
|
||||
} else {
|
||||
return processer(job);
|
||||
@@ -249,7 +255,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
@@ -264,7 +270,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region deliver
|
||||
{
|
||||
this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => {
|
||||
if (this.config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: Deliver' }, () => this.deliverProcessorService.process(job));
|
||||
} else {
|
||||
return this.deliverProcessorService.process(job);
|
||||
@@ -289,7 +295,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||
if (config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
@@ -304,7 +310,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region inbox
|
||||
{
|
||||
this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => {
|
||||
if (this.config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: Inbox' }, () => this.inboxProcessorService.process(job));
|
||||
} else {
|
||||
return this.inboxProcessorService.process(job);
|
||||
@@ -329,7 +335,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
@@ -344,7 +350,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region user-webhook deliver
|
||||
{
|
||||
this.userWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.USER_WEBHOOK_DELIVER, (job) => {
|
||||
if (this.config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: UserWebhookDeliver' }, () => this.userWebhookDeliverProcessorService.process(job));
|
||||
} else {
|
||||
return this.userWebhookDeliverProcessorService.process(job);
|
||||
@@ -369,7 +375,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||
if (config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
@@ -384,7 +390,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region system-webhook deliver
|
||||
{
|
||||
this.systemWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.SYSTEM_WEBHOOK_DELIVER, (job) => {
|
||||
if (this.config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: SystemWebhookDeliver' }, () => this.systemWebhookDeliverProcessorService.process(job));
|
||||
} else {
|
||||
return this.systemWebhookDeliverProcessorService.process(job);
|
||||
@@ -409,7 +415,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
|
||||
if (config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
@@ -434,7 +440,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
};
|
||||
|
||||
this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
|
||||
if (this.config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: Relationship: ' + job.name }, () => processer(job));
|
||||
} else {
|
||||
return processer(job);
|
||||
@@ -456,7 +462,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
@@ -479,7 +485,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
};
|
||||
|
||||
this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
|
||||
if (this.config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: ObjectStorage: ' + job.name }, () => processer(job));
|
||||
} else {
|
||||
return processer(job);
|
||||
@@ -497,7 +503,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => {
|
||||
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
|
||||
if (config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
extra: { job, err },
|
||||
@@ -512,7 +518,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region ended poll notification
|
||||
{
|
||||
this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => {
|
||||
if (this.config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: EndedPollNotification' }, () => this.endedPollNotificationProcessorService.process(job));
|
||||
} else {
|
||||
return this.endedPollNotificationProcessorService.process(job);
|
||||
@@ -527,7 +533,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
//#region post scheduled note
|
||||
{
|
||||
this.postScheduledNoteQueueWorker = new Bull.Worker(QUEUE.POST_SCHEDULED_NOTE, async (job) => {
|
||||
if (this.config.sentryForBackend) {
|
||||
if (Sentry != null) {
|
||||
return Sentry.startSpan({ name: 'Queue: PostScheduledNote' }, () => this.postScheduledNoteProcessorService.process(job));
|
||||
} else {
|
||||
return this.postScheduledNoteProcessorService.process(job);
|
||||
|
||||
@@ -25,6 +25,7 @@ import { SignupApiService } from './api/SignupApiService.js';
|
||||
import { StreamingApiServerService } from './api/StreamingApiServerService.js';
|
||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||
import { ClientServerService } from './web/ClientServerService.js';
|
||||
import { HtmlTemplateService } from './web/HtmlTemplateService.js';
|
||||
import { FeedService } from './web/FeedService.js';
|
||||
import { UrlPreviewService } from './web/UrlPreviewService.js';
|
||||
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
||||
@@ -58,6 +59,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
|
||||
providers: [
|
||||
ClientServerService,
|
||||
ClientLoggerService,
|
||||
HtmlTemplateService,
|
||||
FeedService,
|
||||
HealthServerService,
|
||||
UrlPreviewService,
|
||||
|
||||
@@ -7,7 +7,6 @@ import { randomUUID } from 'node:crypto';
|
||||
import * as fs from 'node:fs';
|
||||
import * as stream from 'node:stream/promises';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||
@@ -37,6 +36,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
private logger: Logger;
|
||||
private userIpHistories: Map<MiUser['id'], Set<string>>;
|
||||
private userIpHistoriesClearIntervalId: NodeJS.Timeout;
|
||||
private Sentry: typeof import('@sentry/node') | null = null;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
@@ -59,6 +59,12 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
this.userIpHistoriesClearIntervalId = setInterval(() => {
|
||||
this.userIpHistories.clear();
|
||||
}, 1000 * 60 * 60);
|
||||
|
||||
if (this.config.sentryForBackend) {
|
||||
import('@sentry/node').then((Sentry) => {
|
||||
this.Sentry = Sentry;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#sendApiError(reply: FastifyReply, err: ApiError): void {
|
||||
@@ -120,8 +126,8 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
},
|
||||
});
|
||||
|
||||
if (this.config.sentryForBackend) {
|
||||
Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
|
||||
if (this.Sentry != null) {
|
||||
this.Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
|
||||
level: 'error',
|
||||
user: {
|
||||
id: userId,
|
||||
@@ -432,8 +438,8 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
// API invoking
|
||||
if (this.config.sentryForBackend) {
|
||||
return await Sentry.startSpan({
|
||||
if (this.Sentry != null) {
|
||||
return await this.Sentry.startSpan({
|
||||
name: 'API: ' + ep.name,
|
||||
}, () => ep.exec(data, user, token, file, request.ip, request.headers)
|
||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
|
||||
|
||||
@@ -12,12 +12,9 @@ import ipaddr from 'ipaddr.js';
|
||||
import oauth2orize, { type OAuth2, AuthorizationError, ValidateFunctionArity2, OAuth2Req, MiddlewareRequest } from 'oauth2orize';
|
||||
import oauth2Pkce from 'oauth2orize-pkce';
|
||||
import fastifyCors from '@fastify/cors';
|
||||
import fastifyView from '@fastify/view';
|
||||
import pug from 'pug';
|
||||
import bodyParser from 'body-parser';
|
||||
import fastifyExpress from '@fastify/express';
|
||||
import { verifyChallenge } from 'pkce-challenge';
|
||||
import { mf2 } from 'microformats-parser';
|
||||
import { permissions as kinds } from 'misskey-js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
@@ -32,6 +29,8 @@ import { MemoryKVCache } from '@/misc/cache.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import Logger from '@/logger.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { HtmlTemplateService } from '@/server/web/HtmlTemplateService.js';
|
||||
import { OAuthPage } from '@/server/web/views/oauth.js';
|
||||
import type { ServerResponse } from 'node:http';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
|
||||
@@ -98,6 +97,32 @@ interface ClientInformation {
|
||||
logo: string | null;
|
||||
}
|
||||
|
||||
function parseMicroformats(doc: htmlParser.HTMLElement, baseUrl: string, id: string): { name: string | null; logo: string | null; } {
|
||||
let name: string | null = null;
|
||||
let logo: string | null = null;
|
||||
|
||||
const hApp = doc.querySelector('.h-app');
|
||||
if (hApp == null) return { name, logo };
|
||||
|
||||
const nameEl = hApp.querySelector('.p-name');
|
||||
if (nameEl != null) {
|
||||
const href = nameEl.attributes.href || nameEl.attributes.src;
|
||||
if (href != null && new URL(href, baseUrl).toString() === new URL(id).toString()) {
|
||||
name = nameEl.textContent.trim();
|
||||
}
|
||||
}
|
||||
|
||||
const logoEl = hApp.querySelector('.u-logo');
|
||||
if (logoEl != null) {
|
||||
const href = logoEl.attributes.href || logoEl.attributes.src;
|
||||
if (href != null) {
|
||||
logo = new URL(href, baseUrl).toString();
|
||||
}
|
||||
}
|
||||
|
||||
return { name, logo };
|
||||
}
|
||||
|
||||
// https://indieauth.spec.indieweb.org/#client-information-discovery
|
||||
// "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id,
|
||||
// and if there is an [h-app] with a url property matching the client_id URL,
|
||||
@@ -120,24 +145,19 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
const fragment = htmlParser.parse(`<div>${text}</div>`);
|
||||
const doc = htmlParser.parse(`<div>${text}</div>`);
|
||||
|
||||
redirectUris.push(...[...fragment.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href));
|
||||
redirectUris.push(...[...doc.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href));
|
||||
|
||||
let name = id;
|
||||
let logo: string | null = null;
|
||||
if (text) {
|
||||
const microformats = mf2(text, { baseUrl: res.url });
|
||||
const correspondingProperties = microformats.items.find(item => item.type?.includes('h-app') && item.properties.url.includes(id));
|
||||
if (correspondingProperties) {
|
||||
const nameProperty = correspondingProperties.properties.name?.[0];
|
||||
if (typeof nameProperty === 'string') {
|
||||
name = nameProperty;
|
||||
}
|
||||
const logoProperty = correspondingProperties.properties.logo?.[0];
|
||||
if (typeof logoProperty === 'string') {
|
||||
logo = logoProperty;
|
||||
}
|
||||
const microformats = parseMicroformats(doc, res.url, id);
|
||||
if (typeof microformats.name === 'string') {
|
||||
name = microformats.name;
|
||||
}
|
||||
if (typeof microformats.logo === 'string') {
|
||||
logo = microformats.logo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,6 +273,7 @@ export class OAuth2ProviderService {
|
||||
private usersRepository: UsersRepository,
|
||||
private cacheService: CacheService,
|
||||
loggerService: LoggerService,
|
||||
private htmlTemplateService: HtmlTemplateService,
|
||||
) {
|
||||
this.#logger = loggerService.getLogger('oauth');
|
||||
|
||||
@@ -386,24 +407,16 @@ export class OAuth2ProviderService {
|
||||
this.#logger.info(`Rendering authorization page for "${oauth2.client.name}"`);
|
||||
|
||||
reply.header('Cache-Control', 'no-store');
|
||||
return await reply.view('oauth', {
|
||||
return await HtmlTemplateService.replyHtml(reply, OAuthPage({
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
transactionId: oauth2.transactionID,
|
||||
clientName: oauth2.client.name,
|
||||
clientLogo: oauth2.client.logo,
|
||||
scope: oauth2.req.scope.join(' '),
|
||||
});
|
||||
clientLogo: oauth2.client.logo ?? undefined,
|
||||
scope: oauth2.req.scope,
|
||||
}));
|
||||
});
|
||||
fastify.post('/decision', async () => { });
|
||||
|
||||
fastify.register(fastifyView, {
|
||||
root: fileURLToPath(new URL('../web/views', import.meta.url)),
|
||||
engine: { pug },
|
||||
defaultContext: {
|
||||
version: this.config.version,
|
||||
config: this.config,
|
||||
},
|
||||
});
|
||||
|
||||
await fastify.register(fastifyExpress);
|
||||
fastify.use('/authorize', this.#server.authorize(((areq, done) => {
|
||||
(async (): Promise<Parameters<typeof done>> => {
|
||||
|
||||
@@ -9,20 +9,16 @@ import { fileURLToPath } from 'node:url';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import sharp from 'sharp';
|
||||
import pug from 'pug';
|
||||
import { In, IsNull } from 'typeorm';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import fastifyView from '@fastify/view';
|
||||
import fastifyProxy from '@fastify/http-proxy';
|
||||
import vary from 'vary';
|
||||
import 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';
|
||||
@@ -41,14 +37,33 @@ import type {
|
||||
} from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
|
||||
import { htmlSafeJsonStringify } from '@/misc/json-stringify-html-safe.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
|
||||
import { FeedService } from './FeedService.js';
|
||||
import { UrlPreviewService } from './UrlPreviewService.js';
|
||||
import { ClientLoggerService } from './ClientLoggerService.js';
|
||||
import { HtmlTemplateService } from './HtmlTemplateService.js';
|
||||
|
||||
import { BasePage } from './views/base.js';
|
||||
import { UserPage } from './views/user.js';
|
||||
import { NotePage } from './views/note.js';
|
||||
import { PagePage } from './views/page.js';
|
||||
import { ClipPage } from './views/clip.js';
|
||||
import { FlashPage } from './views/flash.js';
|
||||
import { GalleryPostPage } from './views/gallery-post.js';
|
||||
import { ChannelPage } from './views/channel.js';
|
||||
import { ReversiGamePage } from './views/reversi-game.js';
|
||||
import { AnnouncementPage } from './views/announcement.js';
|
||||
import { BaseEmbed } from './views/base-embed.js';
|
||||
import { InfoCardPage } from './views/info-card.js';
|
||||
import { BiosPage } from './views/bios.js';
|
||||
import { CliPage } from './views/cli.js';
|
||||
import { FlushPage } from './views/flush.js';
|
||||
import { ErrorPage } from './views/error.js';
|
||||
|
||||
import type { FastifyError, FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
@@ -62,20 +77,6 @@ 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;
|
||||
@@ -121,7 +122,6 @@ export class ClientServerService {
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private pageEntityService: PageEntityService,
|
||||
private metaEntityService: MetaEntityService,
|
||||
private galleryPostEntityService: GalleryPostEntityService,
|
||||
private clipEntityService: ClipEntityService,
|
||||
private channelEntityService: ChannelEntityService,
|
||||
@@ -129,7 +129,7 @@ export class ClientServerService {
|
||||
private announcementEntityService: AnnouncementEntityService,
|
||||
private urlPreviewService: UrlPreviewService,
|
||||
private feedService: FeedService,
|
||||
private roleService: RoleService,
|
||||
private htmlTemplateService: HtmlTemplateService,
|
||||
private clientLoggerService: ClientLoggerService,
|
||||
) {
|
||||
//this.createServer = this.createServer.bind(this);
|
||||
@@ -195,38 +195,10 @@ export class ClientServerService {
|
||||
return (manifest);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async generateCommonPugData(meta: MiMeta) {
|
||||
return {
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
icon: meta.iconUrl,
|
||||
appleTouchIcon: meta.app512IconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg',
|
||||
infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',
|
||||
notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg',
|
||||
instanceUrl: this.config.url,
|
||||
metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)),
|
||||
now: Date.now(),
|
||||
federationEnabled: this.meta.federation !== 'none',
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
const configUrl = new URL(this.config.url);
|
||||
|
||||
fastify.register(fastifyView, {
|
||||
root: _dirname + '/views',
|
||||
engine: {
|
||||
pug: pug,
|
||||
},
|
||||
defaultContext: {
|
||||
version: this.config.version,
|
||||
config: this.config,
|
||||
},
|
||||
});
|
||||
|
||||
fastify.addHook('onRequest', (request, reply, done) => {
|
||||
// クリックジャッキング防止のためiFrameの中に入れられないようにする
|
||||
reply.header('X-Frame-Options', 'DENY');
|
||||
@@ -427,16 +399,15 @@ export class ClientServerService {
|
||||
|
||||
//#endregion
|
||||
|
||||
const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => {
|
||||
const renderBase = async (reply: FastifyReply, data: Partial<Parameters<typeof BasePage>[0]> = {}) => {
|
||||
reply.header('Cache-Control', 'public, max-age=30');
|
||||
return await reply.view('base', {
|
||||
img: this.meta.bannerUrl,
|
||||
url: this.config.url,
|
||||
return await HtmlTemplateService.replyHtml(reply, BasePage({
|
||||
img: this.meta.bannerUrl ?? undefined,
|
||||
title: this.meta.name ?? 'Misskey',
|
||||
desc: this.meta.description,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
desc: this.meta.description ?? undefined,
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
...data,
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
// URL preview endpoint
|
||||
@@ -518,11 +489,6 @@ export class ClientServerService {
|
||||
)
|
||||
) {
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
const me = profile.fields
|
||||
? profile.fields
|
||||
.filter(filed => filed.value != null && filed.value.match(/^https?:/))
|
||||
.map(field => field.value)
|
||||
: [];
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=15');
|
||||
if (profile.preventAiLearning) {
|
||||
@@ -535,15 +501,15 @@ export class ClientServerService {
|
||||
userProfile: profile,
|
||||
});
|
||||
|
||||
return await reply.view('user', {
|
||||
user, profile, me,
|
||||
avatarUrl: _user.avatarUrl,
|
||||
return await HtmlTemplateService.replyHtml(reply, UserPage({
|
||||
user: _user,
|
||||
profile,
|
||||
sub: request.params.sub,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
clientCtx: htmlSafeJsonStringify({
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
clientCtxJson: htmlSafeJsonStringify({
|
||||
user: _user,
|
||||
}),
|
||||
});
|
||||
}));
|
||||
} else {
|
||||
// リモートユーザーなので
|
||||
// モデレータがAPI経由で参照可能にするために404にはしない
|
||||
@@ -594,17 +560,14 @@ export class ClientServerService {
|
||||
reply.header('X-Robots-Tag', 'noimageai');
|
||||
reply.header('X-Robots-Tag', 'noai');
|
||||
}
|
||||
return await reply.view('note', {
|
||||
return await HtmlTemplateService.replyHtml(reply, NotePage({
|
||||
note: _note,
|
||||
profile,
|
||||
avatarUrl: _note.user.avatarUrl,
|
||||
// TODO: Let locale changeable by instance setting
|
||||
summary: getNoteSummary(_note),
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
clientCtx: htmlSafeJsonStringify({
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
clientCtxJson: htmlSafeJsonStringify({
|
||||
note: _note,
|
||||
}),
|
||||
});
|
||||
}));
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
@@ -637,12 +600,11 @@ export class ClientServerService {
|
||||
reply.header('X-Robots-Tag', 'noimageai');
|
||||
reply.header('X-Robots-Tag', 'noai');
|
||||
}
|
||||
return await reply.view('page', {
|
||||
return await HtmlTemplateService.replyHtml(reply, PagePage({
|
||||
page: _page,
|
||||
profile,
|
||||
avatarUrl: _page.user.avatarUrl,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
});
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
}));
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
@@ -662,12 +624,11 @@ export class ClientServerService {
|
||||
reply.header('X-Robots-Tag', 'noimageai');
|
||||
reply.header('X-Robots-Tag', 'noai');
|
||||
}
|
||||
return await reply.view('flash', {
|
||||
return await HtmlTemplateService.replyHtml(reply, FlashPage({
|
||||
flash: _flash,
|
||||
profile,
|
||||
avatarUrl: _flash.user.avatarUrl,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
});
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
}));
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
@@ -687,15 +648,14 @@ export class ClientServerService {
|
||||
reply.header('X-Robots-Tag', 'noimageai');
|
||||
reply.header('X-Robots-Tag', 'noai');
|
||||
}
|
||||
return await reply.view('clip', {
|
||||
return await HtmlTemplateService.replyHtml(reply, ClipPage({
|
||||
clip: _clip,
|
||||
profile,
|
||||
avatarUrl: _clip.user.avatarUrl,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
clientCtx: htmlSafeJsonStringify({
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
clientCtxJson: htmlSafeJsonStringify({
|
||||
clip: _clip,
|
||||
}),
|
||||
});
|
||||
}));
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
@@ -713,12 +673,11 @@ export class ClientServerService {
|
||||
reply.header('X-Robots-Tag', 'noimageai');
|
||||
reply.header('X-Robots-Tag', 'noai');
|
||||
}
|
||||
return await reply.view('gallery-post', {
|
||||
post: _post,
|
||||
return await HtmlTemplateService.replyHtml(reply, GalleryPostPage({
|
||||
galleryPost: _post,
|
||||
profile,
|
||||
avatarUrl: _post.user.avatarUrl,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
});
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
}));
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
@@ -733,10 +692,10 @@ export class ClientServerService {
|
||||
if (channel) {
|
||||
const _channel = await this.channelEntityService.pack(channel);
|
||||
reply.header('Cache-Control', 'public, max-age=15');
|
||||
return await reply.view('channel', {
|
||||
return await HtmlTemplateService.replyHtml(reply, ChannelPage({
|
||||
channel: _channel,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
});
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
}));
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
@@ -751,10 +710,10 @@ export class ClientServerService {
|
||||
if (game) {
|
||||
const _game = await this.reversiGameEntityService.packDetail(game);
|
||||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return await reply.view('reversi-game', {
|
||||
game: _game,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
});
|
||||
return await HtmlTemplateService.replyHtml(reply, ReversiGamePage({
|
||||
reversiGame: _game,
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
}));
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
@@ -770,10 +729,10 @@ export class ClientServerService {
|
||||
if (announcement) {
|
||||
const _announcement = await this.announcementEntityService.pack(announcement);
|
||||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return await reply.view('announcement', {
|
||||
return await HtmlTemplateService.replyHtml(reply, AnnouncementPage({
|
||||
announcement: _announcement,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
});
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
}));
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
@@ -806,13 +765,13 @@ export class ClientServerService {
|
||||
const _user = await this.userEntityService.pack(user);
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return await reply.view('base-embed', {
|
||||
return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
|
||||
title: this.meta.name ?? 'Misskey',
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
embedCtx: htmlSafeJsonStringify({
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
embedCtxJson: htmlSafeJsonStringify({
|
||||
user: _user,
|
||||
}),
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => {
|
||||
@@ -832,13 +791,13 @@ export class ClientServerService {
|
||||
const _note = await this.noteEntityService.pack(note, null, { detail: true });
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return await reply.view('base-embed', {
|
||||
return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
|
||||
title: this.meta.name ?? 'Misskey',
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
embedCtx: htmlSafeJsonStringify({
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
embedCtxJson: htmlSafeJsonStringify({
|
||||
note: _note,
|
||||
}),
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { clip: string; } }>('/embed/clips/:clip', async (request, reply) => {
|
||||
@@ -853,48 +812,46 @@ export class ClientServerService {
|
||||
const _clip = await this.clipEntityService.pack(clip);
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return await reply.view('base-embed', {
|
||||
return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
|
||||
title: this.meta.name ?? 'Misskey',
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
embedCtx: htmlSafeJsonStringify({
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
embedCtxJson: htmlSafeJsonStringify({
|
||||
clip: _clip,
|
||||
}),
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
fastify.get('/embed/*', async (request, reply) => {
|
||||
reply.removeHeader('X-Frame-Options');
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return await reply.view('base-embed', {
|
||||
return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
|
||||
title: this.meta.name ?? 'Misskey',
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
});
|
||||
...await this.htmlTemplateService.getCommonData(),
|
||||
}));
|
||||
});
|
||||
|
||||
fastify.get('/_info_card_', async (request, reply) => {
|
||||
reply.removeHeader('X-Frame-Options');
|
||||
|
||||
return await reply.view('info-card', {
|
||||
return await HtmlTemplateService.replyHtml(reply, InfoCardPage({
|
||||
version: this.config.version,
|
||||
host: this.config.host,
|
||||
config: this.config,
|
||||
meta: this.meta,
|
||||
originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }),
|
||||
originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }),
|
||||
});
|
||||
}));
|
||||
});
|
||||
//#endregion
|
||||
|
||||
fastify.get('/bios', async (request, reply) => {
|
||||
return await reply.view('bios', {
|
||||
return await HtmlTemplateService.replyHtml(reply, BiosPage({
|
||||
version: this.config.version,
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
fastify.get('/cli', async (request, reply) => {
|
||||
return await reply.view('cli', {
|
||||
return await HtmlTemplateService.replyHtml(reply, CliPage({
|
||||
version: this.config.version,
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
const override = (source: string, target: string, depth = 0) =>
|
||||
@@ -917,7 +874,7 @@ export class ClientServerService {
|
||||
reply.header('Clear-Site-Data', '"*"');
|
||||
}
|
||||
reply.header('Set-Cookie', 'http-flush-failed=1; Path=/flush; Max-Age=60');
|
||||
return await reply.view('flush');
|
||||
return await HtmlTemplateService.replyHtml(reply, FlushPage());
|
||||
});
|
||||
|
||||
// streamingに非WebSocketリクエストが来た場合にbase htmlをキャシュ付きで返すと、Proxy等でそのパスがキャッシュされておかしくなる
|
||||
@@ -943,10 +900,10 @@ export class ClientServerService {
|
||||
});
|
||||
reply.code(500);
|
||||
reply.header('Cache-Control', 'max-age=10, must-revalidate');
|
||||
return await reply.view('error', {
|
||||
return await HtmlTemplateService.replyHtml(reply, ErrorPage({
|
||||
code: error.code,
|
||||
id: errId,
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
done();
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { promises as fsp } from 'node:fs';
|
||||
import { languages } from 'i18n/const';
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { htmlSafeJsonStringify } from '@/misc/json-stringify-html-safe.js';
|
||||
import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
|
||||
import type { FastifyReply } from 'fastify';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { MiMeta } from '@/models/Meta.js';
|
||||
import type { CommonData } from './views/_.js';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
||||
const frontendVitePublic = `${_dirname}/../../../../frontend/public/`;
|
||||
const frontendEmbedVitePublic = `${_dirname}/../../../../frontend-embed/public/`;
|
||||
|
||||
@Injectable()
|
||||
export class HtmlTemplateService {
|
||||
private frontendBootloadersFetched = false;
|
||||
public frontendBootloaderJs: string | null = null;
|
||||
public frontendBootloaderCss: string | null = null;
|
||||
public frontendEmbedBootloaderJs: string | null = null;
|
||||
public frontendEmbedBootloaderCss: string | null = null;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
private metaEntityService: MetaEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async prepareFrontendBootloaders() {
|
||||
if (this.frontendBootloadersFetched) return;
|
||||
this.frontendBootloadersFetched = true;
|
||||
|
||||
const [bootJs, bootCss, embedBootJs, embedBootCss] = await Promise.all([
|
||||
fsp.readFile(`${frontendVitePublic}loader/boot.js`, 'utf-8').catch(() => null),
|
||||
fsp.readFile(`${frontendVitePublic}loader/style.css`, 'utf-8').catch(() => null),
|
||||
fsp.readFile(`${frontendEmbedVitePublic}loader/boot.js`, 'utf-8').catch(() => null),
|
||||
fsp.readFile(`${frontendEmbedVitePublic}loader/style.css`, 'utf-8').catch(() => null),
|
||||
]);
|
||||
|
||||
if (bootJs != null) {
|
||||
this.frontendBootloaderJs = bootJs;
|
||||
}
|
||||
|
||||
if (bootCss != null) {
|
||||
this.frontendBootloaderCss = bootCss;
|
||||
}
|
||||
|
||||
if (embedBootJs != null) {
|
||||
this.frontendEmbedBootloaderJs = embedBootJs;
|
||||
}
|
||||
|
||||
if (embedBootCss != null) {
|
||||
this.frontendEmbedBootloaderCss = embedBootCss;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getCommonData(): Promise<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,8 +4,7 @@
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { summaly } from '@misskey-dev/summaly';
|
||||
import { SummalyResult } from '@misskey-dev/summaly/built/summary.js';
|
||||
import type { SummalyResult } from '@misskey-dev/summaly/built/summary.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
@@ -113,7 +112,7 @@ export class UrlPreviewService {
|
||||
}
|
||||
}
|
||||
|
||||
private fetchSummary(url: string, meta: MiMeta, lang?: string): Promise<SummalyResult> {
|
||||
private async fetchSummary(url: string, meta: MiMeta, lang?: string): Promise<SummalyResult> {
|
||||
const agent = this.config.proxy
|
||||
? {
|
||||
http: this.httpRequestService.httpAgent,
|
||||
@@ -121,6 +120,8 @@ export class UrlPreviewService {
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const { summaly } = await import('@misskey-dev/summaly');
|
||||
|
||||
return summaly(url, {
|
||||
followRedirects: this.meta.urlPreviewAllowRedirect,
|
||||
lang: lang ?? 'ja-JP',
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Config } from '@/config.js';
|
||||
|
||||
export const comment = `<!--
|
||||
_____ _ _
|
||||
| |_|___ ___| |_ ___ _ _
|
||||
| | | | |_ -|_ -| '_| -_| | |
|
||||
|_|_|_|_|___|___|_,_|___|_ |
|
||||
|___|
|
||||
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;
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export function Splash(props: {
|
||||
icon?: string | null;
|
||||
}) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
extends ./base
|
||||
|
||||
block vars
|
||||
- const title = announcement.title;
|
||||
- const description = announcement.text.length > 100 ? announcement.text.slice(0, 100) + '…' : announcement.text;
|
||||
- const url = `${config.url}/announcements/${announcement.id}`;
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
||||
block desc
|
||||
meta(name='description' content=description)
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content= description)
|
||||
meta(property='og:url' content= url)
|
||||
if announcement.imageUrl
|
||||
meta(property='og:image' content=announcement.imageUrl)
|
||||
meta(property='twitter:card' content='summary_large_image')
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { CommonProps } from '@/server/web/views/_.js';
|
||||
import { Layout } from '@/server/web/views/base.js';
|
||||
|
||||
export function AnnouncementPage(props: CommonProps<{
|
||||
announcement: Packed<'Announcement'>;
|
||||
}>) {
|
||||
const description = props.announcement.text.length > 100 ? props.announcement.text.slice(0, 100) + '…' : props.announcement.text;
|
||||
|
||||
function ogBlock() {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
block vars
|
||||
|
||||
block loadClientEntry
|
||||
- const entry = config.frontendEmbedEntry;
|
||||
|
||||
doctype html
|
||||
|
||||
html(class='embed')
|
||||
|
||||
head
|
||||
meta(charset='utf-8')
|
||||
meta(name='application-name' content='Misskey')
|
||||
meta(name='referrer' content='origin')
|
||||
meta(name='theme-color' content= themeColor || '#86b300')
|
||||
meta(name='theme-color-orig' content= themeColor || '#86b300')
|
||||
meta(property='og:site_name' content= instanceName || 'Misskey')
|
||||
meta(property='instance_url' content= instanceUrl)
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||
meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no')
|
||||
link(rel='icon' href= icon || '/favicon.ico')
|
||||
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
|
||||
|
||||
if !config.frontendEmbedManifestExists
|
||||
script(type="module" src="/embed_vite/@vite/client")
|
||||
|
||||
if Array.isArray(entry.css)
|
||||
each href in entry.css
|
||||
link(rel='stylesheet' href=`/embed_vite/${href}`)
|
||||
|
||||
title
|
||||
block title
|
||||
= title || 'Misskey'
|
||||
|
||||
block meta
|
||||
meta(name='robots' content='noindex')
|
||||
|
||||
style
|
||||
include ../style.embed.css
|
||||
|
||||
script.
|
||||
var VERSION = "#{version}";
|
||||
var CLIENT_ENTRY = !{JSON.stringify(entry.file)};
|
||||
|
||||
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
||||
!= metaJson
|
||||
|
||||
script(type='application/json' id='misskey_embedCtx' data-generated-at=now)
|
||||
!= embedCtx
|
||||
|
||||
script
|
||||
include ../boot.embed.js
|
||||
|
||||
body
|
||||
noscript: p
|
||||
| JavaScriptを有効にしてください
|
||||
br
|
||||
| Please turn on your JavaScript
|
||||
div#splash
|
||||
img#splashIcon(src= icon || '/static-assets/splash.png')
|
||||
div#splashSpinner
|
||||
<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
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { comment } from '@/server/web/views/_.js';
|
||||
import type { CommonProps } from '@/server/web/views/_.js';
|
||||
import { Splash } from '@/server/web/views/_splash.js';
|
||||
import type { PropsWithChildren, Children } from '@kitajs/html';
|
||||
|
||||
export function BaseEmbed(props: PropsWithChildren<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
block vars
|
||||
|
||||
block loadClientEntry
|
||||
- const entry = config.frontendEntry;
|
||||
- const baseUrl = config.url;
|
||||
|
||||
doctype html
|
||||
|
||||
//
|
||||
-
|
||||
_____ _ _
|
||||
| |_|___ ___| |_ ___ _ _
|
||||
| | | | |_ -|_ -| '_| -_| | |
|
||||
|_|_|_|_|___|___|_,_|___|_ |
|
||||
|___|
|
||||
Thank you for using Misskey!
|
||||
If you are reading this message... how about joining the development?
|
||||
https://github.com/misskey-dev/misskey
|
||||
|
||||
|
||||
html
|
||||
|
||||
head
|
||||
meta(charset='utf-8')
|
||||
meta(name='application-name' content='Misskey')
|
||||
meta(name='referrer' content='origin')
|
||||
meta(name='theme-color' content= themeColor || '#86b300')
|
||||
meta(name='theme-color-orig' content= themeColor || '#86b300')
|
||||
meta(property='og:site_name' content= instanceName || 'Misskey')
|
||||
meta(property='instance_url' content= instanceUrl)
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover')
|
||||
meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no')
|
||||
link(rel='icon' href= icon || '/favicon.ico')
|
||||
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
|
||||
link(rel='manifest' href='/manifest.json')
|
||||
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${baseUrl}/opensearch.xml`)
|
||||
link(rel='prefetch' href=serverErrorImageUrl)
|
||||
link(rel='prefetch' href=infoImageUrl)
|
||||
link(rel='prefetch' href=notFoundImageUrl)
|
||||
|
||||
if !config.frontendManifestExists
|
||||
script(type="module" src="/vite/@vite/client")
|
||||
|
||||
if Array.isArray(entry.css)
|
||||
each href in entry.css
|
||||
link(rel='stylesheet' href=`/vite/${href}`)
|
||||
|
||||
title
|
||||
block title
|
||||
= title || 'Misskey'
|
||||
|
||||
if noindex
|
||||
meta(name='robots' content='noindex')
|
||||
|
||||
block desc
|
||||
meta(name='description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
|
||||
|
||||
block meta
|
||||
|
||||
block og
|
||||
meta(property='og:title' content= title || 'Misskey')
|
||||
meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
|
||||
meta(property='og:image' content= img)
|
||||
meta(property='twitter:card' content='summary')
|
||||
|
||||
style
|
||||
include ../style.css
|
||||
|
||||
script.
|
||||
var VERSION = "#{version}";
|
||||
var CLIENT_ENTRY = !{JSON.stringify(entry.file)};
|
||||
|
||||
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
||||
!= metaJson
|
||||
|
||||
script(type='application/json' id='misskey_clientCtx' data-generated-at=now)
|
||||
!= clientCtx
|
||||
|
||||
script
|
||||
include ../boot.js
|
||||
|
||||
body
|
||||
noscript: p
|
||||
| JavaScriptを有効にしてください
|
||||
br
|
||||
| Please turn on your JavaScript
|
||||
div#splash
|
||||
img#splashIcon(src= icon || '/static-assets/splash.png')
|
||||
div#splashSpinner
|
||||
<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
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { comment, defaultDescription } from '@/server/web/views/_.js';
|
||||
import { Splash } from '@/server/web/views/_splash.js';
|
||||
import type { CommonProps } from '@/server/web/views/_.js';
|
||||
import type { PropsWithChildren, Children } from '@kitajs/html';
|
||||
|
||||
export function Layout(props: PropsWithChildren<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 };
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
doctype html
|
||||
|
||||
html
|
||||
|
||||
head
|
||||
meta(charset='utf-8')
|
||||
meta(name='application-name' content='Misskey')
|
||||
title Misskey Repair Tool
|
||||
style
|
||||
include ../bios.css
|
||||
script
|
||||
include ../bios.js
|
||||
|
||||
body
|
||||
header
|
||||
h1 Misskey Repair Tool #{version}
|
||||
main
|
||||
div.tabs
|
||||
button#ls edit local storage
|
||||
div#content
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
extends ./base
|
||||
|
||||
block vars
|
||||
- const title = channel.name;
|
||||
- const url = `${config.url}/channels/${channel.id}`;
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
||||
block desc
|
||||
meta(name='description' content= channel.description)
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content= channel.description)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= channel.bannerUrl)
|
||||
meta(property='twitter:card' content='summary')
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { CommonProps } from '@/server/web/views/_.js';
|
||||
import { Layout } from '@/server/web/views/base.js';
|
||||
|
||||
export function ChannelPage(props: CommonProps<{
|
||||
channel: Packed<'Channel'>;
|
||||
}>) {
|
||||
|
||||
function ogBlock() {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
doctype html
|
||||
|
||||
html
|
||||
|
||||
head
|
||||
meta(charset='utf-8')
|
||||
meta(name='application-name' content='Misskey')
|
||||
title Misskey Cli
|
||||
style
|
||||
include ../cli.css
|
||||
script
|
||||
include ../cli.js
|
||||
|
||||
body
|
||||
header
|
||||
h1 Misskey Cli #{version}
|
||||
main
|
||||
div#form
|
||||
textarea#text
|
||||
button#submit submit
|
||||
div#tl
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
extends ./base
|
||||
|
||||
block vars
|
||||
- const user = clip.user;
|
||||
- const title = clip.name;
|
||||
- const url = `${config.url}/clips/${clip.id}`;
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
||||
block desc
|
||||
meta(name='description' content= clip.description)
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content= clip.description)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
meta(property='twitter:card' content='summary')
|
||||
|
||||
block meta
|
||||
if profile.noCrawle
|
||||
meta(name='robots' content='noindex')
|
||||
if profile.preventAiLearning
|
||||
meta(name='robots' content='noimageai')
|
||||
meta(name='robots' content='noai')
|
||||
|
||||
meta(name='misskey:user-username' content=user.username)
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
meta(name='misskey:clip-id' content=clip.id)
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import type { CommonProps } from '@/server/web/views/_.js';
|
||||
import { Layout } from '@/server/web/views/base.js';
|
||||
|
||||
export function ClipPage(props: CommonProps<{
|
||||
clip: Packed<'Clip'>;
|
||||
profile: MiUserProfile;
|
||||
}>) {
|
||||
function ogBlock() {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
doctype html
|
||||
|
||||
//
|
||||
-
|
||||
_____ _ _
|
||||
| |_|___ ___| |_ ___ _ _
|
||||
| | | | |_ -|_ -| '_| -_| | |
|
||||
|_|_|_|_|___|___|_,_|___|_ |
|
||||
|___|
|
||||
Thank you for using Misskey!
|
||||
If you are reading this message... how about joining the development?
|
||||
https://github.com/misskey-dev/misskey
|
||||
|
||||
|
||||
html
|
||||
|
||||
head
|
||||
meta(charset='utf-8')
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||
meta(name='application-name' content='Misskey')
|
||||
meta(name='referrer' content='origin')
|
||||
|
||||
title
|
||||
block title
|
||||
= 'An error has occurred... | Misskey'
|
||||
|
||||
style
|
||||
include ../error.css
|
||||
|
||||
script
|
||||
include ../error.js
|
||||
|
||||
body
|
||||
svg.icon-warning(xmlns="http://www.w3.org/2000/svg", viewBox="0 0 24 24", stroke-width="2", stroke="currentColor", fill="none", stroke-linecap="round", stroke-linejoin="round")
|
||||
path(stroke="none", d="M0 0h24v24H0z", fill="none")
|
||||
path(d="M12 9v2m0 4v.01")
|
||||
path(d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75")
|
||||
|
||||
h1(data-i18n="title") Failed to initialize Misskey
|
||||
|
||||
button.button-big(onclick="location.reload();")
|
||||
span.button-label-big(data-i18n-reload) Reload
|
||||
|
||||
p(data-i18n="serverError") If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID.
|
||||
|
||||
div#errors
|
||||
code.
|
||||
ERROR CODE: #{code}
|
||||
ERROR ID: #{id}
|
||||
|
||||
p
|
||||
b(data-i18n="solution") The following actions may solve the problem.
|
||||
|
||||
p(data-i18n="solution1") Update your os and browser
|
||||
p(data-i18n="solution2") Disable an adblocker
|
||||
p(data-i18n="solution3") Clear your browser cache
|
||||
p(data-i18n="solution4") (Tor Browser) Set dom.webaudio.enabled to true
|
||||
|
||||
details(style="color: #86b300;")
|
||||
summary(data-i18n="otherOption") Other options
|
||||
a(href="/flush")
|
||||
button.button-small
|
||||
span.button-label-small(data-i18n="otherOption1") Clear preferences and cache
|
||||
br
|
||||
a(href="/cli")
|
||||
button.button-small
|
||||
span.button-label-small(data-i18n="otherOption2") Start the simple client
|
||||
br
|
||||
a(href="/bios")
|
||||
button.button-small
|
||||
span.button-label-small(data-i18n="otherOption3") Start the repair tool
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { comment } from '@/server/web/views/_.js';
|
||||
import type { CommonPropsMinimum } from '@/server/web/views/_.js';
|
||||
|
||||
export function ErrorPage(props: {
|
||||
title?: string;
|
||||
code: string;
|
||||
id: string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{'<!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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
extends ./base
|
||||
|
||||
block vars
|
||||
- const user = flash.user;
|
||||
- const title = flash.title;
|
||||
- const url = `${config.url}/play/${flash.id}`;
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
||||
block desc
|
||||
meta(name='description' content= flash.summary)
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content= flash.summary)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
meta(property='twitter:card' content='summary')
|
||||
|
||||
block meta
|
||||
if profile.noCrawle
|
||||
meta(name='robots' content='noindex')
|
||||
if profile.preventAiLearning
|
||||
meta(name='robots' content='noimageai')
|
||||
meta(name='robots' content='noai')
|
||||
|
||||
meta(name='misskey:user-username' content=user.username)
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
meta(name='misskey:flash-id' content=flash.id)
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import type { CommonProps } from '@/server/web/views/_.js';
|
||||
import { Layout } from '@/server/web/views/base.js';
|
||||
|
||||
export function FlashPage(props: CommonProps<{
|
||||
flash: Packed<'Flash'>;
|
||||
profile: MiUserProfile;
|
||||
}>) {
|
||||
function ogBlock() {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
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>`)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
extends ./base
|
||||
|
||||
block vars
|
||||
- const user = post.user;
|
||||
- const title = post.title;
|
||||
- const url = `${config.url}/gallery/${post.id}`;
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
||||
block desc
|
||||
meta(name='description' content= post.description)
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content= post.description)
|
||||
meta(property='og:url' content= url)
|
||||
if post.isSensitive
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
meta(property='twitter:card' content='summary')
|
||||
else
|
||||
meta(property='og:image' content= post.files[0].thumbnailUrl)
|
||||
meta(property='twitter:card' content='summary_large_image')
|
||||
|
||||
block meta
|
||||
if user.host || profile.noCrawle
|
||||
meta(name='robots' content='noindex')
|
||||
if profile.preventAiLearning
|
||||
meta(name='robots' content='noimageai')
|
||||
meta(name='robots' content='noai')
|
||||
|
||||
meta(name='misskey:user-username' content=user.username)
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
|
||||
if !user.host
|
||||
link(rel='alternate' href=url type='application/activity+json')
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import type { CommonProps } from '@/server/web/views/_.js';
|
||||
import { Layout } from '@/server/web/views/base.js';
|
||||
|
||||
export function GalleryPostPage(props: CommonProps<{
|
||||
galleryPost: Packed<'GalleryPost'>;
|
||||
profile: MiUserProfile;
|
||||
}>) {
|
||||
function ogBlock() {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
doctype html
|
||||
|
||||
html
|
||||
|
||||
head
|
||||
meta(charset='utf-8')
|
||||
meta(name='application-name' content='Misskey')
|
||||
title= meta.name || host
|
||||
style.
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#banner {
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
#title {
|
||||
display: inline-block;
|
||||
margin: 24px;
|
||||
padding: 0.5em 0.8em;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
font-weight: bold;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
#content {
|
||||
overflow: auto;
|
||||
color: #353c3e;
|
||||
}
|
||||
|
||||
#description {
|
||||
margin: 24px;
|
||||
}
|
||||
|
||||
body
|
||||
a#a(href=`https://${host}` target="_blank")
|
||||
header#banner(style=`background-image: url(${meta.bannerUrl})`)
|
||||
div#title= meta.name || host
|
||||
div#content
|
||||
div#description!= meta.description
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { comment, CommonPropsMinimum } from '@/server/web/views/_.js';
|
||||
import type { MiMeta } from '@/models/Meta.js';
|
||||
|
||||
export function InfoCardPage(props: CommonPropsMinimum<{
|
||||
meta: MiMeta;
|
||||
}>) {
|
||||
// 変数名をsafeで始めることでエラーをスキップ
|
||||
const safeDescription = props.meta.description;
|
||||
|
||||
return (
|
||||
<>
|
||||
{'<!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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
extends ./base
|
||||
|
||||
block vars
|
||||
- const user = note.user;
|
||||
- const title = user.name ? `${user.name} (@${user.username}${user.host ? `@${user.host}` : ''})` : `@${user.username}${user.host ? `@${user.host}` : ''}`;
|
||||
- const url = `${config.url}/notes/${note.id}`;
|
||||
- const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
|
||||
- const images = (note.files || []).filter(file => file.type.startsWith('image/') && !file.isSensitive)
|
||||
- const videos = (note.files || []).filter(file => file.type.startsWith('video/') && !file.isSensitive)
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
||||
block desc
|
||||
meta(name='description' content= summary)
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content= summary)
|
||||
meta(property='og:url' content= url)
|
||||
if videos.length
|
||||
each video in videos
|
||||
meta(property='og:video:url' content= video.url)
|
||||
meta(property='og:video:secure_url' content= video.url)
|
||||
meta(property='og:video:type' content= video.type)
|
||||
// FIXME: add width and height
|
||||
// FIXME: add embed player for Twitter
|
||||
if images.length
|
||||
meta(property='twitter:card' content='summary_large_image')
|
||||
each image in images
|
||||
meta(property='og:image' content= image.url)
|
||||
else
|
||||
meta(property='twitter:card' content='summary')
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
|
||||
|
||||
block meta
|
||||
if user.host || isRenote || profile.noCrawle
|
||||
meta(name='robots' content='noindex')
|
||||
if profile.preventAiLearning
|
||||
meta(name='robots' content='noimageai')
|
||||
meta(name='robots' content='noai')
|
||||
|
||||
meta(name='misskey:user-username' content=user.username)
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
meta(name='misskey:note-id' content=note.id)
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
|
||||
if note.prev
|
||||
link(rel='prev' href=`${config.url}/notes/${note.prev}`)
|
||||
if note.next
|
||||
link(rel='next' href=`${config.url}/notes/${note.next}`)
|
||||
|
||||
if federationEnabled
|
||||
if !user.host
|
||||
link(rel='alternate' href=url type='application/activity+json')
|
||||
if note.uri
|
||||
link(rel='alternate' href=note.uri type='application/activity+json')
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import type { CommonProps } from '@/server/web/views/_.js';
|
||||
import { Layout } from '@/server/web/views/base.js';
|
||||
import { isRenotePacked } from '@/misc/is-renote.js';
|
||||
import { getNoteSummary } from '@/misc/get-note-summary.js';
|
||||
|
||||
export function NotePage(props: CommonProps<{
|
||||
note: Packed<'Note'>;
|
||||
profile: MiUserProfile;
|
||||
}>) {
|
||||
const title = props.note.user.name ? `${props.note.user.name} (@${props.note.user.username}${props.note.user.host ? `@${props.note.user.host}` : ''})` : `@${props.note.user.username}${props.note.user.host ? `@${props.note.user.host}` : ''}`
|
||||
const isRenote = isRenotePacked(props.note);
|
||||
const images = (props.note.files ?? []).filter(f => f.type.startsWith('image/'));
|
||||
const videos = (props.note.files ?? []).filter(f => f.type.startsWith('video/'));
|
||||
const summary = getNoteSummary(props.note);
|
||||
|
||||
function ogBlock() {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
extends ./base
|
||||
|
||||
block meta
|
||||
//- Should be removed by the page when it loads, so that it won't needlessly
|
||||
//- stay when user navigates away via the navigation bar
|
||||
//- XXX: Remove navigation bar in auth page?
|
||||
meta(name='misskey:oauth:transaction-id' content=transactionId)
|
||||
meta(name='misskey:oauth:client-name' content=clientName)
|
||||
if clientLogo
|
||||
meta(name='misskey:oauth:client-logo' content=clientLogo)
|
||||
meta(name='misskey:oauth:scope' content=scope)
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { CommonProps } from '@/server/web/views/_.js';
|
||||
import { Layout } from '@/server/web/views/base.js';
|
||||
|
||||
export function OAuthPage(props: CommonProps<{
|
||||
transactionId: string;
|
||||
clientName: string;
|
||||
clientLogo?: string;
|
||||
scope: string[];
|
||||
}>) {
|
||||
|
||||
//- Should be removed by the page when it loads, so that it won't needlessly
|
||||
//- stay when user navigates away via the navigation bar
|
||||
//- XXX: Remove navigation bar in auth page?
|
||||
function metaBlock() {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
extends ./base
|
||||
|
||||
block vars
|
||||
- const user = page.user;
|
||||
- const title = page.title;
|
||||
- const url = `${config.url}/@${user.username}/pages/${page.name}`;
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
||||
block desc
|
||||
meta(name='description' content= page.summary)
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content= page.summary)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : avatarUrl)
|
||||
meta(property='twitter:card' content= page.eyeCatchingImage ? 'summary_large_image' : 'summary')
|
||||
|
||||
block meta
|
||||
if profile.noCrawle
|
||||
meta(name='robots' content='noindex')
|
||||
if profile.preventAiLearning
|
||||
meta(name='robots' content='noimageai')
|
||||
meta(name='robots' content='noai')
|
||||
|
||||
meta(name='misskey:user-username' content=user.username)
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
meta(name='misskey:page-id' content=page.id)
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import type { CommonProps } from '@/server/web/views/_.js';
|
||||
import { Layout } from '@/server/web/views/base.js';
|
||||
|
||||
export function PagePage(props: CommonProps<{
|
||||
page: Packed<'Page'>;
|
||||
profile: MiUserProfile;
|
||||
}>) {
|
||||
function ogBlock() {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
extends ./base
|
||||
|
||||
block vars
|
||||
- const user1 = game.user1;
|
||||
- const user2 = game.user2;
|
||||
- const title = `${user1.username} vs ${user2.username}`;
|
||||
- const url = `${config.url}/reversi/g/${game.id}`;
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
||||
block desc
|
||||
meta(name='description' content='⚫⚪Misskey Reversi⚪⚫')
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content='⚫⚪Misskey Reversi⚪⚫')
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='twitter:card' content='summary')
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { CommonProps } from '@/server/web/views/_.js';
|
||||
import { Layout } from '@/server/web/views/base.js';
|
||||
|
||||
export function ReversiGamePage(props: CommonProps<{
|
||||
reversiGame: Packed<'ReversiGameDetailed'>;
|
||||
}>) {
|
||||
const title = `${props.reversiGame.user1.username} vs ${props.reversiGame.user2.username}`;
|
||||
const description = `⚫⚪Misskey Reversi⚪⚫`;
|
||||
|
||||
function ogBlock() {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
extends ./base
|
||||
|
||||
block vars
|
||||
- const title = user.name ? `${user.name} (@${user.username}${user.host ? `@${user.host}` : ''})` : `@${user.username}${user.host ? `@${user.host}` : ''}`;
|
||||
- const url = `${config.url}/@${(user.host ? `${user.username}@${user.host}` : user.username)}`;
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
||||
block desc
|
||||
meta(name='description' content= profile.description)
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='blog')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content= profile.description)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
meta(property='twitter:card' content='summary')
|
||||
|
||||
block meta
|
||||
if user.host || profile.noCrawle
|
||||
meta(name='robots' content='noindex')
|
||||
if profile.preventAiLearning
|
||||
meta(name='robots' content='noimageai')
|
||||
meta(name='robots' content='noai')
|
||||
|
||||
meta(name='misskey:user-username' content=user.username)
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
|
||||
if profile.twitter
|
||||
meta(name='twitter:creator' content=`@${profile.twitter.screenName}`)
|
||||
|
||||
if !sub
|
||||
if federationEnabled
|
||||
if !user.host
|
||||
link(rel='alternate' href=`${config.url}/users/${user.id}` type='application/activity+json')
|
||||
if user.uri
|
||||
link(rel='alternate' href=user.uri type='application/activity+json')
|
||||
if profile.url
|
||||
link(rel='alternate' href=profile.url type='text/html')
|
||||
|
||||
each m in me
|
||||
link(rel='me' href=`${m}`)
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import type { CommonProps } from '@/server/web/views/_.js';
|
||||
import { Layout } from '@/server/web/views/base.js';
|
||||
|
||||
export function UserPage(props: CommonProps<{
|
||||
user: Packed<'UserDetailed'>;
|
||||
profile: MiUserProfile;
|
||||
sub?: string;
|
||||
}>) {
|
||||
const title = props.user.name ? `${props.user.name} (@${props.user.username}${props.user.host ? `@${props.user.host}` : ''})` : `@${props.user.username}${props.user.host ? `@${props.user.host}` : ''}`;
|
||||
const me = props.profile.fields
|
||||
? props.profile.fields
|
||||
.filter(field => field.value != null && field.value.match(/^https?:/))
|
||||
.map(field => field.value)
|
||||
: [];
|
||||
|
||||
function ogBlock() {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -50,6 +50,14 @@ services:
|
||||
source: ../../misskey-js/package.json
|
||||
target: /misskey/packages/misskey-js/package.json
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../../i18n/built
|
||||
target: /misskey/packages/i18n/built
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../../i18n/package.json
|
||||
target: /misskey/packages/i18n/package.json
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../../misskey-reversi/built
|
||||
target: /misskey/packages/misskey-reversi/built
|
||||
|
||||
@@ -62,6 +62,14 @@ services:
|
||||
source: ../../misskey-js/package.json
|
||||
target: /misskey/packages/misskey-js/package.json
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../../i18n/built
|
||||
target: /misskey/packages/i18n/built
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../../i18n/package.json
|
||||
target: /misskey/packages/i18n/package.json
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../../../package.json
|
||||
target: /misskey/package.json
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
/* Language and Environment */
|
||||
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
"jsx": "react-jsx", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
"jsxImportSource": "@kitajs/html", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@kitajs/html",
|
||||
"rootDir": "../src",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user