2024-05-23 09:42:14 +00:00
|
|
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
|
|
//
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2023-08-01 16:42:24 +00:00
|
|
|
import type { RegisterServerOptions, MVideoThumbnail, SettingEntries } from '@peertube/peertube-types'
|
2023-12-27 11:48:45 +00:00
|
|
|
import type { ConverseJSTheme, InitConverseJSParams, InitConverseJSParamsError } from '../../../shared/lib/types'
|
2023-08-01 16:42:24 +00:00
|
|
|
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'
|
2024-04-15 16:29:09 +00:00
|
|
|
import { ExternalAuthOIDC } from '../external-auth/oidc'
|
2024-06-04 14:39:25 +00:00
|
|
|
import { Emojis } from '../emojis'
|
2023-08-01 16:42:24 +00:00
|
|
|
|
|
|
|
interface GetConverseJSParamsParams {
|
|
|
|
readonly?: boolean | 'noscroll'
|
|
|
|
transparent?: boolean
|
|
|
|
forcetype?: boolean
|
2024-01-31 17:12:53 +00:00
|
|
|
forceDefaultHideMucParticipants?: boolean
|
2023-08-01 16:42:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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)
|
2024-04-04 08:58:16 +00:00
|
|
|
* @param params various parameters
|
|
|
|
* @param userIsConnected true if user is connected. If undefined, bypass access tests.
|
2023-08-01 16:42:24 +00:00
|
|
|
*/
|
|
|
|
async function getConverseJSParams (
|
|
|
|
options: RegisterServerOptionsV5,
|
|
|
|
roomKey: string,
|
2024-04-04 08:58:16 +00:00
|
|
|
params: GetConverseJSParamsParams,
|
|
|
|
userIsConnected?: boolean
|
2023-08-01 16:42:24 +00:00
|
|
|
): Promise<InitConverseJSParams | InitConverseJSParamsError> {
|
|
|
|
const settings = await options.settingsManager.getSettings([
|
|
|
|
'prosody-room-type',
|
|
|
|
'disable-websocket',
|
|
|
|
'converse-theme',
|
|
|
|
'federation-no-remote-chat',
|
2024-04-04 08:58:16 +00:00
|
|
|
'prosody-room-allow-s2s',
|
2024-05-28 15:56:24 +00:00
|
|
|
'chat-no-anonymous',
|
|
|
|
'disable-channel-configuration'
|
2023-08-01 16:42:24 +00:00
|
|
|
])
|
|
|
|
|
2024-04-04 08:58:16 +00:00
|
|
|
if (settings['chat-no-anonymous'] && userIsConnected === false) {
|
|
|
|
return {
|
|
|
|
isError: true,
|
|
|
|
code: 403,
|
|
|
|
message: 'You must be connected'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-01 16:42:24 +00:00
|
|
|
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
|
|
|
|
|
2024-04-16 15:18:14 +00:00
|
|
|
let externalAuthOIDC
|
|
|
|
if (userIsConnected !== true) {
|
2024-04-18 13:46:38 +00:00
|
|
|
if (remoteConnectionInfos && !remoteConnectionInfos.externalAuthCompatible) {
|
2024-04-18 13:42:06 +00:00
|
|
|
options.peertubeHelpers.logger.debug(
|
|
|
|
'The remote livechat plugin is not compatible with external authentication, not enabling the feature'
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
try {
|
2024-04-22 12:28:55 +00:00
|
|
|
const oidcs = ExternalAuthOIDC.allSingletons()
|
|
|
|
for (const oidc of oidcs) {
|
|
|
|
if (await oidc.isOk()) {
|
|
|
|
const authUrl = oidc.getConnectUrl()
|
|
|
|
const buttonLabel = oidc.getButtonLabel()
|
|
|
|
if (authUrl && buttonLabel) {
|
|
|
|
externalAuthOIDC ??= []
|
|
|
|
externalAuthOIDC.push({
|
|
|
|
type: oidc.type,
|
2024-09-07 12:49:27 +00:00
|
|
|
buttonLabel,
|
2024-04-22 12:28:55 +00:00
|
|
|
url: authUrl
|
|
|
|
})
|
2024-04-18 13:42:06 +00:00
|
|
|
}
|
2024-04-16 15:18:14 +00:00
|
|
|
}
|
|
|
|
}
|
2024-04-18 13:42:06 +00:00
|
|
|
} catch (err) {
|
|
|
|
options.peertubeHelpers.logger.error(err)
|
2024-04-16 09:43:38 +00:00
|
|
|
}
|
2024-04-16 15:18:14 +00:00
|
|
|
}
|
|
|
|
}
|
2024-04-15 16:29:09 +00:00
|
|
|
|
2023-08-01 16:42:24 +00:00
|
|
|
return {
|
2024-04-08 17:02:56 +00:00
|
|
|
peertubeVideoOriginalUrl: roomInfos.video?.url,
|
|
|
|
peertubeVideoUUID: roomInfos.video?.uuid,
|
2023-08-01 16:42:24 +00:00
|
|
|
staticBaseUrl,
|
|
|
|
assetsPath: staticBaseUrl + 'conversejs/',
|
|
|
|
isRemoteChat: !!(roomInfos.video?.remote),
|
2024-04-04 14:48:19 +00:00
|
|
|
localAnonymousJID: !settings['chat-no-anonymous'] ? localAnonymousJID : null,
|
2023-08-01 16:42:24 +00:00
|
|
|
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,
|
2024-09-07 12:49:27 +00:00
|
|
|
authenticationUrl,
|
2023-08-01 16:42:24 +00:00
|
|
|
autoViewerMode,
|
|
|
|
theme: converseJSTheme,
|
|
|
|
forceReadonly,
|
2024-01-31 17:12:53 +00:00
|
|
|
transparent,
|
|
|
|
// forceDefaultHideMucParticipants is for testing purpose
|
|
|
|
// (so we can stress test with the muc participant list hidden by default)
|
2024-04-15 16:29:09 +00:00
|
|
|
forceDefaultHideMucParticipants: params.forceDefaultHideMucParticipants,
|
2024-05-28 15:56:24 +00:00
|
|
|
externalAuthOIDC,
|
|
|
|
customEmojisUrl: connectionInfos.customEmojisUrl
|
2023-08-01 16:42:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function _interfaceParams (
|
|
|
|
options: RegisterServerOptions,
|
|
|
|
settings: SettingEntries,
|
|
|
|
params: GetConverseJSParamsParams
|
|
|
|
): {
|
|
|
|
autoViewerMode: InitConverseJSParams['autoViewerMode']
|
|
|
|
forceReadonly: InitConverseJSParams['forceReadonly']
|
|
|
|
transparent: InitConverseJSParams['transparent']
|
|
|
|
converseJSTheme: InitConverseJSParams['theme']
|
|
|
|
} {
|
2024-09-07 12:49:27 +00:00
|
|
|
let autoViewerMode = false
|
2023-08-01 16:42:24 +00:00
|
|
|
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<RoomInfos | InitConverseJSParamsError> {
|
|
|
|
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
|
2024-05-28 15:56:24 +00:00
|
|
|
customEmojisUrl?: string
|
2023-08-01 16:42:24 +00:00
|
|
|
} | 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
|
2024-05-28 15:56:24 +00:00
|
|
|
let customEmojisUrl: string | undefined
|
2023-08-01 16:42:24 +00:00
|
|
|
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
|
2024-05-28 15:56:24 +00:00
|
|
|
|
2024-06-06 16:04:17 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
|
|
|
|
if (remoteChatInfos && remoteChatInfos.customEmojisUrl) {
|
|
|
|
customEmojisUrl = remoteChatInfos.customEmojisUrl
|
|
|
|
}
|
2023-08-01 16:42:24 +00:00
|
|
|
} else {
|
|
|
|
try {
|
|
|
|
roomJID = await _localRoomJID(
|
|
|
|
options,
|
|
|
|
settings,
|
|
|
|
prosodyDomain,
|
|
|
|
roomKey,
|
|
|
|
video,
|
|
|
|
channelId,
|
|
|
|
params.forcetype ?? false
|
|
|
|
)
|
2024-05-28 15:56:24 +00:00
|
|
|
|
2024-06-06 13:03:12 +00:00
|
|
|
if (video?.channelId) {
|
|
|
|
customEmojisUrl = await Emojis.singletonSafe()?.channelCustomEmojisUrl(video.channelId)
|
2024-05-28 15:56:24 +00:00
|
|
|
}
|
2023-08-01 16:42:24 +00:00
|
|
|
} catch (err) {
|
|
|
|
options.peertubeHelpers.logger.error(err)
|
|
|
|
return {
|
|
|
|
isError: true,
|
|
|
|
code: 500,
|
|
|
|
message: 'An error occured'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
prosodyDomain,
|
|
|
|
localAnonymousJID,
|
|
|
|
localBoshUri,
|
|
|
|
localWsUri,
|
|
|
|
remoteConnectionInfos,
|
2024-05-28 15:56:24 +00:00
|
|
|
roomJID,
|
|
|
|
customEmojisUrl
|
2023-08-01 16:42:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
interface WCRemoteConnectionInfos {
|
|
|
|
roomJID: string
|
|
|
|
anonymous?: {
|
|
|
|
userJID: string
|
|
|
|
boshUri: string
|
|
|
|
wsUri?: string
|
|
|
|
}
|
|
|
|
authenticated?: boolean
|
2024-04-18 13:42:06 +00:00
|
|
|
externalAuthCompatible: boolean
|
2023-08-01 16:42:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function _remoteConnectionInfos (
|
|
|
|
remoteChatInfos: LiveChatJSONLDAttributeV1,
|
|
|
|
canWebsocketS2S: boolean,
|
|
|
|
canDirectS2S: boolean
|
|
|
|
): Promise<WCRemoteConnectionInfos> {
|
|
|
|
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 = {
|
2024-04-18 13:42:06 +00:00
|
|
|
roomJID: remoteChatInfos.jid,
|
|
|
|
externalAuthCompatible: false
|
2023-08-01 16:42:24 +00:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2024-04-18 13:42:06 +00:00
|
|
|
|
|
|
|
if (remoteChatInfos.xmppserver.external) {
|
|
|
|
// To be able to connect to a remote livechat using an external account,
|
|
|
|
// The remote server MUST have livechat >= 9.0.0...
|
|
|
|
// So we flag the connection as compatible or not, and we will disable the feature if not compatible.
|
|
|
|
connectionInfos.externalAuthCompatible = true
|
|
|
|
}
|
|
|
|
|
2023-08-01 16:42:24 +00:00
|
|
|
return connectionInfos
|
|
|
|
}
|
|
|
|
|
|
|
|
async function _localRoomJID (
|
|
|
|
options: RegisterServerOptions,
|
|
|
|
settings: SettingEntries,
|
|
|
|
prosodyDomain: string,
|
|
|
|
roomKey: string,
|
|
|
|
video: MVideoThumbnail | undefined,
|
|
|
|
channelId: number,
|
|
|
|
forceType: boolean
|
|
|
|
): Promise<string> {
|
|
|
|
// 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}}')) {
|
2024-05-17 13:17:36 +00:00
|
|
|
// FIXME: this should no more exists, since we removed options to include other chat server.
|
|
|
|
// So we should remove this code. (and simplify the above code)
|
2023-08-01 16:42:24 +00:00
|
|
|
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 {
|
2024-04-04 08:58:16 +00:00
|
|
|
getConverseJSParams,
|
|
|
|
InitConverseJSParamsError
|
2023-08-01 16:42:24 +00:00
|
|
|
}
|