import type { Router , RequestHandler , Request , Response , NextFunction } from 'express' ; import type { ProxyOptions } from 'express-http-proxy' ; import type { ChatType , ProsodyListRoomsResult , ProsodyListRoomsResultRoom } from '../../../shared/lib/types' ; import { getBaseRouterRoute , getBaseStaticRoute , isUserAdmin } from '../helpers' ; import { asyncMiddleware } from '../middlewares/async' ; import { getProsodyDomain } from '../prosody/config/domain' ; import { getAPIKey } from '../apikey' ; import { getChannelInfosById , getChannelNameById } from '../database/channel' ; import { isAutoColorsAvailable , areAutoColorsValid , AutoColors } from '../../../shared/lib/autocolors' ; import * as path from 'path' ; const bodyParser = require ( 'body-parser' ) ; const got = require ( 'got' ) ; const fs = require ( 'fs' ) . promises ; const proxy = require ( 'express-http-proxy' ) ; let httpBindRoute ; interface ProsodyHttpBindInfo { host : string ; port : string ; } let currentProsodyHttpBindInfo = null ; async function initWebchatRouter ( options ) { const { getRouter , peertubeHelpers , settingsManager } = options ; const converseJSIndex = await fs . readFile ( path . resolve ( __dirname , '../../conversejs/index.html' ) ) ; const router = getRouter ( ) ; router . get ( '/room/:roomKey' , asyncMiddleware ( async ( req , res , _next ) : Promise < void > => { res . removeHeader ( 'X-Frame-Options' ) ; const roomKey = req . params . roomKey ; const settings = await settingsManager . getSettings ( [ 'chat-type' , 'chat-room' , 'chat-server' , 'chat-bosh-uri' , 'chat-ws-uri' , 'prosody-room-type' , 'converse-theme' , 'converse-autocolors' ] ) ; const chatType = ( settings [ 'chat-type' ] ?? 'disabled' ) as ChatType ; let jid ; let room ; let boshUri ; let wsUri ; let authenticationUrl = '' ; let advancedControls = false ; let autoViewerMode = false ; let forceReadonly = 'false' ; let converseJSTheme = settings [ 'converse-theme' ] as string ; if ( ! /^\w+$/ . test ( converseJSTheme ) ) { converseJSTheme = 'peertube' ; } if ( chatType === 'builtin-prosody' ) { const prosodyDomain = await getProsodyDomain ( options ) ; jid = 'anon.' + prosodyDomain ; if ( req . query . forcetype === '1' ) { if ( /^channel\.\d+$/ . test ( roomKey ) ) { room = 'channel.{{CHANNEL_ID}}@room.' + prosodyDomain ; } else { room = '{{VIDEO_UUID}}@room.' + prosodyDomain ; } } else { if ( settings [ 'prosody-room-type' ] === 'channel' ) { room = 'channel.{{CHANNEL_ID}}@room.' + prosodyDomain ; } else { room = '{{VIDEO_UUID}}@room.' + prosodyDomain ; } } boshUri = getBaseRouterRoute ( options ) + 'webchat/http-bind' ; wsUri = '' ; authenticationUrl = options . peertubeHelpers . config . getWebserverUrl ( ) + getBaseRouterRoute ( options ) + 'api/auth' ; advancedControls = true ; if ( req . query . _readonly === 'true' ) { forceReadonly = 'true' ; } else if ( req . query . _readonly === 'noscroll' ) { forceReadonly = 'noscroll' ; } else { autoViewerMode = true ; } } else if ( chatType === 'builtin-converse' ) { if ( ! settings [ 'chat-server' ] ) { throw new Error ( 'Missing chat-server settings.' ) ; } if ( ! settings [ 'chat-room' ] ) { throw new Error ( 'Missing chat-room settings.' ) ; } if ( ! settings [ 'chat-bosh-uri' ] && ! settings [ 'chat-ws-uri' ] ) { throw new Error ( 'Missing BOSH or Websocket uri.' ) ; } jid = settings [ 'chat-server' ] as string ; room = settings [ 'chat-room' ] as string ; boshUri = settings [ 'chat-bosh-uri' ] as string ; wsUri = settings [ 'chat-ws-uri' ] as string ; } else { throw new Error ( 'Builtin chat disabled.' ) ; } let video ; let channelId ; const channelMatches = roomKey . match ( /^channel\.(\d+)$/ ) ; if ( channelMatches ?. [ 1 ] ) { channelId = parseInt ( channelMatches [ 1 ] ) ; if ( chatType !== 'builtin-prosody' ) { throw new Error ( 'Cant access a chat by a channel uri if chatType!==builtin-prosody' ) ; } const channelInfos = await getChannelInfosById ( options , channelId ) ; if ( ! channelInfos ) { throw new Error ( 'Channel not found' ) ; } channelId = channelInfos . id ; } else { const uuid = roomKey ; video = await peertubeHelpers . videos . loadByIdOrUUID ( uuid ) ; if ( ! video ) { throw new Error ( 'Video not found' ) ; } channelId = video . channelId ; } let page = '' + ( converseJSIndex as string ) ; const baseStaticUrl = getBaseStaticRoute ( options ) ; page = page . replace ( /{{BASE_STATIC_URL}}/g , baseStaticUrl ) ; page = page . replace ( /{{JID}}/g , jid ) ; if ( room . includes ( '{{VIDEO_UUID}}' ) ) { if ( ! video ) { throw new Error ( 'Missing video' ) ; } room = room . replace ( /{{VIDEO_UUID}}/g , video . uuid ) ; } room = room . replace ( /{{CHANNEL_ID}}/g , `${ channelId }` ) ; if ( room . includes ( '{{CHANNEL_NAME}}' ) ) { const channelName = await getChannelNameById ( options , channelId ) ; if ( channelName === null ) { throw new Error ( 'Channel not found' ) ; } if ( ! /^[a-zA-Z0-9_.]+$/ . test ( channelName ) ) { peertubeHelpers . logger . error ( `Invalid channel name, contains unauthorized chars: '${ channelName }'` ) ; throw new Error ( 'Invalid channel name, contains unauthorized chars' ) ; } room = room . replace ( /{{CHANNEL_NAME}}/g , channelName ) ; } let autocolorsStyles = '' ; if ( settings [ 'converse-autocolors' ] && isAutoColorsAvailable ( settings [ 'chat-type' ] as ChatType , settings [ 'converse-theme' ] as string ) ) { peertubeHelpers . logger . debug ( 'Trying to load AutoColors...' ) ; const autocolors = { mainForeground : req . query . _ac_mainForeground ?. toString ( ) ?? '' , mainBackground : req . query . _ac_mainBackground ?. toString ( ) ?? '' , greyForeground : req . query . _ac_greyForeground ?. toString ( ) ?? '' , greyBackground : req . query . _ac_greyBackground ?. toString ( ) ?? '' , menuForeground : req . query . _ac_menuForeground ?. toString ( ) ?? '' , menuBackground : req . query . _ac_menuBackground ?. toString ( ) ?? '' , inputForeground : req . query . _ac_inputForeground ?. toString ( ) ?? '' , inputBackground : req . query . _ac_inputBackground ?. toString ( ) ?? '' , buttonForeground : req . query . _ac_buttonForeground ?. toString ( ) ?? '' , buttonBackground : req . query . _ac_buttonBackground ?. toString ( ) ?? '' , link : req . query . _ac_link ?. toString ( ) ?? '' , linkHover : req . query . _ac_linkHover ?. toString ( ) ?? '' } ; const autoColorsTest = areAutoColorsValid ( autocolors ) ; if ( autoColorsTest === true ) { autocolorsStyles = `
          <style>
            :root {
              --peertube-main-foreground: ${ autocolors . mainForeground };
              --peertube-main-background: ${ autocolors . mainBackground };
              --peertube-grey-foreground: ${ autocolors . greyForeground };
              --peertube-grey-background: ${ autocolors . greyBackground };
              --peertube-menu-foreground: ${ autocolors . menuForeground };
              --peertube-menu-background: ${ autocolors . menuBackground };
              --peertube-input-foreground: ${ autocolors . inputForeground };
              --peertube-input-background: ${ autocolors . inputBackground };
              --peertube-button-foreground: ${ autocolors . buttonForeground };
              --peertube-button-background: ${ autocolors . buttonBackground };
              --peertube-link: ${ autocolors . link };
              --peertube-link-hover: ${ autocolors . linkHover };
            }
          </style>
          ` ; } else { peertubeHelpers . logger . error ( 'Provided AutoColors are invalid.' , autoColorsTest ) ; } } else { peertubeHelpers . logger . debug ( 'No AutoColors.' ) ; } page = page . replace ( /{{ROOM}}/g , room ) ; page = page . replace ( /{{BOSH_SERVICE_URL}}/g , boshUri ) ; page = page . replace ( /{{WS_SERVICE_URL}}/g , wsUri ) ; page = page . replace ( /{{AUTHENTICATION_URL}}/g , authenticationUrl ) ; page = page . replace ( /{{ADVANCEDCONTROLS}}/g , advancedControls ? 'true' : 'false' ) ; page = page . replace ( /{{AUTOVIEWERMODE}}/g , autoViewerMode ? 'true' : 'false' ) ; page = page . replace ( /{{CONVERSEJS_THEME}}/g , converseJSTheme ) ; page = page . replace ( /{{CONVERSEJS_AUTOCOLORS}}/g , autocolorsStyles ) ; page = page . replace ( /{{FORCEREADONLY}}/g , forceReadonly ) ; res . status ( 200 ) ; res . type ( 'html' ) ; res . send ( page ) ; } ) ) ; changeHttpBindRoute ( options , null ) ; router . all ( '/http-bind' , bodyParser . raw ( { type : 'text/xml' } ) , ( req , res , next ) => { httpBindRoute ( req , res , next ) ; } ) ; router . get ( '/prosody-list-rooms' , asyncMiddleware ( async ( req , res , _next ) => { if ( ! res . locals . authenticated ) { res . sendStatus ( 403 ) ; return ; } if ( ! await isUserAdmin ( options , res ) ) { res . sendStatus ( 403 ) ; return ; } const chatType = await options . settingsManager . getSetting ( 'chat-type' ) as ChatType ; if ( chatType !== 'builtin-prosody' ) { const message = 'Please save the settings first.' ; res . status ( 200 ) ; const r = { ok : false , error : message } ; res . json ( r ) ; return ; } if ( ! currentProsodyHttpBindInfo ) { throw new Error ( 'It seems that prosody is not binded... Cant list rooms.' ) ; } const apiUrl = `http://localhost:${ currentProsodyHttpBindInfo . port }/peertubelivechat_list_rooms/list-rooms` ; peertubeHelpers . logger . debug ( 'Calling list rooms API on url: ' + apiUrl ) ; const rooms = await got ( apiUrl , { method : 'GET' , headers : { authorization : 'Bearer ' + await getAPIKey ( options ) , host : currentProsodyHttpBindInfo . host } , responseType : 'json' , resolveBodyOnly : true } ) ; if ( Array . isArray ( rooms ) ) { for ( let i = 0 ; i < rooms . length ; i ++ ) { const room = rooms [ i ] ; const matches = room . localpart . match ( /^channel\.(\d+)$/ ) ; if ( matches ?. [ 1 ] ) { const channelId = parseInt ( matches [ 1 ] ) ; const channelInfos = await getChannelInfosById ( options , channelId ) ; if ( channelInfos ) { room . channel = { id : channelInfos . id , name : channelInfos . name , displayName : channelInfos . displayName } ; } } } } res . status ( 200 ) ; const r = { ok : true , rooms : rooms } ; res . json ( r ) ; } ) ) ; return router ; } function changeHttpBindRoute ( { peertubeHelpers } , prosodyHttpBindInfo ) { const logger = peertubeHelpers . logger ; if ( prosodyHttpBindInfo && ! /^\d+$/ . test ( prosodyHttpBindInfo . port ) ) { logger . error ( `Port '${ prosodyHttpBindInfo . port }' is not valid. Replacing by null` ) ; prosodyHttpBindInfo = null ; } if ( ! prosodyHttpBindInfo ) { logger . info ( 'Changing http-bind port for null' ) ; currentProsodyHttpBindInfo = null ; httpBindRoute = ( _req , res , _next ) => { res . status ( 404 ) ; res . send ( 'Not found' ) ; } ; } else { logger . info ( 'Changing http-bind port for ' + prosodyHttpBindInfo . port + ', on host ' + prosodyHttpBindInfo . host ) ; const options = { https : false , proxyReqPathResolver : async ( _req ) : Promise < string > => { return '/http-bind' ; } , parseReqBody : true } ; currentProsodyHttpBindInfo = prosodyHttpBindInfo ; httpBindRoute = proxy ( 'http://localhost:' + prosodyHttpBindInfo . port , options ) ; } } export { initWebchatRouter , changeHttpBindRoute } ;