Unverified Commit f2eb23cd authored by Rigel Kent's avatar Rigel Kent Committed by GitHub
Browse files

emit more specific status codes on video upload (#3423)

- reduce http status codes list to potentially useful codes
- convert more codes to typed ones
- factorize html generator for error responses
parent c977fd3e
......@@ -10,6 +10,7 @@ import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { InstanceService } from '@app/shared/shared-instance'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
import { ServerConfig } from '@shared/models'
@Component({
......@@ -78,7 +79,7 @@ export class ContactAdminModalComponent extends FormReactive implements OnInit {
},
err => {
this.error = err.status === 403
this.error = err.status === HttpStatusCode.FORBIDDEN_403
? $localize`You already sent this form recently`
: err.message
}
......
......@@ -6,6 +6,7 @@ import { AuthService, Notifier, RedirectService, RestExtractor, ScreenService, U
import { Account, AccountService, DropdownAction, ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main'
import { AccountReportComponent } from '@app/shared/shared-moderation'
import { User, UserRight } from '@shared/models'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
@Component({
templateUrl: './accounts.component.html',
......@@ -47,7 +48,10 @@ export class AccountsComponent implements OnInit, OnDestroy {
switchMap(accountId => this.accountService.getAccount(accountId)),
tap(account => this.onAccount(account)),
switchMap(account => this.videoChannelService.listAccountVideoChannels(account)),
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ]))
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [
HttpStatusCode.BAD_REQUEST_400,
HttpStatusCode.NOT_FOUND_404
]))
)
.subscribe(
videoChannels => this.videoChannels = videoChannels.data,
......
......@@ -10,6 +10,7 @@ import {
import { FormValidatorService } from '@app/shared/shared-forms'
import { VideoChannelService } from '@app/shared/shared-main'
import { VideoChannelCreate } from '@shared/models'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
import { MyVideoChannelEdit } from './my-video-channel-edit'
@Component({
......@@ -58,7 +59,7 @@ export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements
},
err => {
if (err.status === 409) {
if (err.status === HttpStatusCode.CONFLICT_409) {
this.error = $localize`This name already exists on this instance.`
return
}
......
......@@ -6,6 +6,7 @@ import { ActivatedRoute } from '@angular/router'
import { AuthService, Notifier, RestExtractor, ScreenService } from '@app/core'
import { ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main'
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
@Component({
templateUrl: './video-channels.component.html',
......@@ -37,7 +38,10 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
map(params => params[ 'videoChannelName' ]),
distinctUntilChanged(),
switchMap(videoChannelName => this.videoChannelService.getVideoChannel(videoChannelName)),
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ]))
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [
HttpStatusCode.BAD_REQUEST_400,
HttpStatusCode.NOT_FOUND_404
]))
)
.subscribe(videoChannel => {
this.videoChannel = videoChannel
......
......@@ -9,6 +9,7 @@ import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/sh
import { LoadingBarService } from '@ngx-loading-bar/core'
import { VideoPrivacy } from '@shared/models'
import { VideoSend } from './video-send'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
@Component({
selector: 'my-video-upload',
......@@ -129,17 +130,17 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
cancelUpload () {
if (this.videoUploadObservable !== null) {
this.videoUploadObservable.unsubscribe()
}
this.isUploadingVideo = false
this.videoUploadPercents = 0
this.videoUploadObservable = null
this.isUploadingVideo = false
this.videoUploadPercents = 0
this.videoUploadObservable = null
this.firstStepError.emit()
this.enableRetryAfterError = false
this.error = ''
this.firstStepError.emit()
this.enableRetryAfterError = false
this.error = ''
this.notifier.info($localize`Upload cancelled`)
}
this.notifier.info($localize`Upload cancelled`)
}
uploadFirstStep (clickedOnButton = false) {
......@@ -229,6 +230,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
notifier: this.notifier,
sticky: false
})
if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413 ||
err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) {
this.cancelUpload()
}
}
)
}
......
......@@ -39,6 +39,7 @@ import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils'
import { environment } from '../../../environments/environment'
import { VideoSupportComponent } from './modal/video-support.component'
import { VideoWatchPlaylistComponent } from './video-watch-playlist.component'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
type URLOptions = CustomizationOptions & { playerMode: PlayerMode }
......@@ -412,13 +413,25 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
$localize`This video is not available on this instance. Do you want to be redirected on the origin instance: <a href="${originUrl}">${originUrl}</a>?`,
$localize`Redirection`
).then(res => {
if (res === false) return this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ])
if (res === false) {
return this.restExtractor.redirectTo404IfNotFound(err, [
HttpStatusCode.BAD_REQUEST_400,
HttpStatusCode.UNAUTHORIZED_401,
HttpStatusCode.FORBIDDEN_403,
HttpStatusCode.NOT_FOUND_404
])
}
return window.location.href = originUrl
})
}
return this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ])
return this.restExtractor.redirectTo404IfNotFound(err, [
HttpStatusCode.BAD_REQUEST_400,
HttpStatusCode.UNAUTHORIZED_401,
HttpStatusCode.FORBIDDEN_403,
HttpStatusCode.NOT_FOUND_404
])
})
)
.subscribe(([ video, captionsResult ]) => {
......@@ -450,7 +463,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.playlistService.getVideoPlaylist(playlistId)
.pipe(
// If 401, the video is private or blocked so redirect to 404
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ]))
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [
HttpStatusCode.BAD_REQUEST_400,
HttpStatusCode.UNAUTHORIZED_401,
HttpStatusCode.FORBIDDEN_403,
HttpStatusCode.NOT_FOUND_404
]))
)
.subscribe(playlist => {
this.playlist = playlist
......
......@@ -11,6 +11,7 @@ import { environment } from '../../../environments/environment'
import { RestExtractor } from '../rest/rest-extractor.service'
import { AuthStatus } from './auth-status.model'
import { AuthUser } from './auth-user.model'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
interface UserLoginWithUsername extends UserLogin {
access_token: string
......@@ -94,7 +95,7 @@ export class AuthService {
error => {
let errorMessage = error.message
if (error.status === 403) {
if (error.status === HttpStatusCode.FORBIDDEN_403) {
errorMessage = $localize`Cannot retrieve OAuth Client credentials: ${error.text}.
Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.`
}
......
......@@ -3,6 +3,7 @@ import { Injectable } from '@angular/core'
import { Router } from '@angular/router'
import { dateToHuman } from '@app/helpers'
import { ResultList } from '@shared/models'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
@Injectable()
export class RestExtractor {
......@@ -57,9 +58,9 @@ export class RestExtractor {
errorMessage = errorsArray.join('. ')
} else if (err.error && err.error.error) {
errorMessage = err.error.error
} else if (err.status === 413) {
} else if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413) {
errorMessage = $localize`Media is too large for the server. Please contact you administrator if you want to increase the limit size.`
} else if (err.status === 429) {
} else if (err.status === HttpStatusCode.TOO_MANY_REQUESTS_429) {
const secondsLeft = err.headers.get('retry-after')
if (secondsLeft) {
const minutesLeft = Math.floor(parseInt(secondsLeft, 10) / 60)
......@@ -67,7 +68,7 @@ export class RestExtractor {
} else {
errorMessage = $localize`Too many attempts, please try again later.`
}
} else if (err.status === 500) {
} else if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) {
errorMessage = $localize`Server error. Please retry later.`
}
......@@ -92,7 +93,7 @@ export class RestExtractor {
return observableThrowError(errorObj)
}
redirectTo404IfNotFound (obj: { status: number }, status = [ 404 ]) {
redirectTo404IfNotFound (obj: { status: number }, status = [ HttpStatusCode.NOT_FOUND_404 ]) {
if (obj && obj.status && status.indexOf(obj.status) !== -1) {
// Do not use redirectService to avoid circular dependencies
this.router.navigate([ '/404' ], { skipLocationChange: true })
......
......@@ -13,6 +13,7 @@ import {
PluginType,
ClientHookName
} from '../../../../shared/models'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager'
import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
import { TranslationsManager } from '../../assets/player/translations-manager'
......@@ -85,7 +86,7 @@ export class PeerTubeEmbed {
refreshFetch (url: string, options?: RequestInit) {
return fetch(url, options)
.then((res: Response) => {
if (res.status !== 401) return res
if (res.status !== HttpStatusCode.UNAUTHORIZED_401) return res
const refreshingTokenPromise = new Promise((resolve, reject) => {
const clientId: string = peertubeLocalStorage.getItem(this.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID)
......@@ -107,7 +108,7 @@ export class PeerTubeEmbed {
method: 'POST',
body: objectToUrlEncoded(data)
}).then(res => {
if (res.status === 401) return undefined
if (res.status === HttpStatusCode.UNAUTHORIZED_401) return undefined
return res.json()
}).then((obj: UserRefreshToken & { code: 'invalid_grant'}) => {
......@@ -338,7 +339,7 @@ export class PeerTubeEmbed {
try {
playlistResponse = await playlistPromise
isResponseOk = playlistResponse.status === 200
isResponseOk = playlistResponse.status === HttpStatusCode.OK_200
} catch (err) {
console.error(err)
isResponseOk = false
......@@ -347,7 +348,7 @@ export class PeerTubeEmbed {
if (!isResponseOk) {
const serverTranslations = await this.translationsPromise
if (playlistResponse?.status === 404) {
if (playlistResponse?.status === HttpStatusCode.NOT_FOUND_404) {
this.playlistNotFound(serverTranslations)
return undefined
}
......@@ -367,7 +368,7 @@ export class PeerTubeEmbed {
try {
videoResponse = await videoPromise
isResponseOk = videoResponse.status === 200
isResponseOk = videoResponse.status === HttpStatusCode.OK_200
} catch (err) {
console.error(err)
......@@ -377,7 +378,7 @@ export class PeerTubeEmbed {
if (!isResponseOk) {
const serverTranslations = await this.translationsPromise
if (videoResponse?.status === 404) {
if (videoResponse?.status === HttpStatusCode.NOT_FOUND_404) {
this.videoNotFound(serverTranslations)
return undefined
}
......
......@@ -3,12 +3,11 @@ import { constants, promises as fs } from 'fs'
import { join } from 'path'
import { CONFIG } from '@server/initializers/config'
import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n'
import { HttpStatusCode } from '@shared/core-utils'
import { root } from '../helpers/core-utils'
import { logger } from '../helpers/logger'
import { ACCEPT_HEADERS, STATIC_MAX_AGE } from '../initializers/constants'
import { ClientHtml } from '../lib/client-html'
import { STATIC_MAX_AGE } from '../initializers/constants'
import { ClientHtml, sendHTML, serveIndexHTML } from '../lib/client-html'
import { asyncMiddleware, embedCSP } from '../middlewares'
import { HttpStatusCode } from '@shared/core-utils'
const clientsRouter = express.Router()
......@@ -118,31 +117,12 @@ function serveServerTranslations (req: express.Request, res: express.Response) {
return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
}
async function serveIndexHTML (req: express.Request, res: express.Response) {
if (req.accepts(ACCEPT_HEADERS) === 'html') {
try {
await generateHTMLPage(req, res, req.params.language)
return
} catch (err) {
logger.error('Cannot generate HTML page.', err)
}
}
return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR_500).end()
}
async function generateEmbedHtmlPage (req: express.Request, res: express.Response) {
const html = await ClientHtml.getEmbedHTML()
return sendHTML(html, res)
}
async function generateHTMLPage (req: express.Request, res: express.Response, paramLang?: string) {
const html = await ClientHtml.getDefaultHTMLPage(req, res, paramLang)
return sendHTML(html, res)
}
async function generateWatchHtmlPage (req: express.Request, res: express.Response) {
const html = await ClientHtml.getWatchHTMLPage(req.params.id + '', req, res)
......@@ -167,12 +147,6 @@ async function generateVideoChannelHtmlPage (req: express.Request, res: express.
return sendHTML(html, res)
}
function sendHTML (html: string, res: express.Response) {
res.set('Content-Type', 'text/html; charset=UTF-8')
return res.send(html)
}
async function generateManifest (req: express.Request, res: express.Response) {
const manifestPhysicalPath = join(root(), 'client', 'dist', 'manifest.webmanifest')
const manifestJson = await fs.readFile(manifestPhysicalPath, 'utf8')
......
......@@ -27,6 +27,7 @@ import { getTorrentFilePath, getVideoFilePath } from '@server/lib/video-paths'
import { getThemeOrDefault } from '../lib/plugins/theme-utils'
import { getEnabledResolutions, getRegisteredPlugins, getRegisteredThemes } from '@server/controllers/api/config'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
import { serveIndexHTML } from '@server/lib/client-html'
const staticRouter = express.Router()
......@@ -119,6 +120,11 @@ staticRouter.get('/robots.txt',
}
)
staticRouter.all('/teapot',
getCup,
asyncMiddleware(serveIndexHTML)
)
// security.txt service
staticRouter.get('/security.txt',
(_, res: express.Response) => {
......@@ -391,3 +397,11 @@ function getHLSPlaylist (video: MVideoFullLight) {
return Object.assign(playlist, { Video: video })
}
function getCup (req: express.Request, res: express.Response, next: express.NextFunction) {
res.status(HttpStatusCode.I_AM_A_TEAPOT_418)
res.setHeader('Accept-Additions', 'Non-Dairy;1,Sugar;1')
res.setHeader('Safe', 'if-sepia-awake')
return next()
}
......@@ -86,6 +86,50 @@ function toIntArray (value: any) {
return value.map(v => validator.toInt(v))
}
function isFileFieldValid (
files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[],
field: string,
optional = false
) {
// Should have files
if (!files) return optional
if (isArray(files)) return optional
// Should have a file
const fileArray = files[field]
if (!fileArray || fileArray.length === 0) {
return optional
}
// The file should exist
const file = fileArray[0]
if (!file || !file.originalname) return false
return file
}
function isFileMimeTypeValid (
files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[],
mimeTypeRegex: string,
field: string,
optional = false
) {
// Should have files
if (!files) return optional
if (isArray(files)) return optional
// Should have a file
const fileArray = files[field]
if (!fileArray || fileArray.length === 0) {
return optional
}
// The file should exist
const file = fileArray[0]
if (!file || !file.originalname) return false
return new RegExp(`^${mimeTypeRegex}$`, 'i').test(file.mimetype)
}
function isFileValid (
files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[],
mimeTypeRegex: string,
......@@ -132,5 +176,7 @@ export {
toIntOrNull,
toArray,
toIntArray,
isFileFieldValid,
isFileMimeTypeValid,
isFileValid
}
......@@ -11,7 +11,7 @@ import {
VIDEO_STATES,
VIDEO_LIVE
} from '../../initializers/constants'
import { exists, isArray, isDateValid, isFileValid } from './misc'
import { exists, isArray, isDateValid, isFileMimeTypeValid, isFileValid } from './misc'
import * as magnetUtil from 'magnet-uri'
const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
......@@ -81,8 +81,8 @@ function isVideoFileExtnameValid (value: string) {
return exists(value) && (value === VIDEO_LIVE.EXTENSION || MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined)
}
function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
return isFileValid(files, MIMETYPES.VIDEO.MIMETYPES_REGEX, 'videofile', null)
function isVideoFileMimeTypeValid (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
return isFileMimeTypeValid(files, MIMETYPES.VIDEO.MIMETYPES_REGEX, 'videofile')
}
const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME
......@@ -143,12 +143,12 @@ export {
isVideoFPSResolutionValid,
isScheduleVideoUpdatePrivacyValid,
isVideoOriginallyPublishedAtValid,
isVideoFile,
isVideoMagnetUriValid,
isVideoStateValid,
isVideoViewsValid,
isVideoRatingTypeValid,
isVideoFileExtnameValid,
isVideoFileMimeTypeValid,
isVideoDurationValid,
isVideoTagValid,
isVideoPrivacyValid,
......
......@@ -7,6 +7,7 @@ import { ensureDir, remove, writeFile } from 'fs-extra'
import * as request from 'request'
import { createWriteStream } from 'fs'
import { CONFIG } from '@server/initializers/config'
import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
export type YoutubeDLInfo = {
name?: string
......@@ -154,7 +155,7 @@ async function updateYoutubeDLBinary () {
return res()
}
if (result.statusCode !== 302) {
if (result.statusCode !== HttpStatusCode.FOUND_302) {
logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode)
return res()
}
......@@ -164,7 +165,7 @@ async function updateYoutubeDLBinary () {
const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(url)[1]
downloadFile.on('response', result => {
if (result.statusCode !== 200) {
if (result.statusCode !== HttpStatusCode.OK_200) {
logger.error('Cannot update youtube-dl: new version response is not 200, it\'s %d.', result.statusCode)
return res()
}
......
......@@ -36,6 +36,7 @@ import {
} from '../../types/models'
import { extname } from 'path'
import { getServerActor } from '@server/models/application/application'
import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
// Set account keys, this could be long so process after the account creation and do not block the client
function setAsyncActorKeys <T extends MActor> (actor: T) {
......@@ -277,7 +278,7 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
const { result, statusCode } = await fetchRemoteActor(actorUrl)
if (statusCode === 404) {
if (statusCode === HttpStatusCode.NOT_FOUND_404) {
logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
actor.Account
? await actor.Account.destroy()
......
......@@ -18,6 +18,7 @@ import { createPlaylistMiniatureFromUrl } from '../thumbnail'
import { FilteredModelAttributes } from '../../types/sequelize'
import { MAccountDefault, MAccountId, MVideoId } from '../../types/models'
import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist'
import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) {
const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
......@@ -120,7 +121,7 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner)
try {
const { statusCode, playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url)
if (statusCode === 404) {
if (statusCode === HttpStatusCode.NOT_FOUND_404) {
logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url)
await videoPlaylist.destroy()
......
......@@ -76,6 +76,7 @@ import { sendCreateVideo, sendUpdateVideo } from './send'
import { addVideoShares, shareVideoByServerAndChannel } from './share'
import { addVideoComments } from './video-comments'
import { createRates } from './video-rates'
import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) {
const video = videoArg as MVideoAP
......@@ -488,7 +489,7 @@ async function refreshVideoIfNeeded (options: {
try {
const { response, videoObject } = await fetchRemoteVideo(video.url)
if (response.statusCode === 404) {
if (response.statusCode === HttpStatusCode.NOT_FOUND_404) {
logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
// Video does not exist anymore
......
import * as express from 'express'
import * as Bluebird from 'bluebird'
import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n'
import {
AVATARS_SIZE,
......@@ -6,7 +7,8 @@ import {
EMBED_SIZE,
PLUGIN_GLOBAL_CSS_PATH,
WEBSERVER,
FILES_CONTENT_HASH
FILES_CONTENT_HASH,
ACCEPT_HEADERS
} from '../initializers/constants'
import { join } from 'path'
import { escapeHTML, isTestInstance, sha256 } from '../helpers/core-utils'
......@@ -18,7 +20,6 @@ import { readFile } from 'fs-extra'
import { getActivityStreamDuration } from '../models/video/video-format-utils'
import { AccountModel } from '../models/account/account'
import { VideoChannelModel } from '../models/video/video-channel'
import * as Bluebird from 'bluebird'
import { CONFIG } from '../initializers/config'
import { logger } from '../helpers/logger'
import { MAccountActor, MChannelActor } from '../types/models'
......@@ -53,7 +54,7 @@ type Tags = {
}
}
export class ClientHtml {
class ClientHtml {
private static htmlCache: { [path: string]: string } = {}
......@@ -505,3 +506,38 @@ export class ClientHtml {
return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.META_TAGS, tagsString)
}
}
function sendHTML (html: string, res: express.Response) {
res.set('Content-Type', 'text/html; charset=UTF-8')
return res.send(html)
}
async function serveIndexHTML (req: express.Request, res: express.Response) {