Chat Federation: using S2S if available.
* if both local and remote instance have external XMPP connections enabled, the user joins the remote room with his local account * some code refactoring (builtin.ts) Note: documentation and settings descriptions are to do. Related to #112
This commit is contained in:
parent
1003378b24
commit
3bc05d88df
@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## ??? (Not Release Yet)
|
||||
|
||||
### New Features
|
||||
|
||||
* Chat Federation: if both local and remote instance have external XMPP connections enabled, you can use your local xmpp account to join remote rooms.
|
||||
|
||||
## 6.3.0
|
||||
|
||||
### New Features
|
||||
|
@ -1,246 +1,77 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface Window {
|
||||
converse: {
|
||||
initialize: (args: any) => void
|
||||
plugins: {
|
||||
add: (name: string, plugin: any) => void
|
||||
}
|
||||
}
|
||||
initConverse: (args: any) => void
|
||||
}
|
||||
import type { InitConverseParams } from './lib/types'
|
||||
import { inIframe } from './lib/utils'
|
||||
import { initDom } from './lib/dom'
|
||||
import {
|
||||
defaultConverseParams,
|
||||
localRoomAnonymousParams,
|
||||
localRoomAuthenticatedParams,
|
||||
remoteRoomAnonymousParams,
|
||||
remoteRoomAuthenticatedParams
|
||||
} from './lib/converse-params'
|
||||
import { getLocalAuthentInfos } from './lib/auth'
|
||||
import { randomNick } from './lib/nick'
|
||||
|
||||
function inIframe (): boolean {
|
||||
try {
|
||||
return window.self !== window.top
|
||||
} catch (e) {
|
||||
return true
|
||||
declare global {
|
||||
interface Window {
|
||||
converse: {
|
||||
initialize: (args: any) => void
|
||||
plugins: {
|
||||
add: (name: string, plugin: any) => void
|
||||
}
|
||||
}
|
||||
initConverse: (args: InitConverseParams) => Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthentInfos {
|
||||
jid: string
|
||||
password: string
|
||||
nickname?: string
|
||||
}
|
||||
async function authenticatedMode (authenticationUrl: string): Promise<false | AuthentInfos> {
|
||||
try {
|
||||
if (!window.fetch) {
|
||||
console.error('Your browser has not the fetch api, we cant log you in')
|
||||
return false
|
||||
}
|
||||
if (!window.localStorage) {
|
||||
// FIXME: is the Peertube token always in localStorage?
|
||||
console.error('Your browser has no localStorage, we cant log you in')
|
||||
return false
|
||||
}
|
||||
const tokenType = window.localStorage.getItem('token_type') ?? ''
|
||||
const accessToken = window.localStorage.getItem('access_token') ?? ''
|
||||
const refreshToken = window.localStorage.getItem('refresh_token') ?? ''
|
||||
if (tokenType === '' && accessToken === '' && refreshToken === '') {
|
||||
console.info('User seems not to be logged in.')
|
||||
return false
|
||||
}
|
||||
|
||||
const response = await window.fetch(authenticationUrl, {
|
||||
method: 'GET',
|
||||
headers: new Headers({
|
||||
Authorization: tokenType + ' ' + accessToken,
|
||||
'content-type': 'application/json;charset=UTF-8'
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed fetching user informations')
|
||||
return false
|
||||
}
|
||||
const data = await response.json()
|
||||
if ((typeof data) !== 'object') {
|
||||
console.error('Failed reading user informations')
|
||||
return false
|
||||
}
|
||||
|
||||
if (!data.jid || !data.password) {
|
||||
console.error('User informations does not contain required fields')
|
||||
return false
|
||||
}
|
||||
return {
|
||||
jid: data.jid,
|
||||
password: data.password,
|
||||
nickname: data.nickname
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function randomNick (base: string): string {
|
||||
// using a 6 digit random number to generate a nickname with low colision risk
|
||||
const n = 100000 + Math.floor(Math.random() * 900000)
|
||||
return base + ' ' + n.toString()
|
||||
}
|
||||
|
||||
interface InitConverseParams {
|
||||
jid: string
|
||||
remoteAnonymousXMPPServer: boolean
|
||||
assetsPath: string
|
||||
room: string
|
||||
boshServiceUrl: string
|
||||
websocketServiceUrl: string
|
||||
authenticationUrl: string
|
||||
autoViewerMode: boolean
|
||||
forceReadonly: boolean | 'noscroll'
|
||||
noScroll: boolean
|
||||
theme: string
|
||||
transparent: boolean
|
||||
}
|
||||
window.initConverse = async function initConverse ({
|
||||
jid,
|
||||
remoteAnonymousXMPPServer,
|
||||
assetsPath,
|
||||
room,
|
||||
boshServiceUrl,
|
||||
websocketServiceUrl,
|
||||
authenticationUrl,
|
||||
autoViewerMode,
|
||||
forceReadonly,
|
||||
theme,
|
||||
transparent
|
||||
}: InitConverseParams) {
|
||||
const isInIframe = inIframe()
|
||||
const converse = window.converse
|
||||
|
||||
const body = document.querySelector('body')
|
||||
if (isInIframe) {
|
||||
if (body) {
|
||||
body.classList.add('livechat-iframe')
|
||||
// prevent horizontal scrollbar when in iframe. (don't know why, but does not work if done by CSS)
|
||||
body.style.overflowX = 'hidden'
|
||||
}
|
||||
}
|
||||
if (forceReadonly) {
|
||||
body?.classList.add('livechat-readonly')
|
||||
if (forceReadonly === 'noscroll') {
|
||||
body?.classList.add('livechat-noscroll')
|
||||
}
|
||||
}
|
||||
if (transparent) {
|
||||
body?.classList.add('livechat-transparent')
|
||||
}
|
||||
|
||||
if (websocketServiceUrl?.startsWith('/')) {
|
||||
websocketServiceUrl = new URL(
|
||||
websocketServiceUrl,
|
||||
window.initConverse = async function initConverse (initConverseParams: InitConverseParams): Promise<void> {
|
||||
// First, fixing relative websocket urls.
|
||||
if (initConverseParams.localWebsocketServiceUrl?.startsWith('/')) {
|
||||
initConverseParams.localWebsocketServiceUrl = new URL(
|
||||
initConverseParams.localWebsocketServiceUrl,
|
||||
(window.location.protocol === 'http:' ? 'ws://' : 'wss://') + window.location.host
|
||||
).toString()
|
||||
}
|
||||
|
||||
const mucShowInfoMessages = forceReadonly
|
||||
? [
|
||||
// in readonly mode, show only following info messages:
|
||||
'301', '307', '321', '322', '332', '333' // disconnected
|
||||
]
|
||||
: [
|
||||
// FIXME: wait for a response here, and rewrite: https://github.com/conversejs/converse.js/issues/3125
|
||||
'100', '102', '103', '172', '173', '174', // visibility_changes
|
||||
'110', // self
|
||||
'104', '201', // non_privacy_changes
|
||||
'170', '171', // muc_logging_changes
|
||||
'210', '303', // nickname_changes
|
||||
'301', '307', '321', '322', '332', '333', // disconnected
|
||||
'owner', 'admin', 'member', 'exadmin', 'exowner', 'exoutcast', 'exmember', // affiliation_changes
|
||||
// 'entered', 'exited', // join_leave_events
|
||||
'op', 'deop', 'voice', 'mute' // role_changes
|
||||
]
|
||||
const {
|
||||
isRemoteChat,
|
||||
remoteAnonymousXMPPServer,
|
||||
remoteAuthenticatedXMPPServer,
|
||||
authenticationUrl,
|
||||
autoViewerMode,
|
||||
forceReadonly
|
||||
} = initConverseParams
|
||||
|
||||
const params: any = {
|
||||
assets_path: assetsPath,
|
||||
const converse = window.converse
|
||||
|
||||
authentication: 'anonymous',
|
||||
ping_interval: 25, // must be set accordingly to c2s_close_timeout backend websocket settings and nginx timeout
|
||||
auto_login: true,
|
||||
auto_join_rooms: [
|
||||
room
|
||||
],
|
||||
keepalive: true,
|
||||
discover_connection_methods: false, // this parameter seems buggy with converseJS 7.0.4
|
||||
bosh_service_url: boshServiceUrl === '' ? undefined : boshServiceUrl,
|
||||
websocket_url: websocketServiceUrl === '' ? undefined : websocketServiceUrl,
|
||||
jid: jid,
|
||||
notify_all_room_messages: [
|
||||
room
|
||||
],
|
||||
show_desktop_notifications: false,
|
||||
show_tab_notifications: false,
|
||||
singleton: true,
|
||||
auto_focus: !isInIframe,
|
||||
hide_muc_participants: isInIframe,
|
||||
play_sounds: false,
|
||||
muc_mention_autocomplete_min_chars: 2,
|
||||
muc_mention_autocomplete_filter: 'contains',
|
||||
muc_instant_rooms: true,
|
||||
show_client_info: false,
|
||||
allow_adhoc_commands: false,
|
||||
allow_contact_requests: false,
|
||||
allow_logout: false,
|
||||
show_controlbox_by_default: false,
|
||||
view_mode: 'fullscreen',
|
||||
allow_message_corrections: 'all',
|
||||
allow_message_retraction: 'all',
|
||||
visible_toolbar_buttons: {
|
||||
call: false,
|
||||
spoiler: false,
|
||||
emoji: true,
|
||||
toggle_occupants: true
|
||||
},
|
||||
theme: theme || 'peertube',
|
||||
dark_theme: theme || 'peertube', // dark theme should be the same as theme
|
||||
persistent_store: 'sessionStorage',
|
||||
show_images_inline: false, // for security reason, and to avoid bugs when image is larger that iframe
|
||||
render_media: false, // for security reason, and to avoid bugs when image is larger that iframe
|
||||
whitelisted_plugins: ['livechatWindowTitlePlugin', 'livechatViewerModePlugin', 'livechatDisconnectOnUnloadPlugin'],
|
||||
show_retraction_warning: false, // No need to use this warning (except if we open to external clients?)
|
||||
muc_show_info_messages: mucShowInfoMessages,
|
||||
send_chat_state_notifications: false // don't send this for performance reason
|
||||
}
|
||||
|
||||
// TODO: params.clear_messages_on_reconnection = true when muc_mam will be available.
|
||||
const isInIframe = inIframe()
|
||||
initDom(initConverseParams, isInIframe)
|
||||
const params = defaultConverseParams(initConverseParams, isInIframe)
|
||||
|
||||
let isAuthenticated: boolean = false
|
||||
let isRemoteWithNicknameSet: boolean = false
|
||||
if (authenticationUrl === '') {
|
||||
throw new Error('Missing authenticationUrl')
|
||||
}
|
||||
|
||||
// The user will never se the «trusted browser» checkbox (that allows to save credentials).
|
||||
// So we have to disable it
|
||||
// (and ensure clear_cache_on_logout is true,
|
||||
// see https://conversejs.org/docs/html/configuration.html#allow-user-trust-override).
|
||||
params.clear_cache_on_logout = true
|
||||
params.allow_user_trust_override = false
|
||||
|
||||
const auth = await authenticatedMode(authenticationUrl)
|
||||
const auth = await getLocalAuthentInfos(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
|
||||
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
|
||||
if (!isRemoteChat) {
|
||||
localRoomAuthenticatedParams(initConverseParams, auth, params)
|
||||
isAuthenticated = true
|
||||
// FIXME: use params.oauth_providers?
|
||||
} else if (remoteAuthenticatedXMPPServer) {
|
||||
remoteRoomAuthenticatedParams(initConverseParams, auth, params)
|
||||
isAuthenticated = true
|
||||
} else if (remoteAnonymousXMPPServer) {
|
||||
// remote server does not allow remote authenticated users, falling back to anonymous mode
|
||||
remoteRoomAnonymousParams(initConverseParams, auth, params)
|
||||
isRemoteWithNicknameSet = true
|
||||
} else {
|
||||
throw new Error('Remote server does not allow remote connection')
|
||||
}
|
||||
} else {
|
||||
if (!isRemoteChat) {
|
||||
localRoomAnonymousParams(initConverseParams, params)
|
||||
} else if (remoteAnonymousXMPPServer) {
|
||||
remoteRoomAnonymousParams(initConverseParams, null, params)
|
||||
} else {
|
||||
throw new Error('Remote server does not allow remote connection')
|
||||
}
|
||||
}
|
||||
|
||||
@ -300,9 +131,9 @@ window.initConverse = async function initConverse ({
|
||||
function refreshViewerMode (canChat: boolean): void {
|
||||
console.log('[livechatViewerModePlugin] refreshViewerMode: ' + (canChat ? 'off' : 'on'))
|
||||
if (canChat) {
|
||||
body?.setAttribute('livechat-viewer-mode', 'off')
|
||||
document.querySelector('body')?.setAttribute('livechat-viewer-mode', 'off')
|
||||
} else {
|
||||
body?.setAttribute('livechat-viewer-mode', 'on')
|
||||
document.querySelector('body')?.setAttribute('livechat-viewer-mode', 'on')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,12 +15,17 @@
|
||||
<div id="conversejs-bg" class="theme-peertube"></div>
|
||||
<script type="text/javascript">
|
||||
initConverse({
|
||||
jid: '{{JID}}',
|
||||
isRemoteChat: '{{IS_REMOTE_CHAT}}' === 'true',
|
||||
localAnonymousJID: '{{LOCAL_ANONYMOUS_JID}}',
|
||||
remoteAnonymousJID: '{{REMOTE_ANONYMOUS_JID}}' === '' ? null : '{{REMOTE_ANONYMOUS_JID}}',
|
||||
remoteAnonymousXMPPServer: '{{REMOTE_ANONYMOUS_XMPP_SERVER}}' === 'true',
|
||||
remoteAuthenticatedXMPPServer: '{{REMOTE_AUTHENTICATED_XMPP_SERVER}}' === 'true',
|
||||
assetsPath : '{{BASE_STATIC_URL}}conversejs/',
|
||||
room: '{{ROOM}}',
|
||||
boshServiceUrl: '{{BOSH_SERVICE_URL}}',
|
||||
websocketServiceUrl: '{{WS_SERVICE_URL}}',
|
||||
localBoshServiceUrl: '{{LOCAL_BOSH_SERVICE_URL}}' === '' ? null : '{{LOCAL_BOSH_SERVICE_URL}}',
|
||||
localWebsocketServiceUrl: '{{LOCAL_WS_SERVICE_URL}}' === '' ? null : '{{LOCAL_WS_SERVICE_URL}}',
|
||||
remoteBoshServiceUrl: '{{REMOTE_BOSH_SERVICE_URL}}' === '' ? null : '{{REMOTE_BOSH_SERVICE_URL}}',
|
||||
remoteWebsocketServiceUrl: '{{REMOTE_WS_SERVICE_URL}}' === '' ? null : '{{REMOTE_WS_SERVICE_URL}}',
|
||||
authenticationUrl: '{{AUTHENTICATION_URL}}',
|
||||
autoViewerMode: '{{AUTOVIEWERMODE}}' === 'true',
|
||||
theme: '{{CONVERSEJS_THEME}}',
|
||||
|
65
conversejs/lib/auth.ts
Normal file
65
conversejs/lib/auth.ts
Normal file
@ -0,0 +1,65 @@
|
||||
interface AuthentInfos {
|
||||
jid: string
|
||||
password: string
|
||||
nickname?: string
|
||||
}
|
||||
async function getLocalAuthentInfos (authenticationUrl: string): Promise<false | AuthentInfos> {
|
||||
try {
|
||||
if (authenticationUrl === '') {
|
||||
console.error('Missing authenticationUrl')
|
||||
return false
|
||||
}
|
||||
if (!window.fetch) {
|
||||
console.error('Your browser has not the fetch api, we cant log you in')
|
||||
return false
|
||||
}
|
||||
if (!window.localStorage) {
|
||||
// FIXME: is the Peertube token always in localStorage?
|
||||
console.error('Your browser has no localStorage, we cant log you in')
|
||||
return false
|
||||
}
|
||||
const tokenType = window.localStorage.getItem('token_type') ?? ''
|
||||
const accessToken = window.localStorage.getItem('access_token') ?? ''
|
||||
const refreshToken = window.localStorage.getItem('refresh_token') ?? ''
|
||||
if (tokenType === '' && accessToken === '' && refreshToken === '') {
|
||||
console.info('User seems not to be logged in.')
|
||||
return false
|
||||
}
|
||||
|
||||
const response = await window.fetch(authenticationUrl, {
|
||||
method: 'GET',
|
||||
headers: new Headers({
|
||||
Authorization: tokenType + ' ' + accessToken,
|
||||
'content-type': 'application/json;charset=UTF-8'
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed fetching user informations')
|
||||
return false
|
||||
}
|
||||
const data = await response.json()
|
||||
if ((typeof data) !== 'object') {
|
||||
console.error('Failed reading user informations')
|
||||
return false
|
||||
}
|
||||
|
||||
if (!data.jid || !data.password) {
|
||||
console.error('User informations does not contain required fields')
|
||||
return false
|
||||
}
|
||||
return {
|
||||
jid: data.jid,
|
||||
password: data.password,
|
||||
nickname: data.nickname
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
AuthentInfos,
|
||||
getLocalAuthentInfos
|
||||
}
|
174
conversejs/lib/converse-params.ts
Normal file
174
conversejs/lib/converse-params.ts
Normal file
@ -0,0 +1,174 @@
|
||||
import type { InitConverseParams } from './types'
|
||||
import type { AuthentInfos } from './auth'
|
||||
|
||||
/**
|
||||
* Instanciate defaults params to use for ConverseJS.
|
||||
* Note: these parameters must be completed with one of the other function present in this module.
|
||||
* @param param0 global parameters
|
||||
* @param isInIframe true if we are in iframe mode (inside Peertube, beside video)
|
||||
* @returns default parameters to provide to ConverseJS.
|
||||
*/
|
||||
function defaultConverseParams (
|
||||
{ forceReadonly, theme, assetsPath, room }: InitConverseParams,
|
||||
isInIframe: boolean
|
||||
): any {
|
||||
const mucShowInfoMessages = forceReadonly
|
||||
? [
|
||||
// in readonly mode, show only following info messages:
|
||||
'301', '307', '321', '322', '332', '333' // disconnected
|
||||
]
|
||||
: [
|
||||
// FIXME: wait for a response here, and rewrite: https://github.com/conversejs/converse.js/issues/3125
|
||||
'100', '102', '103', '172', '173', '174', // visibility_changes
|
||||
'110', // self
|
||||
'104', '201', // non_privacy_changes
|
||||
'170', '171', // muc_logging_changes
|
||||
'210', '303', // nickname_changes
|
||||
'301', '307', '321', '322', '332', '333', // disconnected
|
||||
'owner', 'admin', 'member', 'exadmin', 'exowner', 'exoutcast', 'exmember', // affiliation_changes
|
||||
// 'entered', 'exited', // join_leave_events
|
||||
'op', 'deop', 'voice', 'mute' // role_changes
|
||||
]
|
||||
|
||||
const params: any = {
|
||||
assets_path: assetsPath,
|
||||
|
||||
authentication: 'anonymous',
|
||||
ping_interval: 25, // must be set accordingly to c2s_close_timeout backend websocket settings and nginx timeout
|
||||
auto_login: true,
|
||||
auto_join_rooms: [
|
||||
room
|
||||
],
|
||||
keepalive: true,
|
||||
discover_connection_methods: false, // this parameter seems buggy with converseJS 7.0.4
|
||||
notify_all_room_messages: [
|
||||
room
|
||||
],
|
||||
show_desktop_notifications: false,
|
||||
show_tab_notifications: false,
|
||||
singleton: true,
|
||||
auto_focus: !isInIframe,
|
||||
hide_muc_participants: isInIframe,
|
||||
play_sounds: false,
|
||||
muc_mention_autocomplete_min_chars: 2,
|
||||
muc_mention_autocomplete_filter: 'contains',
|
||||
muc_instant_rooms: true,
|
||||
show_client_info: false,
|
||||
allow_adhoc_commands: false,
|
||||
allow_contact_requests: false,
|
||||
allow_logout: false,
|
||||
show_controlbox_by_default: false,
|
||||
view_mode: 'fullscreen',
|
||||
allow_message_corrections: 'all',
|
||||
allow_message_retraction: 'all',
|
||||
visible_toolbar_buttons: {
|
||||
call: false,
|
||||
spoiler: false,
|
||||
emoji: true,
|
||||
toggle_occupants: true
|
||||
},
|
||||
theme: theme || 'peertube',
|
||||
dark_theme: theme || 'peertube', // dark theme should be the same as theme
|
||||
persistent_store: 'sessionStorage',
|
||||
show_images_inline: false, // for security reason, and to avoid bugs when image is larger that iframe
|
||||
render_media: false, // for security reason, and to avoid bugs when image is larger that iframe
|
||||
whitelisted_plugins: ['livechatWindowTitlePlugin', 'livechatViewerModePlugin', 'livechatDisconnectOnUnloadPlugin'],
|
||||
show_retraction_warning: false, // No need to use this warning (except if we open to external clients?)
|
||||
muc_show_info_messages: mucShowInfoMessages,
|
||||
send_chat_state_notifications: false // don't send this for performance reason
|
||||
}
|
||||
|
||||
// TODO: params.clear_messages_on_reconnection = true when muc_mam will be available.
|
||||
|
||||
// The user will never se the «trusted browser» checkbox (that allows to save credentials).
|
||||
// So we have to disable it
|
||||
// (and ensure clear_cache_on_logout is true,
|
||||
// see https://conversejs.org/docs/html/configuration.html#allow-user-trust-override).
|
||||
params.clear_cache_on_logout = true
|
||||
params.allow_user_trust_override = false
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
/**
|
||||
* The room is local, and we are an authenticated local user
|
||||
* @param initConverseParams global parameters
|
||||
* @param auth authent infos.
|
||||
* @param params ConverseJS parameters to fill
|
||||
*/
|
||||
function localRoomAuthenticatedParams (initConverseParams: InitConverseParams, auth: AuthentInfos, params: any): void {
|
||||
_fillAuthenticatedParams(initConverseParams, auth, params)
|
||||
_fillLocalProtocols(initConverseParams, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* The room is local, and we are an anonymous local user
|
||||
* @param initConverseParams global parameters
|
||||
* @param params ConverseJS parameters to fill
|
||||
*/
|
||||
function localRoomAnonymousParams (initConverseParams: InitConverseParams, params: any): void {
|
||||
params.jid = initConverseParams.localAnonymousJID
|
||||
_fillLocalProtocols(initConverseParams, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* The room is remote, and we are an authenticated local user
|
||||
* @param initConverseParams global parameters
|
||||
* @param auth authent infos.
|
||||
* @param params ConverseJS parameters to fill
|
||||
*/
|
||||
function remoteRoomAuthenticatedParams (initConverseParams: InitConverseParams, auth: AuthentInfos, params: any): void {
|
||||
_fillAuthenticatedParams(initConverseParams, auth, params)
|
||||
_fillLocalProtocols(initConverseParams, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* The room is remote, and we are an anonymous local user
|
||||
* @param initConverseParams global parameters
|
||||
* @param auth optionnal authent infos. Used to get the default nickname
|
||||
* @param params ConverseJS parameters to fill
|
||||
*/
|
||||
function remoteRoomAnonymousParams (
|
||||
initConverseParams: InitConverseParams,
|
||||
auth: AuthentInfos | null,
|
||||
params: any
|
||||
): void {
|
||||
params.jid = initConverseParams.remoteAnonymousJID
|
||||
if (auth?.nickname) {
|
||||
params.nickname = auth.nickname
|
||||
}
|
||||
_fillRemoteProtocols(initConverseParams, params)
|
||||
}
|
||||
|
||||
function _fillAuthenticatedParams (initConverseParams: InitConverseParams, auth: AuthentInfos, params: any): void {
|
||||
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
|
||||
// FIXME: use params.oauth_providers?
|
||||
}
|
||||
|
||||
function _fillLocalProtocols (initConverseParams: InitConverseParams, params: any): void {
|
||||
params.bosh_service_url = initConverseParams.localBoshServiceUrl
|
||||
params.websocket_url = initConverseParams.localWebsocketServiceUrl
|
||||
}
|
||||
|
||||
function _fillRemoteProtocols (initConverseParams: InitConverseParams, params: any): void {
|
||||
params.bosh_service_url = initConverseParams.remoteBoshServiceUrl
|
||||
params.websocket_url = initConverseParams.remoteWebsocketServiceUrl
|
||||
}
|
||||
|
||||
export {
|
||||
defaultConverseParams,
|
||||
localRoomAuthenticatedParams,
|
||||
localRoomAnonymousParams,
|
||||
remoteRoomAnonymousParams,
|
||||
remoteRoomAuthenticatedParams
|
||||
}
|
25
conversejs/lib/dom.ts
Normal file
25
conversejs/lib/dom.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import type { InitConverseParams } from './types'
|
||||
|
||||
function initDom ({ forceReadonly, transparent }: InitConverseParams, isInIframe: boolean): void {
|
||||
const body = document.querySelector('body')
|
||||
if (isInIframe) {
|
||||
if (body) {
|
||||
body.classList.add('livechat-iframe')
|
||||
// prevent horizontal scrollbar when in iframe. (don't know why, but does not work if done by CSS)
|
||||
body.style.overflowX = 'hidden'
|
||||
}
|
||||
}
|
||||
if (forceReadonly) {
|
||||
body?.classList.add('livechat-readonly')
|
||||
if (forceReadonly === 'noscroll') {
|
||||
body?.classList.add('livechat-noscroll')
|
||||
}
|
||||
}
|
||||
if (transparent) {
|
||||
body?.classList.add('livechat-transparent')
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
initDom
|
||||
}
|
9
conversejs/lib/nick.ts
Normal file
9
conversejs/lib/nick.ts
Normal file
@ -0,0 +1,9 @@
|
||||
function randomNick (base: string): string {
|
||||
// using a 6 digit random number to generate a nickname with low colision risk
|
||||
const n = 100000 + Math.floor(Math.random() * 900000)
|
||||
return base + ' ' + n.toString()
|
||||
}
|
||||
|
||||
export {
|
||||
randomNick
|
||||
}
|
23
conversejs/lib/types.ts
Normal file
23
conversejs/lib/types.ts
Normal file
@ -0,0 +1,23 @@
|
||||
interface InitConverseParams {
|
||||
isRemoteChat: boolean
|
||||
localAnonymousJID: string
|
||||
remoteAnonymousJID: string | null
|
||||
remoteAnonymousXMPPServer: boolean
|
||||
remoteAuthenticatedXMPPServer: boolean
|
||||
assetsPath: string
|
||||
room: string
|
||||
localBoshServiceUrl: string | null
|
||||
localWebsocketServiceUrl: string | null
|
||||
remoteBoshServiceUrl: string | null
|
||||
remoteWebsocketServiceUrl: string | null
|
||||
authenticationUrl: string
|
||||
autoViewerMode: boolean
|
||||
forceReadonly: boolean | 'noscroll'
|
||||
noScroll: boolean
|
||||
theme: string
|
||||
transparent: boolean
|
||||
}
|
||||
|
||||
export {
|
||||
InitConverseParams
|
||||
}
|
11
conversejs/lib/utils.ts
Normal file
11
conversejs/lib/utils.ts
Normal file
@ -0,0 +1,11 @@
|
||||
function inIframe (): boolean {
|
||||
try {
|
||||
return window.self !== window.top
|
||||
} catch (e) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
inIframe
|
||||
}
|
@ -31,6 +31,17 @@ function anonymousConnectionInfos (livechatInfos: LiveChatJSONLDAttribute | fals
|
||||
return r
|
||||
}
|
||||
|
||||
export {
|
||||
anonymousConnectionInfos
|
||||
function remoteAuthenticatedConnectionEnabled (livechatInfos: LiveChatJSONLDAttribute | false): boolean {
|
||||
if (!livechatInfos) { return false }
|
||||
if (!livechatInfos.links) { return false }
|
||||
if (livechatInfos.type !== 'xmpp') { return false }
|
||||
for (const link of livechatInfos.links) {
|
||||
if (link.type === 'xmpp-s2s') { return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export {
|
||||
anonymousConnectionInfos,
|
||||
remoteAuthenticatedConnectionEnabled
|
||||
}
|
||||
|
@ -31,7 +31,8 @@ async function videoBuildJSONLD (
|
||||
'disable-websocket',
|
||||
'prosody-room-type',
|
||||
'federation-dont-publish-remotely',
|
||||
'chat-no-anonymous'
|
||||
'chat-no-anonymous',
|
||||
'prosody-room-allow-s2s'
|
||||
])
|
||||
|
||||
if (settings['federation-dont-publish-remotely']) {
|
||||
@ -70,6 +71,11 @@ async function videoBuildJSONLD (
|
||||
}
|
||||
|
||||
const links: LiveChatJSONLDLink[] = []
|
||||
if (settings['prosody-room-allow-s2s']) {
|
||||
links.push({
|
||||
type: 'xmpp-s2s'
|
||||
})
|
||||
}
|
||||
if (!settings['chat-no-anonymous']) {
|
||||
links.push({
|
||||
type: 'xmpp-bosh-anonymous',
|
||||
|
@ -36,6 +36,11 @@ function sanitizePeertubeLiveChatInfos (chatInfos: any): LiveChatJSONLDAttribute
|
||||
url: link.url
|
||||
})
|
||||
}
|
||||
if (link.type === 'xmpp-s2s') {
|
||||
r.links.push({
|
||||
type: link.type
|
||||
})
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
@ -4,6 +4,10 @@ interface VideoBuildResultContext {
|
||||
video: MVideoAP
|
||||
}
|
||||
|
||||
interface LiveChatJSONLDS2SLink {
|
||||
type: 'xmpp-s2s'
|
||||
}
|
||||
|
||||
interface LiveChatJSONLDAnonymousWebsocketLink {
|
||||
type: 'xmpp-websocket-anonymous'
|
||||
url: string
|
||||
@ -16,7 +20,7 @@ interface LiveChatJSONLDAnonymousBOSHLink {
|
||||
jid: string
|
||||
}
|
||||
|
||||
type LiveChatJSONLDLink = LiveChatJSONLDAnonymousBOSHLink | LiveChatJSONLDAnonymousWebsocketLink
|
||||
type LiveChatJSONLDLink = LiveChatJSONLDS2SLink | LiveChatJSONLDAnonymousBOSHLink | LiveChatJSONLDAnonymousWebsocketLink
|
||||
|
||||
interface LiveChatJSONLDInfos {
|
||||
type: 'xmpp'
|
||||
|
@ -156,6 +156,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
|
||||
const prosodyDomain = await getProsodyDomain(options)
|
||||
const paths = await getProsodyFilePaths(options)
|
||||
const roomType = settings['prosody-room-type'] === 'channel' ? 'channel' : 'video'
|
||||
const enableUserS2S = enableRoomS2S && !(settings['federation-no-remote-chat'] as boolean)
|
||||
let certificates: ProsodyConfigCertificates = false
|
||||
|
||||
const apikey = await getAPIKey(options)
|
||||
@ -203,7 +204,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
|
||||
config.useExternalComponents(componentsPort, components)
|
||||
}
|
||||
|
||||
if (enableRoomS2S) {
|
||||
if (enableRoomS2S || enableUserS2S) {
|
||||
certificates = 'generate-self-signed'
|
||||
if (config.paths.certsDirIsCustom) {
|
||||
certificates = 'use-from-dir'
|
||||
@ -223,7 +224,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
|
||||
if (networkInterface.match(/^[a-f0-9:]+$/)) return
|
||||
throw new Error('Invalid s2s interfaces')
|
||||
})
|
||||
config.useRoomS2S(s2sPort, s2sInterfaces)
|
||||
config.useS2S(s2sPort, s2sInterfaces, !enableUserS2S)
|
||||
}
|
||||
|
||||
const logExpiration = readLogExpiration(options, logExpirationSetting)
|
||||
|
@ -258,13 +258,17 @@ class ProsodyConfigContent {
|
||||
this.global.set('c2s_ports', [c2sPort])
|
||||
}
|
||||
|
||||
useRoomS2S (s2sPort: string, s2sInterfaces: string[]): void {
|
||||
useS2S (s2sPort: string, s2sInterfaces: string[], mucOnly: boolean): void {
|
||||
this.global.set('s2s_ports', [s2sPort])
|
||||
this.global.set('s2s_interfaces', s2sInterfaces)
|
||||
this.global.set('s2s_secure_auth', false)
|
||||
this.global.add('modules_enabled', 'tls') // required for s2s and co
|
||||
this.muc.add('modules_enabled', 's2s')
|
||||
this.muc.add('modules_enabled', 'dialback') // This allows s2s connections without certicicates!
|
||||
if (!mucOnly && this.authenticated) {
|
||||
this.authenticated.add('modules_enabled', 's2s')
|
||||
this.authenticated.add('modules_enabled', 'dialback') // This allows s2s connections without certicicates!
|
||||
}
|
||||
}
|
||||
|
||||
useExternalComponents (componentsPort: string, components: ExternalComponent[]): void {
|
||||
|
@ -15,7 +15,7 @@ import { isAutoColorsAvailable, areAutoColorsValid, AutoColors } from '../../../
|
||||
import { getBoshUri, getWSUri } from '../uri/webchat'
|
||||
import { getVideoLiveChatInfos } from '../federation/storage'
|
||||
import { LiveChatJSONLDAttribute } from '../federation/types'
|
||||
import { anonymousConnectionInfos } from '../federation/connection-infos'
|
||||
import { anonymousConnectionInfos, remoteAuthenticatedConnectionEnabled } from '../federation/connection-infos'
|
||||
import * as path from 'path'
|
||||
const got = require('got')
|
||||
|
||||
@ -50,7 +50,8 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
|
||||
'prosody-room-type',
|
||||
'disable-websocket',
|
||||
'converse-theme', 'converse-autocolors',
|
||||
'federation-no-remote-chat'
|
||||
'federation-no-remote-chat',
|
||||
'prosody-room-allow-s2s'
|
||||
])
|
||||
|
||||
let autoViewerMode: boolean = false
|
||||
@ -110,26 +111,38 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
|
||||
const baseStaticUrl = getBaseStaticRoute(options)
|
||||
page = page.replace(/{{BASE_STATIC_URL}}/g, baseStaticUrl)
|
||||
|
||||
let connectionInfos: ConnectionInfos | null
|
||||
const prosodyDomain = await getProsodyDomain(options)
|
||||
const localAnonymousJID = 'anon.' + prosodyDomain
|
||||
const localBoshUri = getBoshUri(options)
|
||||
const localWsUri = settings['disable-websocket']
|
||||
? ''
|
||||
: (getWSUri(options) ?? '')
|
||||
|
||||
let remoteConnectionInfos: WCRemoteConnectionInfos | undefined
|
||||
let roomJID: string
|
||||
if (video?.remote) {
|
||||
connectionInfos = await _remoteConnectionInfos(remoteChatInfos ?? false)
|
||||
remoteConnectionInfos = await _remoteConnectionInfos(remoteChatInfos ?? false)
|
||||
if (!remoteConnectionInfos) {
|
||||
res.status(404)
|
||||
res.send('No compatible way to connect to remote chat')
|
||||
return
|
||||
}
|
||||
roomJID = remoteConnectionInfos.roomJID
|
||||
} else {
|
||||
connectionInfos = await _localConnectionInfos(
|
||||
roomJID = await _localRoomJID(
|
||||
options,
|
||||
settings,
|
||||
prosodyDomain,
|
||||
roomKey,
|
||||
video,
|
||||
channelId,
|
||||
req.query.forcetype === '1'
|
||||
)
|
||||
}
|
||||
if (!connectionInfos) {
|
||||
res.status(404)
|
||||
res.send('No compatible way to connect to remote chat')
|
||||
return
|
||||
}
|
||||
|
||||
page = page.replace(/{{JID}}/g, connectionInfos.userJID)
|
||||
page = page.replace(/{{IS_REMOTE_CHAT}}/g, video?.remote ? 'true' : 'false')
|
||||
page = page.replace(/{{LOCAL_ANONYMOUS_JID}}/g, localAnonymousJID)
|
||||
page = page.replace(/{{REMOTE_ANONYMOUS_JID}}/g, remoteConnectionInfos?.anonymous?.userJID ?? '')
|
||||
|
||||
let autocolorsStyles = ''
|
||||
if (
|
||||
@ -183,10 +196,20 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
|
||||
}
|
||||
|
||||
// ... then inject it in the page.
|
||||
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(/{{ROOM}}/g, roomJID)
|
||||
page = page.replace(/{{LOCAL_BOSH_SERVICE_URL}}/g, localBoshUri)
|
||||
page = page.replace(/{{LOCAL_WS_SERVICE_URL}}/g, localWsUri ?? '')
|
||||
page = page.replace(/{{REMOTE_BOSH_SERVICE_URL}}/g, remoteConnectionInfos?.anonymous?.boshUri ?? '')
|
||||
page = page.replace(/{{REMOTE_WS_SERVICE_URL}}/g, remoteConnectionInfos?.anonymous?.wsUri ?? '')
|
||||
page = page.replace(/{{REMOTE_ANONYMOUS_XMPP_SERVER}}/g, remoteConnectionInfos?.anonymous ? 'true' : 'false')
|
||||
// Note: to be able to connect to remote XMPP server, with a local account,
|
||||
// we must enable prosody-room-allow-s2s
|
||||
// (which is required, so we can use outgoing S2S from the authenticated virtualhost).
|
||||
// TODO: There should be another settings, rather than prosody-room-allow-s2s
|
||||
page = page.replace(
|
||||
/{{REMOTE_AUTHENTICATED_XMPP_SERVER}}/g,
|
||||
settings['prosody-room-allow-s2s'] && remoteConnectionInfos?.authenticated ? 'true' : 'false'
|
||||
)
|
||||
page = page.replace(/{{AUTHENTICATION_URL}}/g, authenticationUrl)
|
||||
page = page.replace(/{{AUTOVIEWERMODE}}/g, autoViewerMode ? 'true' : 'false')
|
||||
page = page.replace(/{{CONVERSEJS_THEME}}/g, converseJSTheme)
|
||||
@ -382,44 +405,45 @@ async function enableProxyRoute (
|
||||
})
|
||||
}
|
||||
|
||||
interface ConnectionInfos {
|
||||
userJID: string
|
||||
interface WCRemoteConnectionInfos {
|
||||
roomJID: string
|
||||
boshUri: string
|
||||
wsUri?: string
|
||||
remoteXMPPServer: boolean
|
||||
anonymous?: {
|
||||
userJID: string
|
||||
boshUri: string
|
||||
wsUri?: string
|
||||
}
|
||||
authenticated?: boolean
|
||||
}
|
||||
|
||||
async function _remoteConnectionInfos (remoteChatInfos: LiveChatJSONLDAttribute): Promise<ConnectionInfos | null> {
|
||||
async function _remoteConnectionInfos (remoteChatInfos: LiveChatJSONLDAttribute): Promise<WCRemoteConnectionInfos> {
|
||||
if (!remoteChatInfos) { throw new Error('Should have remote chat infos for remote videos') }
|
||||
const connectionInfos = anonymousConnectionInfos(remoteChatInfos ?? false)
|
||||
if (!connectionInfos || !connectionInfos.boshUri) {
|
||||
return null
|
||||
if (remoteChatInfos.type !== 'xmpp') { throw new Error('Should have remote xmpp chat infos for remote videos') }
|
||||
const connectionInfos: WCRemoteConnectionInfos = {
|
||||
roomJID: remoteChatInfos.jid
|
||||
}
|
||||
return {
|
||||
userJID: connectionInfos.userJID,
|
||||
roomJID: connectionInfos.roomJID,
|
||||
boshUri: connectionInfos.boshUri,
|
||||
wsUri: connectionInfos.wsUri,
|
||||
remoteXMPPServer: true
|
||||
if (remoteAuthenticatedConnectionEnabled(remoteChatInfos)) {
|
||||
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 _localConnectionInfos (
|
||||
async function _localRoomJID (
|
||||
options: RegisterServerOptions,
|
||||
settings: SettingEntries,
|
||||
prosodyDomain: string,
|
||||
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) ?? '')
|
||||
|
||||
): Promise<string> {
|
||||
// Computing the room name...
|
||||
let room: string
|
||||
if (forceType) {
|
||||
@ -462,13 +486,7 @@ async function _localConnectionInfos (
|
||||
room = room.replace(/{{CHANNEL_NAME}}/g, channelName)
|
||||
}
|
||||
|
||||
return {
|
||||
userJID: jid,
|
||||
roomJID: room,
|
||||
boshUri,
|
||||
wsUri,
|
||||
remoteXMPPServer: false
|
||||
}
|
||||
return room
|
||||
}
|
||||
|
||||
export {
|
||||
|
Loading…
Reference in New Issue
Block a user