From f7a32e95d40da856f58887761b029c269509e176 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Tue, 1 Aug 2023 18:42:24 +0200 Subject: [PATCH] Code refactoring: Cleaning some backend code, for a better readability. --- server/lib/conversejs/params.ts | 343 ++++++++++++++++++++++++++++++++ server/lib/routers/webchat.ts | 237 +++------------------- shared/lib/types.ts | 1 + 3 files changed, 369 insertions(+), 212 deletions(-) create mode 100644 server/lib/conversejs/params.ts diff --git a/server/lib/conversejs/params.ts b/server/lib/conversejs/params.ts new file mode 100644 index 00000000..0b42a9ae --- /dev/null +++ b/server/lib/conversejs/params.ts @@ -0,0 +1,343 @@ +import type { RegisterServerOptions, MVideoThumbnail, SettingEntries } from '@peertube/peertube-types' +import type { ConverseJSTheme, InitConverseJSParams } from '../../../shared/lib/types' +import type { RegisterServerOptionsV5 } from '../helpers' +import type { LiveChatJSONLDAttributeV1 } from '../federation/types' +import { getChannelInfosById, getChannelNameById } from '../database/channel' +import { + anonymousConnectionInfos, compatibleRemoteAuthenticatedConnectionEnabled +} from '../federation/connection-infos' +import { getVideoLiveChatInfos } from '../federation/storage' +import { getBaseRouterRoute, getBaseStaticRoute } from '../helpers' +import { getProsodyDomain } from '../prosody/config/domain' +import { getBoshUri, getWSUri } from '../uri/webchat' + +interface InitConverseJSParamsError { + isError: true + code: 404 | 403 | 500 + message: string +} + +interface GetConverseJSParamsParams { + readonly?: boolean | 'noscroll' + transparent?: boolean + forcetype?: boolean +} + +/** + * Returns ConverseJS options for a given chat room. + * Returns an object describing the error if access can not be granted. + * @param options server options + * @param roomKey chat room key: video UUID (or channel id when forcetype is true) + */ +async function getConverseJSParams ( + options: RegisterServerOptionsV5, + roomKey: string, + params: GetConverseJSParamsParams +): Promise { + const settings = await options.settingsManager.getSettings([ + 'prosody-room-type', + 'disable-websocket', + 'converse-theme', + 'federation-no-remote-chat', + 'prosody-room-allow-s2s' + ]) + + const { + autoViewerMode, forceReadonly, transparent, converseJSTheme + } = _interfaceParams(options, settings, params) + + const staticBaseUrl = getBaseStaticRoute(options) + + const authenticationUrl = options.peertubeHelpers.config.getWebserverUrl() + + getBaseRouterRoute(options) + + 'api/auth' + + const roomInfos = await _readRoomKey(options, settings, roomKey) + if ('isError' in roomInfos) { + return roomInfos // is an InitConverseJSParamsError + } + + const connectionInfos = await _connectionInfos(options, settings, params, roomInfos) + if ('isError' in connectionInfos) { + return connectionInfos // is an InitConverseJSParamsError + } + const { + localAnonymousJID, + localBoshUri, + localWsUri, + remoteConnectionInfos, + roomJID + } = connectionInfos + + return { + staticBaseUrl, + assetsPath: staticBaseUrl + 'conversejs/', + isRemoteChat: !!(roomInfos.video?.remote), + localAnonymousJID: localAnonymousJID, + remoteAnonymousJID: remoteConnectionInfos?.anonymous?.userJID ?? null, + remoteAnonymousXMPPServer: !!(remoteConnectionInfos?.anonymous), + remoteAuthenticatedXMPPServer: !!(remoteConnectionInfos?.authenticated), + room: roomJID, + localBoshServiceUrl: localBoshUri, + localWebsocketServiceUrl: localWsUri, + remoteBoshServiceUrl: remoteConnectionInfos?.anonymous?.boshUri ?? null, + remoteWebsocketServiceUrl: remoteConnectionInfos?.anonymous?.wsUri ?? null, + authenticationUrl: authenticationUrl, + autoViewerMode, + theme: converseJSTheme, + forceReadonly, + transparent + } +} + +function _interfaceParams ( + options: RegisterServerOptions, + settings: SettingEntries, + params: GetConverseJSParamsParams +): { + autoViewerMode: InitConverseJSParams['autoViewerMode'] + forceReadonly: InitConverseJSParams['forceReadonly'] + transparent: InitConverseJSParams['transparent'] + converseJSTheme: InitConverseJSParams['theme'] + } { + let autoViewerMode: boolean = false + const forceReadonly: boolean | 'noscroll' = params.readonly ?? false + if (!forceReadonly) { + autoViewerMode = true // auto join the chat in viewer mode, if not logged in + } + let converseJSTheme: ConverseJSTheme = settings['converse-theme'] as ConverseJSTheme + const transparent: boolean = params.transparent ?? false + if (!/^\w+$/.test(converseJSTheme)) { + converseJSTheme = 'peertube' + } + + return { + autoViewerMode, + forceReadonly, + transparent, + converseJSTheme + } +} + +interface RoomInfos { + video: MVideoThumbnail | undefined + channelId: number + remoteChatInfos: LiveChatJSONLDAttributeV1 | undefined + roomKey: string +} + +async function _readRoomKey ( + options: RegisterServerOptions, + settings: SettingEntries, + roomKey: string +): Promise { + let video: MVideoThumbnail | undefined + let channelId: number + let remoteChatInfos: LiveChatJSONLDAttributeV1 | undefined + const channelMatches = roomKey.match(/^channel\.(\d+)$/) + if (channelMatches?.[1]) { + channelId = parseInt(channelMatches[1]) + // Here we are on a channel room... + const channelInfos = await getChannelInfosById(options, channelId) + if (!channelInfos) { + return { + isError: true, + code: 404, + message: 'Channel Not Found' + } + } + channelId = channelInfos.id + } else { + const uuid = roomKey // must be a video UUID. + video = await options.peertubeHelpers.videos.loadByIdOrUUID(uuid) + if (!video) { + return { + isError: true, + code: 404, + message: 'Not Found' + } + } + if (video.remote) { + remoteChatInfos = settings['federation-no-remote-chat'] ? false : await getVideoLiveChatInfos(options, video) + if (!remoteChatInfos) { + return { + isError: true, + code: 404, + message: 'Not Found' + } + } + } + channelId = video.channelId + } + + return { + video, + channelId, + remoteChatInfos, + roomKey + } +} + +async function _connectionInfos ( + options: RegisterServerOptions, + settings: SettingEntries, + params: GetConverseJSParamsParams, + roomInfos: RoomInfos +): Promise<{ + prosodyDomain: string + localAnonymousJID: string + localBoshUri: string + localWsUri: string | null + remoteConnectionInfos: WCRemoteConnectionInfos | undefined + roomJID: string + } | InitConverseJSParamsError> { + const { video, remoteChatInfos, channelId, roomKey } = roomInfos + + const prosodyDomain = await getProsodyDomain(options) + const localAnonymousJID = 'anon.' + prosodyDomain + const localBoshUri = getBoshUri(options) + const localWsUri = settings['disable-websocket'] + ? null + : (getWSUri(options) ?? null) + + let remoteConnectionInfos: WCRemoteConnectionInfos | undefined + let roomJID: string + if (video?.remote) { + const canWebsocketS2S = !settings['federation-no-remote-chat'] && !settings['disable-websocket'] + const canDirectS2S = !settings['federation-no-remote-chat'] && !!settings['prosody-room-allow-s2s'] + try { + remoteConnectionInfos = await _remoteConnectionInfos(remoteChatInfos ?? false, canWebsocketS2S, canDirectS2S) + } catch (err) { + options.peertubeHelpers.logger.error(err) + remoteConnectionInfos = undefined + } + if (!remoteConnectionInfos) { + return { + isError: true, + code: 404, + message: 'No compatible way to connect to remote chat' + } + } + roomJID = remoteConnectionInfos.roomJID + } else { + try { + roomJID = await _localRoomJID( + options, + settings, + prosodyDomain, + roomKey, + video, + channelId, + params.forcetype ?? false + ) + } catch (err) { + options.peertubeHelpers.logger.error(err) + return { + isError: true, + code: 500, + message: 'An error occured' + } + } + } + + return { + prosodyDomain, + localAnonymousJID, + localBoshUri, + localWsUri, + remoteConnectionInfos, + roomJID + } +} + +interface WCRemoteConnectionInfos { + roomJID: string + anonymous?: { + userJID: string + boshUri: string + wsUri?: string + } + authenticated?: boolean +} + +async function _remoteConnectionInfos ( + remoteChatInfos: LiveChatJSONLDAttributeV1, + canWebsocketS2S: boolean, + canDirectS2S: boolean +): Promise { + if (!remoteChatInfos) { throw new Error('Should have remote chat infos for remote videos') } + if (remoteChatInfos.type !== 'xmpp') { throw new Error('Should have remote xmpp chat infos for remote videos') } + const connectionInfos: WCRemoteConnectionInfos = { + roomJID: remoteChatInfos.jid + } + if (compatibleRemoteAuthenticatedConnectionEnabled(remoteChatInfos, canWebsocketS2S, canDirectS2S)) { + connectionInfos.authenticated = true + } + const anonymousCI = anonymousConnectionInfos(remoteChatInfos ?? false) + if (anonymousCI?.boshUri) { + connectionInfos.anonymous = { + userJID: anonymousCI.userJID, + boshUri: anonymousCI.boshUri, + wsUri: anonymousCI.wsUri + } + } + return connectionInfos +} + +async function _localRoomJID ( + options: RegisterServerOptions, + settings: SettingEntries, + prosodyDomain: string, + roomKey: string, + video: MVideoThumbnail | undefined, + channelId: number, + forceType: boolean +): Promise { + // Computing the room name... + let room: string + if (forceType) { + // We come from the room list in the settings page. + // Here we don't read the prosody-room-type settings, + // but use the roomKey format. + // NB: there is no extra security. Any user can add this parameter. + // This is not an issue: the setting will be tested at the room creation. + // No room can be created in the wrong mode. + 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 + } + } + + 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)) { + // FIXME: see if there is a response here https://github.com/Chocobozzz/PeerTube/issues/4301 for allowed chars + options.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) + } + + return room +} + +export { + getConverseJSParams +} diff --git a/server/lib/routers/webchat.ts b/server/lib/routers/webchat.ts index a7bd6703..42f29e8d 100644 --- a/server/lib/routers/webchat.ts +++ b/server/lib/routers/webchat.ts @@ -1,25 +1,14 @@ -import type { RegisterServerOptions, MVideoThumbnail, SettingEntries } from '@peertube/peertube-types' +import type { RegisterServerOptions } from '@peertube/peertube-types' import type { Router, Request, Response, NextFunction } from 'express' -import type { - ProsodyListRoomsResult, ProsodyListRoomsResultRoom, - InitConverseJSParams, ConverseJSTheme -} from '../../../shared/lib/types' +import type { ProsodyListRoomsResult, ProsodyListRoomsResultRoom } from '../../../shared/lib/types' import { createProxyServer } from 'http-proxy' -import { - RegisterServerOptionsV5, getBaseRouterRoute, getBaseStaticRoute, isUserAdmin -} from '../helpers' +import { RegisterServerOptionsV5, 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 { getChannelInfosById } from '../database/channel' import { isAutoColorsAvailable, areAutoColorsValid, AutoColors } from '../../../shared/lib/autocolors' -import { getBoshUri, getWSUri } from '../uri/webchat' -import { getVideoLiveChatInfos } from '../federation/storage' -import { LiveChatJSONLDAttributeV1 } from '../federation/types' -import { - anonymousConnectionInfos, compatibleRemoteAuthenticatedConnectionEnabled -} from '../federation/connection-infos' import { fetchMissingRemoteServerInfos } from '../federation/fetch-infos' +import { getConverseJSParams } from '../conversejs/params' import * as path from 'path' const got = require('got') @@ -38,8 +27,7 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise { - if (!remoteChatInfos) { throw new Error('Should have remote chat infos for remote videos') } - if (remoteChatInfos.type !== 'xmpp') { throw new Error('Should have remote xmpp chat infos for remote videos') } - const connectionInfos: WCRemoteConnectionInfos = { - roomJID: remoteChatInfos.jid - } - if (compatibleRemoteAuthenticatedConnectionEnabled(remoteChatInfos, canWebsocketS2S, canDirectS2S)) { - connectionInfos.authenticated = true - } - const anonymousCI = anonymousConnectionInfos(remoteChatInfos ?? false) - if (anonymousCI?.boshUri) { - connectionInfos.anonymous = { - userJID: anonymousCI.userJID, - boshUri: anonymousCI.boshUri, - wsUri: anonymousCI.wsUri - } - } - return connectionInfos -} - -async function _localRoomJID ( - options: RegisterServerOptions, - settings: SettingEntries, - prosodyDomain: string, - roomKey: string, - video: MVideoThumbnail | undefined, - channelId: number, - forceType: boolean -): Promise { - // Computing the room name... - let room: string - if (forceType) { - // We come from the room list in the settings page. - // Here we don't read the prosody-room-type settings, - // but use the roomKey format. - // NB: there is no extra security. Any user can add this parameter. - // This is not an issue: the setting will be tested at the room creation. - // No room can be created in the wrong mode. - 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 - } - } - - 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)) { - // FIXME: see if there is a response here https://github.com/Chocobozzz/PeerTube/issues/4301 for allowed chars - options.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) - } - - return room -} - export { initWebchatRouter, disableProxyRoute, diff --git a/shared/lib/types.ts b/shared/lib/types.ts index 08cae5e3..b89311f3 100644 --- a/shared/lib/types.ts +++ b/shared/lib/types.ts @@ -6,6 +6,7 @@ interface InitConverseJSParams { remoteAnonymousJID: string | null remoteAnonymousXMPPServer: boolean remoteAuthenticatedXMPPServer: boolean + staticBaseUrl: string assetsPath: string room: string localBoshServiceUrl: string | null