summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/web/ClientServerService.ts
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2025-12-01 18:36:55 +0900
committerGitHub <noreply@github.com>2025-12-01 18:36:55 +0900
commitf222d7e24d3d134a078868c89363cacde906ccdc (patch)
tree987edf63823a078365a514b0c755258b196a5d1b /packages/backend/src/server/web/ClientServerService.ts
parentfix(frontend): visibilityStateがhiddenな状態でstartViewTransitionしな... (diff)
downloadmisskey-f222d7e24d3d134a078868c89363cacde906ccdc.tar.gz
misskey-f222d7e24d3d134a078868c89363cacde906ccdc.tar.bz2
misskey-f222d7e24d3d134a078868c89363cacde906ccdc.zip
enhance(backend): pugをやめ、JSXベースのテンプレートに変更 (#16908)
* enhance(backend): pugをやめ、JSXベースのテンプレートに変更 (to misskey-dev dev branch) (#16889) * wip * wip * wip * wip * fix lint * attempt to fix test * fix * fix * fix: oauthページの描画がおかしい問題を修正 * typo [ci skip] * fix * fix * fix * fix * fix * refactor * fix * fix * fix broken lockfile * fix: expose supported languages as global variable * remove i18n package from root as it is no longer required [ci skip] * fix * fix: add i18n package.json to Docker target-builder stage for federation tests (#16909) * Initial plan * fix: add i18n package.json to Docker target-builder stage for federation tests Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> * fix: followup-test-federation for enh-remove-pug (#16910) * fix: followup-test-federation for enh-remove-pug * Revert "fix: add i18n package.json to Docker target-builder stage for federation tests (#16909)" This reverts commit 14313468d34f49c363eef4d0a932e9fc0d9a37fb. * fix: CSSが読み込まれない場合がある問題を修正 * fix [ci skip] * fix: propsのデフォルト値をnull合体演算子から論理和演算子に変更(空文字に対処するため) * remove @types/pug * enhance: bootloaderを埋め込むように * fix possible race condition * remove esbuild --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> Co-authored-by: おさむのひと <46447427+samunohito@users.noreply.github.com>
Diffstat (limited to 'packages/backend/src/server/web/ClientServerService.ts')
-rw-r--r--packages/backend/src/server/web/ClientServerService.ts215
1 files changed, 86 insertions, 129 deletions
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index fef6a27087..bcea935409 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -9,20 +9,16 @@ import { fileURLToPath } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import sharp from 'sharp';
-import pug from 'pug';
import { In, IsNull } from 'typeorm';
import fastifyStatic from '@fastify/static';
-import fastifyView from '@fastify/view';
import fastifyProxy from '@fastify/http-proxy';
import vary from 'vary';
import type { Config } from '@/config.js';
-import { getNoteSummary } from '@/misc/get-note-summary.js';
import { DI } from '@/di-symbols.js';
import * as Acct from '@/misc/acct.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js';
-import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
@@ -41,14 +37,33 @@ import type {
} from '@/models/_.js';
import type Logger from '@/logger.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
+import { htmlSafeJsonStringify } from '@/misc/json-stringify-html-safe.js';
import { bindThis } from '@/decorators.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
-import { RoleService } from '@/core/RoleService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { FeedService } from './FeedService.js';
import { UrlPreviewService } from './UrlPreviewService.js';
import { ClientLoggerService } from './ClientLoggerService.js';
+import { HtmlTemplateService } from './HtmlTemplateService.js';
+
+import { BasePage } from './views/base.js';
+import { UserPage } from './views/user.js';
+import { NotePage } from './views/note.js';
+import { PagePage } from './views/page.js';
+import { ClipPage } from './views/clip.js';
+import { FlashPage } from './views/flash.js';
+import { GalleryPostPage } from './views/gallery-post.js';
+import { ChannelPage } from './views/channel.js';
+import { ReversiGamePage } from './views/reversi-game.js';
+import { AnnouncementPage } from './views/announcement.js';
+import { BaseEmbed } from './views/base-embed.js';
+import { InfoCardPage } from './views/info-card.js';
+import { BiosPage } from './views/bios.js';
+import { CliPage } from './views/cli.js';
+import { FlushPage } from './views/flush.js';
+import { ErrorPage } from './views/error.js';
+
import type { FastifyError, FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify';
const _filename = fileURLToPath(import.meta.url);
@@ -62,20 +77,6 @@ const frontendViteOut = `${_dirname}/../../../../../built/_frontend_vite_/`;
const frontendEmbedViteOut = `${_dirname}/../../../../../built/_frontend_embed_vite_/`;
const tarball = `${_dirname}/../../../../../built/tarball/`;
-const ESCAPE_LOOKUP = {
- '&': '\\u0026',
- '>': '\\u003e',
- '<': '\\u003c',
- '\u2028': '\\u2028',
- '\u2029': '\\u2029',
-} as Record<string, string>;
-
-const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
-
-function htmlSafeJsonStringify(obj: any): string {
- return JSON.stringify(obj).replace(ESCAPE_REGEX, x => ESCAPE_LOOKUP[x]);
-}
-
@Injectable()
export class ClientServerService {
private logger: Logger;
@@ -121,7 +122,6 @@ export class ClientServerService {
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private pageEntityService: PageEntityService,
- private metaEntityService: MetaEntityService,
private galleryPostEntityService: GalleryPostEntityService,
private clipEntityService: ClipEntityService,
private channelEntityService: ChannelEntityService,
@@ -129,7 +129,7 @@ export class ClientServerService {
private announcementEntityService: AnnouncementEntityService,
private urlPreviewService: UrlPreviewService,
private feedService: FeedService,
- private roleService: RoleService,
+ private htmlTemplateService: HtmlTemplateService,
private clientLoggerService: ClientLoggerService,
) {
//this.createServer = this.createServer.bind(this);
@@ -196,37 +196,9 @@ export class ClientServerService {
}
@bindThis
- private async generateCommonPugData(meta: MiMeta) {
- return {
- instanceName: meta.name ?? 'Misskey',
- icon: meta.iconUrl,
- appleTouchIcon: meta.app512IconUrl,
- themeColor: meta.themeColor,
- serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg',
- infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',
- notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg',
- instanceUrl: this.config.url,
- metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)),
- now: Date.now(),
- federationEnabled: this.meta.federation !== 'none',
- };
- }
-
- @bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
const configUrl = new URL(this.config.url);
- fastify.register(fastifyView, {
- root: _dirname + '/views',
- engine: {
- pug: pug,
- },
- defaultContext: {
- version: this.config.version,
- config: this.config,
- },
- });
-
fastify.addHook('onRequest', (request, reply, done) => {
// クリックジャッキング防止のためiFrameの中に入れられないようにする
reply.header('X-Frame-Options', 'DENY');
@@ -427,16 +399,15 @@ export class ClientServerService {
//#endregion
- const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => {
+ const renderBase = async (reply: FastifyReply, data: Partial<Parameters<typeof BasePage>[0]> = {}) => {
reply.header('Cache-Control', 'public, max-age=30');
- return await reply.view('base', {
- img: this.meta.bannerUrl,
- url: this.config.url,
+ return await HtmlTemplateService.replyHtml(reply, BasePage({
+ img: this.meta.bannerUrl ?? undefined,
title: this.meta.name ?? 'Misskey',
- desc: this.meta.description,
- ...await this.generateCommonPugData(this.meta),
+ desc: this.meta.description ?? undefined,
+ ...await this.htmlTemplateService.getCommonData(),
...data,
- });
+ }));
};
// URL preview endpoint
@@ -518,11 +489,6 @@ export class ClientServerService {
)
) {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
- const me = profile.fields
- ? profile.fields
- .filter(filed => filed.value != null && filed.value.match(/^https?:/))
- .map(field => field.value)
- : [];
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
@@ -535,15 +501,15 @@ export class ClientServerService {
userProfile: profile,
});
- return await reply.view('user', {
- user, profile, me,
- avatarUrl: _user.avatarUrl,
+ return await HtmlTemplateService.replyHtml(reply, UserPage({
+ user: _user,
+ profile,
sub: request.params.sub,
- ...await this.generateCommonPugData(this.meta),
- clientCtx: htmlSafeJsonStringify({
+ ...await this.htmlTemplateService.getCommonData(),
+ clientCtxJson: htmlSafeJsonStringify({
user: _user,
}),
- });
+ }));
} else {
// リモートユーザーなので
// モデレータがAPI経由で参照可能にするために404にはしない
@@ -594,17 +560,14 @@ export class ClientServerService {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
- return await reply.view('note', {
+ return await HtmlTemplateService.replyHtml(reply, NotePage({
note: _note,
profile,
- avatarUrl: _note.user.avatarUrl,
- // TODO: Let locale changeable by instance setting
- summary: getNoteSummary(_note),
- ...await this.generateCommonPugData(this.meta),
- clientCtx: htmlSafeJsonStringify({
+ ...await this.htmlTemplateService.getCommonData(),
+ clientCtxJson: htmlSafeJsonStringify({
note: _note,
}),
- });
+ }));
} else {
return await renderBase(reply);
}
@@ -637,12 +600,11 @@ export class ClientServerService {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
- return await reply.view('page', {
+ return await HtmlTemplateService.replyHtml(reply, PagePage({
page: _page,
profile,
- avatarUrl: _page.user.avatarUrl,
- ...await this.generateCommonPugData(this.meta),
- });
+ ...await this.htmlTemplateService.getCommonData(),
+ }));
} else {
return await renderBase(reply);
}
@@ -662,12 +624,11 @@ export class ClientServerService {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
- return await reply.view('flash', {
+ return await HtmlTemplateService.replyHtml(reply, FlashPage({
flash: _flash,
profile,
- avatarUrl: _flash.user.avatarUrl,
- ...await this.generateCommonPugData(this.meta),
- });
+ ...await this.htmlTemplateService.getCommonData(),
+ }));
} else {
return await renderBase(reply);
}
@@ -687,15 +648,14 @@ export class ClientServerService {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
- return await reply.view('clip', {
+ return await HtmlTemplateService.replyHtml(reply, ClipPage({
clip: _clip,
profile,
- avatarUrl: _clip.user.avatarUrl,
- ...await this.generateCommonPugData(this.meta),
- clientCtx: htmlSafeJsonStringify({
+ ...await this.htmlTemplateService.getCommonData(),
+ clientCtxJson: htmlSafeJsonStringify({
clip: _clip,
}),
- });
+ }));
} else {
return await renderBase(reply);
}
@@ -713,12 +673,11 @@ export class ClientServerService {
reply.header('X-Robots-Tag', 'noimageai');
reply.header('X-Robots-Tag', 'noai');
}
- return await reply.view('gallery-post', {
- post: _post,
+ return await HtmlTemplateService.replyHtml(reply, GalleryPostPage({
+ galleryPost: _post,
profile,
- avatarUrl: _post.user.avatarUrl,
- ...await this.generateCommonPugData(this.meta),
- });
+ ...await this.htmlTemplateService.getCommonData(),
+ }));
} else {
return await renderBase(reply);
}
@@ -733,10 +692,10 @@ export class ClientServerService {
if (channel) {
const _channel = await this.channelEntityService.pack(channel);
reply.header('Cache-Control', 'public, max-age=15');
- return await reply.view('channel', {
+ return await HtmlTemplateService.replyHtml(reply, ChannelPage({
channel: _channel,
- ...await this.generateCommonPugData(this.meta),
- });
+ ...await this.htmlTemplateService.getCommonData(),
+ }));
} else {
return await renderBase(reply);
}
@@ -751,10 +710,10 @@ export class ClientServerService {
if (game) {
const _game = await this.reversiGameEntityService.packDetail(game);
reply.header('Cache-Control', 'public, max-age=3600');
- return await reply.view('reversi-game', {
- game: _game,
- ...await this.generateCommonPugData(this.meta),
- });
+ return await HtmlTemplateService.replyHtml(reply, ReversiGamePage({
+ reversiGame: _game,
+ ...await this.htmlTemplateService.getCommonData(),
+ }));
} else {
return await renderBase(reply);
}
@@ -770,10 +729,10 @@ export class ClientServerService {
if (announcement) {
const _announcement = await this.announcementEntityService.pack(announcement);
reply.header('Cache-Control', 'public, max-age=3600');
- return await reply.view('announcement', {
+ return await HtmlTemplateService.replyHtml(reply, AnnouncementPage({
announcement: _announcement,
- ...await this.generateCommonPugData(this.meta),
- });
+ ...await this.htmlTemplateService.getCommonData(),
+ }));
} else {
return await renderBase(reply);
}
@@ -806,13 +765,13 @@ export class ClientServerService {
const _user = await this.userEntityService.pack(user);
reply.header('Cache-Control', 'public, max-age=3600');
- return await reply.view('base-embed', {
+ return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
title: this.meta.name ?? 'Misskey',
- ...await this.generateCommonPugData(this.meta),
- embedCtx: htmlSafeJsonStringify({
+ ...await this.htmlTemplateService.getCommonData(),
+ embedCtxJson: htmlSafeJsonStringify({
user: _user,
}),
- });
+ }));
});
fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => {
@@ -832,13 +791,13 @@ export class ClientServerService {
const _note = await this.noteEntityService.pack(note, null, { detail: true });
reply.header('Cache-Control', 'public, max-age=3600');
- return await reply.view('base-embed', {
+ return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
title: this.meta.name ?? 'Misskey',
- ...await this.generateCommonPugData(this.meta),
- embedCtx: htmlSafeJsonStringify({
+ ...await this.htmlTemplateService.getCommonData(),
+ embedCtxJson: htmlSafeJsonStringify({
note: _note,
}),
- });
+ }));
});
fastify.get<{ Params: { clip: string; } }>('/embed/clips/:clip', async (request, reply) => {
@@ -853,48 +812,46 @@ export class ClientServerService {
const _clip = await this.clipEntityService.pack(clip);
reply.header('Cache-Control', 'public, max-age=3600');
- return await reply.view('base-embed', {
+ return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
title: this.meta.name ?? 'Misskey',
- ...await this.generateCommonPugData(this.meta),
- embedCtx: htmlSafeJsonStringify({
+ ...await this.htmlTemplateService.getCommonData(),
+ embedCtxJson: htmlSafeJsonStringify({
clip: _clip,
}),
- });
+ }));
});
fastify.get('/embed/*', async (request, reply) => {
reply.removeHeader('X-Frame-Options');
reply.header('Cache-Control', 'public, max-age=3600');
- return await reply.view('base-embed', {
+ return await HtmlTemplateService.replyHtml(reply, BaseEmbed({
title: this.meta.name ?? 'Misskey',
- ...await this.generateCommonPugData(this.meta),
- });
+ ...await this.htmlTemplateService.getCommonData(),
+ }));
});
fastify.get('/_info_card_', async (request, reply) => {
reply.removeHeader('X-Frame-Options');
- return await reply.view('info-card', {
+ return await HtmlTemplateService.replyHtml(reply, InfoCardPage({
version: this.config.version,
- host: this.config.host,
+ config: this.config,
meta: this.meta,
- originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }),
- originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }),
- });
+ }));
});
//#endregion
fastify.get('/bios', async (request, reply) => {
- return await reply.view('bios', {
+ return await HtmlTemplateService.replyHtml(reply, BiosPage({
version: this.config.version,
- });
+ }));
});
fastify.get('/cli', async (request, reply) => {
- return await reply.view('cli', {
+ return await HtmlTemplateService.replyHtml(reply, CliPage({
version: this.config.version,
- });
+ }));
});
const override = (source: string, target: string, depth = 0) =>
@@ -917,7 +874,7 @@ export class ClientServerService {
reply.header('Clear-Site-Data', '"*"');
}
reply.header('Set-Cookie', 'http-flush-failed=1; Path=/flush; Max-Age=60');
- return await reply.view('flush');
+ return await HtmlTemplateService.replyHtml(reply, FlushPage());
});
// streamingに非WebSocketリクエストが来た場合にbase htmlをキャシュ付きで返すと、Proxy等でそのパスがキャッシュされておかしくなる
@@ -943,10 +900,10 @@ export class ClientServerService {
});
reply.code(500);
reply.header('Cache-Control', 'max-age=10, must-revalidate');
- return await reply.view('error', {
+ return await HtmlTemplateService.replyHtml(reply, ErrorPage({
code: error.code,
id: errId,
- });
+ }));
});
done();