Chat Federation, connection to remote chat:

Front-end connect using BOSH or WS directly on the remote server.
If use is logged-in, his nickname is use as default nickname.
This commit is contained in:
John Livingston 2023-04-21 16:56:48 +02:00
parent b85a1dc90a
commit 5d323b2dfd
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
7 changed files with 236 additions and 79 deletions

View File

@ -1,6 +1,6 @@
import type { Video } from '@peertube/peertube-types' import type { Video } from '@peertube/peertube-types'
import type { RegisterClientOptions } from '@peertube/peertube-types/client' import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import { videoHasWebchat } from 'shared/lib/video' import { videoHasWebchat, videoHasRemoteWebchat } from 'shared/lib/video'
import { logger } from './videowatch/logger' import { logger } from './videowatch/logger'
import { closeSVG, openBlankChatSVG, openChatSVG, shareChatUrlSVG } from './videowatch/buttons' import { closeSVG, openBlankChatSVG, openChatSVG, shareChatUrlSVG } from './videowatch/buttons'
import { displayButton, displayButtonOptions } from './videowatch/button' import { displayButton, displayButtonOptions } from './videowatch/button'
@ -247,19 +247,21 @@ function register (registerOptions: RegisterClientOptions): void {
logger.log('No chat for anonymous users') logger.log('No chat for anonymous users')
return return
} }
if (!videoHasWebchat(s, video)) { if (!videoHasWebchat(s, video) && !videoHasRemoteWebchat(s, video)) {
logger.log('This video has no webchat') logger.log('This video has no webchat')
return return
} }
let showShareUrlButton: boolean = false let showShareUrlButton: boolean = false
const chatShareUrl = settings['chat-share-url'] ?? '' if (video.isLocal) { // No need for shareButton on remote chats.
if (chatShareUrl === 'everyone') { const chatShareUrl = settings['chat-share-url'] ?? ''
showShareUrlButton = true if (chatShareUrl === 'everyone') {
} else if (chatShareUrl === 'owner') { showShareUrlButton = true
showShareUrlButton = guessIsMine(registerOptions, video) } else if (chatShareUrl === 'owner') {
} else if (chatShareUrl === 'owner+moderators') { showShareUrlButton = guessIsMine(registerOptions, video)
showShareUrlButton = guessIsMine(registerOptions, video) || guessIamIModerator(registerOptions) } else if (chatShareUrl === 'owner+moderators') {
showShareUrlButton = guessIsMine(registerOptions, video) || guessIamIModerator(registerOptions)
}
} }
insertChatDom(container as HTMLElement, video, !!settings['chat-open-blank'], showShareUrlButton).then(() => { insertChatDom(container as HTMLElement, video, !!settings['chat-open-blank'], showShareUrlButton).then(() => {

View File

@ -82,6 +82,7 @@ function randomNick (base: string): string {
interface InitConverseParams { interface InitConverseParams {
jid: string jid: string
remoteAnonymousXMPPServer: boolean
assetsPath: string assetsPath: string
room: string room: string
boshServiceUrl: string boshServiceUrl: string
@ -95,6 +96,7 @@ interface InitConverseParams {
} }
window.initConverse = async function initConverse ({ window.initConverse = async function initConverse ({
jid, jid,
remoteAnonymousXMPPServer,
assetsPath, assetsPath,
room, room,
boshServiceUrl, boshServiceUrl,
@ -205,6 +207,7 @@ window.initConverse = async function initConverse ({
// TODO: params.clear_messages_on_reconnection = true when muc_mam will be available. // TODO: params.clear_messages_on_reconnection = true when muc_mam will be available.
let isAuthenticated: boolean = false let isAuthenticated: boolean = false
let isRemoteWithNicknameSet: boolean = false
if (authenticationUrl === '') { if (authenticationUrl === '') {
throw new Error('Missing authenticationUrl') throw new Error('Missing authenticationUrl')
} }
@ -218,19 +221,27 @@ window.initConverse = async function initConverse ({
const auth = await authenticatedMode(authenticationUrl) const auth = await authenticatedMode(authenticationUrl)
if (auth) { if (auth) {
params.authentication = 'login' if (remoteAnonymousXMPPServer) {
params.auto_login = true // Spécial case: anonymous connection to remote XMPP server.
params.jid = auth.jid if (auth.nickname) {
params.password = auth.password params.nickname = auth.nickname
if (auth.nickname) { isRemoteWithNicknameSet = true
params.nickname = auth.nickname }
} else { } else {
params.muc_nickname_from_jid = true params.authentication = 'login'
params.auto_login = true
params.jid = auth.jid
params.password = auth.password
if (auth.nickname) {
params.nickname = auth.nickname
} else {
params.muc_nickname_from_jid = true
}
// We dont need the keepalive. And I suppose it is related to some bugs when opening a previous chat window.
params.keepalive = false
isAuthenticated = true
// FIXME: use params.oauth_providers?
} }
// We dont need the keepalive. And I suppose it is related to some bugs when opening a previous chat window.
params.keepalive = false
isAuthenticated = true
// FIXME: use params.oauth_providers?
} }
if (!isAuthenticated) { if (!isAuthenticated) {
@ -267,7 +278,7 @@ window.initConverse = async function initConverse ({
} }
}) })
if (autoViewerMode && !isAuthenticated) { if (autoViewerMode && !isAuthenticated && !isRemoteWithNicknameSet) {
converse.plugins.add('livechatViewerModePlugin', { converse.plugins.add('livechatViewerModePlugin', {
dependencies: ['converse-muc', 'converse-muc-views'], dependencies: ['converse-muc', 'converse-muc-views'],
initialize: function () { initialize: function () {

View File

@ -16,6 +16,7 @@
<script type="text/javascript"> <script type="text/javascript">
initConverse({ initConverse({
jid: '{{JID}}', jid: '{{JID}}',
remoteAnonymousXMPPServer: '{{REMOTE_ANONYMOUS_XMPP_SERVER}}' === 'true',
assetsPath : '{{BASE_STATIC_URL}}conversejs/', assetsPath : '{{BASE_STATIC_URL}}conversejs/',
room: '{{ROOM}}', room: '{{ROOM}}',
boshServiceUrl: '{{BOSH_SERVICE_URL}}', boshServiceUrl: '{{BOSH_SERVICE_URL}}',

View File

@ -0,0 +1,36 @@
import type { LiveChatJSONLDAttribute } from './types'
interface AnonymousConnectionInfos {
roomJID: string
boshUri?: string
wsUri?: string
userJID: string
}
function anonymousConnectionInfos (livechatInfos: LiveChatJSONLDAttribute | false): AnonymousConnectionInfos | null {
if (!livechatInfos) { return null }
if (!livechatInfos.links) { return null }
if (livechatInfos.type !== 'xmpp') { return null }
const r: AnonymousConnectionInfos = {
roomJID: livechatInfos.jid,
userJID: ''
}
for (const link of livechatInfos.links) {
// Note: userJID is on both links. But should have the same value.
if (link.type === 'xmpp-bosh-anonymous') {
r.boshUri = link.url
r.userJID = link.jid
} else if (link.type === 'xmpp-websocket-anonymous') {
r.wsUri = link.url
r.userJID = link.jid
}
}
if (r.userJID === '') {
return null
}
return r
}
export {
anonymousConnectionInfos
}

View File

@ -7,7 +7,7 @@ import { prosodyCheckUserPassword, prosodyRegisterUser, prosodyUserRegistered }
import { getUserNickname } from '../helpers' import { getUserNickname } from '../helpers'
import { Affiliations, getVideoAffiliations, getChannelAffiliations } from '../prosody/config/affiliations' import { Affiliations, getVideoAffiliations, getChannelAffiliations } from '../prosody/config/affiliations'
import { getProsodyDomain } from '../prosody/config/domain' import { getProsodyDomain } from '../prosody/config/domain'
import { fillVideoCustomFields, fillVideoRemoteLiveChat } from '../custom-fields' import { fillVideoCustomFields } from '../custom-fields'
import { getChannelInfosById } from '../database/channel' import { getChannelInfosById } from '../database/channel'
// See here for description: https://modules.prosody.im/mod_muc_http_defaults.html // See here for description: https://modules.prosody.im/mod_muc_http_defaults.html
@ -98,7 +98,6 @@ async function initApiRouter (options: RegisterServerOptions): Promise<Router> {
// Adding the custom fields and data: // Adding the custom fields and data:
await fillVideoCustomFields(options, video) await fillVideoCustomFields(options, video)
if (video.remote) { await fillVideoRemoteLiveChat(options, video) }
// check settings (chat enabled for this video?) // check settings (chat enabled for this video?)
const settings = await options.settingsManager.getSettings([ const settings = await options.settingsManager.getSettings([

View File

@ -1,4 +1,4 @@
import type { RegisterServerOptions, MVideoThumbnail } from '@peertube/peertube-types' import type { RegisterServerOptions, MVideoThumbnail, SettingEntries } from '@peertube/peertube-types'
import type { Router, Request, Response, NextFunction } from 'express' import type { Router, Request, Response, NextFunction } from 'express'
import type { import type {
ProsodyListRoomsResult, ProsodyListRoomsResultRoom ProsodyListRoomsResult, ProsodyListRoomsResultRoom
@ -13,6 +13,9 @@ import { getAPIKey } from '../apikey'
import { getChannelInfosById, getChannelNameById } from '../database/channel' import { getChannelInfosById, getChannelNameById } from '../database/channel'
import { isAutoColorsAvailable, areAutoColorsValid, AutoColors } from '../../../shared/lib/autocolors' import { isAutoColorsAvailable, areAutoColorsValid, AutoColors } from '../../../shared/lib/autocolors'
import { getBoshUri, getWSUri } from '../uri/webchat' import { getBoshUri, getWSUri } from '../uri/webchat'
import { getVideoLiveChatInfos } from '../federation/storage'
import { LiveChatJSONLDAttribute } from '../federation/types'
import { anonymousConnectionInfos } from '../federation/connection-infos'
import * as path from 'path' import * as path from 'path'
const got = require('got') const got = require('got')
@ -46,15 +49,10 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
const settings = await settingsManager.getSettings([ const settings = await settingsManager.getSettings([
'prosody-room-type', 'prosody-room-type',
'disable-websocket', 'disable-websocket',
'converse-theme', 'converse-autocolors' 'converse-theme', 'converse-autocolors',
'federation-no-remote-chat'
]) ])
const boshUri = getBoshUri(options)
const wsUri = settings['disable-websocket']
? ''
: (getWSUri(options) ?? '')
let room: string
let autoViewerMode: boolean = false let autoViewerMode: boolean = false
let forceReadonly: 'true' | 'false' | 'noscroll' = 'false' let forceReadonly: 'true' | 'false' | 'noscroll' = 'false'
let converseJSTheme: string = settings['converse-theme'] as string let converseJSTheme: string = settings['converse-theme'] as string
@ -62,27 +60,6 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
if (!/^\w+$/.test(converseJSTheme)) { if (!/^\w+$/.test(converseJSTheme)) {
converseJSTheme = 'peertube' converseJSTheme = 'peertube'
} }
const prosodyDomain = await getProsodyDomain(options)
const jid = 'anon.' + prosodyDomain
if (req.query.forcetype === '1') {
// 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
}
}
const authenticationUrl = options.peertubeHelpers.config.getWebserverUrl() + const authenticationUrl = options.peertubeHelpers.config.getWebserverUrl() +
getBaseRouterRoute(options) + getBaseRouterRoute(options) +
@ -100,6 +77,7 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
let video: MVideoThumbnail | undefined let video: MVideoThumbnail | undefined
let channelId: number let channelId: number
let remoteChatInfos: LiveChatJSONLDAttribute | undefined
const channelMatches = roomKey.match(/^channel\.(\d+)$/) const channelMatches = roomKey.match(/^channel\.(\d+)$/)
if (channelMatches?.[1]) { if (channelMatches?.[1]) {
channelId = parseInt(channelMatches[1]) channelId = parseInt(channelMatches[1])
@ -113,7 +91,17 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
const uuid = roomKey // must be a video UUID. const uuid = roomKey // must be a video UUID.
video = await peertubeHelpers.videos.loadByIdOrUUID(uuid) video = await peertubeHelpers.videos.loadByIdOrUUID(uuid)
if (!video) { if (!video) {
throw new Error('Video not found') res.status(404)
res.send('Not found')
return
}
if (video.remote) {
remoteChatInfos = settings['federation-no-remote-chat'] ? false : await getVideoLiveChatInfos(options, video)
if (!remoteChatInfos) {
res.status(404)
res.send('Not found')
return
}
} }
channelId = video.channelId channelId = video.channelId
} }
@ -121,28 +109,28 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
let page = '' + (converseJSIndex as string) let page = '' + (converseJSIndex as string)
const baseStaticUrl = getBaseStaticRoute(options) const baseStaticUrl = getBaseStaticRoute(options)
page = page.replace(/{{BASE_STATIC_URL}}/g, baseStaticUrl) page = page.replace(/{{BASE_STATIC_URL}}/g, baseStaticUrl)
page = page.replace(/{{JID}}/g, jid)
// Computing the room name... let connectionInfos: ConnectionInfos | null
if (room.includes('{{VIDEO_UUID}}')) { if (video?.remote) {
if (!video) { connectionInfos = await _remoteConnectionInfos(remoteChatInfos ?? false)
throw new Error('Missing video') } else {
} connectionInfos = await _localConnectionInfos(
room = room.replace(/{{VIDEO_UUID}}/g, video.uuid) options,
settings,
roomKey,
video,
channelId,
req.query.forcetype === '1'
)
} }
room = room.replace(/{{CHANNEL_ID}}/g, `${channelId}`) if (!connectionInfos) {
if (room.includes('{{CHANNEL_NAME}}')) { res.status(404)
const channelName = await getChannelNameById(options, channelId) res.send('No compatible way to connect to remote chat')
if (channelName === null) { return
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
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)
} }
page = page.replace(/{{JID}}/g, connectionInfos.userJID)
let autocolorsStyles = '' let autocolorsStyles = ''
if ( if (
settings['converse-autocolors'] && settings['converse-autocolors'] &&
@ -195,9 +183,10 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
} }
// ... then inject it in the page. // ... then inject it in the page.
page = page.replace(/{{ROOM}}/g, room) page = page.replace(/{{ROOM}}/g, connectionInfos.roomJID)
page = page.replace(/{{BOSH_SERVICE_URL}}/g, boshUri) page = page.replace(/{{BOSH_SERVICE_URL}}/g, connectionInfos.boshUri)
page = page.replace(/{{WS_SERVICE_URL}}/g, wsUri) page = page.replace(/{{WS_SERVICE_URL}}/g, connectionInfos.wsUri ?? '')
page = page.replace(/{{REMOTE_ANONYMOUS_XMPP_SERVER}}/g, connectionInfos.remoteXMPPServer ? 'true' : 'false')
page = page.replace(/{{AUTHENTICATION_URL}}/g, authenticationUrl) page = page.replace(/{{AUTHENTICATION_URL}}/g, authenticationUrl)
page = page.replace(/{{AUTOVIEWERMODE}}/g, autoViewerMode ? 'true' : 'false') page = page.replace(/{{AUTOVIEWERMODE}}/g, autoViewerMode ? 'true' : 'false')
page = page.replace(/{{CONVERSEJS_THEME}}/g, converseJSTheme) page = page.replace(/{{CONVERSEJS_THEME}}/g, converseJSTheme)
@ -377,6 +366,95 @@ async function enableProxyRoute (
}) })
} }
interface ConnectionInfos {
userJID: string
roomJID: string
boshUri: string
wsUri?: string
remoteXMPPServer: boolean
}
async function _remoteConnectionInfos (remoteChatInfos: LiveChatJSONLDAttribute): Promise<ConnectionInfos | null> {
if (!remoteChatInfos) { throw new Error('Should have remote chat infos for remote videos') }
const connectionInfos = anonymousConnectionInfos(remoteChatInfos ?? false)
if (!connectionInfos || !connectionInfos.boshUri) {
return null
}
return {
userJID: connectionInfos.userJID,
roomJID: connectionInfos.roomJID,
boshUri: connectionInfos.boshUri,
wsUri: connectionInfos.wsUri,
remoteXMPPServer: true
}
}
async function _localConnectionInfos (
options: RegisterServerOptions,
settings: SettingEntries,
roomKey: string,
video: MVideoThumbnail | undefined,
channelId: number,
forceType: boolean
): Promise<ConnectionInfos> {
const prosodyDomain = await getProsodyDomain(options)
const jid = 'anon.' + prosodyDomain
const boshUri = getBoshUri(options)
const wsUri = settings['disable-websocket']
? ''
: (getWSUri(options) ?? '')
// 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 {
userJID: jid,
roomJID: room,
boshUri,
wsUri,
remoteXMPPServer: false
}
}
export { export {
initWebchatRouter, initWebchatRouter,
disableProxyRoute, disableProxyRoute,

View File

@ -1,6 +1,6 @@
import { parseConfigUUIDs } from './config' import { parseConfigUUIDs } from './config'
interface SharedSettings { interface VideoHasWebchatSettings {
'chat-per-live-video': boolean 'chat-per-live-video': boolean
'chat-all-lives': boolean 'chat-all-lives': boolean
'chat-all-non-lives': boolean 'chat-all-non-lives': boolean
@ -12,6 +12,7 @@ interface SharedVideoBase {
isLive: boolean isLive: boolean
pluginData?: { pluginData?: {
'livechat-active'?: boolean 'livechat-active'?: boolean
'livechat-remote'?: boolean
} }
} }
@ -25,7 +26,13 @@ interface SharedVideoBackend extends SharedVideoBase {
type SharedVideo = SharedVideoBackend | SharedVideoFrontend type SharedVideo = SharedVideoBackend | SharedVideoFrontend
function videoHasWebchat (settings: SharedSettings, video: SharedVideo): boolean { /**
* Indicate if the video has a local chat.
* @param settings plugin settings
* @param video the video
* @returns true if the video has a local chat
*/
function videoHasWebchat (settings: VideoHasWebchatSettings, video: SharedVideo): boolean {
// Never use webchat on remote videos. // Never use webchat on remote videos.
if ('isLocal' in video) { if ('isLocal' in video) {
if (!video.isLocal) return false if (!video.isLocal) return false
@ -53,6 +60,29 @@ function videoHasWebchat (settings: SharedSettings, video: SharedVideo): boolean
return false return false
} }
export { interface VideoHasRemoteWebchatSettings {
videoHasWebchat 'federation-no-remote-chat': boolean
}
/**
* Indicates if the video has a remote chat.
* @param settings plugin settings
* @param video the video
* @returns true if the video has a remote chat
*/
function videoHasRemoteWebchat (settings: VideoHasRemoteWebchatSettings, video: SharedVideo): boolean {
if (settings['federation-no-remote-chat']) { return false }
if ('isLocal' in video) {
if (video.isLocal) return false
} else {
if (!video.remote) return false
}
if (!video.pluginData) { return false }
if (!video.pluginData['livechat-remote']) { return false }
return true
}
export {
videoHasWebchat,
videoHasRemoteWebchat
} }