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:
parent
b85a1dc90a
commit
5d323b2dfd
@ -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']) {
|
||||
|
@ -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 () {
|
||||
|
@ -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}}',
|
||||
|
36
server/lib/federation/connection-infos.ts
Normal file
36
server/lib/federation/connection-infos.ts
Normal 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
|
||||
}
|
@ -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([
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user