diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2025-12-01 18:36:55 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-01 18:36:55 +0900 |
| commit | f222d7e24d3d134a078868c89363cacde906ccdc (patch) | |
| tree | 987edf63823a078365a514b0c755258b196a5d1b /packages/backend/src/server/web/ClientServerService.ts | |
| parent | fix(frontend): visibilityStateがhiddenな状態でstartViewTransitionしな... (diff) | |
| download | misskey-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.ts | 215 |
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(); |