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

View File

@ -82,6 +82,7 @@ function randomNick (base: string): string {
interface InitConverseParams {
jid: string
remoteAnonymousXMPPServer: boolean
assetsPath: string
room: string
boshServiceUrl: string
@ -95,6 +96,7 @@ interface InitConverseParams {
}
window.initConverse = async function initConverse ({
jid,
remoteAnonymousXMPPServer,
assetsPath,
room,
boshServiceUrl,
@ -205,6 +207,7 @@ window.initConverse = async function initConverse ({
// TODO: params.clear_messages_on_reconnection = true when muc_mam will be available.
let isAuthenticated: boolean = false
let isRemoteWithNicknameSet: boolean = false
if (authenticationUrl === '') {
throw new Error('Missing authenticationUrl')
}
@ -218,6 +221,13 @@ window.initConverse = async function initConverse ({
const auth = await authenticatedMode(authenticationUrl)
if (auth) {
if (remoteAnonymousXMPPServer) {
// Spécial case: anonymous connection to remote XMPP server.
if (auth.nickname) {
params.nickname = auth.nickname
isRemoteWithNicknameSet = true
}
} else {
params.authentication = 'login'
params.auto_login = true
params.jid = auth.jid
@ -232,6 +242,7 @@ window.initConverse = async function initConverse ({
isAuthenticated = true
// FIXME: use params.oauth_providers?
}
}
if (!isAuthenticated) {
console.log('User is not authenticated.')
@ -267,7 +278,7 @@ window.initConverse = async function initConverse ({
}
})
if (autoViewerMode && !isAuthenticated) {
if (autoViewerMode && !isAuthenticated && !isRemoteWithNicknameSet) {
converse.plugins.add('livechatViewerModePlugin', {
dependencies: ['converse-muc', 'converse-muc-views'],
initialize: function () {

View File

@ -16,6 +16,7 @@
<script type="text/javascript">
initConverse({
jid: '{{JID}}',
remoteAnonymousXMPPServer: '{{REMOTE_ANONYMOUS_XMPP_SERVER}}' === 'true',
assetsPath : '{{BASE_STATIC_URL}}conversejs/',
room: '{{ROOM}}',
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 { Affiliations, getVideoAffiliations, getChannelAffiliations } from '../prosody/config/affiliations'
import { getProsodyDomain } from '../prosody/config/domain'
import { fillVideoCustomFields, fillVideoRemoteLiveChat } from '../custom-fields'
import { fillVideoCustomFields } from '../custom-fields'
import { getChannelInfosById } from '../database/channel'
// 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:
await fillVideoCustomFields(options, video)
if (video.remote) { await fillVideoRemoteLiveChat(options, video) }
// check settings (chat enabled for this video?)
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 {
ProsodyListRoomsResult, ProsodyListRoomsResultRoom
@ -13,6 +13,9 @@ import { getAPIKey } from '../apikey'
import { getChannelInfosById, getChannelNameById } from '../database/channel'
import { isAutoColorsAvailable, areAutoColorsValid, AutoColors } from '../../../shared/lib/autocolors'
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'
const got = require('got')
@ -46,15 +49,10 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
const settings = await settingsManager.getSettings([
'prosody-room-type',
'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 forceReadonly: 'true' | 'false' | 'noscroll' = 'false'
let converseJSTheme: string = settings['converse-theme'] as string
@ -62,27 +60,6 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
if (!/^\w+$/.test(converseJSTheme)) {
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() +
getBaseRouterRoute(options) +
@ -100,6 +77,7 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
let video: MVideoThumbnail | undefined
let channelId: number
let remoteChatInfos: LiveChatJSONLDAttribute | undefined
const channelMatches = roomKey.match(/^channel\.(\d+)$/)
if (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.
video = await peertubeHelpers.videos.loadByIdOrUUID(uuid)
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
}
@ -121,28 +109,28 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
let page = '' + (converseJSIndex as string)
const baseStaticUrl = getBaseStaticRoute(options)
page = page.replace(/{{BASE_STATIC_URL}}/g, baseStaticUrl)
page = page.replace(/{{JID}}/g, jid)
// Computing the room name...
if (room.includes('{{VIDEO_UUID}}')) {
if (!video) {
throw new Error('Missing video')
let connectionInfos: ConnectionInfos | null
if (video?.remote) {
connectionInfos = await _remoteConnectionInfos(remoteChatInfos ?? false)
} else {
connectionInfos = await _localConnectionInfos(
options,
settings,
roomKey,
video,
channelId,
req.query.forcetype === '1'
)
}
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
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)
if (!connectionInfos) {
res.status(404)
res.send('No compatible way to connect to remote chat')
return
}
page = page.replace(/{{JID}}/g, connectionInfos.userJID)
let autocolorsStyles = ''
if (
settings['converse-autocolors'] &&
@ -195,9 +183,10 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
}
// ... then inject it in the page.
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(/{{ROOM}}/g, connectionInfos.roomJID)
page = page.replace(/{{BOSH_SERVICE_URL}}/g, connectionInfos.boshUri)
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(/{{AUTOVIEWERMODE}}/g, autoViewerMode ? 'true' : 'false')
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 {
initWebchatRouter,
disableProxyRoute,

View File

@ -1,6 +1,6 @@
import { parseConfigUUIDs } from './config'
interface SharedSettings {
interface VideoHasWebchatSettings {
'chat-per-live-video': boolean
'chat-all-lives': boolean
'chat-all-non-lives': boolean
@ -12,6 +12,7 @@ interface SharedVideoBase {
isLive: boolean
pluginData?: {
'livechat-active'?: boolean
'livechat-remote'?: boolean
}
}
@ -25,7 +26,13 @@ interface SharedVideoBackend extends SharedVideoBase {
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.
if ('isLocal' in video) {
if (!video.isLocal) return false
@ -53,6 +60,29 @@ function videoHasWebchat (settings: SharedSettings, video: SharedVideo): boolean
return false
}
export {
videoHasWebchat
interface VideoHasRemoteWebchatSettings {
'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
}