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:
John Livingston
2023-05-04 19:14:23 +02:00
parent 1003378b24
commit 3bc05d88df
16 changed files with 483 additions and 285 deletions

View File

@ -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')
}
}

View File

@ -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
View 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
}

View 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
View 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
View 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
View 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
View File

@ -0,0 +1,11 @@
function inIframe (): boolean {
try {
return window.self !== window.top
} catch (e) {
return true
}
}
export {
inIframe
}