mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-06-25 19:14:53 +02:00
Compare commits
19 Commits
2025.11.1
...
copilot/mi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b697c419a7 | ||
|
|
c87fb96195 | ||
|
|
d55f51a69b | ||
|
|
fe01a5a28f | ||
|
|
e5f1af9606 | ||
|
|
d5256a290f | ||
|
|
d1d50d19e0 | ||
|
|
32b5583432 | ||
|
|
5fbe801d35 | ||
|
|
3c11797c6d | ||
|
|
10242d5f14 | ||
|
|
0455187a68 | ||
|
|
1ae8e7900d | ||
|
|
81635d9f1c | ||
|
|
4bdbe794a6 | ||
|
|
cad93071da | ||
|
|
2effd9da6e | ||
|
|
2732034447 | ||
|
|
17a4d4fad9 |
7
.github/workflows/lint.yml
vendored
7
.github/workflows/lint.yml
vendored
@@ -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
|
||||
|
||||
29
.github/workflows/locale.yml
vendored
29
.github/workflows/locale.yml
vendored
@@ -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
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,3 +1,17 @@
|
||||
## 2025.11.2
|
||||
|
||||
### General
|
||||
-
|
||||
|
||||
### Client
|
||||
-
|
||||
|
||||
### Server
|
||||
- Enhance: メモリ使用量を削減しました
|
||||
- Enhance: ActivityPubアクティビティを送信する際のパフォーマンス向上
|
||||
- Enhance: 依存関係の更新
|
||||
|
||||
|
||||
## 2025.11.1
|
||||
|
||||
### Client
|
||||
|
||||
@@ -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 . ./
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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: "无法与此服务器通信"
|
||||
|
||||
20
package.json
20
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.11.1",
|
||||
"version": "2025.11.2-alpha.1",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -8,21 +8,24 @@
|
||||
},
|
||||
"packageManager": "pnpm@10.22.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": {
|
||||
"build-pre": "node ./scripts/build-pre.js",
|
||||
"build-assets": "node ./scripts/build-assets.mjs",
|
||||
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
|
||||
"minify-node-modules": "node ./scripts/minify-node-modules.mjs",
|
||||
"build-storybook": "pnpm --filter frontend build-storybook",
|
||||
"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
|
||||
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
|
||||
@@ -68,6 +71,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.39.1",
|
||||
"i18n": "workspace:*",
|
||||
"@misskey-dev/eslint-plugin": "2.2.0",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/node": "24.10.1",
|
||||
|
||||
@@ -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",
|
||||
"@fastify/view": "11.1.1",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/summaly": "5.2.5",
|
||||
"@napi-rs/canvas": "0.1.82",
|
||||
@@ -90,61 +90,59 @@
|
||||
"@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",
|
||||
"happy-dom": "20.0.10",
|
||||
"hpagent": "1.2.0",
|
||||
"htmlescape": "1.1.1",
|
||||
"http-link-header": "1.1.3",
|
||||
"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",
|
||||
"jsdom": "26.1.0",
|
||||
"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",
|
||||
"nanoid": "5.1.6",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"node-html-parser": "7.0.1",
|
||||
"nodemailer": "7.0.10",
|
||||
"nsfwjs": "4.2.0",
|
||||
"oauth": "0.10.2",
|
||||
@@ -152,9 +150,8 @@
|
||||
"oauth2orize-pkce": "0.1.2",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.4.1",
|
||||
"parse5": "7.3.0",
|
||||
"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",
|
||||
@@ -163,13 +160,12 @@
|
||||
"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",
|
||||
@@ -182,7 +178,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",
|
||||
@@ -190,28 +186,25 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@nestjs/platform-express": "10.4.20",
|
||||
"@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",
|
||||
"@types/fluent-ffmpeg": "2.1.28",
|
||||
"@types/htmlescape": "1.1.3",
|
||||
"@types/http-link-header": "1.0.7",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsdom": "21.1.7",
|
||||
"@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",
|
||||
@@ -224,7 +217,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",
|
||||
@@ -234,14 +227,15 @@
|
||||
"@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"
|
||||
}
|
||||
|
||||
13
packages/backend/src/@types/redis-lock.d.ts
vendored
13
packages/backend/src/@types/redis-lock.d.ts
vendored
@@ -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()] : []),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
import { URL } from 'node:url';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import * as Redis from 'ioredis';
|
||||
import * as htmlParser from 'node-html-parser';
|
||||
import type { MiInstance } from '@/models/Instance.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -15,7 +15,6 @@ import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import type { DOMWindow } from 'jsdom';
|
||||
|
||||
type NodeInfo = {
|
||||
openRegistrations?: unknown;
|
||||
@@ -59,7 +58,7 @@ export class FetchInstanceMetadataService {
|
||||
return await this.redisClient.set(
|
||||
`fetchInstanceMetadata:mutex:v2:${host}`, '1',
|
||||
'EX', 30, // 30秒したら自動でロック解除 https://github.com/misskey-dev/misskey/issues/13506#issuecomment-1975375395
|
||||
'GET' // 古い値を返す(なかったらnull)
|
||||
'GET', // 古い値を返す(なかったらnull)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -181,15 +180,14 @@ export class FetchInstanceMetadataService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async fetchDom(instance: MiInstance): Promise<Document> {
|
||||
private async fetchDom(instance: MiInstance): Promise<htmlParser.HTMLElement> {
|
||||
this.logger.info(`Fetching HTML of ${instance.host} ...`);
|
||||
|
||||
const url = 'https://' + instance.host;
|
||||
|
||||
const html = await this.httpRequestService.getHtml(url);
|
||||
|
||||
const { window } = new JSDOM(html);
|
||||
const doc = window.document;
|
||||
const doc = htmlParser.parse(html);
|
||||
|
||||
return doc;
|
||||
}
|
||||
@@ -206,12 +204,12 @@ export class FetchInstanceMetadataService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async fetchFaviconUrl(instance: MiInstance, doc: Document | null): Promise<string | null> {
|
||||
private async fetchFaviconUrl(instance: MiInstance, doc: htmlParser.HTMLElement | null): Promise<string | null> {
|
||||
const url = 'https://' + instance.host;
|
||||
|
||||
if (doc) {
|
||||
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
|
||||
const href = Array.from(doc.getElementsByTagName('link')).reverse().find(link => link.relList.contains('icon'))?.href;
|
||||
const href = Array.from(doc.getElementsByTagName('link')).reverse().find(link => link.attributes.rel === 'icon')?.attributes.href;
|
||||
|
||||
if (href) {
|
||||
return (new URL(href, url)).href;
|
||||
@@ -232,7 +230,7 @@ export class FetchInstanceMetadataService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async fetchIconUrl(instance: MiInstance, doc: Document | null, manifest: Record<string, any> | null): Promise<string | null> {
|
||||
private async fetchIconUrl(instance: MiInstance, doc: htmlParser.HTMLElement | null, manifest: Record<string, any> | null): Promise<string | null> {
|
||||
if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) {
|
||||
const url = 'https://' + instance.host;
|
||||
return (new URL(manifest.icons[0].src, url)).href;
|
||||
@@ -246,9 +244,9 @@ export class FetchInstanceMetadataService {
|
||||
// https://github.com/misskey-dev/misskey/pull/8220/files/0ec4eba22a914e31b86874f12448f88b3e58dd5a#r796487559
|
||||
const href =
|
||||
[
|
||||
links.find(link => link.relList.contains('apple-touch-icon-precomposed'))?.href,
|
||||
links.find(link => link.relList.contains('apple-touch-icon'))?.href,
|
||||
links.find(link => link.relList.contains('icon'))?.href,
|
||||
links.find(link => link.attributes.rel?.split(/\s+/).includes('apple-touch-icon-precomposed'))?.attributes.href,
|
||||
links.find(link => link.attributes.rel?.split(/\s+/).includes('apple-touch-icon'))?.attributes.href,
|
||||
links.find(link => link.attributes.rel?.split(/\s+/).includes('icon'))?.attributes.href,
|
||||
]
|
||||
.find(href => href);
|
||||
|
||||
@@ -261,7 +259,7 @@ export class FetchInstanceMetadataService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async getThemeColor(info: NodeInfo | null, doc: Document | null, manifest: Record<string, any> | null): Promise<string | null> {
|
||||
private async getThemeColor(info: NodeInfo | null, doc: htmlParser.HTMLElement | null, manifest: Record<string, any> | null): Promise<string | null> {
|
||||
const themeColor = info?.metadata?.themeColor ?? doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') ?? manifest?.theme_color;
|
||||
|
||||
if (themeColor) {
|
||||
@@ -273,7 +271,7 @@ export class FetchInstanceMetadataService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async getSiteName(info: NodeInfo | null, doc: Document | null, manifest: Record<string, any> | null): Promise<string | null> {
|
||||
private async getSiteName(info: NodeInfo | null, doc: htmlParser.HTMLElement | null, manifest: Record<string, any> | null): Promise<string | null> {
|
||||
if (info && info.metadata) {
|
||||
if (typeof info.metadata.nodeName === 'string') {
|
||||
return info.metadata.nodeName;
|
||||
@@ -298,7 +296,7 @@ export class FetchInstanceMetadataService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async getDescription(info: NodeInfo | null, doc: Document | null, manifest: Record<string, any> | null): Promise<string | null> {
|
||||
private async getDescription(info: NodeInfo | null, doc: htmlParser.HTMLElement | null, manifest: Record<string, any> | null): Promise<string | null> {
|
||||
if (info && info.metadata) {
|
||||
if (typeof info.metadata.nodeDescription === 'string') {
|
||||
return info.metadata.nodeDescription;
|
||||
|
||||
@@ -5,26 +5,19 @@
|
||||
|
||||
import { URL } from 'node:url';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as parse5 from 'parse5';
|
||||
import { type Document, type HTMLParagraphElement, Window, XMLSerializer } from 'happy-dom';
|
||||
import * as htmlParser from 'node-html-parser';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { intersperse } from '@/misc/prelude/array.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { DefaultTreeAdapterMap } from 'parse5';
|
||||
import { escapeHtml } from '@/misc/escape-html.js';
|
||||
import type * as mfm from 'mfm-js';
|
||||
|
||||
const treeAdapter = parse5.defaultTreeAdapter;
|
||||
type Node = DefaultTreeAdapterMap['node'];
|
||||
type ChildNode = DefaultTreeAdapterMap['childNode'];
|
||||
|
||||
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
|
||||
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
|
||||
|
||||
export type Appender = (document: Document, body: HTMLParagraphElement) => void;
|
||||
|
||||
@Injectable()
|
||||
export class MfmService {
|
||||
constructor(
|
||||
@@ -40,68 +33,68 @@ export class MfmService {
|
||||
|
||||
const normalizedHashtagNames = hashtagNames == null ? undefined : new Set<string>(hashtagNames.map(x => normalizeForSearch(x)));
|
||||
|
||||
const dom = parse5.parseFragment(html);
|
||||
const doc = htmlParser.parse(`<div>${html}</div>`);
|
||||
|
||||
let text = '';
|
||||
|
||||
for (const n of dom.childNodes) {
|
||||
for (const n of doc.childNodes) {
|
||||
analyze(n);
|
||||
}
|
||||
|
||||
return text.trim();
|
||||
|
||||
function getText(node: Node): string {
|
||||
if (treeAdapter.isTextNode(node)) return node.value;
|
||||
if (!treeAdapter.isElementNode(node)) return '';
|
||||
if (node.nodeName === 'br') return '\n';
|
||||
function getText(node: htmlParser.Node): string {
|
||||
if (node instanceof htmlParser.TextNode) return node.textContent;
|
||||
if (!(node instanceof htmlParser.HTMLElement)) return '';
|
||||
if (node.tagName === 'BR') return '\n';
|
||||
|
||||
if (node.childNodes) {
|
||||
if (node.childNodes != null) {
|
||||
return node.childNodes.map(n => getText(n)).join('');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function appendChildren(childNodes: ChildNode[]): void {
|
||||
if (childNodes) {
|
||||
function analyzeChildren(childNodes: htmlParser.Node[] | null): void {
|
||||
if (childNodes != null) {
|
||||
for (const n of childNodes) {
|
||||
analyze(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function analyze(node: Node) {
|
||||
if (treeAdapter.isTextNode(node)) {
|
||||
text += node.value;
|
||||
function analyze(node: htmlParser.Node) {
|
||||
if (node instanceof htmlParser.TextNode) {
|
||||
text += node.textContent;
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip comment or document type node
|
||||
if (!treeAdapter.isElementNode(node)) {
|
||||
if (!(node instanceof htmlParser.HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (node.nodeName) {
|
||||
case 'br': {
|
||||
switch (node.tagName) {
|
||||
case 'BR': {
|
||||
text += '\n';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'a': {
|
||||
case 'A': {
|
||||
const txt = getText(node);
|
||||
const rel = node.attrs.find(x => x.name === 'rel');
|
||||
const href = node.attrs.find(x => x.name === 'href');
|
||||
const rel = node.attributes.rel;
|
||||
const href = node.attributes.href;
|
||||
|
||||
// ハッシュタグ
|
||||
if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) {
|
||||
if (normalizedHashtagNames && href != null && normalizedHashtagNames.has(normalizeForSearch(txt))) {
|
||||
text += txt;
|
||||
// メンション
|
||||
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
|
||||
} else if (txt.startsWith('@') && !(rel != null && rel.startsWith('me '))) {
|
||||
const part = txt.split('@');
|
||||
|
||||
if (part.length === 2 && href) {
|
||||
//#region ホスト名部分が省略されているので復元する
|
||||
const acct = `${txt}@${(new URL(href.value)).hostname}`;
|
||||
const acct = `${txt}@${(new URL(href)).hostname}`;
|
||||
text += acct;
|
||||
//#endregion
|
||||
} else if (part.length === 3) {
|
||||
@@ -116,17 +109,17 @@ export class MfmService {
|
||||
if (!href) {
|
||||
return txt;
|
||||
}
|
||||
if (!txt || txt === href.value) { // #6383: Missing text node
|
||||
if (href.value.match(urlRegexFull)) {
|
||||
return href.value;
|
||||
if (!txt || txt === href) { // #6383: Missing text node
|
||||
if (href.match(urlRegexFull)) {
|
||||
return href;
|
||||
} else {
|
||||
return `<${href.value}>`;
|
||||
return `<${href}>`;
|
||||
}
|
||||
}
|
||||
if (href.value.match(urlRegex) && !href.value.match(urlRegexFull)) {
|
||||
return `[${txt}](<${href.value}>)`; // #6846
|
||||
if (href.match(urlRegex) && !href.match(urlRegexFull)) {
|
||||
return `[${txt}](<${href}>)`; // #6846
|
||||
} else {
|
||||
return `[${txt}](${href.value})`;
|
||||
return `[${txt}](${href})`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -135,60 +128,64 @@ export class MfmService {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'h1': {
|
||||
case 'H1': {
|
||||
text += '【';
|
||||
appendChildren(node.childNodes);
|
||||
analyzeChildren(node.childNodes);
|
||||
text += '】\n';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'b':
|
||||
case 'strong': {
|
||||
case 'B':
|
||||
case 'STRONG': {
|
||||
text += '**';
|
||||
appendChildren(node.childNodes);
|
||||
analyzeChildren(node.childNodes);
|
||||
text += '**';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'small': {
|
||||
case 'SMALL': {
|
||||
text += '<small>';
|
||||
appendChildren(node.childNodes);
|
||||
analyzeChildren(node.childNodes);
|
||||
text += '</small>';
|
||||
break;
|
||||
}
|
||||
|
||||
case 's':
|
||||
case 'del': {
|
||||
case 'S':
|
||||
case 'DEL': {
|
||||
text += '~~';
|
||||
appendChildren(node.childNodes);
|
||||
analyzeChildren(node.childNodes);
|
||||
text += '~~';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'i':
|
||||
case 'em': {
|
||||
case 'I':
|
||||
case 'EM': {
|
||||
text += '<i>';
|
||||
appendChildren(node.childNodes);
|
||||
analyzeChildren(node.childNodes);
|
||||
text += '</i>';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ruby': {
|
||||
case 'RUBY': {
|
||||
let ruby: [string, string][] = [];
|
||||
for (const child of node.childNodes) {
|
||||
if (child.nodeName === 'rp') {
|
||||
if ((child instanceof htmlParser.TextNode) && !/\s|\[|\]/.test(child.textContent)) {
|
||||
ruby.push([child.textContent, '']);
|
||||
continue;
|
||||
}
|
||||
if (treeAdapter.isTextNode(child) && !/\s|\[|\]/.test(child.value)) {
|
||||
ruby.push([child.value, '']);
|
||||
|
||||
if (!(child instanceof htmlParser.HTMLElement)) continue;
|
||||
|
||||
if (child.tagName === 'RP') {
|
||||
continue;
|
||||
}
|
||||
if (child.nodeName === 'rt' && ruby.length > 0) {
|
||||
|
||||
if (child.tagName === 'RT' && ruby.length > 0) {
|
||||
const rt = getText(child);
|
||||
if (/\s|\[|\]/.test(rt)) {
|
||||
// If any space is included in rt, it is treated as a normal text
|
||||
ruby = [];
|
||||
appendChildren(node.childNodes);
|
||||
analyzeChildren(node.childNodes);
|
||||
break;
|
||||
} else {
|
||||
ruby.at(-1)![1] = rt;
|
||||
@@ -197,7 +194,7 @@ export class MfmService {
|
||||
}
|
||||
// If any other element is included in ruby, it is treated as a normal text
|
||||
ruby = [];
|
||||
appendChildren(node.childNodes);
|
||||
analyzeChildren(node.childNodes);
|
||||
break;
|
||||
}
|
||||
for (const [base, rt] of ruby) {
|
||||
@@ -207,26 +204,30 @@ export class MfmService {
|
||||
}
|
||||
|
||||
// block code (<pre><code>)
|
||||
case 'pre': {
|
||||
if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
|
||||
case 'PRE': {
|
||||
if (node.childNodes.length === 1 && (node.childNodes[0] instanceof htmlParser.HTMLElement) && node.childNodes[0].tagName === 'CODE') {
|
||||
text += '\n```\n';
|
||||
text += getText(node.childNodes[0]);
|
||||
text += '\n```\n';
|
||||
} else if (node.childNodes.length === 1 && (node.childNodes[0] instanceof htmlParser.TextNode) && node.childNodes[0].textContent.startsWith('<code>') && node.childNodes[0].textContent.endsWith('</code>')) {
|
||||
text += '\n```\n';
|
||||
text += node.childNodes[0].textContent.slice(6, -7);
|
||||
text += '\n```\n';
|
||||
} else {
|
||||
appendChildren(node.childNodes);
|
||||
analyzeChildren(node.childNodes);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// inline code (<code>)
|
||||
case 'code': {
|
||||
case 'CODE': {
|
||||
text += '`';
|
||||
appendChildren(node.childNodes);
|
||||
analyzeChildren(node.childNodes);
|
||||
text += '`';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'blockquote': {
|
||||
case 'BLOCKQUOTE': {
|
||||
const t = getText(node);
|
||||
if (t) {
|
||||
text += '\n> ';
|
||||
@@ -235,33 +236,33 @@ export class MfmService {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'p':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
case 'h4':
|
||||
case 'h5':
|
||||
case 'h6': {
|
||||
case 'P':
|
||||
case 'H2':
|
||||
case 'H3':
|
||||
case 'H4':
|
||||
case 'H5':
|
||||
case 'H6': {
|
||||
text += '\n\n';
|
||||
appendChildren(node.childNodes);
|
||||
analyzeChildren(node.childNodes);
|
||||
break;
|
||||
}
|
||||
|
||||
// other block elements
|
||||
case 'div':
|
||||
case 'header':
|
||||
case 'footer':
|
||||
case 'article':
|
||||
case 'li':
|
||||
case 'dt':
|
||||
case 'dd': {
|
||||
case 'DIV':
|
||||
case 'HEADER':
|
||||
case 'FOOTER':
|
||||
case 'ARTICLE':
|
||||
case 'LI':
|
||||
case 'DT':
|
||||
case 'DD': {
|
||||
text += '\n';
|
||||
appendChildren(node.childNodes);
|
||||
analyzeChildren(node.childNodes);
|
||||
break;
|
||||
}
|
||||
|
||||
default: // includes inline elements
|
||||
{
|
||||
appendChildren(node.childNodes);
|
||||
analyzeChildren(node.childNodes);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -269,52 +270,35 @@ export class MfmService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], additionalAppenders: Appender[] = []) {
|
||||
public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], extraHtml: string | null = null) {
|
||||
if (nodes == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { happyDOM, window } = new Window();
|
||||
|
||||
const doc = window.document;
|
||||
|
||||
const body = doc.createElement('p');
|
||||
|
||||
function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
|
||||
if (children) {
|
||||
for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
|
||||
}
|
||||
function toHtml(children?: mfm.MfmNode[]): string {
|
||||
if (children == null) return '';
|
||||
return children.map(x => handlers[x.type](x)).join('');
|
||||
}
|
||||
|
||||
function fnDefault(node: mfm.MfmFn) {
|
||||
const el = doc.createElement('i');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
return `<i>${toHtml(node.children)}</i>`;
|
||||
}
|
||||
|
||||
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
|
||||
const handlers = {
|
||||
bold: (node) => {
|
||||
const el = doc.createElement('b');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
return `<b>${toHtml(node.children)}</b>`;
|
||||
},
|
||||
|
||||
small: (node) => {
|
||||
const el = doc.createElement('small');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
return `<small>${toHtml(node.children)}</small>`;
|
||||
},
|
||||
|
||||
strike: (node) => {
|
||||
const el = doc.createElement('del');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
return `<del>${toHtml(node.children)}</del>`;
|
||||
},
|
||||
|
||||
italic: (node) => {
|
||||
const el = doc.createElement('i');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
return `<i>${toHtml(node.children)}</i>`;
|
||||
},
|
||||
|
||||
fn: (node) => {
|
||||
@@ -323,10 +307,7 @@ export class MfmService {
|
||||
const text = node.children[0].type === 'text' ? node.children[0].props.text : '';
|
||||
try {
|
||||
const date = new Date(parseInt(text, 10) * 1000);
|
||||
const el = doc.createElement('time');
|
||||
el.setAttribute('datetime', date.toISOString());
|
||||
el.textContent = date.toISOString();
|
||||
return el;
|
||||
return `<time datetime="${escapeHtml(date.toISOString())}">${escapeHtml(date.toISOString())}</time>`;
|
||||
} catch (err) {
|
||||
return fnDefault(node);
|
||||
}
|
||||
@@ -336,21 +317,9 @@ export class MfmService {
|
||||
if (node.children.length === 1) {
|
||||
const child = node.children[0];
|
||||
const text = child.type === 'text' ? child.props.text : '';
|
||||
const rubyEl = doc.createElement('ruby');
|
||||
const rtEl = doc.createElement('rt');
|
||||
|
||||
// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
|
||||
const rpStartEl = doc.createElement('rp');
|
||||
rpStartEl.appendChild(doc.createTextNode('('));
|
||||
const rpEndEl = doc.createElement('rp');
|
||||
rpEndEl.appendChild(doc.createTextNode(')'));
|
||||
|
||||
rubyEl.appendChild(doc.createTextNode(text.split(' ')[0]));
|
||||
rtEl.appendChild(doc.createTextNode(text.split(' ')[1]));
|
||||
rubyEl.appendChild(rpStartEl);
|
||||
rubyEl.appendChild(rtEl);
|
||||
rubyEl.appendChild(rpEndEl);
|
||||
return rubyEl;
|
||||
// ruby未対応のHTMLサニタイザーを通したときにルビが「対象テキスト(ルビテキスト)」にフォールバックするようにする
|
||||
return `<ruby>${escapeHtml(text.split(' ')[0])}<rp>(</rp><rt>${escapeHtml(text.split(' ')[1])}</rt><rp>)</rp></ruby>`;
|
||||
} else {
|
||||
const rt = node.children.at(-1);
|
||||
|
||||
@@ -359,21 +328,9 @@ export class MfmService {
|
||||
}
|
||||
|
||||
const text = rt.type === 'text' ? rt.props.text : '';
|
||||
const rubyEl = doc.createElement('ruby');
|
||||
const rtEl = doc.createElement('rt');
|
||||
|
||||
// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
|
||||
const rpStartEl = doc.createElement('rp');
|
||||
rpStartEl.appendChild(doc.createTextNode('('));
|
||||
const rpEndEl = doc.createElement('rp');
|
||||
rpEndEl.appendChild(doc.createTextNode(')'));
|
||||
|
||||
appendChildren(node.children.slice(0, node.children.length - 1), rubyEl);
|
||||
rtEl.appendChild(doc.createTextNode(text.trim()));
|
||||
rubyEl.appendChild(rpStartEl);
|
||||
rubyEl.appendChild(rtEl);
|
||||
rubyEl.appendChild(rpEndEl);
|
||||
return rubyEl;
|
||||
// ruby未対応のHTMLサニタイザーを通したときにルビが「対象テキスト(ルビテキスト)」にフォールバックするようにする
|
||||
return `<ruby>${toHtml(node.children.slice(0, node.children.length - 1))}<rp>(</rp><rt>${escapeHtml(text.trim())}</rt><rp>)</rp></ruby>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,125 +341,98 @@ export class MfmService {
|
||||
},
|
||||
|
||||
blockCode: (node) => {
|
||||
const pre = doc.createElement('pre');
|
||||
const inner = doc.createElement('code');
|
||||
inner.textContent = node.props.code;
|
||||
pre.appendChild(inner);
|
||||
return pre;
|
||||
return `<pre><code>${escapeHtml(node.props.code)}</code></pre>`;
|
||||
},
|
||||
|
||||
center: (node) => {
|
||||
const el = doc.createElement('div');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
return `<div style="text-align: center;">${toHtml(node.children)}</div>`;
|
||||
},
|
||||
|
||||
emojiCode: (node) => {
|
||||
return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
|
||||
return `\u200B:${escapeHtml(node.props.name)}:\u200B`;
|
||||
},
|
||||
|
||||
unicodeEmoji: (node) => {
|
||||
return doc.createTextNode(node.props.emoji);
|
||||
return node.props.emoji;
|
||||
},
|
||||
|
||||
hashtag: (node) => {
|
||||
const a = doc.createElement('a');
|
||||
a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
|
||||
a.textContent = `#${node.props.hashtag}`;
|
||||
a.setAttribute('rel', 'tag');
|
||||
return a;
|
||||
return `<a href="${escapeHtml(`${this.config.url}/tags/${encodeURIComponent(node.props.hashtag)}`)}" rel="tag">#${escapeHtml(node.props.hashtag)}</a>`;
|
||||
},
|
||||
|
||||
inlineCode: (node) => {
|
||||
const el = doc.createElement('code');
|
||||
el.textContent = node.props.code;
|
||||
return el;
|
||||
return `<code>${escapeHtml(node.props.code)}</code>`;
|
||||
},
|
||||
|
||||
mathInline: (node) => {
|
||||
const el = doc.createElement('code');
|
||||
el.textContent = node.props.formula;
|
||||
return el;
|
||||
return `<code>${escapeHtml(node.props.formula)}</code>`;
|
||||
},
|
||||
|
||||
mathBlock: (node) => {
|
||||
const el = doc.createElement('code');
|
||||
el.textContent = node.props.formula;
|
||||
return el;
|
||||
return `<pre><code>${escapeHtml(node.props.formula)}</code></pre>`;
|
||||
},
|
||||
|
||||
link: (node) => {
|
||||
const a = doc.createElement('a');
|
||||
a.setAttribute('href', node.props.url);
|
||||
appendChildren(node.children, a);
|
||||
return a;
|
||||
try {
|
||||
const url = new URL(node.props.url);
|
||||
return `<a href="${escapeHtml(url.href)}">${toHtml(node.children)}</a>`;
|
||||
} catch (err) {
|
||||
return `[${toHtml(node.children)}](${escapeHtml(node.props.url)})`;
|
||||
}
|
||||
},
|
||||
|
||||
mention: (node) => {
|
||||
const a = doc.createElement('a');
|
||||
const { username, host, acct } = node.props;
|
||||
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase());
|
||||
a.setAttribute('href', remoteUserInfo
|
||||
const href = remoteUserInfo
|
||||
? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri)
|
||||
: `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`);
|
||||
a.className = 'u-url mention';
|
||||
a.textContent = acct;
|
||||
return a;
|
||||
: `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`;
|
||||
try {
|
||||
const url = new URL(href);
|
||||
return `<a href="${escapeHtml(url.href)}" class="u-url mention">${escapeHtml(acct)}</a>`;
|
||||
} catch (err) {
|
||||
return escapeHtml(acct);
|
||||
}
|
||||
},
|
||||
|
||||
quote: (node) => {
|
||||
const el = doc.createElement('blockquote');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
return `<blockquote>${toHtml(node.children)}</blockquote>`;
|
||||
},
|
||||
|
||||
text: (node) => {
|
||||
if (!node.props.text.match(/[\r\n]/)) {
|
||||
return doc.createTextNode(node.props.text);
|
||||
return escapeHtml(node.props.text);
|
||||
}
|
||||
|
||||
const el = doc.createElement('span');
|
||||
const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
|
||||
let html = '';
|
||||
|
||||
for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
|
||||
el.appendChild(x === 'br' ? doc.createElement('br') : x);
|
||||
const lines = node.props.text.split(/\r\n|\r|\n/).map(x => escapeHtml(x));
|
||||
|
||||
for (const x of intersperse<FIXME | 'br'>('br', lines)) {
|
||||
html += x === 'br' ? '<br />' : x;
|
||||
}
|
||||
|
||||
return el;
|
||||
return html;
|
||||
},
|
||||
|
||||
url: (node) => {
|
||||
const a = doc.createElement('a');
|
||||
a.setAttribute('href', node.props.url);
|
||||
a.textContent = node.props.url;
|
||||
return a;
|
||||
try {
|
||||
const url = new URL(node.props.url);
|
||||
return `<a href="${escapeHtml(url.href)}">${escapeHtml(node.props.url)}</a>`;
|
||||
} catch (err) {
|
||||
return escapeHtml(node.props.url);
|
||||
}
|
||||
},
|
||||
|
||||
search: (node) => {
|
||||
const a = doc.createElement('a');
|
||||
a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
|
||||
a.textContent = node.props.content;
|
||||
return a;
|
||||
return `<a href="${escapeHtml(`https://www.google.com/search?q=${encodeURIComponent(node.props.query)}`)}">${escapeHtml(node.props.content)}</a>`;
|
||||
},
|
||||
|
||||
plain: (node) => {
|
||||
const el = doc.createElement('span');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
return `<span>${toHtml(node.children)}</span>`;
|
||||
},
|
||||
};
|
||||
} satisfies { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => string } as { [K in mfm.MfmNode['type']]: (node: mfm.MfmNode) => string };
|
||||
|
||||
appendChildren(nodes, body);
|
||||
|
||||
for (const additionalAppender of additionalAppenders) {
|
||||
additionalAppender(doc, body);
|
||||
}
|
||||
|
||||
// Remove the unnecessary namespace
|
||||
const serialized = new XMLSerializer().serializeToString(body).replace(/^\s*<p xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">/, '<p>');
|
||||
|
||||
happyDOM.close().catch(err => {});
|
||||
|
||||
return serialized;
|
||||
return `${toHtml(nodes)}${extraHtml ?? ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +5,7 @@
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as mfm from 'mfm-js';
|
||||
import { MfmService, Appender } from '@/core/MfmService.js';
|
||||
import { MfmService } from '@/core/MfmService.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { extractApHashtagObjects } from './models/tag.js';
|
||||
@@ -25,17 +25,17 @@ export class ApMfmService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getNoteHtml(note: Pick<MiNote, 'text' | 'mentionedRemoteUsers'>, additionalAppender: Appender[] = []) {
|
||||
public getNoteHtml(note: Pick<MiNote, 'text' | 'mentionedRemoteUsers'>, extraHtml: string | null = null) {
|
||||
let noMisskeyContent = false;
|
||||
const srcMfm = (note.text ?? '');
|
||||
|
||||
const parsed = mfm.parse(srcMfm);
|
||||
|
||||
if (!additionalAppender.length && parsed.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
|
||||
if (extraHtml == null && parsed.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
|
||||
noMisskeyContent = true;
|
||||
}
|
||||
|
||||
const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers), additionalAppender);
|
||||
const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers), extraHtml);
|
||||
|
||||
return {
|
||||
content,
|
||||
|
||||
@@ -19,7 +19,7 @@ import type { MiEmoji } from '@/models/Emoji.js';
|
||||
import type { MiPoll } from '@/models/Poll.js';
|
||||
import type { MiPollVote } from '@/models/PollVote.js';
|
||||
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
||||
import { MfmService, type Appender } from '@/core/MfmService.js';
|
||||
import { MfmService } from '@/core/MfmService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import type { MiUserKeypair } from '@/models/UserKeypair.js';
|
||||
@@ -28,6 +28,7 @@ import { bindThis } from '@/decorators.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { escapeHtml } from '@/misc/escape-html.js';
|
||||
import { JsonLdService } from './JsonLdService.js';
|
||||
import { ApMfmService } from './ApMfmService.js';
|
||||
import { CONTEXT } from './misc/contexts.js';
|
||||
@@ -384,7 +385,7 @@ export class ApRendererService {
|
||||
inReplyTo = null;
|
||||
}
|
||||
|
||||
let quote;
|
||||
let quote: string | undefined;
|
||||
|
||||
if (note.renoteId) {
|
||||
const renote = await this.notesRepository.findOneBy({ id: note.renoteId });
|
||||
@@ -430,29 +431,18 @@ export class ApRendererService {
|
||||
poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
||||
}
|
||||
|
||||
const apAppend: Appender[] = [];
|
||||
let extraHtml: string | null = null;
|
||||
|
||||
if (quote) {
|
||||
if (quote != null) {
|
||||
// Append quote link as `<br><br><span class="quote-inline">RE: <a href="...">...</a></span>`
|
||||
// the claas name `quote-inline` is used in non-misskey clients for styling quote notes.
|
||||
// the class name `quote-inline` is used in non-misskey clients for styling quote notes.
|
||||
// For compatibility, the span part should be kept as possible.
|
||||
apAppend.push((doc, body) => {
|
||||
body.appendChild(doc.createElement('br'));
|
||||
body.appendChild(doc.createElement('br'));
|
||||
const span = doc.createElement('span');
|
||||
span.className = 'quote-inline';
|
||||
span.appendChild(doc.createTextNode('RE: '));
|
||||
const link = doc.createElement('a');
|
||||
link.setAttribute('href', quote);
|
||||
link.textContent = quote;
|
||||
span.appendChild(link);
|
||||
body.appendChild(span);
|
||||
});
|
||||
extraHtml = `<br><br><span class="quote-inline">RE: <a href="${escapeHtml(quote)}">${escapeHtml(quote)}</a></span>`;
|
||||
}
|
||||
|
||||
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
||||
|
||||
const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, apAppend);
|
||||
const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, extraHtml);
|
||||
|
||||
const emojis = await this.getEmojis(note.emojis);
|
||||
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import * as crypto from 'node:crypto';
|
||||
import { URL } from 'node:url';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Window } from 'happy-dom';
|
||||
import * as htmlParser from 'node-html-parser';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
@@ -215,29 +215,9 @@ export class ApRequestService {
|
||||
_followAlternate === true
|
||||
) {
|
||||
const html = await res.text();
|
||||
const { window, happyDOM } = new Window({
|
||||
settings: {
|
||||
disableJavaScriptEvaluation: true,
|
||||
disableJavaScriptFileLoading: true,
|
||||
disableCSSFileLoading: true,
|
||||
disableComputedStyleRendering: true,
|
||||
handleDisabledFileLoadingAsSuccess: true,
|
||||
navigation: {
|
||||
disableMainFrameNavigation: true,
|
||||
disableChildFrameNavigation: true,
|
||||
disableChildPageNavigation: true,
|
||||
disableFallbackToSetURL: true,
|
||||
},
|
||||
timer: {
|
||||
maxTimeout: 0,
|
||||
maxIntervalTime: 0,
|
||||
maxIntervalIterations: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
const document = window.document;
|
||||
|
||||
try {
|
||||
document.documentElement.innerHTML = html;
|
||||
const document = htmlParser.parse(html);
|
||||
|
||||
const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
|
||||
if (alternate) {
|
||||
@@ -248,8 +228,6 @@ export class ApRequestService {
|
||||
}
|
||||
} catch (e) {
|
||||
// something went wrong parsing the HTML, ignore the whole thing
|
||||
} finally {
|
||||
happyDOM.close().catch(err => {});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
@@ -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>>> {
|
||||
|
||||
49
packages/backend/src/misc/distributed-lock.ts
Normal file
49
packages/backend/src/misc/distributed-lock.ts
Normal file
@@ -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);
|
||||
}
|
||||
13
packages/backend/src/misc/escape-html.ts
Normal file
13
packages/backend/src/misc/escape-html.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -7,7 +7,7 @@ import RE2 from 're2';
|
||||
import * as mfm from 'mfm-js';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import * as htmlParser from 'node-html-parser';
|
||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
@@ -569,16 +569,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
try {
|
||||
const html = await this.httpRequestService.getHtml(url);
|
||||
|
||||
const { window } = new JSDOM(html);
|
||||
const doc: Document = window.document;
|
||||
const doc = htmlParser.parse(html);
|
||||
|
||||
const myLink = `${this.config.url}/@${user.username}`;
|
||||
|
||||
const aEls = Array.from(doc.getElementsByTagName('a'));
|
||||
const linkEls = Array.from(doc.getElementsByTagName('link'));
|
||||
|
||||
const includesMyLink = aEls.some(a => a.href === myLink);
|
||||
const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink);
|
||||
const includesMyLink = aEls.some(a => a.attributes.href === myLink);
|
||||
const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.attributes.rel?.split(/\s+/).includes('me') && link.attributes.href === myLink);
|
||||
|
||||
if (includesMyLink || includesRelMeLinks) {
|
||||
await this.userProfilesRepository.createQueryBuilder('profile').update()
|
||||
@@ -588,8 +587,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
window.close();
|
||||
} catch (err) {
|
||||
// なにもしない
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import dns from 'node:dns/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import * as htmlParser from 'node-html-parser';
|
||||
import httpLinkHeader from 'http-link-header';
|
||||
import ipaddr from 'ipaddr.js';
|
||||
import oauth2orize, { type OAuth2, AuthorizationError, ValidateFunctionArity2, OAuth2Req, MiddlewareRequest } from 'oauth2orize';
|
||||
@@ -120,9 +120,9 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
const fragment = JSDOM.fragment(text);
|
||||
const fragment = htmlParser.parse(`<div>${text}</div>`);
|
||||
|
||||
redirectUris.push(...[...fragment.querySelectorAll<HTMLLinkElement>('link[rel=redirect_uri][href]')].map(el => el.href));
|
||||
redirectUris.push(...[...fragment.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href));
|
||||
|
||||
let name = id;
|
||||
let logo: string | null = null;
|
||||
|
||||
@@ -15,7 +15,6 @@ import fastifyStatic from '@fastify/static';
|
||||
import fastifyView from '@fastify/view';
|
||||
import fastifyProxy from '@fastify/http-proxy';
|
||||
import vary from 'vary';
|
||||
import htmlSafeJsonStringify from 'htmlescape';
|
||||
import type { Config } from '@/config.js';
|
||||
import { getNoteSummary } from '@/misc/get-note-summary.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -63,6 +62,20 @@ const frontendViteOut = `${_dirname}/../../../../../built/_frontend_vite_/`;
|
||||
const frontendEmbedViteOut = `${_dirname}/../../../../../built/_frontend_embed_vite_/`;
|
||||
const tarball = `${_dirname}/../../../../../built/tarball/`;
|
||||
|
||||
const ESCAPE_LOOKUP = {
|
||||
'&': '\\u0026',
|
||||
'>': '\\u003e',
|
||||
'<': '\\u003c',
|
||||
'\u2028': '\\u2028',
|
||||
'\u2029': '\\u2029',
|
||||
} as Record<string, string>;
|
||||
|
||||
const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
|
||||
|
||||
function htmlSafeJsonStringify(obj: any): string {
|
||||
return JSON.stringify(obj).replace(ESCAPE_REGEX, x => ESCAPE_LOOKUP[x]);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ClientServerService {
|
||||
private logger: Logger;
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('export-clips', () => {
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
|
||||
// XXX: Any better way to get the result?
|
||||
async function pollFirstDriveFile() {
|
||||
async function pollFirstDriveFile(): Promise<any> {
|
||||
while (true) {
|
||||
const files = (await api('drive/files', {}, alice)).body;
|
||||
if (!files.length) {
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('Webリソース', () => {
|
||||
};
|
||||
|
||||
const metaTag = (res: SimpleGetResponse, key: string, superkey = 'name'): string => {
|
||||
return res.body.window.document.querySelector('meta[' + superkey + '="' + key + '"]')?.content;
|
||||
return res.body.querySelector('meta[' + superkey + '="' + key + '"]')?.attributes.content;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
ResourceOwnerPassword,
|
||||
} from 'simple-oauth2';
|
||||
import pkceChallenge from 'pkce-challenge';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import * as htmlParser from 'node-html-parser';
|
||||
import Fastify, { type FastifyInstance, type FastifyReply } from 'fastify';
|
||||
import { api, port, sendEnvUpdateRequest, signup } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
@@ -73,11 +73,11 @@ const clientConfig: ModuleOptions<'client_id'> = {
|
||||
};
|
||||
|
||||
function getMeta(html: string): { transactionId: string | undefined, clientName: string | undefined, clientLogo: string | undefined } {
|
||||
const fragment = JSDOM.fragment(html);
|
||||
const doc = htmlParser.parse(`<div>${html}</div>`);
|
||||
return {
|
||||
transactionId: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]')?.content,
|
||||
clientName: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-name"]')?.content,
|
||||
clientLogo: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-logo"]')?.content,
|
||||
transactionId: doc.querySelector('meta[name="misskey:oauth:transaction-id"]')?.attributes.content,
|
||||
clientName: doc.querySelector('meta[name="misskey:oauth:client-name"]')?.attributes.content,
|
||||
clientLogo: doc.querySelector('meta[name="misskey:oauth:client-logo"]')?.attributes.content,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ function assertIndirectError(response: Response, error: string): void {
|
||||
async function assertDirectError(response: Response, status: number, error: string): Promise<void> {
|
||||
assert.strictEqual(response.status, status);
|
||||
|
||||
const data = await response.json();
|
||||
const data = await response.json() as any;
|
||||
assert.strictEqual(data.error, error);
|
||||
}
|
||||
|
||||
@@ -704,7 +704,7 @@ describe('OAuth', () => {
|
||||
const response = await fetch(new URL('.well-known/oauth-authorization-server', host));
|
||||
assert.strictEqual(response.status, 200);
|
||||
|
||||
const body = await response.json();
|
||||
const body = await response.json() as any;
|
||||
assert.strictEqual(body.issuer, 'http://misskey.local');
|
||||
assert.ok(body.scopes_supported.includes('write:notes'));
|
||||
});
|
||||
|
||||
@@ -9,3 +9,4 @@ beforeAll(async () => {
|
||||
await initTestDb(false);
|
||||
await sendEnvResetRequest();
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import type { MockFunctionMetadata } from 'jest-mock';
|
||||
import type { MockMetadata } from 'jest-mock';
|
||||
|
||||
const moduleMocker = new ModuleMocker(global);
|
||||
|
||||
@@ -84,7 +84,7 @@ describe('AnnouncementService', () => {
|
||||
log: jest.fn(),
|
||||
};
|
||||
} else if (typeof token === 'function') {
|
||||
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
|
||||
const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata<any, any>;
|
||||
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
|
||||
return new Mock();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Test } from '@nestjs/testing';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import { ApMfmService } from '@/core/activitypub/ApMfmService.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { MiNote } from '@/models/Note.js';
|
||||
|
||||
describe('ApMfmService', () => {
|
||||
let apMfmService: ApMfmService;
|
||||
@@ -31,7 +30,7 @@ describe('ApMfmService', () => {
|
||||
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
||||
|
||||
assert.equal(noMisskeyContent, true, 'noMisskeyContent');
|
||||
assert.equal(content, '<p>テキスト <a href="http://misskey.local/tags/タグ" rel="tag">#タグ</a> <a href="http://misskey.local/@mention" class="u-url mention">@mention</a> 🍊 :emoji: <a href="https://example.com">https://example.com</a></p>', 'content');
|
||||
assert.equal(content, 'テキスト <a href="http://misskey.local/tags/%E3%82%BF%E3%82%B0" rel="tag">#タグ</a> <a href="http://misskey.local/@mention" class="u-url mention">@mention</a> 🍊 :emoji: <a href="https://example.com/">https://example.com</a>', 'content');
|
||||
});
|
||||
|
||||
test('Provide _misskey_content for MFM', () => {
|
||||
@@ -43,7 +42,7 @@ describe('ApMfmService', () => {
|
||||
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
||||
|
||||
assert.equal(noMisskeyContent, false, 'noMisskeyContent');
|
||||
assert.equal(content, '<p><i>foo</i></p>', 'content');
|
||||
assert.equal(content, '<i>foo</i>', 'content');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -446,7 +446,7 @@ describe('CaptchaService', () => {
|
||||
if (!res.success) {
|
||||
expect(res.error.code).toBe(code);
|
||||
}
|
||||
expect(metaService.update).not.toBeCalled();
|
||||
expect(metaService.update).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
describe('invalidParameters', () => {
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('DriveService', () => {
|
||||
s3Mock.on(DeleteObjectCommand)
|
||||
.rejects(new InvalidObjectState({ $metadata: {}, message: '' }));
|
||||
|
||||
await expect(driveService.deleteObjectStorageFile('unexpected')).rejects.toThrowError(Error);
|
||||
await expect(driveService.deleteObjectStorageFile('unexpected')).rejects.toThrow(Error);
|
||||
});
|
||||
|
||||
test('delete a file with no valid key', async () => {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { FileInfo, FileInfoService } from '@/core/FileInfoService.js';
|
||||
import { AiService } from '@/core/AiService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import type { MockFunctionMetadata } from 'jest-mock';
|
||||
import type { MockMetadata } from 'jest-mock';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
@@ -34,7 +34,7 @@ describe('FileInfoService', () => {
|
||||
delete fi.sensitive;
|
||||
delete fi.blurhash;
|
||||
delete fi.porn;
|
||||
|
||||
|
||||
return fi;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('FileInfoService', () => {
|
||||
// return { };
|
||||
//}
|
||||
if (typeof token === 'function') {
|
||||
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
|
||||
const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata<any, any>;
|
||||
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
|
||||
return new Mock();
|
||||
}
|
||||
|
||||
@@ -24,25 +24,25 @@ describe('MfmService', () => {
|
||||
describe('toHtml', () => {
|
||||
test('br', () => {
|
||||
const input = 'foo\nbar\nbaz';
|
||||
const output = '<p><span>foo<br />bar<br />baz</span></p>';
|
||||
const output = 'foo<br />bar<br />baz';
|
||||
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
|
||||
});
|
||||
|
||||
test('br alt', () => {
|
||||
const input = 'foo\r\nbar\rbaz';
|
||||
const output = '<p><span>foo<br />bar<br />baz</span></p>';
|
||||
const output = 'foo<br />bar<br />baz';
|
||||
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
|
||||
});
|
||||
|
||||
test('Do not generate unnecessary span', () => {
|
||||
const input = 'foo $[tada bar]';
|
||||
const output = '<p>foo <i>bar</i></p>';
|
||||
const output = 'foo <i>bar</i>';
|
||||
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
|
||||
});
|
||||
|
||||
test('escape', () => {
|
||||
const input = '```\n<p>Hello, world!</p>\n```';
|
||||
const output = '<p><pre><code><p>Hello, world!</p></code></pre></p>';
|
||||
const output = '<pre><code><p>Hello, world!</p></code></pre>';
|
||||
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
|
||||
});
|
||||
});
|
||||
@@ -118,7 +118,7 @@ describe('MfmService', () => {
|
||||
assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミス キー</rt><rp>)</rp> b</ruby> c</p>'), 'a Misskey(ミス キー) b c');
|
||||
assert.deepStrictEqual(
|
||||
mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミス キー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp></ruby> b</p>'),
|
||||
'a Misskey(ミスキー)Misskey(ミス キー)Misskey(ミスキー) b'
|
||||
'a Misskey(ミスキー)Misskey(ミス キー)Misskey(ミスキー) b',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { jest } from '@jest/globals';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { ModuleMocker } from 'jest-mock';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import type { MockFunctionMetadata } from 'jest-mock';
|
||||
import type { MockMetadata } from 'jest-mock';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
@@ -45,7 +45,7 @@ describe('RelayService', () => {
|
||||
return { deliver: jest.fn() };
|
||||
}
|
||||
if (typeof token === 'function') {
|
||||
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
|
||||
const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata<any, any>;
|
||||
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
|
||||
return new Mock();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ModuleMocker } from 'jest-mock';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import * as lolex from '@sinonjs/fake-timers';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import type { MockFunctionMetadata } from 'jest-mock';
|
||||
import type { MockMetadata } from 'jest-mock';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import {
|
||||
@@ -104,6 +104,8 @@ describe('RoleService', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
clock = lolex.install({
|
||||
// https://github.com/sinonjs/sinon/issues/2620
|
||||
toFake: Object.keys(lolex.timers).filter((key) => !['nextTick', 'queueMicrotask'].includes(key)) as lolex.FakeMethod[],
|
||||
now: new Date(),
|
||||
shouldClearNativeTimers: true,
|
||||
});
|
||||
@@ -135,7 +137,7 @@ describe('RoleService', () => {
|
||||
return { fetch: jest.fn() };
|
||||
}
|
||||
if (typeof token === 'function') {
|
||||
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
|
||||
const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata<any, any>;
|
||||
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
|
||||
return new Mock();
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ describe('S3Service', () => {
|
||||
Bucket: 'fake',
|
||||
Key: 'fake',
|
||||
Body: 'x',
|
||||
})).rejects.toThrowError(Error);
|
||||
})).rejects.toThrow(Error);
|
||||
});
|
||||
|
||||
test('upload a large file error', async () => {
|
||||
@@ -82,7 +82,7 @@ describe('S3Service', () => {
|
||||
Bucket: 'fake',
|
||||
Key: 'fake',
|
||||
Body: 'x'.repeat(8 * 1024 * 1024 + 1), // デフォルトpartSizeにしている 8 * 1024 * 1024 を越えるサイズ
|
||||
})).rejects.toThrowError(Error);
|
||||
})).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { AuthenticationResponseJSON } from '@simplewebauthn/types';
|
||||
import { HttpHeader } from 'fastify/types/utils.js';
|
||||
import { MockFunctionMetadata, ModuleMocker } from 'jest-mock';
|
||||
import { MockMetadata, ModuleMocker } from 'jest-mock';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
import { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
@@ -95,7 +95,7 @@ describe('SigninWithPasskeyApiService', () => {
|
||||
],
|
||||
}).useMocker((token) => {
|
||||
if (typeof token === 'function') {
|
||||
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
|
||||
const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata<any, any>;
|
||||
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
|
||||
return new Mock();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as assert from 'assert';
|
||||
import { jest } from '@jest/globals';
|
||||
import * as lolex from '@sinonjs/fake-timers';
|
||||
import { DataSource } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import TestChart from '@/core/chart/charts/test.js';
|
||||
import TestGroupedChart from '@/core/chart/charts/test-grouped.js';
|
||||
import TestUniqueChart from '@/core/chart/charts/test-unique.js';
|
||||
@@ -18,16 +19,16 @@ import { entity as TestGroupedChartEntity } from '@/core/chart/charts/entities/t
|
||||
import { entity as TestUniqueChartEntity } from '@/core/chart/charts/entities/test-unique.js';
|
||||
import { entity as TestIntersectionChartEntity } from '@/core/chart/charts/entities/test-intersection.js';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import type { AppLockService } from '@/core/AppLockService.js';
|
||||
import Logger from '@/logger.js';
|
||||
|
||||
describe('Chart', () => {
|
||||
const config = loadConfig();
|
||||
const appLockService = {
|
||||
getChartInsertLock: () => () => Promise.resolve(() => {}),
|
||||
} as unknown as jest.Mocked<AppLockService>;
|
||||
|
||||
let db: DataSource | undefined;
|
||||
let redisClient = {
|
||||
set: () => Promise.resolve('OK'),
|
||||
get: () => Promise.resolve(null),
|
||||
} as unknown as jest.Mocked<Redis.Redis>;
|
||||
|
||||
let testChart: TestChart;
|
||||
let testGroupedChart: TestGroupedChart;
|
||||
@@ -64,12 +65,14 @@ describe('Chart', () => {
|
||||
await db.initialize();
|
||||
|
||||
const logger = new Logger('chart'); // TODO: モックにする
|
||||
testChart = new TestChart(db, appLockService, logger);
|
||||
testGroupedChart = new TestGroupedChart(db, appLockService, logger);
|
||||
testUniqueChart = new TestUniqueChart(db, appLockService, logger);
|
||||
testIntersectionChart = new TestIntersectionChart(db, appLockService, logger);
|
||||
testChart = new TestChart(db, redisClient, logger);
|
||||
testGroupedChart = new TestGroupedChart(db, redisClient, logger);
|
||||
testUniqueChart = new TestUniqueChart(db, redisClient, logger);
|
||||
testIntersectionChart = new TestIntersectionChart(db, redisClient, logger);
|
||||
|
||||
clock = lolex.install({
|
||||
// https://github.com/sinonjs/sinon/issues/2620
|
||||
toFake: Object.keys(lolex.timers).filter((key) => !['nextTick', 'queueMicrotask'].includes(key)) as lolex.FakeMethod[],
|
||||
now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)),
|
||||
shouldClearNativeTimers: true,
|
||||
});
|
||||
|
||||
@@ -141,6 +141,8 @@ describe('CheckModeratorsActivityProcessorService', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
clock = lolex.install({
|
||||
// https://github.com/sinonjs/sinon/issues/2620
|
||||
toFake: Object.keys(lolex.timers).filter((key) => !['nextTick', 'queueMicrotask'].includes(key)) as lolex.FakeMethod[],
|
||||
now: new Date(baseDate),
|
||||
shouldClearNativeTimers: true,
|
||||
});
|
||||
|
||||
@@ -10,8 +10,8 @@ import { randomUUID } from 'node:crypto';
|
||||
import { inspect } from 'node:util';
|
||||
import WebSocket, { ClientOptions } from 'ws';
|
||||
import fetch, { File, RequestInit, type Headers } from 'node-fetch';
|
||||
import * as htmlParser from 'node-html-parser';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { type Response } from 'node-fetch';
|
||||
import Fastify from 'fastify';
|
||||
import { entities } from '../src/postgres.js';
|
||||
@@ -468,7 +468,7 @@ export function makeStreamCatcher<T>(
|
||||
|
||||
export type SimpleGetResponse = {
|
||||
status: number,
|
||||
body: any | JSDOM | null,
|
||||
body: any | null,
|
||||
type: string | null,
|
||||
location: string | null
|
||||
};
|
||||
@@ -499,7 +499,7 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde
|
||||
|
||||
const body =
|
||||
jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() :
|
||||
htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) :
|
||||
htmlTypes.includes(res.headers.get('content-type') ?? '') ? htmlParser.parse(await res.text()) :
|
||||
await bodyExtractor(res);
|
||||
|
||||
return {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { collectModifications } from './locale-inliner/collect-modifications.js'
|
||||
import { applyWithLocale } from './locale-inliner/apply-with-locale.js';
|
||||
import { blankLogger } from './logger.js';
|
||||
import type { Logger } from './logger.js';
|
||||
import type { Locale } from '../../locales/index.js';
|
||||
import type { Locale } from 'i18n';
|
||||
import type { Manifest as ViteManifest } from 'vite';
|
||||
|
||||
export class LocaleInliner {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import MagicString from 'magic-string';
|
||||
import { assertNever } from '../utils.js';
|
||||
import type { Locale, ILocale } from '../../../locales/index.js';
|
||||
import type { ILocale, Locale } from 'i18n';
|
||||
import type { TextModification } from '../locale-inliner.js';
|
||||
import type { Logger } from '../logger.js';
|
||||
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18n": "workspace:*",
|
||||
"estree-walker": "3.0.3",
|
||||
"magic-string": "0.30.21",
|
||||
"vite": "7.2.2"
|
||||
"vite": "7.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as fs from 'fs/promises';
|
||||
import url from 'node:url';
|
||||
import path from 'node:path';
|
||||
import { execa } from 'execa';
|
||||
import locales from '../../locales/index.js';
|
||||
import locales from 'i18n';
|
||||
import { LocaleInliner } from '../frontend-builder/locale-inliner.js'
|
||||
import { createLogger } from '../frontend-builder/logger';
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordapp/twemoji": "16.0.1",
|
||||
"i18n": "workspace:*",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "6.0.3",
|
||||
"@rollup/pluginutils": "5.3.0",
|
||||
@@ -27,14 +28,14 @@
|
||||
"misskey-js": "workspace:*",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.53.3",
|
||||
"sass": "1.94.1",
|
||||
"sass": "1.94.2",
|
||||
"shiki": "3.15.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.9.3",
|
||||
"uuid": "13.0.0",
|
||||
"vite": "7.2.2",
|
||||
"vite": "7.2.4",
|
||||
"vue": "3.5.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -49,12 +50,12 @@
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.47.0",
|
||||
"@typescript-eslint/parser": "8.47.0",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"@vitest/coverage-v8": "4.0.13",
|
||||
"@vue/runtime-core": "3.5.24",
|
||||
"acorn": "8.15.0",
|
||||
"cross-env": "10.1.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-vue": "10.5.1",
|
||||
"eslint-plugin-vue": "10.6.0",
|
||||
"fast-glob": "3.3.3",
|
||||
"happy-dom": "20.0.10",
|
||||
"intersection-observer": "0.12.2",
|
||||
@@ -62,11 +63,11 @@
|
||||
"msw": "2.12.2",
|
||||
"nodemon": "3.1.11",
|
||||
"prettier": "3.6.2",
|
||||
"start-server-and-test": "2.1.2",
|
||||
"start-server-and-test": "2.1.3",
|
||||
"tsx": "4.20.6",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vue-component-type-helpers": "3.1.4",
|
||||
"vue-component-type-helpers": "3.1.5",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"vue-tsc": "3.1.4"
|
||||
"vue-tsc": "3.1.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script setup lang="ts" generic="T extends string | ParameterizedString">
|
||||
import { computed, h } from 'vue';
|
||||
import type { ParameterizedString } from '../../../../locales/index.js';
|
||||
import type { ParameterizedString } from 'i18n';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
src: T;
|
||||
@@ -25,7 +25,7 @@ const slots = defineSlots<T extends ParameterizedString<infer R> ? { [K in R]: (
|
||||
const parsed = computed(() => {
|
||||
let str = props.src as string;
|
||||
const value: (string | { arg: string; })[] = [];
|
||||
for (;;) {
|
||||
for (; ;) {
|
||||
const nextBracketOpen = str.indexOf('{');
|
||||
const nextBracketClose = str.indexOf('}');
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { markRaw } from 'vue';
|
||||
import { I18n } from '@@/js/i18n.js';
|
||||
import { locale } from '@@/js/locale.js';
|
||||
import type { Locale } from '../../../locales/index.js';
|
||||
import type { Locale } from 'i18n';
|
||||
|
||||
export const i18n = markRaw(new I18n<Locale>(locale, _DEV_));
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import path from 'path';
|
||||
import pluginVue from '@vitejs/plugin-vue';
|
||||
import { type UserConfig, defineConfig } from 'vite';
|
||||
import { defineConfig, type UserConfig } from 'vite';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { promises as fsp } from 'fs';
|
||||
|
||||
import locales from '../../locales/index.js';
|
||||
import locales from 'i18n';
|
||||
import meta from '../../package.json';
|
||||
import packageInfo from './package.json' with { type: 'json' };
|
||||
import pluginJson5 from './vite.json5.js';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { ILocale, ParameterizedString } from '../../../locales/index.js';
|
||||
import type { ILocale, ParameterizedString } from 'i18n';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type TODO = any;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { lang, version } from '@@/js/config.js';
|
||||
import type { Locale } from '../../../locales/index.js';
|
||||
import type { Locale } from 'i18n';
|
||||
|
||||
// ここはビルド時に const locale = JSON.parse("...") みたいな感じで置き換えられるので top-level await は消える
|
||||
export let locale: Locale = await window.fetch(`/assets/locales/${lang}.${version}.json`).then(r => r.json(), () => null);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Locale } from '../../../locales/index.js';
|
||||
import type { Locale } from 'i18n';
|
||||
|
||||
type BootLoaderLocaleBody = Locale['_bootErrors'] & { reload: Locale['reload'] };
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "8.47.0",
|
||||
"@typescript-eslint/parser": "8.47.0",
|
||||
"esbuild": "0.27.0",
|
||||
"eslint-plugin-vue": "10.5.1",
|
||||
"eslint-plugin-vue": "10.6.0",
|
||||
"nodemon": "3.1.11",
|
||||
"typescript": "5.9.3",
|
||||
"vue-eslint-parser": "10.2.0"
|
||||
@@ -34,6 +34,7 @@
|
||||
"js-built"
|
||||
],
|
||||
"dependencies": {
|
||||
"i18n": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
"vue": "3.5.24"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import locales from '../../../locales/index.js';
|
||||
import locales from 'i18n';
|
||||
|
||||
await writeFile(
|
||||
new URL('locale.ts', import.meta.url),
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as fs from 'fs/promises';
|
||||
import url from 'node:url';
|
||||
import path from 'node:path';
|
||||
import { execa } from 'execa';
|
||||
import locales from '../../locales/index.js';
|
||||
import locales from 'i18n';
|
||||
import { LocaleInliner } from '../frontend-builder/locale-inliner.js'
|
||||
import { createLogger } from '../frontend-builder/logger';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import path from 'node:path'
|
||||
import locales from '../../../locales/index.js';
|
||||
import locales from 'i18n';
|
||||
|
||||
const localesDir = path.resolve(__dirname, '../../../locales')
|
||||
|
||||
@@ -13,14 +13,14 @@ const localesDir = path.resolve(__dirname, '../../../locales')
|
||||
* @returns {import('vite').Plugin}
|
||||
*/
|
||||
export default function pluginWatchLocales() {
|
||||
return {
|
||||
name: 'watch-locales',
|
||||
return {
|
||||
name: 'watch-locales',
|
||||
|
||||
configureServer(server) {
|
||||
const localeYmlPaths = Object.keys(locales).map(locale => path.join(localesDir, `${locale}.yml`));
|
||||
configureServer(server) {
|
||||
const localeYmlPaths = Object.keys(locales).map(locale => path.join(localesDir, `${locale}.yml`));
|
||||
|
||||
// watcherにパスを追加
|
||||
server.watcher.add(localeYmlPaths);
|
||||
// watcherにパスを追加
|
||||
server.watcher.add(localeYmlPaths);
|
||||
|
||||
server.watcher.on('change', (filePath) => {
|
||||
if (localeYmlPaths.includes(filePath)) {
|
||||
@@ -31,6 +31,6 @@ export default function pluginWatchLocales() {
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@discordapp/twemoji": "16.0.1",
|
||||
"@github/webauthn-json": "2.1.1",
|
||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||
"i18n": "workspace:*",
|
||||
"@misskey-dev/browser-image-resizer": "2024.1.0",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "6.0.3",
|
||||
@@ -58,7 +59,7 @@
|
||||
"json5": "2.2.3",
|
||||
"magic-string": "0.30.21",
|
||||
"matter-js": "0.20.0",
|
||||
"mediabunny": "1.25.0",
|
||||
"mediabunny": "1.25.1",
|
||||
"mfm-js": "0.25.0",
|
||||
"misskey-bubble-game": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
@@ -69,7 +70,7 @@
|
||||
"qr-scanner": "1.4.2",
|
||||
"rollup": "4.53.3",
|
||||
"sanitize-html": "2.17.0",
|
||||
"sass": "1.94.1",
|
||||
"sass": "1.94.2",
|
||||
"shiki": "3.15.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
@@ -80,7 +81,7 @@
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.9.3",
|
||||
"v-code-diff": "1.13.1",
|
||||
"vite": "7.2.2",
|
||||
"vite": "7.2.4",
|
||||
"vue": "3.5.24",
|
||||
"vuedraggable": "next",
|
||||
"wanakana": "5.3.1"
|
||||
@@ -89,7 +90,7 @@
|
||||
"@misskey-dev/summaly": "5.2.5",
|
||||
"@storybook/addon-essentials": "8.6.14",
|
||||
"@storybook/addon-interactions": "8.6.14",
|
||||
"@storybook/addon-links": "9.1.16",
|
||||
"@storybook/addon-links": "10.0.8",
|
||||
"@storybook/addon-mdx-gfm": "8.6.14",
|
||||
"@storybook/addon-storysource": "8.6.14",
|
||||
"@storybook/blocks": "8.6.14",
|
||||
@@ -97,13 +98,13 @@
|
||||
"@storybook/core-events": "8.6.14",
|
||||
"@storybook/manager-api": "8.6.14",
|
||||
"@storybook/preview-api": "8.6.14",
|
||||
"@storybook/react": "9.1.16",
|
||||
"@storybook/react-vite": "9.1.16",
|
||||
"@storybook/react": "10.0.8",
|
||||
"@storybook/react-vite": "10.0.8",
|
||||
"@storybook/test": "8.6.14",
|
||||
"@storybook/theming": "8.6.14",
|
||||
"@storybook/types": "8.6.14",
|
||||
"@storybook/vue3": "9.1.16",
|
||||
"@storybook/vue3-vite": "9.1.16",
|
||||
"@storybook/vue3": "10.0.8",
|
||||
"@storybook/vue3-vite": "10.0.8",
|
||||
"@tabler/icons-webfont": "3.35.0",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/canvas-confetti": "1.9.0",
|
||||
@@ -119,14 +120,14 @@
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.47.0",
|
||||
"@typescript-eslint/parser": "8.47.0",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"@vitest/coverage-v8": "4.0.13",
|
||||
"@vue/compiler-core": "3.5.24",
|
||||
"@vue/runtime-core": "3.5.24",
|
||||
"acorn": "8.15.0",
|
||||
"cross-env": "10.1.0",
|
||||
"cypress": "15.6.0",
|
||||
"cypress": "15.7.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-vue": "10.5.1",
|
||||
"eslint-plugin-vue": "10.6.0",
|
||||
"fast-glob": "3.3.3",
|
||||
"happy-dom": "20.0.10",
|
||||
"intersection-observer": "0.12.2",
|
||||
@@ -139,16 +140,16 @@
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"start-server-and-test": "2.1.2",
|
||||
"storybook": "9.1.16",
|
||||
"start-server-and-test": "2.1.3",
|
||||
"storybook": "10.0.8",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"tsx": "4.20.6",
|
||||
"vite-plugin-glsl": "1.5.4",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "3.2.4",
|
||||
"vitest": "4.0.13",
|
||||
"vitest-fetch-mock": "0.4.5",
|
||||
"vue-component-type-helpers": "3.1.4",
|
||||
"vue-component-type-helpers": "3.1.5",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"vue-tsc": "3.1.4"
|
||||
"vue-tsc": "3.1.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script setup lang="ts" generic="T extends string | ParameterizedString">
|
||||
import { computed, h } from 'vue';
|
||||
import type { ParameterizedString } from '../../../../../locales/index.js';
|
||||
import type { ParameterizedString } from 'i18n';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
src: T;
|
||||
@@ -25,7 +25,7 @@ const slots = defineSlots<T extends ParameterizedString<infer R> ? { [K in R]: (
|
||||
const parsed = computed(() => {
|
||||
let str = props.src as string;
|
||||
const value: (string | { arg: string; })[] = [];
|
||||
for (;;) {
|
||||
for (; ;) {
|
||||
const nextBracketOpen = str.indexOf('{');
|
||||
const nextBracketClose = str.indexOf('}');
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { markRaw } from 'vue';
|
||||
import { I18n } from '@@/js/i18n.js';
|
||||
import { locale } from '@@/js/locale.js';
|
||||
import type { Locale } from '../../../locales/index.js';
|
||||
import type { Locale } from 'i18n';
|
||||
|
||||
export const i18n = markRaw(new I18n<Locale>(locale, _DEV_));
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
|
||||
import { errors, Interpreter, Parser, values } from '@syuilo/aiscript';
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
@@ -80,8 +79,9 @@ describe('AiScript common API', () => {
|
||||
});
|
||||
|
||||
describe('readline', () => {
|
||||
afterEach(() => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test.sequential('ok', async () => {
|
||||
@@ -176,8 +176,9 @@ describe('AiScript common API', () => {
|
||||
});
|
||||
|
||||
describe('dialog', () => {
|
||||
afterEach(() => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test.sequential('ok', async () => {
|
||||
@@ -215,8 +216,9 @@ describe('AiScript common API', () => {
|
||||
});
|
||||
|
||||
describe('confirm', () => {
|
||||
afterEach(() => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test.sequential('ok', async () => {
|
||||
@@ -272,8 +274,9 @@ describe('AiScript common API', () => {
|
||||
});
|
||||
|
||||
describe('api', () => {
|
||||
afterEach(() => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test.sequential('successful', async () => {
|
||||
@@ -347,7 +350,7 @@ describe('AiScript common API', () => {
|
||||
miLocalStorage.removeItem('aiscript:widget:key');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
beforeEach(() => {
|
||||
miLocalStorage.removeItem('aiscript:widget:key');
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { I18n } from '../../frontend-shared/js/i18n.js'; // @@で参照できなかったので
|
||||
import type { ParameterizedString } from '../../../locales/index.js';
|
||||
import type { ParameterizedString } from 'i18n';
|
||||
|
||||
// TODO: このテストはfrontend-sharedに移動する
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ import { vi } from 'vitest';
|
||||
import createFetchMock from 'vitest-fetch-mock';
|
||||
import type { Ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
// Set i18n
|
||||
import locales from 'i18n';
|
||||
import { updateI18n } from '@/i18n.js';
|
||||
|
||||
const fetchMocker = createFetchMock(vi);
|
||||
fetchMocker.enableMocks();
|
||||
|
||||
// Set i18n
|
||||
import locales from '../../../locales/index.js';
|
||||
import { updateI18n } from '@/i18n.js';
|
||||
updateI18n(locales['en-US']);
|
||||
|
||||
// XXX: misskey-js panics if WebSocket is not defined
|
||||
|
||||
@@ -2,18 +2,18 @@ import path from 'path';
|
||||
import pluginReplace from '@rollup/plugin-replace';
|
||||
import pluginVue from '@vitejs/plugin-vue';
|
||||
import pluginGlsl from 'vite-plugin-glsl';
|
||||
import { defineConfig } from 'vite';
|
||||
import type { UserConfig } from 'vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { promises as fsp } from 'fs';
|
||||
|
||||
import locales from '../../locales/index.js';
|
||||
import locales from 'i18n';
|
||||
import meta from '../../package.json';
|
||||
import packageInfo from './package.json' with { type: 'json' };
|
||||
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js';
|
||||
import pluginJson5 from './vite.json5.js';
|
||||
import pluginCreateSearchIndex from './lib/vite-plugin-create-search-index.js';
|
||||
import type { Options as SearchIndexOptions } from './lib/vite-plugin-create-search-index.js';
|
||||
import pluginCreateSearchIndex from './lib/vite-plugin-create-search-index.js';
|
||||
import pluginWatchLocales from './lib/vite-plugin-watch-locales.js';
|
||||
import { pluginRemoveUnrefI18n } from '../frontend-builder/rollup-plugin-remove-unref-i18n.js';
|
||||
|
||||
|
||||
5
packages/i18n/README.md
Normal file
5
packages/i18n/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Misskey i18n
|
||||
|
||||
Misskey の言語ファイル本体 (ja-JP.yml など) はリポジトリ直下の `/locales` に置かれており、そこから Crowdin 連携やビルド資産が生成されます。
|
||||
|
||||
このパッケージは Misskey モノレポ内で、これらの言語ファイルを共通で扱うためのヘルパー群や型情報をまとめる位置づけです。バックエンド / フロントエンド / Service Worker など各パッケージが同じ翻訳データと型定義を利用できるようにすることを目的としており、npm での外部配布は想定していません。
|
||||
163
packages/i18n/build.ts
Normal file
163
packages/i18n/build.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { watch as chokidarWatch } from 'chokidar';
|
||||
import * as esbuild from 'esbuild';
|
||||
import { build } from 'esbuild';
|
||||
import { execa } from 'execa';
|
||||
import { globSync } from 'glob';
|
||||
import { generateLocaleInterface } from './scripts/generateLocaleInterface.js';
|
||||
import type { BuildOptions, BuildResult, Plugin, PluginBuild } from 'esbuild';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
|
||||
const _rootPackageDir = resolve(_dirname, '../../');
|
||||
const _rootPackage = JSON.parse(fs.readFileSync(resolve(_rootPackageDir, 'package.json'), 'utf-8'));
|
||||
const _frontendLocalesDir = resolve(_dirname, '../../built/_frontend_dist_/locales');
|
||||
const _localesDir = resolve(_rootPackageDir, 'locales');
|
||||
|
||||
const entryPoints = globSync('./src/**/**.{ts,tsx}');
|
||||
|
||||
const options: BuildOptions = {
|
||||
entryPoints,
|
||||
minify: process.env.NODE_ENV === 'production',
|
||||
sourceRoot: 'src',
|
||||
outdir: './built',
|
||||
target: 'es2022',
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
sourcemap: 'linked',
|
||||
};
|
||||
|
||||
// コマンドライン引数を取得
|
||||
const args = process.argv.slice(2).map(arg => arg.toLowerCase());
|
||||
|
||||
// built配下をすべて削除する
|
||||
if (!args.includes('--no-clean')) {
|
||||
fs.rmSync('./built', { recursive: true, force: true });
|
||||
}
|
||||
|
||||
if (args.includes('--watch')) {
|
||||
await watchSrc();
|
||||
} else {
|
||||
await buildSrc();
|
||||
}
|
||||
|
||||
function copyLocales(): void {
|
||||
const srcDir = _localesDir;
|
||||
const destDir = resolve(_dirname, 'built/locales');
|
||||
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
|
||||
const files = fs.readdirSync(srcDir).filter(f => f.endsWith('.yml'));
|
||||
for (const file of files) {
|
||||
fs.copyFileSync(resolve(srcDir, file), resolve(destDir, file));
|
||||
}
|
||||
console.log(`[${_package.name}] locales copied (${files.length} files).`);
|
||||
}
|
||||
|
||||
/**
|
||||
* フロントエンド用の locale JSON を書き出す
|
||||
* Service Worker が HTTP 経由で取得するために必要
|
||||
*/
|
||||
async function writeFrontendLocalesJson(): Promise<void> {
|
||||
// 動的 import でビルド済みモジュールから読み込み(循環参照回避)
|
||||
const { writeFrontendLocalesJson: write } = await import('./built/index.js');
|
||||
await write(_frontendLocalesDir, _rootPackage.version);
|
||||
console.log(`[${_package.name}] frontend locales JSON written to ${_frontendLocalesDir}`);
|
||||
}
|
||||
|
||||
async function buildSrc(): Promise<void> {
|
||||
console.log(`[${_package.name}] start building...`);
|
||||
|
||||
await generateLocaleInterface(_localesDir);
|
||||
|
||||
await build(options)
|
||||
.then(() => {
|
||||
console.log(`[${_package.name}] build succeeded.`);
|
||||
})
|
||||
.catch((err) => {
|
||||
process.stderr.write(err.stderr);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
copyLocales();
|
||||
await writeFrontendLocalesJson();
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.log(`[${_package.name}] skip building d.ts because NODE_ENV is production.`);
|
||||
} else {
|
||||
await buildDts();
|
||||
}
|
||||
|
||||
console.log(`[${_package.name}] finish building.`);
|
||||
}
|
||||
|
||||
function buildDts(): Promise<unknown> {
|
||||
return execa(
|
||||
'tsc',
|
||||
[
|
||||
'--project', 'tsconfig.json',
|
||||
'--rootDir', 'src',
|
||||
'--outDir', 'built',
|
||||
'--declaration', 'true',
|
||||
'--emitDeclarationOnly', 'true',
|
||||
],
|
||||
{
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function watchSrc(): Promise<void> {
|
||||
const localesWatcher = chokidarWatch(_localesDir, {
|
||||
ignoreInitial: true,
|
||||
});
|
||||
localesWatcher.on('all', async (event, path) => {
|
||||
if (!path.endsWith('.yml')) return;
|
||||
console.log(`[${_package.name}] locales changed: ${event} ${path}`);
|
||||
copyLocales();
|
||||
await writeFrontendLocalesJson();
|
||||
await generateLocaleInterface(_localesDir);
|
||||
});
|
||||
|
||||
const plugins: Plugin[] = [{
|
||||
name: 'gen-dts',
|
||||
setup(build: PluginBuild) {
|
||||
build.onStart(() => {
|
||||
console.log(`[${_package.name}] detect changed...`);
|
||||
});
|
||||
build.onEnd(async (result: BuildResult) => {
|
||||
if (result.errors.length > 0) {
|
||||
console.error(`[${_package.name}] watch build failed:`, result);
|
||||
return;
|
||||
}
|
||||
await buildDts();
|
||||
});
|
||||
},
|
||||
}];
|
||||
|
||||
console.log(`[${_package.name}] start watching...`);
|
||||
|
||||
const context = await esbuild.context({ ...options, plugins });
|
||||
await context.watch();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
process.on('SIGHUP', resolve);
|
||||
process.on('SIGINT', resolve);
|
||||
process.on('SIGTERM', resolve);
|
||||
process.on('uncaughtException', reject);
|
||||
process.on('exit', resolve);
|
||||
}).finally(async () => {
|
||||
await context.dispose();
|
||||
await localesWatcher.close();
|
||||
console.log(`[${_package.name}] finish watching.`);
|
||||
});
|
||||
}
|
||||
35
packages/i18n/eslint.config.js
Normal file
35
packages/i18n/eslint.config.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import sharedConfig from '../shared/eslint.config.js';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default [
|
||||
...sharedConfig,
|
||||
{
|
||||
ignores: [
|
||||
'**/node_modules',
|
||||
'built',
|
||||
'coverage',
|
||||
'vitest.config.ts',
|
||||
'test',
|
||||
'test-d',
|
||||
'generator',
|
||||
],
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: tsParser,
|
||||
project: ['./tsconfig.eslint.json'],
|
||||
sourceType: 'module',
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/autogen/**/*.ts', 'src/autogen/**/*.tsx'],
|
||||
rules: {
|
||||
'@stylistic/indent': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
42
packages/i18n/package.json
Normal file
42
packages/i18n/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "i18n",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"main": "./built/index.js",
|
||||
"types": "./built/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./built/index.d.ts",
|
||||
"import": "./built/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "tsx scripts/generateLocaleInterface.ts",
|
||||
"verify": "tsx scripts/verify.ts",
|
||||
"build": "tsx ./build.ts",
|
||||
"watch": "nodemon -w package.json -e json --exec \"tsx ./build.ts --watch\"",
|
||||
"tsd": "tsd",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "pnpm typecheck && pnpm eslint",
|
||||
"lint:fix": "pnpm eslint --fix"
|
||||
},
|
||||
"files": [
|
||||
"built"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/node": "24.10.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.47.0",
|
||||
"@typescript-eslint/parser": "8.47.0",
|
||||
"chokidar": "4.0.3",
|
||||
"esbuild": "0.27.0",
|
||||
"execa": "9.6.0",
|
||||
"glob": "11.1.0",
|
||||
"nodemon": "3.1.11",
|
||||
"tsx": "4.20.6",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"js-yaml": "4.1.1"
|
||||
}
|
||||
}
|
||||
153
packages/i18n/scripts/generateLocaleInterface.ts
Normal file
153
packages/i18n/scripts/generateLocaleInterface.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } 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;
|
||||
|
||||
interface LocaleRecord {
|
||||
[key: string]: string | LocaleRecord;
|
||||
}
|
||||
|
||||
function createMemberType(item: string | LocaleRecord): ts.TypeNode {
|
||||
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.createLiteralTypeNode(ts.factory.createStringLiteral(parameter)),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
||||
}
|
||||
|
||||
function createMembers(record: LocaleRecord): ts.TypeElement[] {
|
||||
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 async function generateLocaleInterface(localesDir: string): Promise<void> {
|
||||
const locale = yaml.load(fs.readFileSync(`${localesDir}/ja-JP.yml`, 'utf-8').toString()) as LocaleRecord;
|
||||
const members = createMembers(locale);
|
||||
|
||||
const elements: ts.Statement[] = [
|
||||
ts.factory.createImportDeclaration(
|
||||
undefined,
|
||||
ts.factory.createImportClause(
|
||||
false,
|
||||
undefined,
|
||||
ts.factory.createNamedImports([
|
||||
ts.factory.createImportSpecifier(
|
||||
true,
|
||||
undefined,
|
||||
ts.factory.createIdentifier('ILocale'),
|
||||
),
|
||||
ts.factory.createImportSpecifier(
|
||||
true,
|
||||
undefined,
|
||||
ts.factory.createIdentifier('ParameterizedString'),
|
||||
),
|
||||
]),
|
||||
),
|
||||
ts.factory.createStringLiteral('../types.js'),
|
||||
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.addSyntheticLeadingComment(
|
||||
elements[0],
|
||||
ts.SyntaxKind.MultiLineCommentTrivia,
|
||||
' eslint-disable ',
|
||||
true,
|
||||
);
|
||||
ts.addSyntheticLeadingComment(
|
||||
elements[0],
|
||||
ts.SyntaxKind.SingleLineCommentTrivia,
|
||||
' This file is generated by scripts/generateLocaleInterface.ts',
|
||||
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(
|
||||
'locale.ts',
|
||||
'',
|
||||
ts.ScriptTarget.ESNext,
|
||||
true,
|
||||
ts.ScriptKind.TS,
|
||||
),
|
||||
);
|
||||
|
||||
const autogenDir = `${__dirname}/../src/autogen`;
|
||||
fs.mkdirSync(autogenDir, { recursive: true });
|
||||
|
||||
// 一瞬ファイルが存在しなくなって途切れる→不安定になるらしいので、リネームで対処
|
||||
fs.writeFileSync(`${autogenDir}/_locale.ts`, printed, 'utf-8');
|
||||
fs.renameSync(`${autogenDir}/_locale.ts`, `${autogenDir}/locale.ts`);
|
||||
}
|
||||
|
||||
// スクリプトとして直接実行された場合
|
||||
const isMain = import.meta.url === `file://${process.argv[1]}`;
|
||||
if (isMain) {
|
||||
await generateLocaleInterface(resolve(__dirname, '../../../locales'));
|
||||
}
|
||||
70
packages/i18n/scripts/verify.ts
Normal file
70
packages/i18n/scripts/verify.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
let valid = true;
|
||||
|
||||
interface LocaleRecord {
|
||||
[key: string]: string | LocaleRecord;
|
||||
}
|
||||
|
||||
interface ErrorData {
|
||||
expected?: string;
|
||||
actual?: string;
|
||||
parameter?: string;
|
||||
}
|
||||
|
||||
function writeError(type: string, lang: string, tree: string, data: ErrorData): void {
|
||||
process.stderr.write(JSON.stringify({ type, lang, tree, data }));
|
||||
process.stderr.write('\n');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
function verify(expected: LocaleRecord, actual: LocaleRecord, lang: string, trace?: string): void {
|
||||
for (const 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] as LocaleRecord, actual[key] as LocaleRecord, 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] as string).match(/\{[^}]+\}/g)?.map((s) => s.slice(1, -1)));
|
||||
const actualParameters = new Set((actual[key] as string).match(/\{[^}]+\}/g)?.map((s) => s.slice(1, -1)));
|
||||
for (const parameter of expectedParameters) {
|
||||
if (!actualParameters.has(parameter)) {
|
||||
writeError('missing_parameter', lang, trace ? `${trace}.${key}` : key, { parameter });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// index.tsはtsのまま動かすことを想定していない(ビルド成果物を外部に公開する).
|
||||
// よってビルド後のものを検証する
|
||||
const locales = await import('../built/index.js');
|
||||
const { 'ja-JP': original, ...verifiees } = locales as unknown as Record<string, LocaleRecord>;
|
||||
|
||||
for (const lang in verifiees) {
|
||||
if (!Object.prototype.hasOwnProperty.call(locales, lang)) {
|
||||
continue;
|
||||
}
|
||||
verify(original, verifiees[lang], lang);
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,13 +1,7 @@
|
||||
/* eslint-disable */
|
||||
// This file is generated by locales/generateDTS.js
|
||||
// This file is generated by scripts/generateLocaleInterface.ts
|
||||
// Do not edit this file directly.
|
||||
declare const kParameters: unique symbol;
|
||||
export type ParameterizedString<T extends string = string> = string & {
|
||||
[kParameters]: T;
|
||||
};
|
||||
export interface ILocale {
|
||||
[_: string]: string | ParameterizedString | ILocale;
|
||||
}
|
||||
import { type ILocale, type ParameterizedString } from "../types.js";
|
||||
export interface Locale extends ILocale {
|
||||
/**
|
||||
* 日本語
|
||||
@@ -13089,8 +13083,3 @@ export interface Locale extends ILocale {
|
||||
"mfm": string;
|
||||
};
|
||||
}
|
||||
declare const locales: {
|
||||
[lang: string]: Locale;
|
||||
};
|
||||
export function build(): Locale;
|
||||
export default locales;
|
||||
166
packages/i18n/src/index.ts
Normal file
166
packages/i18n/src/index.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* Languages Loader
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as yaml from 'js-yaml';
|
||||
import type { Locale } from './autogen/locale.js';
|
||||
import type { ILocale, ParameterizedString } from './types.js';
|
||||
|
||||
const languages = [
|
||||
'ar-SA',
|
||||
'ca-ES',
|
||||
'cs-CZ',
|
||||
'da-DK',
|
||||
'de-DE',
|
||||
'en-US',
|
||||
'es-ES',
|
||||
'fr-FR',
|
||||
'id-ID',
|
||||
'it-IT',
|
||||
'ja-JP',
|
||||
'ja-KS',
|
||||
'kab-KAB',
|
||||
'kn-IN',
|
||||
'ko-KR',
|
||||
'nl-NL',
|
||||
'no-NO',
|
||||
'pl-PL',
|
||||
'pt-PT',
|
||||
'ru-RU',
|
||||
'sk-SK',
|
||||
'th-TH',
|
||||
'tr-TR',
|
||||
'ug-CN',
|
||||
'uk-UA',
|
||||
'vi-VN',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
] as const;
|
||||
|
||||
type Language = typeof languages[number];
|
||||
|
||||
const primaries = {
|
||||
'en': 'US',
|
||||
'ja': 'JP',
|
||||
'zh': 'CN',
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
type PrimaryLang = keyof typeof primaries;
|
||||
|
||||
type Locales = Record<Language, ILocale>;
|
||||
|
||||
/**
|
||||
* オブジェクトを再帰的にマージする
|
||||
*/
|
||||
function merge<T extends ILocale>(...args: (T | ILocale | undefined)[]): T {
|
||||
return args.reduce<ILocale>((a, c) => ({
|
||||
...a,
|
||||
...c,
|
||||
...Object.entries(a)
|
||||
.filter(([k]) => c && typeof c[k] === 'object')
|
||||
.reduce<Record<string, ILocale[string]>>((acc, [k, v]) => {
|
||||
acc[k] = merge(v as ILocale, (c as ILocale)[k] as ILocale);
|
||||
return acc;
|
||||
}, {}),
|
||||
}), {} as ILocale) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く
|
||||
*/
|
||||
function clean (text: string) {
|
||||
return text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
|
||||
*/
|
||||
function removeEmpty<T extends ILocale>(obj: T): T {
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
if (v === '') {
|
||||
delete obj[k];
|
||||
} else if (typeof v === 'object') {
|
||||
removeEmpty(v as ILocale);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function build(): Record<Language, Locale> {
|
||||
// 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<Locales>((a, lang) => {
|
||||
a[lang] = (yaml.load(clean(fs.readFileSync(new URL(`./locales/${lang}.yml`, metaUrl), 'utf-8'))) ?? {}) as ILocale;
|
||||
return a;
|
||||
}, {} as Locales);
|
||||
|
||||
removeEmpty(locales);
|
||||
|
||||
return Object.entries(locales).reduce<Record<Language, Locale>>((a, [k, v]) => {
|
||||
const lang = k.split('-')[0];
|
||||
const key = k as Language;
|
||||
|
||||
switch (key) {
|
||||
case 'ja-JP':
|
||||
a[key] = v as Locale;
|
||||
break;
|
||||
case 'ja-KS':
|
||||
case 'en-US':
|
||||
a[key] = merge<Locale>(locales['ja-JP'] as Locale, v);
|
||||
break;
|
||||
default: {
|
||||
const primaryLang = lang as PrimaryLang;
|
||||
const primaryKey = (lang in primaries ? `${lang}-${primaries[primaryLang]}` : undefined) as Language | undefined;
|
||||
a[key] = merge<Locale>(
|
||||
locales['ja-JP'] as Locale,
|
||||
locales['en-US'],
|
||||
primaryKey ? locales[primaryKey] : {},
|
||||
v,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return a;
|
||||
}, {} as Record<Language, Locale>);
|
||||
}
|
||||
|
||||
const locales = build() as {
|
||||
[lang: string]: Locale;
|
||||
};
|
||||
|
||||
/**
|
||||
* フロントエンド用の locale JSON を書き出す
|
||||
* Service Worker が HTTP 経由で取得するために必要
|
||||
* @param destDir 出力先ディレクトリ(例: built/_frontend_dist_/locales)
|
||||
* @param version バージョン文字列(ファイル名とJSON内に埋め込まれる)
|
||||
*/
|
||||
async function writeFrontendLocalesJson(destDir: string, version: string): Promise<void> {
|
||||
const { mkdir, writeFile } = await import('node:fs/promises');
|
||||
const { resolve } = await import('node:path');
|
||||
|
||||
await mkdir(destDir, { recursive: true });
|
||||
|
||||
const builtLocales = build();
|
||||
const v = { '_version_': version };
|
||||
|
||||
for (const [lang, locale] of Object.entries(builtLocales)) {
|
||||
await writeFile(
|
||||
resolve(destDir, `${lang}.${version}.json`),
|
||||
JSON.stringify({ ...locale, ...v }),
|
||||
'utf-8',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { locales, build, writeFrontendLocalesJson };
|
||||
export type { Language, Locale, ILocale, ParameterizedString };
|
||||
export default locales;
|
||||
14
packages/i18n/src/types.ts
Normal file
14
packages/i18n/src/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
declare const kParameters: unique symbol;
|
||||
|
||||
export type ParameterizedString<T extends string = string> = string & {
|
||||
[kParameters]: T;
|
||||
};
|
||||
|
||||
export interface ILocale {
|
||||
[_: string]: string | ParameterizedString | ILocale;
|
||||
}
|
||||
8
packages/i18n/tsconfig.eslint.json
Normal file
8
packages/i18n/tsconfig.eslint.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"scripts/**/*.ts",
|
||||
"build.ts"
|
||||
]
|
||||
}
|
||||
14
packages/i18n/tsconfig.json
Normal file
14
packages/i18n/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2025.11.1",
|
||||
"version": "2025.11.2-alpha.1",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user