[${(new Date()).toString()}] ${text.replace(/\n/g,'
')}
)
- case 'PRE': {
- if (node.childNodes.length === 1 && (node.childNodes[0] instanceof htmlParser.HTMLElement) && node.childNodes[0].tagName === 'CODE') {
+ case 'pre': {
+ if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
text += '\n```\n';
text += getText(node.childNodes[0]);
text += '\n```\n';
- } else if (node.childNodes.length === 1 && (node.childNodes[0] instanceof htmlParser.TextNode) && node.childNodes[0].textContent.startsWith('') && node.childNodes[0].textContent.endsWith('')) {
- text += '\n```\n';
- text += node.childNodes[0].textContent.slice(6, -7);
- text += '\n```\n';
} else {
- analyzeChildren(node.childNodes);
+ appendChildren(node.childNodes);
}
break;
}
// inline code ()
- case 'CODE': {
+ case 'code': {
text += '`';
- analyzeChildren(node.childNodes);
+ appendChildren(node.childNodes);
text += '`';
break;
}
- case 'BLOCKQUOTE': {
+ case 'blockquote': {
const t = getText(node);
if (t) {
text += '\n> ';
@@ -236,33 +235,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';
- analyzeChildren(node.childNodes);
+ appendChildren(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';
- analyzeChildren(node.childNodes);
+ appendChildren(node.childNodes);
break;
}
default: // includes inline elements
{
- analyzeChildren(node.childNodes);
+ appendChildren(node.childNodes);
break;
}
}
@@ -270,35 +269,52 @@ export class MfmService {
}
@bindThis
- public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], extraHtml: string | null = null) {
+ public toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], additionalAppenders: Appender[] = []) {
if (nodes == null) {
return null;
}
- function toHtml(children?: mfm.MfmNode[]): string {
- if (children == null) return '';
- return children.map(x => handlers[x.type](x)).join('');
+ 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 fnDefault(node: mfm.MfmFn) {
- return `${toHtml(node.children)}`;
+ const el = doc.createElement('i');
+ appendChildren(node.children, el);
+ return el;
}
- const handlers = {
+ const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => any } = {
bold: (node) => {
- return `${toHtml(node.children)}`;
+ const el = doc.createElement('b');
+ appendChildren(node.children, el);
+ return el;
},
small: (node) => {
- return `${toHtml(node.children)}`;
+ const el = doc.createElement('small');
+ appendChildren(node.children, el);
+ return el;
},
strike: (node) => {
- return `${toHtml(node.children)}`;
+ const el = doc.createElement('del');
+ appendChildren(node.children, el);
+ return el;
},
italic: (node) => {
- return `${toHtml(node.children)}`;
+ const el = doc.createElement('i');
+ appendChildren(node.children, el);
+ return el;
},
fn: (node) => {
@@ -307,7 +323,10 @@ export class MfmService {
const text = node.children[0].type === 'text' ? node.children[0].props.text : '';
try {
const date = new Date(parseInt(text, 10) * 1000);
- return ``;
+ const el = doc.createElement('time');
+ el.setAttribute('datetime', date.toISOString());
+ el.textContent = date.toISOString();
+ return el;
} catch (err) {
return fnDefault(node);
}
@@ -317,9 +336,21 @@ export class MfmService {
if (node.children.length === 1) {
const child = node.children[0];
const text = child.type === 'text' ? child.props.text : '';
-
- // ruby未対応のHTMLサニタイザーを通したときにルビが「対象テキスト(ルビテキスト)」にフォールバックするようにする
- return `${escapeHtml(text.split(' ')[0])}`;
+ 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;
} else {
const rt = node.children.at(-1);
@@ -328,9 +359,21 @@ export class MfmService {
}
const text = rt.type === 'text' ? rt.props.text : '';
-
- // ruby未対応のHTMLサニタイザーを通したときにルビが「対象テキスト(ルビテキスト)」にフォールバックするようにする
- return `${toHtml(node.children.slice(0, node.children.length - 1))}`;
+ 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;
}
}
@@ -341,98 +384,125 @@ export class MfmService {
},
blockCode: (node) => {
- return `${escapeHtml(node.props.code)}
`;
+ const pre = doc.createElement('pre');
+ const inner = doc.createElement('code');
+ inner.textContent = node.props.code;
+ pre.appendChild(inner);
+ return pre;
},
center: (node) => {
- return `${toHtml(node.children)}`;
+ const el = doc.createElement('div');
+ appendChildren(node.children, el);
+ return el;
},
emojiCode: (node) => {
- return `\u200B:${escapeHtml(node.props.name)}:\u200B`;
+ return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
},
unicodeEmoji: (node) => {
- return node.props.emoji;
+ return doc.createTextNode(node.props.emoji);
},
hashtag: (node) => {
- return `#${escapeHtml(node.props.hashtag)}`;
+ 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;
},
inlineCode: (node) => {
- return `${escapeHtml(node.props.code)}`;
+ const el = doc.createElement('code');
+ el.textContent = node.props.code;
+ return el;
},
mathInline: (node) => {
- return `${escapeHtml(node.props.formula)}`;
+ const el = doc.createElement('code');
+ el.textContent = node.props.formula;
+ return el;
},
mathBlock: (node) => {
- return `${escapeHtml(node.props.formula)}
`;
+ const el = doc.createElement('code');
+ el.textContent = node.props.formula;
+ return el;
},
link: (node) => {
- try {
- const url = new URL(node.props.url);
- return `${toHtml(node.children)}`;
- } catch (err) {
- return `[${toHtml(node.children)}](${escapeHtml(node.props.url)})`;
- }
+ const a = doc.createElement('a');
+ a.setAttribute('href', node.props.url);
+ appendChildren(node.children, a);
+ return a;
},
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());
- const href = remoteUserInfo
+ a.setAttribute('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}`;
- try {
- const url = new URL(href);
- return `${escapeHtml(acct)}`;
- } catch (err) {
- return escapeHtml(acct);
- }
+ : `${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;
},
quote: (node) => {
- return `${toHtml(node.children)}
`;
+ const el = doc.createElement('blockquote');
+ appendChildren(node.children, el);
+ return el;
},
text: (node) => {
if (!node.props.text.match(/[\r\n]/)) {
- return escapeHtml(node.props.text);
+ return doc.createTextNode(node.props.text);
}
- let html = '';
-
- const lines = node.props.text.split(/\r\n|\r|\n/).map(x => escapeHtml(x));
+ const el = doc.createElement('span');
+ const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
- for (const x of intersperse('br', lines)) {
- html += x === 'br' ? '
' : x;
+ for (const x of intersperse('br', nodes)) {
+ el.appendChild(x === 'br' ? doc.createElement('br') : x);
}
- return html;
+ return el;
},
url: (node) => {
- try {
- const url = new URL(node.props.url);
- return `${escapeHtml(node.props.url)}`;
- } catch (err) {
- return escapeHtml(node.props.url);
- }
+ const a = doc.createElement('a');
+ a.setAttribute('href', node.props.url);
+ a.textContent = node.props.url;
+ return a;
},
search: (node) => {
- return `${escapeHtml(node.props.content)}`;
+ const a = doc.createElement('a');
+ a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
+ a.textContent = node.props.content;
+ return a;
},
plain: (node) => {
- return `${toHtml(node.children)}`;
+ const el = doc.createElement('span');
+ appendChildren(node.children, el);
+ return el;
},
- } satisfies { [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => string } as { [K in mfm.MfmNode['type']]: (node: mfm.MfmNode) => string };
+ };
+
+ appendChildren(nodes, body);
+
+ for (const additionalAppender of additionalAppenders) {
+ additionalAppender(doc, body);
+ }
+
+ // Remove the unnecessary namespace
+ const serialized = new XMLSerializer().serializeToString(body).replace(/^\s*/, '
');
+
+ happyDOM.close().catch(err => {});
- return `${toHtml(nodes)}${extraHtml ?? ''}`;
+ return serialized;
}
}
diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts
index 310ffec7cee..eeade4569bc 100644
--- a/packages/backend/src/core/NotificationService.ts
+++ b/packages/backend/src/core/NotificationService.ts
@@ -202,7 +202,7 @@ export class NotificationService implements OnApplicationShutdown {
}
// TODO
- //const locales = await import('i18n');
+ //const locales = await import('../../../../locales/index.js');
// TODO: locale ファイルをクライアント用とサーバー用で分けたい
@@ -271,7 +271,7 @@ export class NotificationService implements OnApplicationShutdown {
let untilTime = untilId ? this.toXListId(untilId) : null;
let notifications: MiNotification[];
- for (; ;) {
+ for (;;) {
let notificationsRes: [id: string, fields: string[]][];
// sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照
diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts
index 31c8d67c609..372e1e2ab71 100644
--- a/packages/backend/src/core/WebAuthnService.ts
+++ b/packages/backend/src/core/WebAuthnService.ts
@@ -66,6 +66,7 @@ export class WebAuthnService {
userID: isoUint8Array.fromUTF8String(userId),
userName: userName,
userDisplayName: userDisplayName,
+ attestationType: 'indirect',
excludeCredentials: keys.map(key => (<{ id: string; transports?: AuthenticatorTransportFuture[]; }>{
id: key.id,
transports: key.transports ?? undefined,
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index 81637580e38..e88f60b8062 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -5,7 +5,6 @@
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';
@@ -15,8 +14,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';
@@ -49,8 +48,8 @@ export class ApInboxService {
@Inject(DI.config)
private config: Config,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
+ @Inject(DI.meta)
+ private meta: MiMeta,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -77,6 +76,7 @@ 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 acquireApObjectLock(this.redisClient, uri);
+ const unlock = await this.appLockService.getApLock(uri);
try {
// 既に同じURIを持つものが登録されていないかチェック
@@ -438,7 +438,7 @@ export class ApInboxService {
}
}
- const unlock = await acquireApObjectLock(this.redisClient, uri);
+ const unlock = await this.appLockService.getApLock(uri);
try {
const exist = await this.apNoteService.fetchNote(note);
@@ -522,7 +522,7 @@ export class ApInboxService {
private async deleteNote(actor: MiRemoteUser, uri: string): Promise {
this.logger.info(`Deleting the Note: ${uri}`);
- const unlock = await acquireApObjectLock(this.redisClient, uri);
+ const unlock = await this.appLockService.getApLock(uri);
try {
const note = await this.apDbResolverService.getNoteFromApId(uri);
diff --git a/packages/backend/src/core/activitypub/ApMfmService.ts b/packages/backend/src/core/activitypub/ApMfmService.ts
index a928ed5ccf7..f4c07e472c5 100644
--- a/packages/backend/src/core/activitypub/ApMfmService.ts
+++ b/packages/backend/src/core/activitypub/ApMfmService.ts
@@ -5,7 +5,7 @@
import { Injectable } from '@nestjs/common';
import * as mfm from 'mfm-js';
-import { MfmService } from '@/core/MfmService.js';
+import { MfmService, Appender } 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, extraHtml: string | null = null) {
+ public getNoteHtml(note: Pick, additionalAppender: Appender[] = []) {
let noMisskeyContent = false;
const srcMfm = (note.text ?? '');
const parsed = mfm.parse(srcMfm);
- if (extraHtml == null && parsed.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
+ if (!additionalAppender.length && parsed.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) {
noMisskeyContent = true;
}
- const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers), extraHtml);
+ const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers), additionalAppender);
return {
content,
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index 4570977c5de..55521d6e3af 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -19,7 +19,7 @@ import type { MiEmoji } from '@/models/Emoji.js';
import type { MiPoll } from '@/models/Poll.js';
import type { MiPollVote } from '@/models/PollVote.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
-import { MfmService } from '@/core/MfmService.js';
+import { MfmService, type Appender } 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,7 +28,6 @@ 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';
@@ -385,7 +384,7 @@ export class ApRendererService {
inReplyTo = null;
}
- let quote: string | undefined;
+ let quote;
if (note.renoteId) {
const renote = await this.notesRepository.findOneBy({ id: note.renoteId });
@@ -431,18 +430,29 @@ export class ApRendererService {
poll = await this.pollsRepository.findOneBy({ noteId: note.id });
}
- let extraHtml: string | null = null;
+ const apAppend: Appender[] = [];
- if (quote != null) {
+ if (quote) {
// Append quote link as `
RE: ...`
- // the class name `quote-inline` is used in non-misskey clients for styling quote notes.
+ // the claas name `quote-inline` is used in non-misskey clients for styling quote notes.
// For compatibility, the span part should be kept as possible.
- extraHtml = `
RE: ${escapeHtml(quote)}`;
+ 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);
+ });
}
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
- const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, extraHtml);
+ const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, apAppend);
const emojis = await this.getEmojis(note.emojis);
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts
index 49298a1d22c..61d328ccac3 100644
--- a/packages/backend/src/core/activitypub/ApRequestService.ts
+++ b/packages/backend/src/core/activitypub/ApRequestService.ts
@@ -6,7 +6,7 @@
import * as crypto from 'node:crypto';
import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
-import * as htmlParser from 'node-html-parser';
+import { Window } from 'happy-dom';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { MiUser } from '@/models/User.js';
@@ -215,9 +215,29 @@ 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 {
- const document = htmlParser.parse(html);
+ document.documentElement.innerHTML = html;
const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
if (alternate) {
@@ -228,6 +248,8 @@ export class ApRequestService {
}
} catch (e) {
// something went wrong parsing the HTML, ignore the whole thing
+ } finally {
+ happyDOM.close().catch(err => {});
}
}
//#endregion
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 214d32f67ff..8abacd293fc 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -5,15 +5,14 @@
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';
@@ -49,9 +48,6 @@ export class ApNoteService {
@Inject(DI.meta)
private meta: MiMeta,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
@Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
@@ -71,6 +67,7 @@ export class ApNoteService {
private apMentionService: ApMentionService,
private apImageService: ApImageService,
private apQuestionService: ApQuestionService,
+ private appLockService: AppLockService,
private pollService: PollService,
private noteCreateService: NoteCreateService,
private apDbResolverService: ApDbResolverService,
@@ -357,7 +354,7 @@ export class ApNoteService {
throw new StatusError('blocked host', 451);
}
- const unlock = await acquireApObjectLock(this.redisClient, uri);
+ const unlock = await this.appLockService.getApLock(uri);
try {
//#region このサーバーに既に登録されていたらそれを返す
diff --git a/packages/backend/src/core/chart/charts/active-users.ts b/packages/backend/src/core/chart/charts/active-users.ts
index 7b9840af87d..05905f37829 100644
--- a/packages/backend/src/core/chart/charts/active-users.ts
+++ b/packages/backend/src/core/chart/charts/active-users.ts
@@ -5,12 +5,11 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import * as Redis from 'ioredis';
+import { AppLockService } from '@/core/AppLockService.js';
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';
@@ -29,13 +28,11 @@ export default class ActiveUsersChart extends Chart { // eslint-d
@Inject(DI.db)
private db: DataSource,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
+ private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService,
private idService: IdService,
) {
- super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
+ super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/ap-request.ts b/packages/backend/src/core/chart/charts/ap-request.ts
index ed790de7b5c..04e771a95b4 100644
--- a/packages/backend/src/core/chart/charts/ap-request.ts
+++ b/packages/backend/src/core/chart/charts/ap-request.ts
@@ -5,10 +5,9 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import * as Redis from 'ioredis';
+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/ap-request.js';
@@ -23,12 +22,10 @@ export default class ApRequestChart extends Chart { // eslint-dis
@Inject(DI.db)
private db: DataSource,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
+ private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
+ super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/drive.ts b/packages/backend/src/core/chart/charts/drive.ts
index 782873809ad..613e074a9f1 100644
--- a/packages/backend/src/core/chart/charts/drive.ts
+++ b/packages/backend/src/core/chart/charts/drive.ts
@@ -5,11 +5,10 @@
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';
@@ -24,12 +23,10 @@ export default class DriveChart extends Chart { // eslint-disable
@Inject(DI.db)
private db: DataSource,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
+ private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
+ super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts
index b7a7f640b87..c9b43cc66d7 100644
--- a/packages/backend/src/core/chart/charts/federation.ts
+++ b/packages/backend/src/core/chart/charts/federation.ts
@@ -5,11 +5,10 @@
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';
@@ -27,18 +26,16 @@ export default class FederationChart extends Chart { // eslint-di
@Inject(DI.meta)
private meta: MiMeta,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
+ private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
+ super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/instance.ts b/packages/backend/src/core/chart/charts/instance.ts
index b1657e0a0bd..97f3bc6f2bd 100644
--- a/packages/backend/src/core/chart/charts/instance.ts
+++ b/packages/backend/src/core/chart/charts/instance.ts
@@ -5,14 +5,13 @@
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';
@@ -27,9 +26,6 @@ export default class InstanceChart extends Chart { // eslint-disa
@Inject(DI.db)
private db: DataSource,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -43,9 +39,10 @@ export default class InstanceChart extends Chart { // eslint-disa
private followingsRepository: FollowingsRepository,
private utilityService: UtilityService,
+ private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/notes.ts b/packages/backend/src/core/chart/charts/notes.ts
index aa64e2329a7..f763b5fffa7 100644
--- a/packages/backend/src/core/chart/charts/notes.ts
+++ b/packages/backend/src/core/chart/charts/notes.ts
@@ -5,12 +5,11 @@
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';
@@ -25,15 +24,13 @@ export default class NotesChart extends Chart { // eslint-disable
@Inject(DI.db)
private db: DataSource,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
+ private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
+ super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/per-user-drive.ts b/packages/backend/src/core/chart/charts/per-user-drive.ts
index f7e92aecead..404964d8b7c 100644
--- a/packages/backend/src/core/chart/charts/per-user-drive.ts
+++ b/packages/backend/src/core/chart/charts/per-user-drive.ts
@@ -5,13 +5,12 @@
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';
@@ -26,16 +25,14 @@ export default class PerUserDriveChart extends Chart { // eslint-
@Inject(DI.db)
private db: DataSource,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
+ private appLockService: AppLockService,
private driveFileEntityService: DriveFileEntityService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/per-user-following.ts b/packages/backend/src/core/chart/charts/per-user-following.ts
index ea431a51314..588ac638de9 100644
--- a/packages/backend/src/core/chart/charts/per-user-following.ts
+++ b/packages/backend/src/core/chart/charts/per-user-following.ts
@@ -5,13 +5,12 @@
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';
@@ -26,16 +25,14 @@ export default class PerUserFollowingChart extends Chart { // esl
@Inject(DI.db)
private db: DataSource,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
+ private appLockService: AppLockService,
private userEntityService: UserEntityService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/per-user-notes.ts b/packages/backend/src/core/chart/charts/per-user-notes.ts
index 824d60042d5..e4900772bb9 100644
--- a/packages/backend/src/core/chart/charts/per-user-notes.ts
+++ b/packages/backend/src/core/chart/charts/per-user-notes.ts
@@ -5,13 +5,12 @@
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';
@@ -26,15 +25,13 @@ export default class PerUserNotesChart extends Chart { // eslint-
@Inject(DI.db)
private db: DataSource,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
+ private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/per-user-pv.ts b/packages/backend/src/core/chart/charts/per-user-pv.ts
index b3e1b2cea16..31708fefa86 100644
--- a/packages/backend/src/core/chart/charts/per-user-pv.ts
+++ b/packages/backend/src/core/chart/charts/per-user-pv.ts
@@ -5,11 +5,10 @@
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';
@@ -24,12 +23,10 @@ export default class PerUserPvChart extends Chart { // eslint-dis
@Inject(DI.db)
private db: DataSource,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
+ private appLockService: AppLockService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/per-user-reactions.ts b/packages/backend/src/core/chart/charts/per-user-reactions.ts
index 7bc1d9e7fa2..c29c4d28709 100644
--- a/packages/backend/src/core/chart/charts/per-user-reactions.ts
+++ b/packages/backend/src/core/chart/charts/per-user-reactions.ts
@@ -5,13 +5,12 @@
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';
@@ -26,13 +25,11 @@ export default class PerUserReactionsChart extends Chart { // esl
@Inject(DI.db)
private db: DataSource,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
+ private appLockService: AppLockService,
private userEntityService: UserEntityService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema, true);
+ super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/test-grouped.ts b/packages/backend/src/core/chart/charts/test-grouped.ts
index 8dd1a5d9968..7a2844f4ed4 100644
--- a/packages/backend/src/core/chart/charts/test-grouped.ts
+++ b/packages/backend/src/core/chart/charts/test-grouped.ts
@@ -5,11 +5,10 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import * as Redis from 'ioredis';
+import { AppLockService } from '@/core/AppLockService.js';
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';
@@ -25,12 +24,10 @@ export default class TestGroupedChart extends Chart { // eslint-d
@Inject(DI.db)
private db: DataSource,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
+ private appLockService: AppLockService,
logger: Logger,
) {
- super(db, (k) => acquireChartInsertLock(redisClient, k), logger, name, schema, true);
+ super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema, true);
}
protected async tickMajor(group: string): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/test-intersection.ts b/packages/backend/src/core/chart/charts/test-intersection.ts
index 23b8649cce8..b8d0556c9fa 100644
--- a/packages/backend/src/core/chart/charts/test-intersection.ts
+++ b/packages/backend/src/core/chart/charts/test-intersection.ts
@@ -5,11 +5,10 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import * as Redis from 'ioredis';
+import { AppLockService } from '@/core/AppLockService.js';
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';
@@ -23,12 +22,10 @@ export default class TestIntersectionChart extends Chart { // esl
@Inject(DI.db)
private db: DataSource,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
+ private appLockService: AppLockService,
logger: Logger,
) {
- super(db, (k) => acquireChartInsertLock(redisClient, k), logger, name, schema);
+ super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/test-unique.ts b/packages/backend/src/core/chart/charts/test-unique.ts
index b84dd419ba4..f94e008059a 100644
--- a/packages/backend/src/core/chart/charts/test-unique.ts
+++ b/packages/backend/src/core/chart/charts/test-unique.ts
@@ -5,11 +5,10 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import * as Redis from 'ioredis';
+import { AppLockService } from '@/core/AppLockService.js';
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';
@@ -23,12 +22,10 @@ export default class TestUniqueChart extends Chart { // eslint-di
@Inject(DI.db)
private db: DataSource,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
+ private appLockService: AppLockService,
logger: Logger,
) {
- super(db, (k) => acquireChartInsertLock(redisClient, k), logger, name, schema);
+ super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/test.ts b/packages/backend/src/core/chart/charts/test.ts
index 0e95ce9239c..a90dc8f99b4 100644
--- a/packages/backend/src/core/chart/charts/test.ts
+++ b/packages/backend/src/core/chart/charts/test.ts
@@ -5,11 +5,10 @@
import { Injectable, Inject } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import * as Redis from 'ioredis';
+import { AppLockService } from '@/core/AppLockService.js';
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';
@@ -25,12 +24,10 @@ export default class TestChart extends Chart { // eslint-disable-
@Inject(DI.db)
private db: DataSource,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
+ private appLockService: AppLockService,
logger: Logger,
) {
- super(db, (k) => acquireChartInsertLock(redisClient, k), logger, name, schema);
+ super(db, (k) => appLockService.getChartInsertLock(k), logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/core/chart/charts/users.ts b/packages/backend/src/core/chart/charts/users.ts
index 4471c1df23d..d148fc629b1 100644
--- a/packages/backend/src/core/chart/charts/users.ts
+++ b/packages/backend/src/core/chart/charts/users.ts
@@ -5,13 +5,12 @@
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';
@@ -26,16 +25,14 @@ export default class UsersChart extends Chart { // eslint-disable
@Inject(DI.db)
private db: DataSource,
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
+ private appLockService: AppLockService,
private userEntityService: UserEntityService,
private chartLoggerService: ChartLoggerService,
) {
- super(db, (k) => acquireChartInsertLock(redisClient, k), chartLoggerService.logger, name, schema);
+ super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema);
}
protected async tickMajor(): Promise>> {
diff --git a/packages/backend/src/misc/distributed-lock.ts b/packages/backend/src/misc/distributed-lock.ts
deleted file mode 100644
index 93bd741f624..00000000000
--- a/packages/backend/src/misc/distributed-lock.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import * as Redis from 'ioredis';
-
-export async function acquireDistributedLock(
- redis: Redis.Redis,
- name: string,
- timeout: number,
- maxRetries: number,
- retryInterval: number,
-): Promise<() => Promise> {
- const lockKey = `lock:${name}`;
- const identifier = Math.random().toString(36).slice(2);
-
- let retries = 0;
- while (retries < maxRetries) {
- const result = await redis.set(lockKey, identifier, 'PX', timeout, 'NX');
- if (result === 'OK') {
- return async () => {
- const currentIdentifier = await redis.get(lockKey);
- if (currentIdentifier === identifier) {
- await redis.del(lockKey);
- }
- };
- }
-
- await new Promise(resolve => setTimeout(resolve, retryInterval));
- retries++;
- }
-
- throw new Error(`Failed to acquire lock ${name}`);
-}
-
-export function acquireApObjectLock(
- redis: Redis.Redis,
- uri: string,
-): Promise<() => Promise> {
- return acquireDistributedLock(redis, `ap-object:${uri}`, 30 * 1000, 50, 100);
-}
-
-export function acquireChartInsertLock(
- redis: Redis.Redis,
- name: string,
-): Promise<() => Promise> {
- return acquireDistributedLock(redis, `chart-insert:${name}`, 30 * 1000, 50, 500);
-}
diff --git a/packages/backend/src/misc/escape-html.ts b/packages/backend/src/misc/escape-html.ts
deleted file mode 100644
index 819aeeed525..00000000000
--- a/packages/backend/src/misc/escape-html.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function escapeHtml(text: string): string {
- return text
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
-}
diff --git a/packages/backend/src/misc/json-stringify-html-safe.ts b/packages/backend/src/misc/json-stringify-html-safe.ts
deleted file mode 100644
index aac12d57db0..00000000000
--- a/packages/backend/src/misc/json-stringify-html-safe.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-const ESCAPE_LOOKUP = {
- '&': '\\u0026',
- '>': '\\u003e',
- '<': '\\u003c',
- '\u2028': '\\u2028',
- '\u2029': '\\u2029',
-} as Record;
-
-const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
-
-export function htmlSafeJsonStringify(obj: any): string {
- return JSON.stringify(obj).replace(ESCAPE_REGEX, x => ESCAPE_LOOKUP[x]);
-}
diff --git a/packages/backend/src/misc/should-hide-note-by-time.ts b/packages/backend/src/misc/should-hide-note-by-time.ts
index ea1951e66cd..14304340942 100644
--- a/packages/backend/src/misc/should-hide-note-by-time.ts
+++ b/packages/backend/src/misc/should-hide-note-by-time.ts
@@ -20,10 +20,10 @@ export function shouldHideNoteByTime(hiddenBefore: number | null | undefined, cr
// 負の値: 作成からの経過時間(秒)で判定
const elapsedSeconds = (Date.now() - createdAtTime) / 1000;
const hideAfterSeconds = Math.abs(hiddenBefore);
- return elapsedSeconds >= hideAfterSeconds;
+ return elapsedSeconds > hideAfterSeconds;
} else {
// 正の値: 絶対的なタイムスタンプ(秒)で判定
const createdAtSeconds = createdAtTime / 1000;
- return createdAtSeconds <= hiddenBefore;
+ return createdAtSeconds < hiddenBefore;
}
}
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index 2b3b3fc0add..642d3fc8ad3 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -5,6 +5,7 @@
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';
@@ -156,13 +157,6 @@ export class QueueProcessorService implements OnApplicationShutdown {
};
}
- let Sentry: typeof import('@sentry/node') | undefined;
- if (this.config.sentryForBackend) {
- import('@sentry/node').then((mod) => {
- Sentry = mod;
- });
- }
-
//#region system
{
const processer = (job: Bull.Job) => {
@@ -181,7 +175,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
};
this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => {
- if (Sentry != null) {
+ if (this.config.sentryForBackend) {
return Sentry.startSpan({ name: 'Queue: System: ' + job.name }, () => processer(job));
} else {
return processer(job);
@@ -198,7 +192,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 (Sentry != null) {
+ if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -238,7 +232,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
};
this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => {
- if (Sentry != null) {
+ if (this.config.sentryForBackend) {
return Sentry.startSpan({ name: 'Queue: DB: ' + job.name }, () => processer(job));
} else {
return processer(job);
@@ -255,7 +249,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 (Sentry != null) {
+ if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -270,7 +264,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region deliver
{
this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => {
- if (Sentry != null) {
+ if (this.config.sentryForBackend) {
return Sentry.startSpan({ name: 'Queue: Deliver' }, () => this.deliverProcessorService.process(job));
} else {
return this.deliverProcessorService.process(job);
@@ -295,7 +289,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 (Sentry != null) {
+ if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -310,7 +304,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region inbox
{
this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => {
- if (Sentry != null) {
+ if (this.config.sentryForBackend) {
return Sentry.startSpan({ name: 'Queue: Inbox' }, () => this.inboxProcessorService.process(job));
} else {
return this.inboxProcessorService.process(job);
@@ -335,7 +329,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 (Sentry != null) {
+ if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -350,7 +344,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region user-webhook deliver
{
this.userWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.USER_WEBHOOK_DELIVER, (job) => {
- if (Sentry != null) {
+ if (this.config.sentryForBackend) {
return Sentry.startSpan({ name: 'Queue: UserWebhookDeliver' }, () => this.userWebhookDeliverProcessorService.process(job));
} else {
return this.userWebhookDeliverProcessorService.process(job);
@@ -375,7 +369,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 (Sentry != null) {
+ if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -390,7 +384,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region system-webhook deliver
{
this.systemWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.SYSTEM_WEBHOOK_DELIVER, (job) => {
- if (Sentry != null) {
+ if (this.config.sentryForBackend) {
return Sentry.startSpan({ name: 'Queue: SystemWebhookDeliver' }, () => this.systemWebhookDeliverProcessorService.process(job));
} else {
return this.systemWebhookDeliverProcessorService.process(job);
@@ -415,7 +409,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 (Sentry != null) {
+ if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -440,7 +434,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
};
this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
- if (Sentry != null) {
+ if (this.config.sentryForBackend) {
return Sentry.startSpan({ name: 'Queue: Relationship: ' + job.name }, () => processer(job));
} else {
return processer(job);
@@ -462,7 +456,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 (Sentry != null) {
+ if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -485,7 +479,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
};
this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => {
- if (Sentry != null) {
+ if (this.config.sentryForBackend) {
return Sentry.startSpan({ name: 'Queue: ObjectStorage: ' + job.name }, () => processer(job));
} else {
return processer(job);
@@ -503,7 +497,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 (Sentry != null) {
+ if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
@@ -518,7 +512,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region ended poll notification
{
this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => {
- if (Sentry != null) {
+ if (this.config.sentryForBackend) {
return Sentry.startSpan({ name: 'Queue: EndedPollNotification' }, () => this.endedPollNotificationProcessorService.process(job));
} else {
return this.endedPollNotificationProcessorService.process(job);
@@ -533,7 +527,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#region post scheduled note
{
this.postScheduledNoteQueueWorker = new Bull.Worker(QUEUE.POST_SCHEDULED_NOTE, async (job) => {
- if (Sentry != null) {
+ if (this.config.sentryForBackend) {
return Sentry.startSpan({ name: 'Queue: PostScheduledNote' }, () => this.postScheduledNoteProcessorService.process(job));
} else {
return this.postScheduledNoteProcessorService.process(job);
diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts
index 111421472d7..0223650329d 100644
--- a/packages/backend/src/server/ServerModule.ts
+++ b/packages/backend/src/server/ServerModule.ts
@@ -25,7 +25,6 @@ import { SignupApiService } from './api/SignupApiService.js';
import { StreamingApiServerService } from './api/StreamingApiServerService.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { ClientServerService } from './web/ClientServerService.js';
-import { HtmlTemplateService } from './web/HtmlTemplateService.js';
import { FeedService } from './web/FeedService.js';
import { UrlPreviewService } from './web/UrlPreviewService.js';
import { ClientLoggerService } from './web/ClientLoggerService.js';
@@ -59,7 +58,6 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
providers: [
ClientServerService,
ClientLoggerService,
- HtmlTemplateService,
FeedService,
HealthServerService,
UrlPreviewService,
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 4e05322b12d..1286b4dad67 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown {
@bindThis
public async launch(): Promise {
const fastify = Fastify({
- trustProxy: this.config.trustProxy ?? false,
+ trustProxy: this.config.trustProxy ?? true,
logger: false,
});
this.#fastify = fastify;
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 27c79ab4382..7a4af407a37 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -7,6 +7,7 @@ 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';
@@ -36,7 +37,6 @@ export class ApiCallService implements OnApplicationShutdown {
private logger: Logger;
private userIpHistories: Map>;
private userIpHistoriesClearIntervalId: NodeJS.Timeout;
- private Sentry: typeof import('@sentry/node') | null = null;
constructor(
@Inject(DI.meta)
@@ -59,12 +59,6 @@ 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 {
@@ -126,8 +120,8 @@ export class ApiCallService implements OnApplicationShutdown {
},
});
- if (this.Sentry != null) {
- this.Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
+ if (this.config.sentryForBackend) {
+ Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
level: 'error',
user: {
id: userId,
@@ -438,8 +432,8 @@ export class ApiCallService implements OnApplicationShutdown {
}
// API invoking
- if (this.Sentry != null) {
- return await this.Sentry.startSpan({
+ if (this.config.sentryForBackend) {
+ return await Sentry.startSpan({
name: 'API: ' + ep.name,
}, () => ep.exec(data, user, token, file, request.ip, request.headers)
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 9971a1ea4d3..113a09cb141 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -7,7 +7,7 @@ import RE2 from 're2';
import * as mfm from 'mfm-js';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
-import * as htmlParser from 'node-html-parser';
+import { JSDOM } from 'jsdom';
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,15 +569,16 @@ export default class extends Endpoint { // eslint-
try {
const html = await this.httpRequestService.getHtml(url);
- const doc = htmlParser.parse(html);
+ const { window } = new JSDOM(html);
+ const doc: Document = window.document;
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.attributes.href === myLink);
- const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.attributes.rel?.split(/\s+/).includes('me') && link.attributes.href === myLink);
+ const includesMyLink = aEls.some(a => a.href === myLink);
+ const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink);
if (includesMyLink || includesRelMeLinks) {
await this.userProfilesRepository.createQueryBuilder('profile').update()
@@ -587,6 +588,8 @@ export default class extends Endpoint { // eslint-
})
.execute();
}
+
+ window.close();
} catch (err) {
// なにもしない
}
diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts
index d2391c43aba..cdd7102666e 100644
--- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts
+++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts
@@ -6,15 +6,18 @@
import dns from 'node:dns/promises';
import { fileURLToPath } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
-import * as htmlParser from 'node-html-parser';
+import { JSDOM } from 'jsdom';
import httpLinkHeader from 'http-link-header';
import ipaddr from 'ipaddr.js';
import oauth2orize, { type OAuth2, AuthorizationError, ValidateFunctionArity2, OAuth2Req, MiddlewareRequest } from 'oauth2orize';
import oauth2Pkce from 'oauth2orize-pkce';
import fastifyCors from '@fastify/cors';
+import fastifyView from '@fastify/view';
+import pug from 'pug';
import bodyParser from 'body-parser';
import fastifyExpress from '@fastify/express';
import { verifyChallenge } from 'pkce-challenge';
+import { mf2 } from 'microformats-parser';
import { permissions as kinds } from 'misskey-js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
@@ -29,8 +32,6 @@ import { MemoryKVCache } from '@/misc/cache.js';
import { LoggerService } from '@/core/LoggerService.js';
import Logger from '@/logger.js';
import { StatusError } from '@/misc/status-error.js';
-import { HtmlTemplateService } from '@/server/web/HtmlTemplateService.js';
-import { OAuthPage } from '@/server/web/views/oauth.js';
import type { ServerResponse } from 'node:http';
import type { FastifyInstance } from 'fastify';
@@ -97,32 +98,6 @@ interface ClientInformation {
logo: string | null;
}
-function parseMicroformats(doc: htmlParser.HTMLElement, baseUrl: string, id: string): { name: string | null; logo: string | null; } {
- let name: string | null = null;
- let logo: string | null = null;
-
- const hApp = doc.querySelector('.h-app');
- if (hApp == null) return { name, logo };
-
- const nameEl = hApp.querySelector('.p-name');
- if (nameEl != null) {
- const href = nameEl.attributes.href || nameEl.attributes.src;
- if (href != null && new URL(href, baseUrl).toString() === new URL(id).toString()) {
- name = nameEl.textContent.trim();
- }
- }
-
- const logoEl = hApp.querySelector('.u-logo');
- if (logoEl != null) {
- const href = logoEl.attributes.href || logoEl.attributes.src;
- if (href != null) {
- logo = new URL(href, baseUrl).toString();
- }
- }
-
- return { name, logo };
-}
-
// https://indieauth.spec.indieweb.org/#client-information-discovery
// "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id,
// and if there is an [h-app] with a url property matching the client_id URL,
@@ -145,19 +120,24 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
}
const text = await res.text();
- const doc = htmlParser.parse(`${text}`);
+ const fragment = JSDOM.fragment(text);
- redirectUris.push(...[...doc.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.attributes.href));
+ redirectUris.push(...[...fragment.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.href));
let name = id;
let logo: string | null = null;
if (text) {
- const microformats = parseMicroformats(doc, res.url, id);
- if (typeof microformats.name === 'string') {
- name = microformats.name;
- }
- if (typeof microformats.logo === 'string') {
- logo = microformats.logo;
+ const microformats = mf2(text, { baseUrl: res.url });
+ const correspondingProperties = microformats.items.find(item => item.type?.includes('h-app') && item.properties.url.includes(id));
+ if (correspondingProperties) {
+ const nameProperty = correspondingProperties.properties.name?.[0];
+ if (typeof nameProperty === 'string') {
+ name = nameProperty;
+ }
+ const logoProperty = correspondingProperties.properties.logo?.[0];
+ if (typeof logoProperty === 'string') {
+ logo = logoProperty;
+ }
}
}
@@ -273,7 +253,6 @@ export class OAuth2ProviderService {
private usersRepository: UsersRepository,
private cacheService: CacheService,
loggerService: LoggerService,
- private htmlTemplateService: HtmlTemplateService,
) {
this.#logger = loggerService.getLogger('oauth');
@@ -407,16 +386,24 @@ export class OAuth2ProviderService {
this.#logger.info(`Rendering authorization page for "${oauth2.client.name}"`);
reply.header('Cache-Control', 'no-store');
- return await HtmlTemplateService.replyHtml(reply, OAuthPage({
- ...await this.htmlTemplateService.getCommonData(),
+ return await reply.view('oauth', {
transactionId: oauth2.transactionID,
clientName: oauth2.client.name,
- clientLogo: oauth2.client.logo ?? undefined,
- scope: oauth2.req.scope,
- }));
+ clientLogo: oauth2.client.logo,
+ scope: oauth2.req.scope.join(' '),
+ });
});
fastify.post('/decision', async () => { });
+ fastify.register(fastifyView, {
+ root: fileURLToPath(new URL('../web/views', import.meta.url)),
+ engine: { pug },
+ defaultContext: {
+ version: this.config.version,
+ config: this.config,
+ },
+ });
+
await fastify.register(fastifyExpress);
fastify.use('/authorize', this.#server.authorize(((areq, done) => {
(async (): Promise> => {
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index bcea9354098..f9d904f3cd4 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -9,16 +9,21 @@ import { fileURLToPath } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import sharp from 'sharp';
+import pug from 'pug';
import { In, IsNull } from 'typeorm';
import fastifyStatic from '@fastify/static';
+import fastifyView from '@fastify/view';
import fastifyProxy from '@fastify/http-proxy';
import vary from 'vary';
+import htmlSafeJsonStringify from 'htmlescape';
import type { Config } from '@/config.js';
+import { getNoteSummary } from '@/misc/get-note-summary.js';
import { DI } from '@/di-symbols.js';
import * as Acct from '@/misc/acct.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js';
+import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
@@ -37,33 +42,14 @@ import type {
} from '@/models/_.js';
import type Logger from '@/logger.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
-import { htmlSafeJsonStringify } from '@/misc/json-stringify-html-safe.js';
import { bindThis } from '@/decorators.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
+import { RoleService } from '@/core/RoleService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { FeedService } from './FeedService.js';
import { UrlPreviewService } from './UrlPreviewService.js';
import { ClientLoggerService } from './ClientLoggerService.js';
-import { HtmlTemplateService } from './HtmlTemplateService.js';
-
-import { BasePage } from './views/base.js';
-import { UserPage } from './views/user.js';
-import { NotePage } from './views/note.js';
-import { PagePage } from './views/page.js';
-import { ClipPage } from './views/clip.js';
-import { FlashPage } from './views/flash.js';
-import { GalleryPostPage } from './views/gallery-post.js';
-import { ChannelPage } from './views/channel.js';
-import { ReversiGamePage } from './views/reversi-game.js';
-import { AnnouncementPage } from './views/announcement.js';
-import { BaseEmbed } from './views/base-embed.js';
-import { InfoCardPage } from './views/info-card.js';
-import { BiosPage } from './views/bios.js';
-import { CliPage } from './views/cli.js';
-import { FlushPage } from './views/flush.js';
-import { ErrorPage } from './views/error.js';
-
import type { FastifyError, FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify';
const _filename = fileURLToPath(import.meta.url);
@@ -122,6 +108,7 @@ export class ClientServerService {
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private pageEntityService: PageEntityService,
+ private metaEntityService: MetaEntityService,
private galleryPostEntityService: GalleryPostEntityService,
private clipEntityService: ClipEntityService,
private channelEntityService: ChannelEntityService,
@@ -129,7 +116,7 @@ export class ClientServerService {
private announcementEntityService: AnnouncementEntityService,
private urlPreviewService: UrlPreviewService,
private feedService: FeedService,
- private htmlTemplateService: HtmlTemplateService,
+ private roleService: RoleService,
private clientLoggerService: ClientLoggerService,
) {
//this.createServer = this.createServer.bind(this);
@@ -195,10 +182,38 @@ export class ClientServerService {
return (manifest);
}
+ @bindThis
+ private async generateCommonPugData(meta: MiMeta) {
+ return {
+ instanceName: meta.name ?? 'Misskey',
+ icon: meta.iconUrl,
+ appleTouchIcon: meta.app512IconUrl,
+ themeColor: meta.themeColor,
+ serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg',
+ infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',
+ notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg',
+ instanceUrl: this.config.url,
+ metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)),
+ now: Date.now(),
+ federationEnabled: this.meta.federation !== 'none',
+ };
+ }
+
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
const configUrl = new URL(this.config.url);
+ fastify.register(fastifyView, {
+ root: _dirname + '/views',
+ engine: {
+ pug: pug,
+ },
+ defaultContext: {
+ version: this.config.version,
+ config: this.config,
+ },
+ });
+
fastify.addHook('onRequest', (request, reply, done) => {
// クリックジャッキング防止のためiFrameの中に入れられないようにする
reply.header('X-Frame-Options', 'DENY');
@@ -399,15 +414,16 @@ export class ClientServerService {
//#endregion
- const renderBase = async (reply: FastifyReply, data: Partial[0]> = {}) => {
+ const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => {
reply.header('Cache-Control', 'public, max-age=30');
- return await HtmlTemplateService.replyHtml(reply, BasePage({
- img: this.meta.bannerUrl ?? undefined,
+ return await reply.view('base', {
+ img: this.meta.bannerUrl,
+ url: this.config.url,
title: this.meta.name ?? 'Misskey',
- desc: this.meta.description ?? undefined,
- ...await this.htmlTemplateService.getCommonData(),
+ desc: this.meta.description,
+ ...await this.generateCommonPugData(this.meta),
...data,
- }));
+ });
};
// URL preview endpoint
@@ -489,6 +505,11 @@ export class ClientServerService {
)
) {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
+ const me = profile.fields
+ ? profile.fields
+ .filter(filed => filed.value != null && filed.value.match(/^https?:/))
+ .map(field => field.value)
+ : [];
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
@@ -501,15 +522,15 @@ export class ClientServerService {
userProfile: profile,
});
- return await HtmlTemplateService.replyHtml(reply, UserPage({
- user: _user,
- profile,
+ return await reply.view('user', {
+ user, profile, me,
+ avatarUrl: _user.avatarUrl,
sub: request.params.sub,
- ...await this.htmlTemplateService.getCommonData(),
- clientCtxJson: htmlSafeJsonStringify({
+ ...await this.generateCommonPugData(this.meta),
+ clientCtx: htmlSafeJsonStringify({
user: _user,
}),
- }));
+ });
} else {
// リモートユーザーなので
// モデレータがAPI経由で参照可能にするために404にはしない
@@ -560,14 +581,17 @@ export class ClientServerService {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
- return await HtmlTemplateService.replyHtml(reply, NotePage({
+ return await reply.view('note', {
note: _note,
profile,
- ...await this.htmlTemplateService.getCommonData(),
- clientCtxJson: htmlSafeJsonStringify({
+ avatarUrl: _note.user.avatarUrl,
+ // TODO: Let locale changeable by instance setting
+ summary: getNoteSummary(_note),
+ ...await this.generateCommonPugData(this.meta),
+ clientCtx: htmlSafeJsonStringify({
note: _note,
}),
- }));
+ });
} else {
return await renderBase(reply);
}
@@ -600,11 +624,12 @@ export class ClientServerService {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
- return await HtmlTemplateService.replyHtml(reply, PagePage({
+ return await reply.view('page', {
page: _page,
profile,
- ...await this.htmlTemplateService.getCommonData(),
- }));
+ avatarUrl: _page.user.avatarUrl,
+ ...await this.generateCommonPugData(this.meta),
+ });
} else {
return await renderBase(reply);
}
@@ -624,11 +649,12 @@ export class ClientServerService {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
- return await HtmlTemplateService.replyHtml(reply, FlashPage({
+ return await reply.view('flash', {
flash: _flash,
profile,
- ...await this.htmlTemplateService.getCommonData(),
- }));
+ avatarUrl: _flash.user.avatarUrl,
+ ...await this.generateCommonPugData(this.meta),
+ });
} else {
return await renderBase(reply);
}
@@ -648,14 +674,15 @@ export class ClientServerService {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
- return await HtmlTemplateService.replyHtml(reply, ClipPage({
+ return await reply.view('clip', {
clip: _clip,
profile,
- ...await this.htmlTemplateService.getCommonData(),
- clientCtxJson: htmlSafeJsonStringify({
+ avatarUrl: _clip.user.avatarUrl,
+ ...await this.generateCommonPugData(this.meta),
+ clientCtx: htmlSafeJsonStringify({
clip: _clip,
}),
- }));
+ });
} else {
return await renderBase(reply);
}
@@ -673,11 +700,12 @@ export class ClientServerService {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
- return await HtmlTemplateService.replyHtml(reply, GalleryPostPage({
- galleryPost: _post,
+ return await reply.view('gallery-post', {
+ post: _post,
profile,
- ...await this.htmlTemplateService.getCommonData(),
- }));
+ avatarUrl: _post.user.avatarUrl,
+ ...await this.generateCommonPugData(this.meta),
+ });
} else {
return await renderBase(reply);
}
@@ -692,10 +720,10 @@ export class ClientServerService {
if (channel) {
const _channel = await this.channelEntityService.pack(channel);
reply.header('Cache-Control', 'public, max-age=15');
- return await HtmlTemplateService.replyHtml(reply, ChannelPage({
+ return await reply.view('channel', {
channel: _channel,
- ...await this.htmlTemplateService.getCommonData(),
- }));
+ ...await this.generateCommonPugData(this.meta),
+ });
} else {
return await renderBase(reply);
}
@@ -710,10 +738,10 @@ export class ClientServerService {
if (game) {
const _game = await this.reversiGameEntityService.packDetail(game);
reply.header('Cache-Control', 'public, max-age=3600');
- return await HtmlTemplateService.replyHtml(reply, ReversiGamePage({
- reversiGame: _game,
- ...await this.htmlTemplateService.getCommonData(),
- }));
+ return await reply.view('reversi-game', {
+ game: _game,
+ ...await this.generateCommonPugData(this.meta),
+ });
} else {
return await renderBase(reply);
}
@@ -729,10 +757,10 @@ export class ClientServerService {
if (announcement) {
const _announcement = await this.announcementEntityService.pack(announcement);
reply.header('Cache-Control', 'public, max-age=3600');
- return await HtmlTemplateService.replyHtml(reply, AnnouncementPage({
+ return await reply.view('announcement', {
announcement: _announcement,
- ...await this.htmlTemplateService.getCommonData(),
- }));
+ ...await this.generateCommonPugData(this.meta),
+ });
} else {
return await renderBase(reply);
}
@@ -765,13 +793,13 @@ export class ClientServerService {
const _user = await this.userEntityService.pack(user);
reply.header('Cache-Control', 'public, max-age=3600');
- return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
+ return await reply.view('base-embed', {
title: this.meta.name ?? 'Misskey',
- ...await this.htmlTemplateService.getCommonData(),
- embedCtxJson: htmlSafeJsonStringify({
+ ...await this.generateCommonPugData(this.meta),
+ embedCtx: htmlSafeJsonStringify({
user: _user,
}),
- }));
+ });
});
fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => {
@@ -791,13 +819,13 @@ export class ClientServerService {
const _note = await this.noteEntityService.pack(note, null, { detail: true });
reply.header('Cache-Control', 'public, max-age=3600');
- return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
+ return await reply.view('base-embed', {
title: this.meta.name ?? 'Misskey',
- ...await this.htmlTemplateService.getCommonData(),
- embedCtxJson: htmlSafeJsonStringify({
+ ...await this.generateCommonPugData(this.meta),
+ embedCtx: htmlSafeJsonStringify({
note: _note,
}),
- }));
+ });
});
fastify.get<{ Params: { clip: string; } }>('/embed/clips/:clip', async (request, reply) => {
@@ -812,46 +840,48 @@ export class ClientServerService {
const _clip = await this.clipEntityService.pack(clip);
reply.header('Cache-Control', 'public, max-age=3600');
- return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
+ return await reply.view('base-embed', {
title: this.meta.name ?? 'Misskey',
- ...await this.htmlTemplateService.getCommonData(),
- embedCtxJson: htmlSafeJsonStringify({
+ ...await this.generateCommonPugData(this.meta),
+ embedCtx: htmlSafeJsonStringify({
clip: _clip,
}),
- }));
+ });
});
fastify.get('/embed/*', async (request, reply) => {
reply.removeHeader('X-Frame-Options');
reply.header('Cache-Control', 'public, max-age=3600');
- return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
+ return await reply.view('base-embed', {
title: this.meta.name ?? 'Misskey',
- ...await this.htmlTemplateService.getCommonData(),
- }));
+ ...await this.generateCommonPugData(this.meta),
+ });
});
fastify.get('/_info_card_', async (request, reply) => {
reply.removeHeader('X-Frame-Options');
- return await HtmlTemplateService.replyHtml(reply, InfoCardPage({
+ return await reply.view('info-card', {
version: this.config.version,
- config: this.config,
+ host: this.config.host,
meta: this.meta,
- }));
+ originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }),
+ originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }),
+ });
});
//#endregion
fastify.get('/bios', async (request, reply) => {
- return await HtmlTemplateService.replyHtml(reply, BiosPage({
+ return await reply.view('bios', {
version: this.config.version,
- }));
+ });
});
fastify.get('/cli', async (request, reply) => {
- return await HtmlTemplateService.replyHtml(reply, CliPage({
+ return await reply.view('cli', {
version: this.config.version,
- }));
+ });
});
const override = (source: string, target: string, depth = 0) =>
@@ -874,7 +904,7 @@ export class ClientServerService {
reply.header('Clear-Site-Data', '"*"');
}
reply.header('Set-Cookie', 'http-flush-failed=1; Path=/flush; Max-Age=60');
- return await HtmlTemplateService.replyHtml(reply, FlushPage());
+ return await reply.view('flush');
});
// streamingに非WebSocketリクエストが来た場合にbase htmlをキャシュ付きで返すと、Proxy等でそのパスがキャッシュされておかしくなる
@@ -900,10 +930,10 @@ export class ClientServerService {
});
reply.code(500);
reply.header('Cache-Control', 'max-age=10, must-revalidate');
- return await HtmlTemplateService.replyHtml(reply, ErrorPage({
+ return await reply.view('error', {
code: error.code,
id: errId,
- }));
+ });
});
done();
diff --git a/packages/backend/src/server/web/HtmlTemplateService.ts b/packages/backend/src/server/web/HtmlTemplateService.ts
deleted file mode 100644
index 8ff985530de..00000000000
--- a/packages/backend/src/server/web/HtmlTemplateService.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { dirname } from 'node:path';
-import { fileURLToPath } from 'node:url';
-import { promises as fsp } from 'node:fs';
-import { languages } from 'i18n/const';
-import { Injectable, Inject } from '@nestjs/common';
-import { DI } from '@/di-symbols.js';
-import { bindThis } from '@/decorators.js';
-import { htmlSafeJsonStringify } from '@/misc/json-stringify-html-safe.js';
-import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
-import type { FastifyReply } from 'fastify';
-import type { Config } from '@/config.js';
-import type { MiMeta } from '@/models/Meta.js';
-import type { CommonData } from './views/_.js';
-
-const _filename = fileURLToPath(import.meta.url);
-const _dirname = dirname(_filename);
-
-const frontendVitePublic = `${_dirname}/../../../../frontend/public/`;
-const frontendEmbedVitePublic = `${_dirname}/../../../../frontend-embed/public/`;
-
-@Injectable()
-export class HtmlTemplateService {
- private frontendBootloadersFetched = false;
- public frontendBootloaderJs: string | null = null;
- public frontendBootloaderCss: string | null = null;
- public frontendEmbedBootloaderJs: string | null = null;
- public frontendEmbedBootloaderCss: string | null = null;
-
- constructor(
- @Inject(DI.config)
- private config: Config,
-
- @Inject(DI.meta)
- private meta: MiMeta,
-
- private metaEntityService: MetaEntityService,
- ) {
- }
-
- @bindThis
- private async prepareFrontendBootloaders() {
- if (this.frontendBootloadersFetched) return;
- this.frontendBootloadersFetched = true;
-
- const [bootJs, bootCss, embedBootJs, embedBootCss] = await Promise.all([
- fsp.readFile(`${frontendVitePublic}loader/boot.js`, 'utf-8').catch(() => null),
- fsp.readFile(`${frontendVitePublic}loader/style.css`, 'utf-8').catch(() => null),
- fsp.readFile(`${frontendEmbedVitePublic}loader/boot.js`, 'utf-8').catch(() => null),
- fsp.readFile(`${frontendEmbedVitePublic}loader/style.css`, 'utf-8').catch(() => null),
- ]);
-
- if (bootJs != null) {
- this.frontendBootloaderJs = bootJs;
- }
-
- if (bootCss != null) {
- this.frontendBootloaderCss = bootCss;
- }
-
- if (embedBootJs != null) {
- this.frontendEmbedBootloaderJs = embedBootJs;
- }
-
- if (embedBootCss != null) {
- this.frontendEmbedBootloaderCss = embedBootCss;
- }
- }
-
- @bindThis
- public async getCommonData(): Promise {
- await this.prepareFrontendBootloaders();
-
- return {
- version: this.config.version,
- config: this.config,
- langs: [...languages],
- instanceName: this.meta.name ?? 'Misskey',
- icon: this.meta.iconUrl,
- appleTouchIcon: this.meta.app512IconUrl,
- themeColor: this.meta.themeColor,
- serverErrorImageUrl: this.meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg',
- infoImageUrl: this.meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',
- notFoundImageUrl: this.meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg',
- instanceUrl: this.config.url,
- metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(this.meta)),
- now: Date.now(),
- federationEnabled: this.meta.federation !== 'none',
- frontendBootloaderJs: this.frontendBootloaderJs,
- frontendBootloaderCss: this.frontendBootloaderCss,
- frontendEmbedBootloaderJs: this.frontendEmbedBootloaderJs,
- frontendEmbedBootloaderCss: this.frontendEmbedBootloaderCss,
- };
- }
-
- public static async replyHtml(reply: FastifyReply, html: string | Promise) {
- reply.header('Content-Type', 'text/html; charset=utf-8');
- const _html = await html;
- return reply.send(_html);
- }
-}
diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts
index bd1dbb430c9..b9a40150311 100644
--- a/packages/backend/src/server/web/UrlPreviewService.ts
+++ b/packages/backend/src/server/web/UrlPreviewService.ts
@@ -4,7 +4,8 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import type { SummalyResult } from '@misskey-dev/summaly/built/summary.js';
+import { summaly } from '@misskey-dev/summaly';
+import { SummalyResult } from '@misskey-dev/summaly/built/summary.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
@@ -112,7 +113,7 @@ export class UrlPreviewService {
}
}
- private async fetchSummary(url: string, meta: MiMeta, lang?: string): Promise {
+ private fetchSummary(url: string, meta: MiMeta, lang?: string): Promise {
const agent = this.config.proxy
? {
http: this.httpRequestService.httpAgent,
@@ -120,8 +121,6 @@ export class UrlPreviewService {
}
: undefined;
- const { summaly } = await import('@misskey-dev/summaly');
-
return summaly(url, {
followRedirects: this.meta.urlPreviewAllowRedirect,
lang: lang ?? 'ja-JP',
diff --git a/packages/backend/assets/misc/bios.css b/packages/backend/src/server/web/bios.css
similarity index 100%
rename from packages/backend/assets/misc/bios.css
rename to packages/backend/src/server/web/bios.css
diff --git a/packages/backend/assets/misc/bios.js b/packages/backend/src/server/web/bios.js
similarity index 100%
rename from packages/backend/assets/misc/bios.js
rename to packages/backend/src/server/web/bios.js
diff --git a/packages/frontend-embed/public/loader/boot.js b/packages/backend/src/server/web/boot.embed.js
similarity index 99%
rename from packages/frontend-embed/public/loader/boot.js
rename to packages/backend/src/server/web/boot.embed.js
index 9b3d27873be..ba6366b3db5 100644
--- a/packages/frontend-embed/public/loader/boot.js
+++ b/packages/backend/src/server/web/boot.embed.js
@@ -70,8 +70,6 @@
importAppScript();
});
}
-
- localStorage.setItem('lang', lang);
//#endregion
async function addStyle(styleText) {
diff --git a/packages/frontend/public/loader/boot.js b/packages/backend/src/server/web/boot.js
similarity index 99%
rename from packages/frontend/public/loader/boot.js
rename to packages/backend/src/server/web/boot.js
index 8aafb282aa6..ab4b1582877 100644
--- a/packages/frontend/public/loader/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -42,8 +42,6 @@
console.error('invalid lang value detected!!!', typeof lang, lang);
lang = 'en-US';
}
-
- localStorage.setItem('lang', lang);
//#endregion
//#region Script
diff --git a/packages/backend/assets/misc/cli.css b/packages/backend/src/server/web/cli.css
similarity index 100%
rename from packages/backend/assets/misc/cli.css
rename to packages/backend/src/server/web/cli.css
diff --git a/packages/backend/assets/misc/cli.js b/packages/backend/src/server/web/cli.js
similarity index 100%
rename from packages/backend/assets/misc/cli.js
rename to packages/backend/src/server/web/cli.js
diff --git a/packages/backend/assets/misc/error.css b/packages/backend/src/server/web/error.css
similarity index 100%
rename from packages/backend/assets/misc/error.css
rename to packages/backend/src/server/web/error.css
diff --git a/packages/backend/assets/misc/error.js b/packages/backend/src/server/web/error.js
similarity index 100%
rename from packages/backend/assets/misc/error.js
rename to packages/backend/src/server/web/error.js
diff --git a/packages/frontend/public/loader/style.css b/packages/backend/src/server/web/style.css
similarity index 100%
rename from packages/frontend/public/loader/style.css
rename to packages/backend/src/server/web/style.css
diff --git a/packages/frontend-embed/public/loader/style.css b/packages/backend/src/server/web/style.embed.css
similarity index 100%
rename from packages/frontend-embed/public/loader/style.css
rename to packages/backend/src/server/web/style.embed.css
diff --git a/packages/backend/src/server/web/views/_.ts b/packages/backend/src/server/web/views/_.ts
deleted file mode 100644
index ac7418f3625..00000000000
--- a/packages/backend/src/server/web/views/_.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { Config } from '@/config.js';
-
-export const comment = ``;
-
-export const defaultDescription = '✨🌎✨ A interplanetary communication platform ✨🚀✨';
-
-export type MinimumCommonData = {
- version: string;
- config: Config;
-};
-
-export type CommonData = MinimumCommonData & {
- langs: string[];
- instanceName: string;
- icon: string | null;
- appleTouchIcon: string | null;
- themeColor: string | null;
- serverErrorImageUrl: string;
- infoImageUrl: string;
- notFoundImageUrl: string;
- instanceUrl: string;
- now: number;
- federationEnabled: boolean;
- frontendBootloaderJs: string | null;
- frontendBootloaderCss: string | null;
- frontendEmbedBootloaderJs: string | null;
- frontendEmbedBootloaderCss: string | null;
- metaJson?: string;
- clientCtxJson?: string;
-};
-
-export type CommonPropsMinimum> = MinimumCommonData & T;
-
-export type CommonProps> = CommonData & T;
diff --git a/packages/backend/src/server/web/views/_splash.tsx b/packages/backend/src/server/web/views/_splash.tsx
deleted file mode 100644
index ea79b8d61d7..00000000000
--- a/packages/backend/src/server/web/views/_splash.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function Splash(props: {
- icon?: string | null;
-}) {
- return (
-
-
-
-
-
-
-
- );
-}
diff --git a/packages/backend/src/server/web/views/announcement.pug b/packages/backend/src/server/web/views/announcement.pug
new file mode 100644
index 00000000000..7a4052e8a45
--- /dev/null
+++ b/packages/backend/src/server/web/views/announcement.pug
@@ -0,0 +1,21 @@
+extends ./base
+
+block vars
+ - const title = announcement.title;
+ - const description = announcement.text.length > 100 ? announcement.text.slice(0, 100) + '…' : announcement.text;
+ - const url = `${config.url}/announcements/${announcement.id}`;
+
+block title
+ = `${title} | ${instanceName}`
+
+block desc
+ meta(name='description' content=description)
+
+block og
+ meta(property='og:type' content='article')
+ meta(property='og:title' content= title)
+ meta(property='og:description' content= description)
+ meta(property='og:url' content= url)
+ if announcement.imageUrl
+ meta(property='og:image' content=announcement.imageUrl)
+ meta(property='twitter:card' content='summary_large_image')
diff --git a/packages/backend/src/server/web/views/announcement.tsx b/packages/backend/src/server/web/views/announcement.tsx
deleted file mode 100644
index bc1c8081771..00000000000
--- a/packages/backend/src/server/web/views/announcement.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { Packed } from '@/misc/json-schema.js';
-import type { CommonProps } from '@/server/web/views/_.js';
-import { Layout } from '@/server/web/views/base.js';
-
-export function AnnouncementPage(props: CommonProps<{
- announcement: Packed<'Announcement'>;
-}>) {
- const description = props.announcement.text.length > 100 ? props.announcement.text.slice(0, 100) + '…' : props.announcement.text;
-
- function ogBlock() {
- return (
- <>
-
-
-
-
- {props.announcement.imageUrl ? (
- <>
-
-
- >
- ) : null}
- >
- );
- }
-
- return (
-
-
- );
-}
diff --git a/packages/backend/src/server/web/views/base-embed.pug b/packages/backend/src/server/web/views/base-embed.pug
new file mode 100644
index 00000000000..29de86b8b6d
--- /dev/null
+++ b/packages/backend/src/server/web/views/base-embed.pug
@@ -0,0 +1,71 @@
+block vars
+
+block loadClientEntry
+ - const entry = config.frontendEmbedEntry;
+
+doctype html
+
+html(class='embed')
+
+ head
+ meta(charset='utf-8')
+ meta(name='application-name' content='Misskey')
+ meta(name='referrer' content='origin')
+ meta(name='theme-color' content= themeColor || '#86b300')
+ meta(name='theme-color-orig' content= themeColor || '#86b300')
+ meta(property='og:site_name' content= instanceName || 'Misskey')
+ meta(property='instance_url' content= instanceUrl)
+ meta(name='viewport' content='width=device-width, initial-scale=1')
+ meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no')
+ link(rel='icon' href= icon || '/favicon.ico')
+ link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
+
+ if !config.frontendEmbedManifestExists
+ script(type="module" src="/embed_vite/@vite/client")
+
+ if Array.isArray(entry.css)
+ each href in entry.css
+ link(rel='stylesheet' href=`/embed_vite/${href}`)
+
+ title
+ block title
+ = title || 'Misskey'
+
+ block meta
+ meta(name='robots' content='noindex')
+
+ style
+ include ../style.embed.css
+
+ script.
+ var VERSION = "#{version}";
+ var CLIENT_ENTRY = !{JSON.stringify(entry.file)};
+
+ script(type='application/json' id='misskey_meta' data-generated-at=now)
+ != metaJson
+
+ script(type='application/json' id='misskey_embedCtx' data-generated-at=now)
+ != embedCtx
+
+ script
+ include ../boot.embed.js
+
+ body
+ noscript: p
+ | JavaScriptを有効にしてください
+ br
+ | Please turn on your JavaScript
+ div#splash
+ img#splashIcon(src= icon || '/static-assets/splash.png')
+ div#splashSpinner
+
+
+ block content
diff --git a/packages/backend/src/server/web/views/base-embed.tsx b/packages/backend/src/server/web/views/base-embed.tsx
deleted file mode 100644
index 011b66592e8..00000000000
--- a/packages/backend/src/server/web/views/base-embed.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { comment } from '@/server/web/views/_.js';
-import type { CommonProps } from '@/server/web/views/_.js';
-import { Splash } from '@/server/web/views/_splash.js';
-import type { PropsWithChildren, Children } from '@kitajs/html';
-
-export function BaseEmbed(props: PropsWithChildren>) {
- const now = Date.now();
-
- // 変数名をsafeで始めることでエラーをスキップ
- const safeMetaJson = props.metaJson;
- const safeEmbedCtxJson = props.embedCtxJson;
-
- return (
- <>
- {''}
- {comment}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {!props.config.frontendEmbedManifestExists ? : null}
-
- {props.config.frontendEmbedEntry.css != null ? props.config.frontendEmbedEntry.css.map((href) => (
-
- )) : null}
-
- {props.titleSlot ?? {props.title || 'Misskey'} }
-
- {props.metaSlot}
-
-
-
- {props.frontendEmbedBootloaderCss != null ? : }
-
-
-
- {safeMetaJson != null ? : null}
- {safeEmbedCtxJson != null ? : null}
-
- {props.frontendEmbedBootloaderJs != null ? : }
-
-
-
-
- {props.children}
-
-
- >
- );
-}
-
diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug
new file mode 100644
index 00000000000..46b365a9c7f
--- /dev/null
+++ b/packages/backend/src/server/web/views/base.pug
@@ -0,0 +1,100 @@
+block vars
+
+block loadClientEntry
+ - const entry = config.frontendEntry;
+ - const baseUrl = config.url;
+
+doctype html
+
+//
+ -
+ _____ _ _
+ | |_|___ ___| |_ ___ _ _
+ | | | | |_ -|_ -| '_| -_| | |
+ |_|_|_|_|___|___|_,_|___|_ |
+ |___|
+ Thank you for using Misskey!
+ If you are reading this message... how about joining the development?
+ https://github.com/misskey-dev/misskey
+
+
+html
+
+ head
+ meta(charset='utf-8')
+ meta(name='application-name' content='Misskey')
+ meta(name='referrer' content='origin')
+ meta(name='theme-color' content= themeColor || '#86b300')
+ meta(name='theme-color-orig' content= themeColor || '#86b300')
+ meta(property='og:site_name' content= instanceName || 'Misskey')
+ meta(property='instance_url' content= instanceUrl)
+ meta(name='viewport' content='width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover')
+ meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no')
+ link(rel='icon' href= icon || '/favicon.ico')
+ link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
+ link(rel='manifest' href='/manifest.json')
+ link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${baseUrl}/opensearch.xml`)
+ link(rel='prefetch' href=serverErrorImageUrl)
+ link(rel='prefetch' href=infoImageUrl)
+ link(rel='prefetch' href=notFoundImageUrl)
+
+ if !config.frontendManifestExists
+ script(type="module" src="/vite/@vite/client")
+
+ if Array.isArray(entry.css)
+ each href in entry.css
+ link(rel='stylesheet' href=`/vite/${href}`)
+
+ title
+ block title
+ = title || 'Misskey'
+
+ if noindex
+ meta(name='robots' content='noindex')
+
+ block desc
+ meta(name='description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
+
+ block meta
+
+ block og
+ meta(property='og:title' content= title || 'Misskey')
+ meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
+ meta(property='og:image' content= img)
+ meta(property='twitter:card' content='summary')
+
+ style
+ include ../style.css
+
+ script.
+ var VERSION = "#{version}";
+ var CLIENT_ENTRY = !{JSON.stringify(entry.file)};
+
+ script(type='application/json' id='misskey_meta' data-generated-at=now)
+ != metaJson
+
+ script(type='application/json' id='misskey_clientCtx' data-generated-at=now)
+ != clientCtx
+
+ script
+ include ../boot.js
+
+ body
+ noscript: p
+ | JavaScriptを有効にしてください
+ br
+ | Please turn on your JavaScript
+ div#splash
+ img#splashIcon(src= icon || '/static-assets/splash.png')
+ div#splashSpinner
+
+
+ block content
diff --git a/packages/backend/src/server/web/views/base.tsx b/packages/backend/src/server/web/views/base.tsx
deleted file mode 100644
index 6fa3395fb8c..00000000000
--- a/packages/backend/src/server/web/views/base.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { comment, defaultDescription } from '@/server/web/views/_.js';
-import { Splash } from '@/server/web/views/_splash.js';
-import type { CommonProps } from '@/server/web/views/_.js';
-import type { PropsWithChildren, Children } from '@kitajs/html';
-
-export function Layout(props: PropsWithChildren>) {
- const now = Date.now();
-
- // 変数名をsafeで始めることでエラーをスキップ
- const safeMetaJson = props.metaJson;
- const safeClientCtxJson = props.clientCtxJson;
-
- return (
- <>
- {''}
- {comment}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {props.serverErrorImageUrl != null ? : null}
- {props.infoImageUrl != null ? : null}
- {props.notFoundImageUrl != null ? : null}
-
- {!props.config.frontendManifestExists ? : null}
-
- {props.config.frontendEntry.css != null ? props.config.frontendEntry.css.map((href) => (
-
- )) : null}
-
- {props.titleSlot ?? {props.title || 'Misskey'} }
-
- {props.noindex ? : null}
-
- {props.descSlot ?? (props.desc != null ? : null)}
-
- {props.metaSlot}
-
- {props.ogSlot ?? (
- <>
-
-
- {props.img != null ? : null}
-
- >
- )}
-
- {props.frontendBootloaderCss != null ? : }
-
-
-
- {safeMetaJson != null ? : null}
- {safeClientCtxJson != null ? : null}
-
- {props.frontendBootloaderJs != null ? : }
-
-
-
-
- {props.children}
-
-
- >
- );
-}
-
-export { Layout as BasePage };
-
diff --git a/packages/backend/src/server/web/views/bios.pug b/packages/backend/src/server/web/views/bios.pug
new file mode 100644
index 00000000000..39a151a29b7
--- /dev/null
+++ b/packages/backend/src/server/web/views/bios.pug
@@ -0,0 +1,20 @@
+doctype html
+
+html
+
+ head
+ meta(charset='utf-8')
+ meta(name='application-name' content='Misskey')
+ title Misskey Repair Tool
+ style
+ include ../bios.css
+ script
+ include ../bios.js
+
+ body
+ header
+ h1 Misskey Repair Tool #{version}
+ main
+ div.tabs
+ button#ls edit local storage
+ div#content
diff --git a/packages/backend/src/server/web/views/bios.tsx b/packages/backend/src/server/web/views/bios.tsx
deleted file mode 100644
index 9010de8d75d..00000000000
--- a/packages/backend/src/server/web/views/bios.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function BiosPage(props: {
- version: string;
-}) {
- return (
- <>
- {''}
-
-
-
-
- Misskey Repair Tool
-
-
-
-
-
- Misskey Repair Tool {props.version}
-
-
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/packages/backend/src/server/web/views/channel.pug b/packages/backend/src/server/web/views/channel.pug
new file mode 100644
index 00000000000..c514025e0b4
--- /dev/null
+++ b/packages/backend/src/server/web/views/channel.pug
@@ -0,0 +1,19 @@
+extends ./base
+
+block vars
+ - const title = channel.name;
+ - const url = `${config.url}/channels/${channel.id}`;
+
+block title
+ = `${title} | ${instanceName}`
+
+block desc
+ meta(name='description' content= channel.description)
+
+block og
+ meta(property='og:type' content='article')
+ meta(property='og:title' content= title)
+ meta(property='og:description' content= channel.description)
+ meta(property='og:url' content= url)
+ meta(property='og:image' content= channel.bannerUrl)
+ meta(property='twitter:card' content='summary')
diff --git a/packages/backend/src/server/web/views/channel.tsx b/packages/backend/src/server/web/views/channel.tsx
deleted file mode 100644
index 7d8123ea85d..00000000000
--- a/packages/backend/src/server/web/views/channel.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { Packed } from '@/misc/json-schema.js';
-import type { CommonProps } from '@/server/web/views/_.js';
-import { Layout } from '@/server/web/views/base.js';
-
-export function ChannelPage(props: CommonProps<{
- channel: Packed<'Channel'>;
-}>) {
-
- function ogBlock() {
- return (
- <>
-
-
- {props.channel.description != null ? : null}
-
- {props.channel.bannerUrl ? (
- <>
-
-
- >
- ) : null}
- >
- );
- }
-
- return (
-
-
- );
-}
diff --git a/packages/backend/src/server/web/views/cli.pug b/packages/backend/src/server/web/views/cli.pug
new file mode 100644
index 00000000000..d2cf7c4335d
--- /dev/null
+++ b/packages/backend/src/server/web/views/cli.pug
@@ -0,0 +1,21 @@
+doctype html
+
+html
+
+ head
+ meta(charset='utf-8')
+ meta(name='application-name' content='Misskey')
+ title Misskey Cli
+ style
+ include ../cli.css
+ script
+ include ../cli.js
+
+ body
+ header
+ h1 Misskey Cli #{version}
+ main
+ div#form
+ textarea#text
+ button#submit submit
+ div#tl
diff --git a/packages/backend/src/server/web/views/cli.tsx b/packages/backend/src/server/web/views/cli.tsx
deleted file mode 100644
index 009d982b356..00000000000
--- a/packages/backend/src/server/web/views/cli.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function CliPage(props: {
- version: string;
-}) {
- return (
- <>
- {''}
-
-
-
-
- Misskey CLI Tool
-
-
-
-
-
-
- Misskey CLI {props.version}
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/packages/backend/src/server/web/views/clip.pug b/packages/backend/src/server/web/views/clip.pug
new file mode 100644
index 00000000000..5a0018803a6
--- /dev/null
+++ b/packages/backend/src/server/web/views/clip.pug
@@ -0,0 +1,35 @@
+extends ./base
+
+block vars
+ - const user = clip.user;
+ - const title = clip.name;
+ - const url = `${config.url}/clips/${clip.id}`;
+
+block title
+ = `${title} | ${instanceName}`
+
+block desc
+ meta(name='description' content= clip.description)
+
+block og
+ meta(property='og:type' content='article')
+ meta(property='og:title' content= title)
+ meta(property='og:description' content= clip.description)
+ meta(property='og:url' content= url)
+ meta(property='og:image' content= avatarUrl)
+ meta(property='twitter:card' content='summary')
+
+block meta
+ if profile.noCrawle
+ meta(name='robots' content='noindex')
+ if profile.preventAiLearning
+ meta(name='robots' content='noimageai')
+ meta(name='robots' content='noai')
+
+ meta(name='misskey:user-username' content=user.username)
+ meta(name='misskey:user-id' content=user.id)
+ meta(name='misskey:clip-id' content=clip.id)
+
+ // todo
+ if user.twitter
+ meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
diff --git a/packages/backend/src/server/web/views/clip.tsx b/packages/backend/src/server/web/views/clip.tsx
deleted file mode 100644
index c3cc505e356..00000000000
--- a/packages/backend/src/server/web/views/clip.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { Packed } from '@/misc/json-schema.js';
-import type { MiUserProfile } from '@/models/UserProfile.js';
-import type { CommonProps } from '@/server/web/views/_.js';
-import { Layout } from '@/server/web/views/base.js';
-
-export function ClipPage(props: CommonProps<{
- clip: Packed<'Clip'>;
- profile: MiUserProfile;
-}>) {
- function ogBlock() {
- return (
- <>
-
-
- {props.clip.description != null ? : null}
-
- {props.clip.user.avatarUrl ? (
- <>
-
-
- >
- ) : null}
- >
- );
- }
-
- function metaBlock() {
- return (
- <>
- {props.profile.noCrawle ? : null}
- {props.profile.preventAiLearning ? (
- <>
-
-
- >
- ) : null}
-
-
-
- >
- );
- }
-
- return (
-
-
- );
-}
diff --git a/packages/backend/src/server/web/views/error.pug b/packages/backend/src/server/web/views/error.pug
new file mode 100644
index 00000000000..6a78d1878c2
--- /dev/null
+++ b/packages/backend/src/server/web/views/error.pug
@@ -0,0 +1,71 @@
+doctype html
+
+//
+ -
+ _____ _ _
+ | |_|___ ___| |_ ___ _ _
+ | | | | |_ -|_ -| '_| -_| | |
+ |_|_|_|_|___|___|_,_|___|_ |
+ |___|
+ Thank you for using Misskey!
+ If you are reading this message... how about joining the development?
+ https://github.com/misskey-dev/misskey
+
+
+html
+
+ head
+ meta(charset='utf-8')
+ meta(name='viewport' content='width=device-width, initial-scale=1')
+ meta(name='application-name' content='Misskey')
+ meta(name='referrer' content='origin')
+
+ title
+ block title
+ = 'An error has occurred... | Misskey'
+
+ style
+ include ../error.css
+
+ script
+ include ../error.js
+
+body
+ svg.icon-warning(xmlns="http://www.w3.org/2000/svg", viewBox="0 0 24 24", stroke-width="2", stroke="currentColor", fill="none", stroke-linecap="round", stroke-linejoin="round")
+ path(stroke="none", d="M0 0h24v24H0z", fill="none")
+ path(d="M12 9v2m0 4v.01")
+ path(d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75")
+
+ h1(data-i18n="title") Failed to initialize Misskey
+
+ button.button-big(onclick="location.reload();")
+ span.button-label-big(data-i18n-reload) Reload
+
+ p(data-i18n="serverError") If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID.
+
+ div#errors
+ code.
+ ERROR CODE: #{code}
+ ERROR ID: #{id}
+
+ p
+ b(data-i18n="solution") The following actions may solve the problem.
+
+ p(data-i18n="solution1") Update your os and browser
+ p(data-i18n="solution2") Disable an adblocker
+ p(data-i18n="solution3") Clear your browser cache
+ p(data-i18n="solution4") (Tor Browser) Set dom.webaudio.enabled to true
+
+ details(style="color: #86b300;")
+ summary(data-i18n="otherOption") Other options
+ a(href="/flush")
+ button.button-small
+ span.button-label-small(data-i18n="otherOption1") Clear preferences and cache
+ br
+ a(href="/cli")
+ button.button-small
+ span.button-label-small(data-i18n="otherOption2") Start the simple client
+ br
+ a(href="/bios")
+ button.button-small
+ span.button-label-small(data-i18n="otherOption3") Start the repair tool
diff --git a/packages/backend/src/server/web/views/error.tsx b/packages/backend/src/server/web/views/error.tsx
deleted file mode 100644
index 9d0e60aa306..00000000000
--- a/packages/backend/src/server/web/views/error.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { comment } from '@/server/web/views/_.js';
-import type { CommonPropsMinimum } from '@/server/web/views/_.js';
-
-export function ErrorPage(props: {
- title?: string;
- code: string;
- id: string;
-}) {
- return (
- <>
- {''}
- {comment}
-
-
-
-
-
-
- {props.title ?? 'An error has occurred... | Misskey'}
-
-
-
-
-
- Failed to initialize Misskey
-
-
-
-
- If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID.
-
-
-
-
- ERROR CODE: {props.code}
- ERROR ID: {props.id}
-
-
-
- The following actions may solve the problem.
-
- Update your os and browser
- Disable an adblocker
- Clear your browser cache
- (Tor Browser) Set dom.webaudio.enabled to true
-
-
-
-
- >
- );
-}
diff --git a/packages/backend/src/server/web/views/flash.pug b/packages/backend/src/server/web/views/flash.pug
new file mode 100644
index 00000000000..1549aa79061
--- /dev/null
+++ b/packages/backend/src/server/web/views/flash.pug
@@ -0,0 +1,35 @@
+extends ./base
+
+block vars
+ - const user = flash.user;
+ - const title = flash.title;
+ - const url = `${config.url}/play/${flash.id}`;
+
+block title
+ = `${title} | ${instanceName}`
+
+block desc
+ meta(name='description' content= flash.summary)
+
+block og
+ meta(property='og:type' content='article')
+ meta(property='og:title' content= title)
+ meta(property='og:description' content= flash.summary)
+ meta(property='og:url' content= url)
+ meta(property='og:image' content= avatarUrl)
+ meta(property='twitter:card' content='summary')
+
+block meta
+ if profile.noCrawle
+ meta(name='robots' content='noindex')
+ if profile.preventAiLearning
+ meta(name='robots' content='noimageai')
+ meta(name='robots' content='noai')
+
+ meta(name='misskey:user-username' content=user.username)
+ meta(name='misskey:user-id' content=user.id)
+ meta(name='misskey:flash-id' content=flash.id)
+
+ // todo
+ if user.twitter
+ meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
diff --git a/packages/backend/src/server/web/views/flash.tsx b/packages/backend/src/server/web/views/flash.tsx
deleted file mode 100644
index 25a6b2c0ae0..00000000000
--- a/packages/backend/src/server/web/views/flash.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { Packed } from '@/misc/json-schema.js';
-import type { MiUserProfile } from '@/models/UserProfile.js';
-import type { CommonProps } from '@/server/web/views/_.js';
-import { Layout } from '@/server/web/views/base.js';
-
-export function FlashPage(props: CommonProps<{
- flash: Packed<'Flash'>;
- profile: MiUserProfile;
-}>) {
- function ogBlock() {
- return (
- <>
-
-
-
-
- {props.flash.user.avatarUrl ? (
- <>
-
-
- >
- ) : null}
- >
- );
- }
-
- function metaBlock() {
- return (
- <>
- {props.profile.noCrawle ? : null}
- {props.profile.preventAiLearning ? (
- <>
-
-
- >
- ) : null}
-
-
-
- >
- );
- }
-
- return (
-
-
- );
-}
diff --git a/packages/backend/src/server/web/views/flush.pug b/packages/backend/src/server/web/views/flush.pug
new file mode 100644
index 00000000000..7884495d08c
--- /dev/null
+++ b/packages/backend/src/server/web/views/flush.pug
@@ -0,0 +1,51 @@
+doctype html
+
+html
+ #msg
+ script.
+ const msg = document.getElementById('msg');
+ const successText = `\nSuccess Flush! Back to Misskey\n成功しました。Misskeyを開き直してください。`;
+
+ if (!document.cookie) {
+ message('Your site data is fully cleared by your browser.');
+ message(successText);
+ } else {
+ message('Your browser does not support Clear-Site-Data header. Start opportunistic flushing.');
+ (async function() {
+ try {
+ localStorage.clear();
+ message('localStorage cleared.');
+
+ const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => {
+ const delidb = indexedDB.deleteDatabase(name);
+ delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`));
+ delidb.onerror = e => rej(e)
+ }));
+
+ await Promise.all(idbPromises);
+
+ if (navigator.serviceWorker.controller) {
+ navigator.serviceWorker.controller.postMessage('clear');
+ await navigator.serviceWorker.getRegistrations()
+ .then(registrations => {
+ return Promise.all(registrations.map(registration => registration.unregister()));
+ })
+ .catch(e => { throw new Error(e) });
+ }
+
+ message(successText);
+ } catch (e) {
+ message(`\n${e}\n\nFlush Failed. Please retry.\n失敗しました。もう一度試してみてください。`);
+ message(`\nIf you retry more than 3 times, try manually clearing the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを手動で消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`)
+
+ console.error(e);
+ setTimeout(() => {
+ location = '/';
+ }, 10000)
+ }
+ })();
+ }
+
+ function message(text) {
+ msg.insertAdjacentHTML('beforeend', `[${(new Date()).toString()}] ${text.replace(/\n/g,'
')}
`)
+ }
diff --git a/packages/backend/src/server/web/views/flush.tsx b/packages/backend/src/server/web/views/flush.tsx
deleted file mode 100644
index f3fdc8fcb07..00000000000
--- a/packages/backend/src/server/web/views/flush.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function FlushPage(props?: {}) {
- return (
- <>
- {''}
-
-
-
-
- Clear preferences and cache
-
-
-
-
-
-
- >
- );
-}
diff --git a/packages/backend/src/server/web/views/gallery-post.pug b/packages/backend/src/server/web/views/gallery-post.pug
new file mode 100644
index 00000000000..9ae25d9ac84
--- /dev/null
+++ b/packages/backend/src/server/web/views/gallery-post.pug
@@ -0,0 +1,41 @@
+extends ./base
+
+block vars
+ - const user = post.user;
+ - const title = post.title;
+ - const url = `${config.url}/gallery/${post.id}`;
+
+block title
+ = `${title} | ${instanceName}`
+
+block desc
+ meta(name='description' content= post.description)
+
+block og
+ meta(property='og:type' content='article')
+ meta(property='og:title' content= title)
+ meta(property='og:description' content= post.description)
+ meta(property='og:url' content= url)
+ if post.isSensitive
+ meta(property='og:image' content= avatarUrl)
+ meta(property='twitter:card' content='summary')
+ else
+ meta(property='og:image' content= post.files[0].thumbnailUrl)
+ meta(property='twitter:card' content='summary_large_image')
+
+block meta
+ if user.host || profile.noCrawle
+ meta(name='robots' content='noindex')
+ if profile.preventAiLearning
+ meta(name='robots' content='noimageai')
+ meta(name='robots' content='noai')
+
+ meta(name='misskey:user-username' content=user.username)
+ meta(name='misskey:user-id' content=user.id)
+
+ // todo
+ if user.twitter
+ meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
+
+ if !user.host
+ link(rel='alternate' href=url type='application/activity+json')
diff --git a/packages/backend/src/server/web/views/gallery-post.tsx b/packages/backend/src/server/web/views/gallery-post.tsx
deleted file mode 100644
index 2bec2de930d..00000000000
--- a/packages/backend/src/server/web/views/gallery-post.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { Packed } from '@/misc/json-schema.js';
-import type { MiUserProfile } from '@/models/UserProfile.js';
-import type { CommonProps } from '@/server/web/views/_.js';
-import { Layout } from '@/server/web/views/base.js';
-
-export function GalleryPostPage(props: CommonProps<{
- galleryPost: Packed<'GalleryPost'>;
- profile: MiUserProfile;
-}>) {
- function ogBlock() {
- return (
- <>
-
-
- {props.galleryPost.description != null ? : null}
-
- {props.galleryPost.isSensitive && props.galleryPost.user.avatarUrl ? (
- <>
-
-
- >
- ) : null}
- {!props.galleryPost.isSensitive && props.galleryPost.files != null ? (
- <>
-
-
- >
- ) : null}
- >
- );
- }
-
- function metaBlock() {
- return (
- <>
- {props.profile.noCrawle ? : null}
- {props.profile.preventAiLearning ? (
- <>
-
-
- >
- ) : null}
-
-
-
- >
- );
- }
-
- return (
-
-
- );
-}
diff --git a/packages/backend/src/server/web/views/info-card.pug b/packages/backend/src/server/web/views/info-card.pug
new file mode 100644
index 00000000000..2a4954ec8bc
--- /dev/null
+++ b/packages/backend/src/server/web/views/info-card.pug
@@ -0,0 +1,50 @@
+doctype html
+
+html
+
+ head
+ meta(charset='utf-8')
+ meta(name='application-name' content='Misskey')
+ title= meta.name || host
+ style.
+ html, body {
+ margin: 0;
+ padding: 0;
+ min-height: 100vh;
+ background: #fff;
+ }
+
+ #a {
+ display: block;
+ }
+
+ #banner {
+ background-size: cover;
+ background-position: center center;
+ }
+
+ #title {
+ display: inline-block;
+ margin: 24px;
+ padding: 0.5em 0.8em;
+ color: #fff;
+ background: rgba(0, 0, 0, 0.5);
+ font-weight: bold;
+ font-size: 1.3em;
+ }
+
+ #content {
+ overflow: auto;
+ color: #353c3e;
+ }
+
+ #description {
+ margin: 24px;
+ }
+
+ body
+ a#a(href=`https://${host}` target="_blank")
+ header#banner(style=`background-image: url(${meta.bannerUrl})`)
+ div#title= meta.name || host
+ div#content
+ div#description!= meta.description
diff --git a/packages/backend/src/server/web/views/info-card.tsx b/packages/backend/src/server/web/views/info-card.tsx
deleted file mode 100644
index 27be4c69e8f..00000000000
--- a/packages/backend/src/server/web/views/info-card.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { comment, CommonPropsMinimum } from '@/server/web/views/_.js';
-import type { MiMeta } from '@/models/Meta.js';
-
-export function InfoCardPage(props: CommonPropsMinimum<{
- meta: MiMeta;
-}>) {
- // 変数名をsafeで始めることでエラーをスキップ
- const safeDescription = props.meta.description;
-
- return (
- <>
- {''}
- {comment}
-
-
-
-
-
- {props.meta.name ?? props.config.url}
-
-
-
-
-
- {props.meta.name ?? props.config.url}
-
-
-
- {safeDescription}
-
-
-
- >
- );
-}
diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug
new file mode 100644
index 00000000000..ea1993aed03
--- /dev/null
+++ b/packages/backend/src/server/web/views/note.pug
@@ -0,0 +1,62 @@
+extends ./base
+
+block vars
+ - const user = note.user;
+ - const title = user.name ? `${user.name} (@${user.username}${user.host ? `@${user.host}` : ''})` : `@${user.username}${user.host ? `@${user.host}` : ''}`;
+ - const url = `${config.url}/notes/${note.id}`;
+ - const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
+ - const images = (note.files || []).filter(file => file.type.startsWith('image/') && !file.isSensitive)
+ - const videos = (note.files || []).filter(file => file.type.startsWith('video/') && !file.isSensitive)
+
+block title
+ = `${title} | ${instanceName}`
+
+block desc
+ meta(name='description' content= summary)
+
+block og
+ meta(property='og:type' content='article')
+ meta(property='og:title' content= title)
+ meta(property='og:description' content= summary)
+ meta(property='og:url' content= url)
+ if videos.length
+ each video in videos
+ meta(property='og:video:url' content= video.url)
+ meta(property='og:video:secure_url' content= video.url)
+ meta(property='og:video:type' content= video.type)
+ // FIXME: add width and height
+ // FIXME: add embed player for Twitter
+ if images.length
+ meta(property='twitter:card' content='summary_large_image')
+ each image in images
+ meta(property='og:image' content= image.url)
+ else
+ meta(property='twitter:card' content='summary')
+ meta(property='og:image' content= avatarUrl)
+
+
+block meta
+ if user.host || isRenote || profile.noCrawle
+ meta(name='robots' content='noindex')
+ if profile.preventAiLearning
+ meta(name='robots' content='noimageai')
+ meta(name='robots' content='noai')
+
+ meta(name='misskey:user-username' content=user.username)
+ meta(name='misskey:user-id' content=user.id)
+ meta(name='misskey:note-id' content=note.id)
+
+ // todo
+ if user.twitter
+ meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
+
+ if note.prev
+ link(rel='prev' href=`${config.url}/notes/${note.prev}`)
+ if note.next
+ link(rel='next' href=`${config.url}/notes/${note.next}`)
+
+ if federationEnabled
+ if !user.host
+ link(rel='alternate' href=url type='application/activity+json')
+ if note.uri
+ link(rel='alternate' href=note.uri type='application/activity+json')
diff --git a/packages/backend/src/server/web/views/note.tsx b/packages/backend/src/server/web/views/note.tsx
deleted file mode 100644
index 803c3d25374..00000000000
--- a/packages/backend/src/server/web/views/note.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { Packed } from '@/misc/json-schema.js';
-import type { MiUserProfile } from '@/models/UserProfile.js';
-import type { CommonProps } from '@/server/web/views/_.js';
-import { Layout } from '@/server/web/views/base.js';
-import { isRenotePacked } from '@/misc/is-renote.js';
-import { getNoteSummary } from '@/misc/get-note-summary.js';
-
-export function NotePage(props: CommonProps<{
- note: Packed<'Note'>;
- profile: MiUserProfile;
-}>) {
- const title = props.note.user.name ? `${props.note.user.name} (@${props.note.user.username}${props.note.user.host ? `@${props.note.user.host}` : ''})` : `@${props.note.user.username}${props.note.user.host ? `@${props.note.user.host}` : ''}`
- const isRenote = isRenotePacked(props.note);
- const images = (props.note.files ?? []).filter(f => f.type.startsWith('image/'));
- const videos = (props.note.files ?? []).filter(f => f.type.startsWith('video/'));
- const summary = getNoteSummary(props.note);
-
- function ogBlock() {
- return (
- <>
-
-
-
-
- {videos.map(video => (
- <>
-
-
-
- {video.thumbnailUrl ? : null}
- {video.properties.width != null ? : null}
- {video.properties.height != null ? : null}
- >
- ))}
- {images.length > 0 ? (
- <>
-
- {images.map(image => (
- <>
-
- {image.properties.width != null ? : null}
- {image.properties.height != null ? : null}
- >
- ))}
- >
- ) : (
- <>
-
-
- >
- )}
- >
- );
- }
-
- function metaBlock() {
- return (
- <>
- {props.note.user.host != null || isRenote || props.profile.noCrawle ? : null}
- {props.profile.preventAiLearning ? (
- <>
-
-
- >
- ) : null}
-
-
-
-
- {props.federationEnabled ? (
- <>
- {props.note.user.host == null ? : null}
- {props.note.uri != null ? : null}
- >
- ) : null}
- >
- );
- }
-
- return (
-
- )
-}
diff --git a/packages/backend/src/server/web/views/oauth.pug b/packages/backend/src/server/web/views/oauth.pug
new file mode 100644
index 00000000000..4195ccc3a30
--- /dev/null
+++ b/packages/backend/src/server/web/views/oauth.pug
@@ -0,0 +1,11 @@
+extends ./base
+
+block meta
+ //- Should be removed by the page when it loads, so that it won't needlessly
+ //- stay when user navigates away via the navigation bar
+ //- XXX: Remove navigation bar in auth page?
+ meta(name='misskey:oauth:transaction-id' content=transactionId)
+ meta(name='misskey:oauth:client-name' content=clientName)
+ if clientLogo
+ meta(name='misskey:oauth:client-logo' content=clientLogo)
+ meta(name='misskey:oauth:scope' content=scope)
diff --git a/packages/backend/src/server/web/views/oauth.tsx b/packages/backend/src/server/web/views/oauth.tsx
deleted file mode 100644
index d12b0d15fdb..00000000000
--- a/packages/backend/src/server/web/views/oauth.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { CommonProps } from '@/server/web/views/_.js';
-import { Layout } from '@/server/web/views/base.js';
-
-export function OAuthPage(props: CommonProps<{
- transactionId: string;
- clientName: string;
- clientLogo?: string;
- scope: string[];
-}>) {
-
- //- Should be removed by the page when it loads, so that it won't needlessly
- //- stay when user navigates away via the navigation bar
- //- XXX: Remove navigation bar in auth page?
- function metaBlock() {
- return (
- <>
-
-
- {props.clientLogo ? : null}
-
- >
- );
- }
-
- return (
-
-
- );
-}
diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug
new file mode 100644
index 00000000000..03c50eca8a5
--- /dev/null
+++ b/packages/backend/src/server/web/views/page.pug
@@ -0,0 +1,35 @@
+extends ./base
+
+block vars
+ - const user = page.user;
+ - const title = page.title;
+ - const url = `${config.url}/@${user.username}/pages/${page.name}`;
+
+block title
+ = `${title} | ${instanceName}`
+
+block desc
+ meta(name='description' content= page.summary)
+
+block og
+ meta(property='og:type' content='article')
+ meta(property='og:title' content= title)
+ meta(property='og:description' content= page.summary)
+ meta(property='og:url' content= url)
+ meta(property='og:image' content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : avatarUrl)
+ meta(property='twitter:card' content= page.eyeCatchingImage ? 'summary_large_image' : 'summary')
+
+block meta
+ if profile.noCrawle
+ meta(name='robots' content='noindex')
+ if profile.preventAiLearning
+ meta(name='robots' content='noimageai')
+ meta(name='robots' content='noai')
+
+ meta(name='misskey:user-username' content=user.username)
+ meta(name='misskey:user-id' content=user.id)
+ meta(name='misskey:page-id' content=page.id)
+
+ // todo
+ if user.twitter
+ meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
diff --git a/packages/backend/src/server/web/views/page.tsx b/packages/backend/src/server/web/views/page.tsx
deleted file mode 100644
index d0484612df4..00000000000
--- a/packages/backend/src/server/web/views/page.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { Packed } from '@/misc/json-schema.js';
-import type { MiUserProfile } from '@/models/UserProfile.js';
-import type { CommonProps } from '@/server/web/views/_.js';
-import { Layout } from '@/server/web/views/base.js';
-
-export function PagePage(props: CommonProps<{
- page: Packed<'Page'>;
- profile: MiUserProfile;
-}>) {
- function ogBlock() {
- return (
- <>
-
-
- {props.page.summary != null ? : null}
-
- {props.page.eyeCatchingImage != null ? (
- <>
-
-
- >
- ) : props.page.user.avatarUrl ? (
- <>
-
-
- >
- ) : null}
- >
- );
- }
-
- function metaBlock() {
- return (
- <>
- {props.profile.noCrawle ? : null}
- {props.profile.preventAiLearning ? (
- <>
-
-
- >
- ) : null}
-
-
-
- >
- );
- }
-
- return (
-
-
- );
-}
diff --git a/packages/backend/src/server/web/views/reversi-game.pug b/packages/backend/src/server/web/views/reversi-game.pug
new file mode 100644
index 00000000000..0b5ffb2bb09
--- /dev/null
+++ b/packages/backend/src/server/web/views/reversi-game.pug
@@ -0,0 +1,20 @@
+extends ./base
+
+block vars
+ - const user1 = game.user1;
+ - const user2 = game.user2;
+ - const title = `${user1.username} vs ${user2.username}`;
+ - const url = `${config.url}/reversi/g/${game.id}`;
+
+block title
+ = `${title} | ${instanceName}`
+
+block desc
+ meta(name='description' content='⚫⚪Misskey Reversi⚪⚫')
+
+block og
+ meta(property='og:type' content='article')
+ meta(property='og:title' content= title)
+ meta(property='og:description' content='⚫⚪Misskey Reversi⚪⚫')
+ meta(property='og:url' content= url)
+ meta(property='twitter:card' content='summary')
diff --git a/packages/backend/src/server/web/views/reversi-game.tsx b/packages/backend/src/server/web/views/reversi-game.tsx
deleted file mode 100644
index 22609311fd4..00000000000
--- a/packages/backend/src/server/web/views/reversi-game.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { Packed } from '@/misc/json-schema.js';
-import type { CommonProps } from '@/server/web/views/_.js';
-import { Layout } from '@/server/web/views/base.js';
-
-export function ReversiGamePage(props: CommonProps<{
- reversiGame: Packed<'ReversiGameDetailed'>;
-}>) {
- const title = `${props.reversiGame.user1.username} vs ${props.reversiGame.user2.username}`;
- const description = `⚫⚪Misskey Reversi⚪⚫`;
-
- function ogBlock() {
- return (
- <>
-
-
-
-
-
- >
- );
- }
-
- return (
-
-
- );
-}
diff --git a/packages/backend/src/server/web/views/user.pug b/packages/backend/src/server/web/views/user.pug
new file mode 100644
index 00000000000..b9f740f5b6a
--- /dev/null
+++ b/packages/backend/src/server/web/views/user.pug
@@ -0,0 +1,44 @@
+extends ./base
+
+block vars
+ - const title = user.name ? `${user.name} (@${user.username}${user.host ? `@${user.host}` : ''})` : `@${user.username}${user.host ? `@${user.host}` : ''}`;
+ - const url = `${config.url}/@${(user.host ? `${user.username}@${user.host}` : user.username)}`;
+
+block title
+ = `${title} | ${instanceName}`
+
+block desc
+ meta(name='description' content= profile.description)
+
+block og
+ meta(property='og:type' content='blog')
+ meta(property='og:title' content= title)
+ meta(property='og:description' content= profile.description)
+ meta(property='og:url' content= url)
+ meta(property='og:image' content= avatarUrl)
+ meta(property='twitter:card' content='summary')
+
+block meta
+ if user.host || profile.noCrawle
+ meta(name='robots' content='noindex')
+ if profile.preventAiLearning
+ meta(name='robots' content='noimageai')
+ meta(name='robots' content='noai')
+
+ meta(name='misskey:user-username' content=user.username)
+ meta(name='misskey:user-id' content=user.id)
+
+ if profile.twitter
+ meta(name='twitter:creator' content=`@${profile.twitter.screenName}`)
+
+ if !sub
+ if federationEnabled
+ if !user.host
+ link(rel='alternate' href=`${config.url}/users/${user.id}` type='application/activity+json')
+ if user.uri
+ link(rel='alternate' href=user.uri type='application/activity+json')
+ if profile.url
+ link(rel='alternate' href=profile.url type='text/html')
+
+ each m in me
+ link(rel='me' href=`${m}`)
diff --git a/packages/backend/src/server/web/views/user.tsx b/packages/backend/src/server/web/views/user.tsx
deleted file mode 100644
index 76c2633ab9b..00000000000
--- a/packages/backend/src/server/web/views/user.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { Packed } from '@/misc/json-schema.js';
-import type { MiUserProfile } from '@/models/UserProfile.js';
-import type { CommonProps } from '@/server/web/views/_.js';
-import { Layout } from '@/server/web/views/base.js';
-
-export function UserPage(props: CommonProps<{
- user: Packed<'UserDetailed'>;
- profile: MiUserProfile;
- sub?: string;
-}>) {
- const title = props.user.name ? `${props.user.name} (@${props.user.username}${props.user.host ? `@${props.user.host}` : ''})` : `@${props.user.username}${props.user.host ? `@${props.user.host}` : ''}`;
- const me = props.profile.fields
- ? props.profile.fields
- .filter(field => field.value != null && field.value.match(/^https?:/))
- .map(field => field.value)
- : [];
-
- function ogBlock() {
- return (
- <>
-
-
- {props.user.description != null ? : null}
-
-
-
- >
- );
- }
-
- function metaBlock() {
- return (
- <>
- {props.user.host != null || props.profile.noCrawle ? : null}
- {props.profile.preventAiLearning ? (
- <>
-
-
- >
- ) : null}
-
-
-
- {props.sub == null && props.federationEnabled ? (
- <>
- {props.user.host == null ? : null}
- {props.user.uri != null ? : null}
- {props.profile.url != null ? : null}
- >
- ) : null}
-
- {me.map((url) => (
-
- ))}
- >
- );
- }
-
- return (
-
-
- );
-}
diff --git a/packages/backend/test-federation/.config/dummy.yml b/packages/backend/test-federation/.config/dummy.yml
deleted file mode 100644
index 841cab97832..00000000000
--- a/packages/backend/test-federation/.config/dummy.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-url: https://example.com/
-port: 3000
diff --git a/packages/backend/test-federation/.config/example.config.json b/packages/backend/test-federation/.config/example.config.json
deleted file mode 100644
index 2035d1a2009..00000000000
--- a/packages/backend/test-federation/.config/example.config.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "url": "https://${HOST}/",
- "port": 3000,
- "db": {
- "host": "db.${HOST}",
- "port": 5432,
- "db": "misskey",
- "user": "postgres",
- "pass": "postgres"
- },
- "dbReplications": false,
- "trustProxy": true,
- "redis": {
- "host": "redis.test",
- "port": 6379
- },
- "id": "aidx",
- "proxyBypassHosts": [
- "api.deepl.com",
- "api-free.deepl.com",
- "www.recaptcha.net",
- "hcaptcha.com",
- "challenges.cloudflare.com"
- ],
- "allowedPrivateNetworks": [
- "127.0.0.1/32",
- "172.20.0.0/16"
- ]
-}
diff --git a/packages/backend/test-federation/.config/example.default.yml b/packages/backend/test-federation/.config/example.default.yml
new file mode 100644
index 00000000000..fd20613885f
--- /dev/null
+++ b/packages/backend/test-federation/.config/example.default.yml
@@ -0,0 +1,22 @@
+url: https://${HOST}/
+port: 3000
+db:
+ host: db.${HOST}
+ port: 5432
+ db: misskey
+ user: postgres
+ pass: postgres
+dbReplications: false
+redis:
+ host: redis.test
+ port: 6379
+id: 'aidx'
+proxyBypassHosts:
+ - api.deepl.com
+ - api-free.deepl.com
+ - www.recaptcha.net
+ - hcaptcha.com
+ - challenges.cloudflare.com
+allowedPrivateNetworks:
+ - 127.0.0.1/32
+ - 172.20.0.0/16
diff --git a/packages/backend/test-federation/compose.a.yml b/packages/backend/test-federation/compose.a.yml
index ec9a2cf2afd..6a305b404cf 100644
--- a/packages/backend/test-federation/compose.a.yml
+++ b/packages/backend/test-federation/compose.a.yml
@@ -37,8 +37,8 @@ services:
- internal_network_a
volumes:
- type: bind
- source: ./.config/a.test.config.json
- target: /misskey/built/._config_.json
+ source: ./.config/a.test.default.yml
+ target: /misskey/.config/default.yml
read_only: true
db.a.test:
@@ -50,7 +50,7 @@ services:
volumes:
- type: bind
source: ./volumes/db.a
- target: /var/lib/postgresql
+ target: /var/lib/postgresql/data
bind:
create_host_path: true
diff --git a/packages/backend/test-federation/compose.b.yml b/packages/backend/test-federation/compose.b.yml
index 92219344069..1158b53baef 100644
--- a/packages/backend/test-federation/compose.b.yml
+++ b/packages/backend/test-federation/compose.b.yml
@@ -37,8 +37,8 @@ services:
- internal_network_b
volumes:
- type: bind
- source: ./.config/b.test.config.json
- target: /misskey/built/._config_.json
+ source: ./.config/b.test.default.yml
+ target: /misskey/.config/default.yml
read_only: true
db.b.test:
@@ -50,7 +50,7 @@ services:
volumes:
- type: bind
source: ./volumes/db.b
- target: /var/lib/postgresql
+ target: /var/lib/postgresql/data
bind:
create_host_path: true
diff --git a/packages/backend/test-federation/compose.tpl.yml b/packages/backend/test-federation/compose.tpl.yml
index 1404345e2a3..92b986736d8 100644
--- a/packages/backend/test-federation/compose.tpl.yml
+++ b/packages/backend/test-federation/compose.tpl.yml
@@ -21,10 +21,6 @@ services:
- type: bind
source: ../../../built
target: /misskey/built
- read_only: false
- - type: bind
- source: ./.config/dummy.yml
- target: /misskey/.config/default.yml
read_only: true
- type: bind
source: ../assets
@@ -46,10 +42,6 @@ services:
source: ../package.json
target: /misskey/packages/backend/package.json
read_only: true
- - type: bind
- source: ../scripts/compile_config.js
- target: /misskey/packages/backend/scripts/compile_config.js
- read_only: true
- type: bind
source: ../../misskey-js/built
target: /misskey/packages/misskey-js/built
@@ -58,14 +50,6 @@ services:
source: ../../misskey-js/package.json
target: /misskey/packages/misskey-js/package.json
read_only: true
- - type: bind
- source: ../../i18n/built
- target: /misskey/packages/i18n/built
- read_only: true
- - type: bind
- source: ../../i18n/package.json
- target: /misskey/packages/i18n/package.json
- read_only: true
- type: bind
source: ../../misskey-reversi/built
target: /misskey/packages/misskey-reversi/built
diff --git a/packages/backend/test-federation/compose.yml b/packages/backend/test-federation/compose.yml
index 25475a89ab7..330cc338540 100644
--- a/packages/backend/test-federation/compose.yml
+++ b/packages/backend/test-federation/compose.yml
@@ -54,10 +54,6 @@ services:
source: ../jest.js
target: /misskey/packages/backend/jest.js
read_only: true
- - type: bind
- source: ../scripts/compile_config.js
- target: /misskey/packages/backend/scripts/compile_config.js
- read_only: true
- type: bind
source: ../../misskey-js/built
target: /misskey/packages/misskey-js/built
@@ -66,14 +62,6 @@ services:
source: ../../misskey-js/package.json
target: /misskey/packages/misskey-js/package.json
read_only: true
- - type: bind
- source: ../../i18n/built
- target: /misskey/packages/i18n/built
- read_only: true
- - type: bind
- source: ../../i18n/package.json
- target: /misskey/packages/i18n/package.json
- read_only: true
- type: bind
source: ../../../package.json
target: /misskey/package.json
diff --git a/packages/backend/test-federation/setup.sh b/packages/backend/test-federation/setup.sh
index 15aa2eee7f3..1bc3a2a87c0 100644
--- a/packages/backend/test-federation/setup.sh
+++ b/packages/backend/test-federation/setup.sh
@@ -28,7 +28,7 @@ function generate {
-days 500
if [ ! -f .config/docker.env ]; then cp .config/example.docker.env .config/docker.env; fi
if [ ! -f .config/$1.conf ]; then sed "s/\${HOST}/$1/g" .config/example.conf > .config/$1.conf; fi
- if [ ! -f .config/$1.default.yml ]; then sed "s/\${HOST}/$1/g" .config/example.config.json > .config/$1.config.json; fi
+ if [ ! -f .config/$1.default.yml ]; then sed "s/\${HOST}/$1/g" .config/example.default.yml > .config/$1.default.yml; fi
}
generate a.test
diff --git a/packages/backend/test-federation/tsconfig.json b/packages/backend/test-federation/tsconfig.json
index 8e74a62e811..3a1cb3b9f3b 100644
--- a/packages/backend/test-federation/tsconfig.json
+++ b/packages/backend/test-federation/tsconfig.json
@@ -13,12 +13,12 @@
/* Language and Environment */
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
- "jsx": "react-jsx", /* Specify what JSX code is generated. */
+ // "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
- "jsxImportSource": "@kitajs/html", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
+ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
diff --git a/packages/backend/test-server/tsconfig.json b/packages/backend/test-server/tsconfig.json
index 7ed7c10ed74..10313699c2e 100644
--- a/packages/backend/test-server/tsconfig.json
+++ b/packages/backend/test-server/tsconfig.json
@@ -23,8 +23,6 @@
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
"isolatedModules": true,
- "jsx": "react-jsx",
- "jsxImportSource": "@kitajs/html",
"rootDir": "../src",
"baseUrl": "./",
"paths": {
diff --git a/packages/backend/test/e2e/exports.ts b/packages/backend/test/e2e/exports.ts
index 19433f3c882..4bcecc9716c 100644
--- a/packages/backend/test/e2e/exports.ts
+++ b/packages/backend/test/e2e/exports.ts
@@ -16,7 +16,7 @@ describe('export-clips', () => {
let bob: misskey.entities.SignupResponse;
// XXX: Any better way to get the result?
- async function pollFirstDriveFile(): Promise {
+ async function pollFirstDriveFile() {
while (true) {
const files = (await api('drive/files', {}, alice)).body;
if (!files.length) {
@@ -168,36 +168,7 @@ describe('export-clips', () => {
assert.strictEqual(exported[1].clipNotes[0].note.text, 'baz2');
});
- test('Clipping other user\'s note (followers only notes are excluded when not following)', async () => {
- const res = await api('clips/create', {
- name: 'kawaii',
- description: 'kawaii',
- }, alice);
- assert.strictEqual(res.status, 200);
- const clip = res.body;
-
- const note = await post(bob, {
- text: 'baz',
- visibility: 'followers',
- });
-
- const res2 = await api('clips/add-note', {
- clipId: clip.id,
- noteId: note.id,
- }, alice);
- assert.strictEqual(res2.status, 204);
-
- const res3 = await api('i/export-clips', {}, alice);
- assert.strictEqual(res3.status, 204);
-
- const exported = await pollFirstDriveFile();
- assert.strictEqual(exported[0].clipNotes.length, 0);
- });
-
- test('Clipping other user\'s note (followers only notes are included when following)', async () => {
- // Alice follows Bob
- await api('following/create', { userId: bob.id }, alice);
-
+ test('Clipping other user\'s note', async () => {
const res = await api('clips/create', {
name: 'kawaii',
description: 'kawaii',
diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts
index f00843de106..bef98893c61 100644
--- a/packages/backend/test/e2e/fetch-resource.ts
+++ b/packages/backend/test/e2e/fetch-resource.ts
@@ -73,7 +73,7 @@ describe('Webリソース', () => {
};
const metaTag = (res: SimpleGetResponse, key: string, superkey = 'name'): string => {
- return res.body.querySelector('meta[' + superkey + '="' + key + '"]')?.attributes.content;
+ return res.body.window.document.querySelector('meta[' + superkey + '="' + key + '"]')?.content;
};
beforeAll(async () => {
diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts
index 96a6311a5ab..f639f90ea6a 100644
--- a/packages/backend/test/e2e/oauth.ts
+++ b/packages/backend/test/e2e/oauth.ts
@@ -19,7 +19,7 @@ import {
ResourceOwnerPassword,
} from 'simple-oauth2';
import pkceChallenge from 'pkce-challenge';
-import * as htmlParser from 'node-html-parser';
+import { JSDOM } from 'jsdom';
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 doc = htmlParser.parse(`${html}`);
+ const fragment = JSDOM.fragment(html);
return {
- 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,
+ transactionId: fragment.querySelector('meta[name="misskey:oauth:transaction-id"]')?.content,
+ clientName: fragment.querySelector('meta[name="misskey:oauth:client-name"]')?.content,
+ clientLogo: fragment.querySelector('meta[name="misskey:oauth:client-logo"]')?.content,
};
}
@@ -148,7 +148,7 @@ function assertIndirectError(response: Response, error: string): void {
async function assertDirectError(response: Response, status: number, error: string): Promise {
assert.strictEqual(response.status, status);
- const data = await response.json() as any;
+ const data = await response.json();
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() as any;
+ const body = await response.json();
assert.strictEqual(body.issuer, 'http://misskey.local');
assert.ok(body.scopes_supported.includes('write:notes'));
});
diff --git a/packages/backend/test/jest.setup.ts b/packages/backend/test/jest.setup.ts
index 9185f58acb8..7c6dd6a55f2 100644
--- a/packages/backend/test/jest.setup.ts
+++ b/packages/backend/test/jest.setup.ts
@@ -9,4 +9,3 @@ beforeAll(async () => {
await initTestDb(false);
await sendEnvResetRequest();
});
-
diff --git a/packages/backend/test/tsconfig.json b/packages/backend/test/tsconfig.json
index c6754c4802a..2b562acda81 100644
--- a/packages/backend/test/tsconfig.json
+++ b/packages/backend/test/tsconfig.json
@@ -23,8 +23,6 @@
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
"isolatedModules": true,
- "jsx": "react-jsx",
- "jsxImportSource": "@kitajs/html",
"baseUrl": "./",
"paths": {
"@/*": ["../src/*"]
diff --git a/packages/backend/test/unit/AnnouncementService.ts b/packages/backend/test/unit/AnnouncementService.ts
index b3f7f426fea..0b24f109f84 100644
--- a/packages/backend/test/unit/AnnouncementService.ts
+++ b/packages/backend/test/unit/AnnouncementService.ts
@@ -26,7 +26,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import type { TestingModule } from '@nestjs/testing';
-import type { MockMetadata } from 'jest-mock';
+import type { MockFunctionMetadata } 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 MockMetadata;
+ const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
diff --git a/packages/backend/test/unit/ApMfmService.ts b/packages/backend/test/unit/ApMfmService.ts
index 93efa5d7d35..e81a321c9be 100644
--- a/packages/backend/test/unit/ApMfmService.ts
+++ b/packages/backend/test/unit/ApMfmService.ts
@@ -9,6 +9,7 @@ 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;
@@ -30,7 +31,7 @@ describe('ApMfmService', () => {
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
assert.equal(noMisskeyContent, true, 'noMisskeyContent');
- assert.equal(content, 'テキスト #タグ @mention 🍊 :emoji: https://example.com', 'content');
+ assert.equal(content, 'テキスト #タグ @mention 🍊 :emoji: https://example.com
', 'content');
});
test('Provide _misskey_content for MFM', () => {
@@ -42,7 +43,7 @@ describe('ApMfmService', () => {
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
assert.equal(noMisskeyContent, false, 'noMisskeyContent');
- assert.equal(content, 'foo', 'content');
+ assert.equal(content, 'foo
', 'content');
});
});
});
diff --git a/packages/backend/test/unit/CaptchaService.ts b/packages/backend/test/unit/CaptchaService.ts
index 24bb81118ed..51b70b05a17 100644
--- a/packages/backend/test/unit/CaptchaService.ts
+++ b/packages/backend/test/unit/CaptchaService.ts
@@ -446,7 +446,7 @@ describe('CaptchaService', () => {
if (!res.success) {
expect(res.error.code).toBe(code);
}
- expect(metaService.update).not.toHaveBeenCalled();
+ expect(metaService.update).not.toBeCalled();
}
describe('invalidParameters', () => {
diff --git a/packages/backend/test/unit/DriveService.ts b/packages/backend/test/unit/DriveService.ts
index 48b108fbba0..964c65ccaaf 100644
--- a/packages/backend/test/unit/DriveService.ts
+++ b/packages/backend/test/unit/DriveService.ts
@@ -53,7 +53,7 @@ describe('DriveService', () => {
s3Mock.on(DeleteObjectCommand)
.rejects(new InvalidObjectState({ $metadata: {}, message: '' }));
- await expect(driveService.deleteObjectStorageFile('unexpected')).rejects.toThrow(Error);
+ await expect(driveService.deleteObjectStorageFile('unexpected')).rejects.toThrowError(Error);
});
test('delete a file with no valid key', async () => {
diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts
index 28a2a971f47..29bd03a2012 100644
--- a/packages/backend/test/unit/FileInfoService.ts
+++ b/packages/backend/test/unit/FileInfoService.ts
@@ -17,7 +17,7 @@ import { FileInfo, FileInfoService } from '@/core/FileInfoService.js';
import { AiService } from '@/core/AiService.js';
import { LoggerService } from '@/core/LoggerService.js';
import type { TestingModule } from '@nestjs/testing';
-import type { MockMetadata } from 'jest-mock';
+import type { MockFunctionMetadata } 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 MockMetadata;
+ const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts
index 2f5f3745dea..7350da3caec 100644
--- a/packages/backend/test/unit/MfmService.ts
+++ b/packages/backend/test/unit/MfmService.ts
@@ -24,25 +24,25 @@ describe('MfmService', () => {
describe('toHtml', () => {
test('br', () => {
const input = 'foo\nbar\nbaz';
- const output = 'foo
bar
baz';
+ const output = 'foo
bar
baz
';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
test('br alt', () => {
const input = 'foo\r\nbar\rbaz';
- const output = 'foo
bar
baz';
+ const output = 'foo
bar
baz
';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
test('Do not generate unnecessary span', () => {
const input = 'foo $[tada bar]';
- const output = 'foo bar';
+ const output = 'foo bar
';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
test('escape', () => {
const input = '```\nHello, world!
\n```';
- const output = '<p>Hello, world!</p>
';
+ const output = '<p>Hello, world!</p>
';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
});
@@ -118,7 +118,7 @@ describe('MfmService', () => {
assert.deepStrictEqual(mfmService.fromHtml('a Misskey b c
'), 'a Misskey(ミス キー) b c');
assert.deepStrictEqual(
mfmService.fromHtml('a MisskeyMisskeyMisskey b
'),
- 'a Misskey(ミスキー)Misskey(ミス キー)Misskey(ミスキー) b',
+ 'a Misskey(ミスキー)Misskey(ミス キー)Misskey(ミスキー) b'
);
});
diff --git a/packages/backend/test/unit/RelayService.ts b/packages/backend/test/unit/RelayService.ts
index bee580d0c75..074430dd312 100644
--- a/packages/backend/test/unit/RelayService.ts
+++ b/packages/backend/test/unit/RelayService.ts
@@ -9,7 +9,7 @@ import { jest } from '@jest/globals';
import { Test } from '@nestjs/testing';
import { ModuleMocker } from 'jest-mock';
import type { TestingModule } from '@nestjs/testing';
-import type { MockMetadata } from 'jest-mock';
+import type { MockFunctionMetadata } 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 MockMetadata;
+ const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts
index 9b17b1fbb9e..71090c8be6f 100644
--- a/packages/backend/test/unit/RoleService.ts
+++ b/packages/backend/test/unit/RoleService.ts
@@ -11,7 +11,7 @@ import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing';
import * as lolex from '@sinonjs/fake-timers';
import type { TestingModule } from '@nestjs/testing';
-import type { MockMetadata } from 'jest-mock';
+import type { MockFunctionMetadata } from 'jest-mock';
import { GlobalModule } from '@/GlobalModule.js';
import { RoleService } from '@/core/RoleService.js';
import {
@@ -104,8 +104,6 @@ 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,
});
@@ -137,7 +135,7 @@ describe('RoleService', () => {
return { fetch: jest.fn() };
}
if (typeof token === 'function') {
- const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata;
+ const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
diff --git a/packages/backend/test/unit/S3Service.ts b/packages/backend/test/unit/S3Service.ts
index 6e7e5a8b598..151f3b826a9 100644
--- a/packages/backend/test/unit/S3Service.ts
+++ b/packages/backend/test/unit/S3Service.ts
@@ -72,7 +72,7 @@ describe('S3Service', () => {
Bucket: 'fake',
Key: 'fake',
Body: 'x',
- })).rejects.toThrow(Error);
+ })).rejects.toThrowError(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.toThrow(Error);
+ })).rejects.toThrowError(Error);
});
});
});
diff --git a/packages/backend/test/unit/SigninWithPasskeyApiService.ts b/packages/backend/test/unit/SigninWithPasskeyApiService.ts
index 8ef46024ac4..0687ed84372 100644
--- a/packages/backend/test/unit/SigninWithPasskeyApiService.ts
+++ b/packages/backend/test/unit/SigninWithPasskeyApiService.ts
@@ -9,7 +9,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { FastifyReply, FastifyRequest } from 'fastify';
import { AuthenticationResponseJSON } from '@simplewebauthn/types';
import { HttpHeader } from 'fastify/types/utils.js';
-import { MockMetadata, ModuleMocker } from 'jest-mock';
+import { MockFunctionMetadata, 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 MockMetadata;
+ const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
diff --git a/packages/backend/test/unit/chart.ts b/packages/backend/test/unit/chart.ts
index 364a2c2fbd5..9dedd3a79da 100644
--- a/packages/backend/test/unit/chart.ts
+++ b/packages/backend/test/unit/chart.ts
@@ -9,7 +9,6 @@ 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';
@@ -19,16 +18,16 @@ import { entity as TestGroupedChartEntity } from '@/core/chart/charts/entities/t
import { entity as TestUniqueChartEntity } from '@/core/chart/charts/entities/test-unique.js';
import { entity as TestIntersectionChartEntity } from '@/core/chart/charts/entities/test-intersection.js';
import { loadConfig } from '@/config.js';
+import type { AppLockService } from '@/core/AppLockService.js';
import Logger from '@/logger.js';
describe('Chart', () => {
const config = loadConfig();
+ const appLockService = {
+ getChartInsertLock: () => () => Promise.resolve(() => {}),
+ } as unknown as jest.Mocked;
let db: DataSource | undefined;
- let redisClient = {
- set: () => Promise.resolve('OK'),
- get: () => Promise.resolve(null),
- } as unknown as jest.Mocked;
let testChart: TestChart;
let testGroupedChart: TestGroupedChart;
@@ -65,14 +64,12 @@ describe('Chart', () => {
await db.initialize();
const logger = new Logger('chart'); // TODO: モックにする
- testChart = new TestChart(db, redisClient, logger);
- testGroupedChart = new TestGroupedChart(db, redisClient, logger);
- testUniqueChart = new TestUniqueChart(db, redisClient, logger);
- testIntersectionChart = new TestIntersectionChart(db, redisClient, logger);
+ testChart = new TestChart(db, appLockService, logger);
+ testGroupedChart = new TestGroupedChart(db, appLockService, logger);
+ testUniqueChart = new TestUniqueChart(db, appLockService, logger);
+ testIntersectionChart = new TestIntersectionChart(db, appLockService, logger);
clock = lolex.install({
- // https://github.com/sinonjs/sinon/issues/2620
- toFake: Object.keys(lolex.timers).filter((key) => !['nextTick', 'queueMicrotask'].includes(key)) as lolex.FakeMethod[],
now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)),
shouldClearNativeTimers: true,
});
diff --git a/packages/backend/test/unit/misc/should-hide-note-by-time.ts b/packages/backend/test/unit/misc/should-hide-note-by-time.ts
index 1c463c82c6f..29cbd751a3f 100644
--- a/packages/backend/test/unit/misc/should-hide-note-by-time.ts
+++ b/packages/backend/test/unit/misc/should-hide-note-by-time.ts
@@ -3,35 +3,30 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { describe, expect, test, beforeEach, afterEach } from '@jest/globals';
-import * as lolex from '@sinonjs/fake-timers';
+import { describe, expect, test, beforeEach, afterEach, jest } from '@jest/globals';
import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
describe('misc:should-hide-note-by-time', () => {
- let clock: lolex.InstalledClock;
- const epoch = Date.UTC(2000, 0, 1, 0, 0, 0);
+ let now: number;
beforeEach(() => {
- clock = lolex.install({
- // https://github.com/sinonjs/sinon/issues/2620
- toFake: Object.keys(lolex.timers).filter((key) => !['nextTick', 'queueMicrotask'].includes(key)) as lolex.FakeMethod[],
- now: new Date(epoch),
- shouldClearNativeTimers: true,
- });
+ now = Date.now();
+ jest.useFakeTimers();
+ jest.setSystemTime(now);
});
afterEach(() => {
- clock.uninstall();
+ jest.useRealTimers();
});
describe('hiddenBefore が null または undefined の場合', () => {
test('hiddenBefore が null のときは false を返す(非表示機能が有効でない)', () => {
- const createdAt = new Date(epoch - 86400000); // 1 day ago
+ const createdAt = new Date(now - 86400000); // 1 day ago
expect(shouldHideNoteByTime(null, createdAt)).toBe(false);
});
test('hiddenBefore が undefined のときは false を返す(非表示機能が有効でない)', () => {
- const createdAt = new Date(epoch - 86400000); // 1 day ago
+ const createdAt = new Date(now - 86400000); // 1 day ago
expect(shouldHideNoteByTime(undefined, createdAt)).toBe(false);
});
});
@@ -39,70 +34,70 @@ describe('misc:should-hide-note-by-time', () => {
describe('相対時間モード (hiddenBefore <= 0)', () => {
test('閾値内に作成されたノートは false を返す(作成からの経過時間がまだ短い→表示)', () => {
const hiddenBefore = -86400; // 1 day in seconds
- const createdAt = new Date(epoch - 3600000); // 1 hour ago
+ const createdAt = new Date(now - 3600000); // 1 hour ago
expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(false);
});
test('閾値を超えて作成されたノートは true を返す(指定期間以上経過している→非表示)', () => {
const hiddenBefore = -86400; // 1 day in seconds
- const createdAt = new Date(epoch - 172800000); // 2 days ago
+ const createdAt = new Date(now - 172800000); // 2 days ago
expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
});
test('ちょうど閾値で作成されたノートは true を返す(閾値に達したら非表示)', () => {
const hiddenBefore = -86400; // 1 day in seconds
- const createdAt = new Date(epoch - 86400000); // exactly 1 day ago
+ const createdAt = new Date(now - 86400000); // exactly 1 day ago
expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
});
test('異なる相対時間値で判定できる(1時間設定と3時間設定の異なる結果)', () => {
- const createdAt = new Date(epoch - 7200000); // 2 hours ago
+ const createdAt = new Date(now - 7200000); // 2 hours ago
expect(shouldHideNoteByTime(-3600, createdAt)).toBe(true); // 1時間経過→非表示
expect(shouldHideNoteByTime(-10800, createdAt)).toBe(false); // 3時間未経過→表示
});
test('ISO 8601 形式の文字列の createdAt に対応できる(文字列でも正しく判定)', () => {
- const createdAtString = new Date(epoch - 86400000).toISOString();
+ const createdAtString = new Date(now - 86400000).toISOString();
const hiddenBefore = -86400; // 1 day in seconds
expect(shouldHideNoteByTime(hiddenBefore, createdAtString)).toBe(true);
});
test('hiddenBefore が 0 の場合に対応できる(0秒以上経過で非表示→ほぼ全て非表示)', () => {
const hiddenBefore = 0;
- const createdAt = new Date(epoch - 1); // 1ms ago
+ const createdAt = new Date(now - 1); // 1ms ago
expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
});
});
describe('絶対時間モード (hiddenBefore > 0)', () => {
test('閾値タイムスタンプより後に作成されたノートは false を返す(指定日時より後→表示)', () => {
- const thresholdSeconds = Math.floor(epoch / 1000);
- const createdAt = new Date(epoch + 3600000); // 1 hour from epoch
+ const thresholdSeconds = Math.floor(now / 1000);
+ const createdAt = new Date(now + 3600000); // 1 hour from now
expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(false);
});
test('閾値タイムスタンプより前に作成されたノートは true を返す(指定日時より前→非表示)', () => {
- const thresholdSeconds = Math.floor(epoch / 1000);
- const createdAt = new Date(epoch - 3600000); // 1 hour ago
+ const thresholdSeconds = Math.floor(now / 1000);
+ const createdAt = new Date(now - 3600000); // 1 hour ago
expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(true);
});
test('ちょうど閾値タイムスタンプで作成されたノートは true を返す(指定日時に達したら非表示)', () => {
- const thresholdSeconds = Math.floor(epoch / 1000);
- const createdAt = new Date(epoch); // exactly epoch
+ const thresholdSeconds = Math.floor(now / 1000);
+ const createdAt = new Date(now); // exactly now
expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(true);
});
test('ISO 8601 形式の文字列の createdAt に対応できる(文字列でも正しく判定)', () => {
- const thresholdSeconds = Math.floor(epoch / 1000);
- const createdAtString = new Date(epoch - 3600000).toISOString();
+ const thresholdSeconds = Math.floor(now / 1000);
+ const createdAtString = new Date(now - 3600000).toISOString();
expect(shouldHideNoteByTime(thresholdSeconds, createdAtString)).toBe(true);
});
test('異なる閾値タイムスタンプで判定できる(2021年設定と現在より1時間前設定の異なる結果)', () => {
- const thresholdSeconds = Math.floor((epoch - 86400000) / 1000); // 1 day ago
- const createdAtBefore = new Date(epoch - 172800000); // 2 days ago
- const createdAtAfter = new Date(epoch - 3600000); // 1 hour ago
+ const thresholdSeconds = Math.floor((now - 86400000) / 1000); // 1 day ago
+ const createdAtBefore = new Date(now - 172800000); // 2 days ago
+ const createdAtAfter = new Date(now - 3600000); // 1 hour ago
expect(shouldHideNoteByTime(thresholdSeconds, createdAtBefore)).toBe(true); // 閾値より前→非表示
expect(shouldHideNoteByTime(thresholdSeconds, createdAtAfter)).toBe(false); // 閾値より後→表示
});
@@ -111,25 +106,25 @@ describe('misc:should-hide-note-by-time', () => {
describe('エッジケース', () => {
test('相対時間モードで非常に古いノートに対応できる(非常に古い→閾値超→非表示)', () => {
const hiddenBefore = -1; // hide notes older than 1 second
- const createdAt = new Date(epoch - 1000000); // very old
+ const createdAt = new Date(now - 1000000); // very old
expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
});
test('相対時間モードで非常に新しいノートに対応できる(非常に新しい→閾値未満→表示)', () => {
const hiddenBefore = -86400; // 1 day
- const createdAt = new Date(epoch - 1); // 1ms ago
+ const createdAt = new Date(now - 1); // 1ms ago
expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(false);
});
test('大きなタイムスタンプ値に対応できる(未来の日時を指定→現在のノートは全て非表示)', () => {
- const thresholdSeconds = Math.floor(epoch / 1000) + 86400; // 1 day from epoch
- const createdAt = new Date(epoch); // created epoch
+ const thresholdSeconds = Math.floor(now / 1000) + 86400; // 1 day from now
+ const createdAt = new Date(now); // created now
expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(true);
});
test('小さな相対時間値に対応できる(1秒設定で2秒前→非表示)', () => {
const hiddenBefore = -1; // 1 second
- const createdAt = new Date(epoch - 2000); // 2 seconds ago
+ const createdAt = new Date(now - 2000); // 2 seconds ago
expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
});
});
diff --git a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
index 01a36c9feff..211846eef21 100644
--- a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
+++ b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
@@ -141,8 +141,6 @@ describe('CheckModeratorsActivityProcessorService', () => {
beforeEach(async () => {
clock = lolex.install({
- // https://github.com/sinonjs/sinon/issues/2620
- toFake: Object.keys(lolex.timers).filter((key) => !['nextTick', 'queueMicrotask'].includes(key)) as lolex.FakeMethod[],
now: new Date(baseDate),
shouldClearNativeTimers: true,
});
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index ecca28b5aff..daae7b96438 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -10,8 +10,8 @@ import { randomUUID } from 'node:crypto';
import { inspect } from 'node:util';
import WebSocket, { ClientOptions } from 'ws';
import fetch, { File, RequestInit, type Headers } from 'node-fetch';
-import * as htmlParser from 'node-html-parser';
import { DataSource } from 'typeorm';
+import { JSDOM } from 'jsdom';
import { type Response } from 'node-fetch';
import Fastify from 'fastify';
import { entities } from '../src/postgres.js';
@@ -468,7 +468,7 @@ export function makeStreamCatcher(
export type SimpleGetResponse = {
status: number,
- body: any | null,
+ body: any | JSDOM | 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') ?? '') ? htmlParser.parse(await res.text()) :
+ htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) :
await bodyExtractor(res);
return {
diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json
index 25584e475da..2b15a5cc7a3 100644
--- a/packages/backend/tsconfig.json
+++ b/packages/backend/tsconfig.json
@@ -23,17 +23,12 @@
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
"isolatedModules": true,
- "jsx": "react-jsx",
- "jsxImportSource": "@kitajs/html",
"rootDir": "./src",
"baseUrl": "./",
"paths": {
"@/*": ["./src/*"]
},
"outDir": "./built",
- "plugins": [
- {"name": "@kitajs/ts-html-plugin"}
- ],
"types": [
"node"
],
@@ -48,8 +43,7 @@
},
"compileOnSave": false,
"include": [
- "./src/**/*.ts",
- "./src/**/*.tsx"
+ "./src/**/*.ts"
],
"exclude": [
"./src/**/*.test.ts"
diff --git a/packages/frontend-builder/locale-inliner.ts b/packages/frontend-builder/locale-inliner.ts
index 191d7250a67..9bef465eebd 100644
--- a/packages/frontend-builder/locale-inliner.ts
+++ b/packages/frontend-builder/locale-inliner.ts
@@ -10,7 +10,7 @@ import { collectModifications } from './locale-inliner/collect-modifications.js'
import { applyWithLocale } from './locale-inliner/apply-with-locale.js';
import { blankLogger } from './logger.js';
import type { Logger } from './logger.js';
-import type { Locale } from 'i18n';
+import type { Locale } from '../../locales/index.js';
import type { Manifest as ViteManifest } from 'vite';
export class LocaleInliner {
diff --git a/packages/frontend-builder/locale-inliner/apply-with-locale.ts b/packages/frontend-builder/locale-inliner/apply-with-locale.ts
index 78851d30295..5e601cdf126 100644
--- a/packages/frontend-builder/locale-inliner/apply-with-locale.ts
+++ b/packages/frontend-builder/locale-inliner/apply-with-locale.ts
@@ -5,7 +5,7 @@
import MagicString from 'magic-string';
import { assertNever } from '../utils.js';
-import type { ILocale, Locale } from 'i18n';
+import type { Locale, ILocale } from '../../../locales/index.js';
import type { TextModification } from '../locale-inliner.js';
import type { Logger } from '../logger.js';
diff --git a/packages/frontend-builder/package.json b/packages/frontend-builder/package.json
index 36c32b915de..ef5c8e0367a 100644
--- a/packages/frontend-builder/package.json
+++ b/packages/frontend-builder/package.json
@@ -12,15 +12,14 @@
"devDependencies": {
"@types/estree": "1.0.8",
"@types/node": "24.10.1",
- "@typescript-eslint/eslint-plugin": "8.48.0",
- "@typescript-eslint/parser": "8.48.0",
+ "@typescript-eslint/eslint-plugin": "8.47.0",
+ "@typescript-eslint/parser": "8.47.0",
"rollup": "4.53.3",
"typescript": "5.9.3"
},
"dependencies": {
- "i18n": "workspace:*",
"estree-walker": "3.0.3",
"magic-string": "0.30.21",
- "vite": "7.2.4"
+ "vite": "7.2.2"
}
}
diff --git a/packages/frontend-embed/build.ts b/packages/frontend-embed/build.ts
index 4e1f5888028..737233a4d09 100644
--- a/packages/frontend-embed/build.ts
+++ b/packages/frontend-embed/build.ts
@@ -2,7 +2,7 @@ import * as fs from 'fs/promises';
import url from 'node:url';
import path from 'node:path';
import { execa } from 'execa';
-import locales from 'i18n';
+import locales from '../../locales/index.js';
import { LocaleInliner } from '../frontend-builder/locale-inliner.js'
import { createLogger } from '../frontend-builder/logger';
diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json
index e82cdc1f279..7bfd32686c7 100644
--- a/packages/frontend-embed/package.json
+++ b/packages/frontend-embed/package.json
@@ -11,12 +11,13 @@
},
"dependencies": {
"@discordapp/twemoji": "16.0.1",
- "i18n": "workspace:*",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.3",
"@rollup/pluginutils": "5.3.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.2",
+ "@vue/compiler-sfc": "3.5.24",
+ "astring": "1.9.0",
"buraha": "0.0.1",
"estree-walker": "3.0.3",
"frontend-shared": "workspace:*",
@@ -26,12 +27,15 @@
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.53.3",
- "sass": "1.94.2",
- "shiki": "3.17.0",
+ "sass": "1.94.1",
+ "shiki": "3.15.0",
"tinycolor2": "1.6.0",
+ "tsc-alias": "1.8.16",
+ "tsconfig-paths": "4.2.0",
+ "typescript": "5.9.3",
"uuid": "13.0.0",
- "vite": "7.2.4",
- "vue": "3.5.25"
+ "vite": "7.2.2",
+ "vue": "3.5.24"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.5",
@@ -43,26 +47,26 @@
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
- "@typescript-eslint/eslint-plugin": "8.48.0",
- "@typescript-eslint/parser": "8.48.0",
- "@vitest/coverage-v8": "4.0.14",
- "@vue/runtime-core": "3.5.25",
+ "@typescript-eslint/eslint-plugin": "8.47.0",
+ "@typescript-eslint/parser": "8.47.0",
+ "@vitest/coverage-v8": "3.2.4",
+ "@vue/runtime-core": "3.5.24",
"acorn": "8.15.0",
"cross-env": "10.1.0",
"eslint-plugin-import": "2.32.0",
- "eslint-plugin-vue": "10.6.2",
- "happy-dom": "20.0.11",
+ "eslint-plugin-vue": "10.5.1",
+ "fast-glob": "3.3.3",
+ "happy-dom": "20.0.10",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
- "msw": "2.12.3",
+ "msw": "2.12.2",
"nodemon": "3.1.11",
- "prettier": "3.7.1",
- "start-server-and-test": "2.1.3",
+ "prettier": "3.6.2",
+ "start-server-and-test": "2.1.2",
"tsx": "4.20.6",
- "typescript": "5.9.3",
"vite-plugin-turbosnap": "1.0.3",
- "vue-component-type-helpers": "3.1.5",
+ "vue-component-type-helpers": "3.1.4",
"vue-eslint-parser": "10.2.0",
- "vue-tsc": "3.1.5"
+ "vue-tsc": "3.1.4"
}
}
diff --git a/packages/frontend-embed/src/components/I18n.vue b/packages/frontend-embed/src/components/I18n.vue
index 9866e509583..b621110ec9e 100644
--- a/packages/frontend-embed/src/components/I18n.vue
+++ b/packages/frontend-embed/src/components/I18n.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only