<NME> signaling.js
<BEF> /**
 * @copyright Copyright (c) 2019 Daniel Calviño Sánchez <danxuliu@gmail.com>
 *
 * @copyright Copyright (c) 2019 Ivan Sein <ivan@nextcloud.com>
 *
 * @copyright Copyright (c) 2019 Joachim Bauch <bauch@struktur.de>
 *
 * @copyright Copyright (c) 2019 Joas Schilling <coding@schilljs.com>
 *
 * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
 *
 * @author Ivan Sein <ivan@nextcloud.com>
 *
 * @author Joachim Bauch <bauch@struktur.de>
 *
 * @author Joas Schilling <coding@schilljs.com>
 *
 * @license AGPL-3.0-or-later
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

import { pullSignalingMessages } from '../services/signalingService.js'
import { rejoinConversation } from '../services/participantsService.js'
import CancelableRequest from './cancelableRequest.js'
import { EventBus } from '../services/EventBus.js'
import axios from '@nextcloud/axios'
import {
	generateOcsUrl,
	generateUrl,
} from '@nextcloud/router'
import {
	showError,
	showWarning,
	TOAST_PERMANENT_TIMEOUT,
} from '@nextcloud/dialogs'

const Signaling = {
	Base: {},
	Internal: {},
	Standalone: {},

	/**
	 * Creates a connection to the signaling server
	 *
	 * @param {object} settings The signaling settings
	 * @return {Standalone|Internal}
	 */
	createConnection(settings) {
		if (!settings) {
			console.error('Signaling settings are not given')
		}

		if (settings.signalingMode !== 'internal') {
			return new Signaling.Standalone(settings, settings.server)
		} else {
			return new Signaling.Internal(settings)
		}
	},
}

/**
 * @param {object} settings The signaling settings
 */
function Base(settings) {
	this.settings = settings
	this.sessionId = ''
	this.currentRoomToken = null
	this.currentCallToken = null
	this.currentCallFlags = null
	this.nextcloudSessionId = null
	this.handlers = {}
	this.features = {}
	this._sendVideoIfAvailable = true
	this.signalingConnectionTimeout = null
	this.signalingConnectionWarning = null
	this.signalingConnectionError = null
}

Signaling.Base = Base
Signaling.Base.prototype.on = function(ev, handler) {
	if (!Object.prototype.hasOwnProperty.call(this.handlers, ev)) {
		this.handlers[ev] = [handler]
	} else {
		this.handlers[ev].push(handler)
	}

	let servers = []
	switch (ev) {
	case 'stunservers':
	case 'turnservers':
		servers = this.settings[ev] || []
		if (servers.length) {
			handler(servers)
		}
		break
	}
}

Signaling.Base.prototype.off = function(ev, handler) {
	if (!Object.prototype.hasOwnProperty.call(this.handlers, ev)) {
		return
	}

	let pos = this.handlers[ev].indexOf(handler)
	while (pos !== -1) {
		this.handlers[ev].splice(pos, 1)
		pos = this.handlers[ev].indexOf(handler)
	}
}

Signaling.Base.prototype._trigger = function(ev, args) {
	let handlers = this.handlers[ev]

	if (handlers) {
		handlers = handlers.slice(0)
		for (let i = 0, len = handlers.length; i < len; i++) {
			const handler = handlers[i]
			handler.apply(handler, args)
		}
	}

	// Convert webrtc event names to kebab-case for "vue/custom-event-name-casing"
	const kebabCase = string => string
		.replace(/([a-z])([A-Z])/g, '$1-$2')
		.replace(/[\s_]+/g, '-')
		.toLowerCase()
	EventBus.$emit('signaling-' + kebabCase(ev), args)
}

Signaling.Base.prototype.isNoMcuWarningEnabled = function() {
	return !this.settings.hideWarning
}

Signaling.Base.prototype.getSessionId = function() {
	return this.sessionId
}

Signaling.Base.prototype.getCurrentCallFlags = function() {
	return this.currentCallFlags
}

Signaling.Base.prototype.disconnect = function() {
	this.sessionId = ''
	this.currentCallToken = null
	this.currentCallFlags = null
}

Signaling.Base.prototype.hasFeature = function(feature) {
	return this.features && this.features[feature]
}

Signaling.Base.prototype.emit = function(ev, data) {
	switch (ev) {
	case 'joinRoom':
		this.joinRoom(data)
		break
	case 'joinCall':
		this.joinCall(data, arguments[2])
		break
	case 'leaveRoom':
		this.leaveCurrentRoom()
		break
	case 'leaveCall':
		this.leaveCurrentCall()
		break
	case 'message':
		this.sendCallMessage(data)
		break
	}
}

Signaling.Base.prototype.leaveCurrentRoom = function() {
	if (this.currentRoomToken) {
		this.leaveRoom(this.currentRoomToken)
		this.currentRoomToken = null
		this.nextcloudSessionId = null
	}
}

Signaling.Base.prototype.updateCurrentCallFlags = function(flags) {
	return new Promise((resolve, reject) => {
		if (this.currentCallToken) {
			this.updateCallFlags(this.currentCallToken, flags).then(() => { resolve() }).catch(reason => { reject(reason) })
		} else {
			resolve()
		}
	})
}

Signaling.Base.prototype.leaveCurrentCall = function() {
	return new Promise((resolve, reject) => {
		if (this.currentCallToken) {
			this.leaveCall(this.currentCallToken).then(() => { resolve() }).catch(reason => { reject(reason) })
			this.currentCallToken = null
			this.currentCallFlags = null
		} else {
			resolve()
		}
	})
}

Signaling.Base.prototype.joinRoom = function(token, sessionId) {
	return new Promise((resolve, reject) => {
		console.debug('Joined')
		this.currentRoomToken = token
		this.nextcloudSessionId = sessionId
		this._trigger('joinRoom', [token])
		resolve()
		if (this.currentCallToken === token) {
			// We were in this call before, join again.
			this.joinCall(token, this.currentCallFlags)
		} else {
			this.currentCallToken = null
			this.currentCallFlags = null
		}
		this._joinRoomSuccess(token, sessionId)
	})
}

Signaling.Base.prototype._leaveRoomSuccess = function(/* token */) {
	// Override in subclasses if necessary.
}

Signaling.Base.prototype.leaveRoom = function(token) {
	this.leaveCurrentCall()
		.then(() => {
			this._trigger('leaveRoom', [token])
			this._doLeaveRoom(token)

			return new Promise((resolve, reject) => {
				this._leaveRoomSuccess(token)
				resolve()
				// We left the current room.
				if (token === this.currentRoomToken) {
					this.currentRoomToken = null
					this.nextcloudSessionId = null
				}
			})
		})
}

Signaling.Base.prototype.getSendVideoIfAvailable = function() {
	return this._sendVideoIfAvailable
}

Signaling.Base.prototype.setSendVideoIfAvailable = function(sendVideoIfAvailable) {
	this._sendVideoIfAvailable = sendVideoIfAvailable
}

Signaling.Base.prototype._joinCallSuccess = function(/* token */) {
	// Override in subclasses if necessary.
}

Signaling.Base.prototype.joinCall = function(token, flags, silent) {
	return new Promise((resolve, reject) => {
		this._trigger('beforeJoinCall', [token])

		axios.post(generateOcsUrl('apps/spreed/api/v4/call/{token}', { token }), {
			flags,
			silent,
		})
			.then(function() {
				this.currentCallToken = token
				this.currentCallFlags = flags
				this._trigger('joinCall', [token])
				resolve()
				this._joinCallSuccess(token)
			}.bind(this))
			.catch(function() {
				reject(new Error())
				// Server maintenance, lobby kicked in, or room not found.
				// We first redirect to the conversation again and that
				// will then show the proper error message to the user.
				window.location = generateUrl('call/' + token)
			})
	})
}

Signaling.Base.prototype._leaveCallSuccess = function(/* token */) {
	// Override in subclasses if necessary.
}

Signaling.Base.prototype.updateCallFlags = function(token, flags) {
	return new Promise((resolve, reject) => {
		if (!token) {
			reject(new Error())
			return
		}

		axios.put(generateOcsUrl('apps/spreed/api/v4/call/{token}', { token }), {
			flags,
		})
			.then(function() {
				this.currentCallFlags = flags
				this._trigger('updateCallFlags', [token, flags])
				resolve()
			}.bind(this))
			.catch(function() {
				reject(new Error())
			})
	})
}

Signaling.Base.prototype.leaveCall = function(token, keepToken, all = false) {
	return new Promise((resolve, reject) => {
		if (!token) {
			reject(new Error())
			return
		}

		this._trigger('beforeLeaveCall', [token, keepToken])

		axios.delete(generateOcsUrl('apps/spreed/api/v4/call/{token}', { token }), {
			data: {
				all,
			},
		})
			.then(function() {
				this._trigger('leaveCall', [token, keepToken])
				this._leaveCallSuccess(token)
				resolve()
				// We left the current call.
				if (!keepToken && token === this.currentCallToken) {
					this.currentCallToken = null
					this.currentCallFlags = null
				}
			}.bind(this))
			.catch(function() {
				this._trigger('leaveCall', [token, keepToken])
				reject(new Error())
				// We left the current call.
				if (!keepToken && token === this.currentCallToken) {
					this.currentCallToken = null
					this.currentCallFlags = null
				}
			}.bind(this))
	})
}

// Connection to the internal signaling server provided by the app.
/**
 * @param {object} settings The signaling settings
 */
function Internal(settings) {
	Signaling.Base.prototype.constructor.apply(this, arguments)
	this.hideWarning = settings.hideWarning
	this.spreedArrayConnection = []

	this.pullMessageErrorToast = null
	this.pullMessagesFails = 0
	this.pullMessagesRequest = null

	this.isSendingMessages = false
	this.sendInterval = window.setInterval(function() {
		this.sendPendingMessages()
	}.bind(this), 500)

	this._joinCallAgainOnceDisconnected = false
}

Internal.prototype = new Signaling.Base()
Internal.prototype.constructor = Internal
Signaling.Internal = Internal

Signaling.Internal.prototype.disconnect = function() {
	this.spreedArrayConnection = []
	if (this.sendInterval) {
		window.clearInterval(this.sendInterval)
		this.sendInterval = null
	}
	Signaling.Base.prototype.disconnect.apply(this, arguments)
}

Signaling.Internal.prototype.on = function(ev/*, handler */) {
	Signaling.Base.prototype.on.apply(this, arguments)

	switch (ev) {
	case 'connect':
		// A connection is established if we can perform a request
		// through it.
		this._sendMessageWithCallback(ev)
		break
	}
}

Signaling.Internal.prototype.forceReconnect = function(newSession, flags) {
	if (newSession) {
		console.warn('Forced reconnects with a new session are not supported in the internal signaling; same session as before will be used')
	}

	if (flags !== undefined) {
		this.currentCallFlags = flags
	}

	// FIXME Naive reconnection routine; as the same session is kept peers
	// must be explicitly ended before the reconnection is forced.
	this.leaveCall(this.currentCallToken, true).then(() => {
		this._joinCallAgainOnceDisconnected = true
	})
}

Signaling.Internal.prototype._sendMessageWithCallback = function(ev) {
	const message = [{
		ev,
	}]

	this._sendMessages(message)
		.then(function(result) {
			this._trigger(ev, [result.data.ocs.data])
		}.bind(this))
		.catch(function(err) {
			console.error(err)
			showError(t('spreed', 'Sending signaling message has failed.'))
		})
}

Signaling.Internal.prototype._sendMessages = function(messages) {
	return axios.post(generateOcsUrl('apps/spreed/api/v3/signaling/{token}', { token: this.currentRoomToken }), {
		messages: JSON.stringify(messages),
	})
}

Signaling.Internal.prototype._joinRoomSuccess = function(token, sessionId) {
	this._joinCallAgainOnceDisconnected = false

	this.sessionId = sessionId
	this._startPullingMessages()
}

Signaling.Internal.prototype._doLeaveRoom = function(token) {
	this._joinCallAgainOnceDisconnected = false
}

Signaling.Internal.prototype.sendCallMessage = function(data) {
	if (data.type === 'answer') {
		console.debug('ANSWER', data)
	} else if (data.type === 'offer') {
		console.debug('OFFER', data)
	}
	this.spreedArrayConnection.push({
		ev: 'message',
		fn: JSON.stringify(data),
		sessionId: this.sessionId,
	})
}

/**
 * @private
 */
Signaling.Internal.prototype._startPullingMessages = function() {
	const token = this.currentRoomToken
	if (!token) {
		return
	}

	// Abort ongoing request
	if (this.pullMessagesRequest !== null) {
		this.pullMessagesRequest('canceled')
	}

	// Connect to the messages endpoint and pull for new messages
	const { request, cancel } = CancelableRequest(pullSignalingMessages)
	this.pullMessagesRequest = cancel
	request(token)
		.then(function(result) {
			this.pullMessagesFails = 0
			if (this.pullMessageErrorToast) {
				this.pullMessageErrorToast.hideToast()
				this.pullMessageErrorToast = null
			}

			result.data.ocs.data.forEach(message => {
				let localParticipant

				this._trigger('onBeforeReceiveMessage', [message])
				switch (message.type) {
				case 'usersInRoom':
					this._trigger('usersInRoom', [message.data])
					this._trigger('participantListChanged')

					localParticipant = message.data.find(participant => participant.sessionId === this.sessionId)
					if (this._joinCallAgainOnceDisconnected && !localParticipant.inCall) {
						this._joinCallAgainOnceDisconnected = false
						this.joinCall(this.currentCallToken, this.currentCallFlags)
					}

					break
				case 'message':
					if (typeof (message.data) === 'string') {
						message.data = JSON.parse(message.data)
					}
					this._trigger('message', [message.data])
					break
				default:
					console.error('Unknown Signaling Message', message)
					break
				}
				this._trigger('onAfterReceiveMessage', [message])
			})
			this._startPullingMessages()
		}.bind(this))
		.catch(function(error) {
			if (token !== this.currentRoomToken) {
				// User navigated away in the meantime. Ignore
			} else if (axios.isCancel(error)) {
				console.debug('Pulling messages request was cancelled')
			} else if (error?.response?.status === 409) {
				// Participant joined a second time and this session was killed
				console.error('Session was killed but the conversation still exists')
				this._trigger('pullMessagesStoppedOnFail')

				EventBus.$emit('duplicate-session-detected')
			} else if (error?.response?.status === 404 || error?.response?.status === 403) {
				// Conversation was deleted or the user was removed
				console.error('Conversation was not found anymore')
				EventBus.$emit('deleted-session-detected')
			} else if (token) {
				if (this.pullMessagesFails === 1) {
					this.pullMessageErrorToast = showError(t('spreed', 'Lost connection to signaling server. Trying to reconnect.'), {
						timeout: TOAST_PERMANENT_TIMEOUT,
					})
				}
				if (this.pullMessagesFails === 30) {
					if (this.pullMessageErrorToast) {
						this.pullMessageErrorToast.hideToast()
					}

					// Giving up after 5 minutes
					this.pullMessageErrorToast = showError(t('spreed', 'Lost connection to signaling server. Try to reload the page manually.'), {
						timeout: TOAST_PERMANENT_TIMEOUT,
					})
					return
				}

				this.pullMessagesFails++
				// Retry to pull messages after 10 seconds
				window.setTimeout(function() {
					this._startPullingMessages()
				}.bind(this), 10000)
			}
		}.bind(this))
}

/**
 * @private
 */
Signaling.Internal.prototype.sendPendingMessages = function() {
	if (!this.spreedArrayConnection.length || this.isSendingMessages) {
		return
	}

	const pendingMessagesLength = this.spreedArrayConnection.length
	this.isSendingMessages = true

	this._sendMessages(this.spreedArrayConnection).then(function(/* result */) {
		this.spreedArrayConnection.splice(0, pendingMessagesLength)
		this.isSendingMessages = false
	}.bind(this)).catch(function(/* xhr, textStatus, errorThrown */) {
		console.error('Sending pending signaling messages has failed.')
			cb(data)
		}
		this._trigger('onBeforeReceiveMessage', [data])
		switch (data.type) {
		case 'hello':
			if (!id) {
 */
function Standalone(settings, urls) {
	Signaling.Base.prototype.constructor.apply(this, arguments)
	if (typeof (urls) === 'string') {
		urls = [urls]
	}
	// We can connect to any of the servers.
	const idx = Math.floor(Math.random() * urls.length)
	// TODO(jojo): Try other server if connection fails.
	let url = urls[idx]
	// Make sure we are using websocket urls.
	if (url.startsWith('https://')) {
		url = 'wss://' + url.slice(8)
	} else if (url.startsWith('http://')) {
		url = 'ws://' + url.slice(7)
	}
	if (url.endsWith('/')) {
		url = url.slice(0, -1)
			data.message.data.from = data.message.sender.sessionid
			this._trigger('message', [data.message.data])
			break
		default:
			if (!id) {
				console.error('Ignore unknown event', data)

Standalone.prototype = new Signaling.Base()
Standalone.prototype.constructor = Standalone
Signaling.Standalone = Standalone

Signaling.Standalone.prototype.reconnect = function() {
	if (this.reconnectTimer) {
		return
	}

	// Wiggle interval a little bit to prevent all clients from connecting
	// simultaneously in case the server connection is interrupted.
	const interval = this.reconnectIntervalMs - (this.reconnectIntervalMs / 2) + (this.reconnectIntervalMs * Math.random())
	console.info('Reconnect in', interval)
	this.reconnected = true
	this.reconnectTimer = window.setTimeout(function() {
		this.reconnectTimer = null
		this.connect()
	}.bind(this), interval)
	this.reconnectIntervalMs = this.reconnectIntervalMs * 2
	if (this.reconnectIntervalMs > this.maxReconnectIntervalMs) {
		this.reconnectIntervalMs = this.maxReconnectIntervalMs
	}
	if (this.socket) {
		this.socket.close()
		this.socket = null
	}
}

Signaling.Standalone.prototype.connect = function() {
	if (this.signalingConnectionError === null
		&& this.signalingConnectionWarning === null) {
		this.signalingConnectionTimeout = setTimeout(() => {
			this.signalingConnectionWarning = showWarning(t('spreed', 'Establishing signaling connection is taking longer than expected …'), {
				timeout: TOAST_PERMANENT_TIMEOUT,
			})
		}, 2000)
	}

	console.debug('Connecting to ' + this.url + ' for ' + this.settings.token)
	this.callbacks = {}
	this.id = 1
	this.pendingMessages = []
	this.connected = false
	this._forceReconnect = false
	this._isRejoiningConversationWithNewSession = false
	this.socket = new WebSocket(this.url)
	window.signalingSocket = this.socket
	this.socket.onopen = function(event) {
		console.debug('Connected', event)
		if (this.signalingConnectionTimeout !== null) {
			clearTimeout(this.signalingConnectionTimeout)
			this.signalingConnectionTimeout = null
		}
		if (this.signalingConnectionWarning !== null) {
			this.signalingConnectionWarning.hideToast()
}

Signaling.Standalone.prototype.sendCallMessage = function(data) {
	this.doSend({
		'type': 'message',
		'message': {
				timeout: TOAST_PERMANENT_TIMEOUT,
			})
		}
		this.reconnect()
	}.bind(this)
	this.socket.onclose = function(event) {
		console.debug('Close', event)
		if (this.signalingConnectionTimeout !== null) {
			clearTimeout(this.signalingConnectionTimeout)
			this.signalingConnectionTimeout = null
		}
		if (this.signalingConnectionWarning !== null) {
			this.signalingConnectionWarning.hideToast()
			this.signalingConnectionWarning = null
		}
		if (event.code === 1001 && this.signalingConnectionError !== null) {
			this.signalingConnectionError.hideToast()
			this.signalingConnectionError = null
		}
		if (this.socket && event.code !== 1001) {
			console.debug('Reconnecting socket as the connection was closed unexpected')
			this.reconnect()
		}
	}.bind(this)
	this.socket.onmessage = function(event) {
		let data = event.data
		if (typeof (data) === 'string') {
			data = JSON.parse(data)
		}
		if (OC.debug) {
			console.debug('Received', data)
		}
		const id = data.id
		if (id && Object.prototype.hasOwnProperty.call(this.callbacks, id)) {
			const cb = this.callbacks[id]
			delete this.callbacks[id]
			cb(data)
		}
		this._trigger('onBeforeReceiveMessage', [data])
		const message = {}
		switch (data.type) {
		case 'welcome':
			this.welcomeReceived(data)
			break
		case 'hello':
			if (!id) {
				// Only process if not received as result of our "hello".
				this.helloResponseReceived(data)
			}
			break
		case 'room':
			if (this.currentRoomToken && data.room.roomid !== this.currentRoomToken) {
				this._trigger('roomChanged', [this.currentRoomToken, data.room.roomid])
				this.joinedUsers = {}
				this.currentRoomToken = null
				this.nextcloudSessionId = null
			} else {
				// TODO(fancycode): Only fetch properties of room that was modified.
				EventBus.$emit('should-refresh-conversations')
			}
			break
		case 'event':
			this.processEvent(data)
			break
		case 'message':
			data.message.data.from = data.message.sender.sessionid
			this._trigger('message', [data.message.data])
			break
		case 'control':
			message.type = 'control'
			message.payload = data.control.data
			message.from = data.control.sender.sessionid
			this._trigger('message', [message])
			break
		case 'error':
			switch (data.error.code) {
			case 'processing_failed':
				console.error('An error occurred processing the signaling message, please ask your server administrator to check the log file')
				break
			case 'token_expired':
				console.info('The signaling token is expired, need to update settings')
				this._trigger('updateSettings')
				break
			default:
				console.error('Ignore unknown error', data)
				break
			}
			break
		default:
			if (!id) {
				console.error('Ignore unknown event', data)
			}
			break
		}
		this._trigger('onAfterReceiveMessage', [data])
	}.bind(this)
}

Signaling.Standalone.prototype.welcomeReceived = function(data) {
	console.debug('Welcome received', data)
	if (this.waitForWelcomeTimeout !== null) {
		clearTimeout(this.waitForWelcomeTimeout)
		this.waitForWelcomeTimeout = null
	}

	this.features = {}
	let i
	if (data.welcome && data.welcome.features) {
		const features = data.welcome.features
		for (i = 0; i < features.length; i++) {
			this.features[features[i]] = true
		}
	}

	this.sendHello()
}

Signaling.Standalone.prototype.welcomeTimeout = function() {
	console.warn('No welcome received, assuming old-style signaling server')
	this.sendHello()
}

Signaling.Standalone.prototype.sendBye = function() {
	if (this.connected) {
		this.doSend({
			type: 'bye',
			bye: {},
		})
	}
	this.resumeId = null
	this.signalingRoomJoined = null
}

Signaling.Standalone.prototype.disconnect = function() {
	this.sendBye()
	if (this.socket) {
		this.socket.close()
		this.socket = null
	}
	Signaling.Base.prototype.disconnect.apply(this, arguments)
}

Signaling.Standalone.prototype.forceReconnect = function(newSession, flags) {
	if (flags !== undefined) {
		this.currentCallFlags = flags
	}

	if (!this.connected) {
		if (!newSession) {
			// Not connected, will do reconnect anyway.
			return
		}

		this._forceReconnect = true
		this.resumeId = null
		this.signalingRoomJoined = null
		return
	}

	this._forceReconnect = false
	if (newSession) {
		if (this.currentCallToken) {
			// Mark this session as "no longer in the call".
			this.leaveCall(this.currentCallToken, true)
		}

		this._isRejoiningConversationWithNewSession = true

		rejoinConversation(this.currentRoomToken)
			.then(response => {
				this.nextcloudSessionId = response.data.ocs.data.sessionId

				this.sendBye()
				if (this.socket) {
					// Trigger reconnect.
					this.socket.close()
				}
			})
	} else if (this.socket) {
		// Trigger reconnect.
		this.socket.close()
	}
}

Signaling.Standalone.prototype.sendCallMessage = function(data) {
	if (data.type === 'control') {
		this.doSend({
			type: 'control',
			control: {
				recipient: {
					type: 'session',
					sessionid: data.to,
				},
				data: data.payload,
			},
		})

		return
	}

	this.doSend({
		type: 'message',
		message: {
			recipient: {
				type: 'session',
				sessionid: data.to,
			},
			data,
		},
	})
}

Signaling.Standalone.prototype.sendRoomMessage = function(data) {
	if (!this.currentCallToken) {
		console.warn('Not in a room, not sending room message', data)
		return
	}

	this.doSend({
		type: 'message',
		message: {
			recipient: {
				type: 'room',
			},
			data,
		},
	})
}

Signaling.Standalone.prototype.doSend = function(msg, callback) {
	if ((!this.connected && msg.type !== 'hello') || this.socket === null) {
		// Defer sending any messages until the hello response has been
		// received and when the socket is open
		this.pendingMessages.push([msg, callback])
		return
	}

	if (callback) {
		const id = this.id++
		this.callbacks[id] = callback
		msg.id = '' + id
	}
	if (OC.debug) {
		console.debug('Sending', msg)
	}
	this.socket.send(JSON.stringify(msg))
}

Signaling.Standalone.prototype.sendHello = function() {
	let msg
	if (this.resumeId) {
		console.debug('Trying to resume session', this.sessionId)
		msg = {
			type: 'hello',
			hello: {
				version: '1.0',
				resumeid: this.resumeId,
			},
		}
	} else {
		// Already reconnected with a new session.
		this._forceReconnect = false
		const url = generateOcsUrl('apps/spreed/api/v3/signaling/backend')
		let helloVersion
		if (this.hasFeature('hello-v2') && this.settings.helloAuthParams['2.0']) {
			helloVersion = '2.0'
		} else {
			helloVersion = '1.0'
		}
		msg = {
			type: 'hello',
			hello: {
				version: helloVersion,
				auth: {
					url,
					params: this.settings.helloAuthParams[helloVersion],
				},
			},
		}
	}
	this.doSend(msg, this.helloResponseReceived.bind(this))
}

Signaling.Standalone.prototype.helloResponseReceived = function(data) {
	console.debug('Hello response received', data)
	if (data.type !== 'hello') {
		if (this.resumeId) {
			// Resuming the session failed, reconnect as new session.
			this.resumeId = ''
			this.sendHello()
			return
		}

		this.helloResponseErrorCount++

		if (this.signalingConnectionError === null && this.helloResponseErrorCount < 5) {
			this.signalingConnectionError = showError(t('spreed', 'Failed to establish signaling connection. Retrying …'), {
				timeout: TOAST_PERMANENT_TIMEOUT,
			})
		} else if (this.helloResponseErrorCount === 5) {
			// Switch to a different message as several errors in a row in hello
			// responses indicate that the signaling server might be unable to
			// connect to Nextcloud.
			if (this.signalingConnectionError) {
				this.signalingConnectionError.hideToast()
			}
			this.signalingConnectionError = showError(t('spreed', 'Failed to establish signaling connection. Something might be wrong in the signaling server configuration'), {
				timeout: TOAST_PERMANENT_TIMEOUT,
			})
		}

		// TODO(fancycode): How should this be handled better?
		console.error('Could not connect to server', data)
		this.reconnect()
		return
	}

	this.helloResponseErrorCount = 0

	if (this.signalingConnectionError !== null) {
		this.signalingConnectionError.hideToast()
		this.signalingConnectionError = null
	}

	const resumedSession = !!this.resumeId
	this.connected = true
	if (this._forceReconnect && resumedSession) {
		console.info('Perform pending forced reconnect')
		this.forceReconnect(true)
		return
	}
	this.sessionId = data.hello.sessionid
	this.resumeId = data.hello.resumeid
	this.features = {}
	let i
	if (data.hello.server && data.hello.server.features) {
		const features = data.hello.server.features
		for (i = 0; i < features.length; i++) {
			this.features[features[i]] = true
		}
	}

	if (!this.hasFeature('audio-video-permissions') || !this.hasFeature('incall-all')) {
		showError(
			t('spreed', 'The configured signaling server needs to be updated to be compatible with this version of Talk. Please contact your administrator.'),
			{
				timeout: TOAST_PERMANENT_TIMEOUT,
			}
		)
		console.error('The configured signaling server needs to be updated to be compatible with this version of Talk. Please contact your administrator.')
	}

	const messages = this.pendingMessages
	this.pendingMessages = []
	for (i = 0; i < messages.length; i++) {
		const msg = messages[i][0]
		const callback = messages[i][1]
		this.doSend(msg, callback)
	}

	this._trigger('connect')
	if (!resumedSession && this.currentRoomToken && this.nextcloudSessionId) {
		this.joinRoom(this.currentRoomToken, this.nextcloudSessionId)
	}
}

Signaling.Standalone.prototype.joinRoom = function(token, sessionId) {
	this.ownSessionJoined = false

	if (!this.sessionId) {
		if (this._pendingJoinRoomPromise && this._pendingJoinRoomPromise.token === token) {
			return this._pendingJoinRoomPromise
		}

		if (this._pendingJoinRoomPromise) {
			this._pendingJoinRoomPromise.reject()
		}

		let pendingJoinRoomPromiseResolve
		let pendingJoinRoomPromiseReject
		this._pendingJoinRoomPromise = new Promise((resolve, reject) => {
			// The Promise executor is run even before the Promise constructor
			// has finished, so "this._pendingJoinRoomPromise" is not available
			// yet.
			pendingJoinRoomPromiseResolve = resolve
			pendingJoinRoomPromiseReject = reject
		})
		this._pendingJoinRoomPromise.resolve = pendingJoinRoomPromiseResolve
		this._pendingJoinRoomPromise.reject = pendingJoinRoomPromiseReject
		this._pendingJoinRoomPromise.token = token

		// If we would join without a connection to the signaling server here,
		// the room would be re-joined again in the "helloResponseReceived"
		// callback, leading to two entries for anonymous participants.
		console.info('Not connected to signaling server yet, defer joining room', token)
		this.currentRoomToken = token
		this.nextcloudSessionId = sessionId
		return this._pendingJoinRoomPromise
	}

	if (this._pendingJoinRoomPromise && this._pendingJoinRoomPromise.token !== token) {
		this._pendingJoinRoomPromise.reject()
		delete this._pendingJoinRoomPromise
	}

	if (!this._pendingJoinRoomPromise) {
		return Signaling.Base.prototype.joinRoom.apply(this, arguments)
	}

	const pendingJoinRoomPromise = this._pendingJoinRoomPromise
	delete this._pendingJoinRoomPromise

	Signaling.Base.prototype.joinRoom.apply(this, arguments)
		.then(() => { pendingJoinRoomPromise.resolve() })
		.catch(reason => { pendingJoinRoomPromise.reject(reason) })

	return pendingJoinRoomPromise
}

Signaling.Standalone.prototype._joinRoomSuccess = function(token, nextcloudSessionId) {
	if (!this.sessionId) {
		console.error('No hello response received yet, not joining room', token)
		return
	}

	console.debug('Join room', token)
	this.doSend({
		type: 'room',
		room: {
			roomid: token,
			// Pass the Nextcloud session id to the signaling server. The
			// session id will be passed through to Nextcloud to check if
			// the (Nextcloud) user is allowed to join the room.
			sessionid: nextcloudSessionId,
		},
	}, function(data) {
		this.joinResponseReceived(data, token)
	}.bind(this))
}

Signaling.Standalone.prototype.joinCall = function(token, flags) {
	if (this.signalingRoomJoined !== token) {
		console.debug('Not joined room yet, not joining call', token)

		if (this.pendingJoinCall && this.pendingJoinCall.token === token) {
			return this.pendingJoinCall.promise
		} else if (this.pendingJoinCall && this.pendingJoinCall.token !== token) {
			this.pendingJoinCall.reject(new Error('Pending join call canceled for ' + this.pendingJoinCall.token))
		}

		const promise = new Promise((resolve, reject) => {
			this.pendingJoinCall = {
				token,
				flags,
				resolve,
				reject,
			}
		})

		this.pendingJoinCall.promise = promise

		return this.pendingJoinCall.promise
	}

	return Signaling.Base.prototype.joinCall.apply(this, arguments)
}

Signaling.Standalone.prototype.joinResponseReceived = function(data, token) {
	console.debug('Joined', data, token)
	this.signalingRoomJoined = token
	if (this.pendingJoinCall && token === this.pendingJoinCall.token) {
		const pendingJoinCallResolve = this.pendingJoinCall.resolve
		const pendingJoinCallReject = this.pendingJoinCall.reject

		this.joinCall(this.pendingJoinCall.token, this.pendingJoinCall.flags).then(() => {
			pendingJoinCallResolve()
		}).catch(error => {
			pendingJoinCallReject(error)
		})

		this.pendingJoinCall = null
	}
	if (this.roomCollection) {
		// The list of rooms is not fetched from the server. Update ping
		// of joined room so it gets sorted to the top.
		this.roomCollection.forEach(function(room) {
			if (room.get('token') === token) {
				room.set('lastPing', (new Date()).getTime() / 1000)
			}
		})
		this.roomCollection.sort()
	}
}

Signaling.Standalone.prototype._doLeaveRoom = function(token) {
	console.debug('Leave room', token)
	this.doSend({
		type: 'room',
		room: {
			roomid: '',
		},
	}, function(data) {
		console.debug('Left', data)
		this.signalingRoomJoined = null
		// Any users we previously had in the room also "left" for us.
		const leftUsers = Object.keys(this.joinedUsers)
		if (leftUsers.length) {
			this._trigger('usersLeft', [leftUsers])
		}
		this.joinedUsers = {}
	}.bind(this))
}

Signaling.Standalone.prototype.processEvent = function(data) {
	switch (data.event.target) {
	case 'room':
		this.processRoomEvent(data)
		break
	case 'roomlist':
		this.processRoomListEvent(data)
		break
	case 'participants':
		this.processRoomParticipantsEvent(data)
		break
	default:
		console.error('Unsupported event target', data)
		break
	}
}

Signaling.Standalone.prototype.processRoomEvent = function(data) {
	let i
	let joinedUsers = []
	let leftSessionIds = []
	switch (data.event.type) {
	case 'join':
		joinedUsers = data.event.join || []
		if (joinedUsers.length) {
			console.debug('Users joined', joinedUsers)
			let leftUsers = {}
			if (this.reconnected) {
				this.reconnected = false
				// The browser reconnected, some of the previous sessions
				// may now no longer exist.
				leftUsers = Object.assign({}, this.joinedUsers)
			}

			let userListIsDirty = false
			for (i = 0; i < joinedUsers.length; i++) {
				this.joinedUsers[joinedUsers[i].sessionid] = true
				delete leftUsers[joinedUsers[i].sessionid]

				if (this.settings.userId && joinedUsers[i].userid === this.settings.userId) {
					if (joinedUsers[i].sessionid === this.sessionId) {
						// We are ignoring joins before we found our own message,
						// as otherwise you get the warning for your own old session immediately
						this.ownSessionJoined = true
					}
				} else {
					userListIsDirty = true
				}
			}
			leftUsers = Object.keys(leftUsers)
			if (leftUsers.length) {
				this._trigger('usersLeft', [leftUsers])

				for (i = 0; i < leftUsers.length; i++) {
					delete this.joinedUsers[leftUsers[i]]

					if (!this.settings.userId || leftUsers[i].userid !== this.settings.userId) {
						userListIsDirty = true
					}
				}
			}
			this._trigger('usersJoined', [joinedUsers])
			if (userListIsDirty) {
				this._trigger('participantListChanged')
			}
		}
		break
	case 'leave':
		leftSessionIds = data.event.leave || []
		if (leftSessionIds.length) {
			console.debug('Users left', leftSessionIds)
			for (i = 0; i < leftSessionIds.length; i++) {
				delete this.joinedUsers[leftSessionIds[i]]
			}
			this._trigger('usersLeft', [leftSessionIds])
			this._trigger('participantListChanged')
		}
		break
	case 'message':
		this.processRoomMessageEvent(data.event.message.data)
		break
	default:
		console.error('Unknown room event', data)
		break
	}
}

Signaling.Standalone.prototype.processRoomMessageEvent = function(data) {
	switch (data.type) {
	case 'chat':
		// FIXME this is not listened to
		EventBus.$emit('should-refresh-chat-messages')
		break
	default:
		console.error('Unknown room message event', data)
	}
}

Signaling.Standalone.prototype.processRoomListEvent = function(data) {
	switch (data.event.type) {
	case 'update':
		if (data.event.update.properties['participant-list']) {
			console.debug('Room list event for participant list', data)
			if (data.event.update.roomid === this.currentRoomToken) {
				this._trigger('participantListChanged')
			} else {
				// Participant list in another room changed, we don't really care
			}
			break
		}
		// eslint-disable-next-line no-fallthrough
	case 'disinvite':
		if (data.event?.disinvite?.roomid === this.currentRoomToken) {
			if (this._isRejoiningConversationWithNewSession) {
				console.debug('Rejoining conversation with new session, "disinvite" message ignored')
				return
			}
			console.error('User or session was removed from the conversation, redirecting')
			EventBus.$emit('deleted-session-detected')
			break
		}
		// eslint-disable-next-line no-fallthrough
	default:
		console.debug('Room list event', data)
		EventBus.$emit('should-refresh-conversations')
		break
	}
}

Signaling.Standalone.prototype.processRoomParticipantsEvent = function(data) {
	switch (data.event.type) {
	case 'update':
		if (data.event.update.all) {
			// With `"all": true`
			if (data.event.update.incall === 0) {
				this._trigger('allUsersChangedInCallToDisconnected')
			} else {
				console.error('Unknown room participant event', data)
			}
		} else {
			// With updated user list
			this._trigger('usersChanged', [data.event.update.users || []])
		}
		this._trigger('participantListChanged')
		break
	case 'flags':
		this._trigger('participantFlagsChanged', [data.event.flags || []])
		break
	default:
		console.error('Unknown room participant event', data)
		break
	}
}

Signaling.Standalone.prototype.requestOffer = function(sessionid, roomType, sid = undefined) {
	if (!this.hasFeature('mcu')) {
		console.warn("Can't request an offer without a MCU.")
		return
	}

	if (typeof (sessionid) !== 'string') {
		// Got a user object.
		sessionid = sessionid.sessionId || sessionid.sessionid
	}
	console.debug('Request offer from', sessionid, sid)
	this.doSend({
		type: 'message',
		message: {
			recipient: {
				type: 'session',
				sessionid,
			},
			data: {
				type: 'requestoffer',
				roomType,
				sid,
			},
		},
	})
}

Signaling.Standalone.prototype.sendOffer = function(sessionid, roomType) {
	// TODO(jojo): This should go away and "requestOffer" should be used
	// instead by peers that want an offer by the MCU. See the calling
	// location for further details.
	if (!this.hasFeature('mcu')) {
		console.warn("Can't send an offer without a MCU.")
		return
	}

	if (typeof (sessionid) !== 'string') {
		// Got a user object.
		sessionid = sessionid.sessionId || sessionid.sessionid
	}
	console.debug('Send offer to', sessionid)
	this.doSend({
		type: 'message',
		message: {
			recipient: {
				type: 'session',
				sessionid,
			},
			data: {
				type: 'sendoffer',
				roomType,
			},
		},
	})
}

export default Signaling

<MSG> Filter control messages not sent by moderators

The internal signaling server now filters signaling messages with type
"control" that are not sent by logged in moderators (including the
owner). This will be used, for example, to filter force mute messages
directly in the server, without needing to rely on the clients filtering
them.

The external signaling server already supports control messages,
although with a slightly different format (instead of being a subtype of
"message" messages they are their own "control" message). Therefore,
internally the WebUI uses the same format as the internal signaling
server for control messages and formats them as needed when sending them
to or receiving them from the external signaling server.

Signed-off-by: Daniel Calviño Sánchez <bd7629e975cc0e00ea7cda4ab6a120f2ee42ac1b@gmail.com>

<DFF> @@ -573,6 +573,7 @@ Signaling.Standalone.prototype.connect = function() {
 			cb(data)
 		}
 		this._trigger('onBeforeReceiveMessage', [data])
+		const message = {}
 		switch (data.type) {
 		case 'hello':
 			if (!id) {
@@ -597,6 +598,12 @@ Signaling.Standalone.prototype.connect = function() {
 			data.message.data.from = data.message.sender.sessionid
 			this._trigger('message', [data.message.data])
 			break
+		case 'control':
+			message.type = 'control'
+			message.payload = data.control.data
+			message.from = data.control.sender.sessionid
+			this._trigger('message', [message])
+			break
 		default:
 			if (!id) {
 				console.error('Ignore unknown event', data)
@@ -659,6 +666,21 @@ Signaling.Standalone.prototype.forceReconnect = function(newSession, flags) {
 }
 
 Signaling.Standalone.prototype.sendCallMessage = function(data) {
+	if (data.type === 'control') {
+		this.doSend({
+			'type': 'control',
+			'control': {
+				'recipient': {
+					'type': 'session',
+					'sessionid': data.to,
+				},
+				'data': data.payload,
+			},
+		})
+
+		return
+	}
+
 	this.doSend({
 		'type': 'message',
 		'message': {

{'commit': "diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php\nindex 77e746afc..eef6baeca 100644\n--- a/lib/Controller/SignalingController.php\n+++ b/lib/Controller/SignalingController.php\n@@ -133,6 +133,15 @@ class SignalingController extends OCSController {\n \t\t\t\t\t}\n \t\t\t\t\t$decodedMessage['from'] = $message['sessionId'];\n \n+\t\t\t\t\tif ($decodedMessage['type'] === 'control') {\n+\t\t\t\t\t\t$room = $this->manager->getRoomForSession($this->userId, $message['sessionId']);\n+\t\t\t\t\t\t$participant = $room->getParticipantBySession($message['sessionId']);\n+\n+\t\t\t\t\t\tif (!$participant->hasModeratorPermissions(false)) {\n+\t\t\t\t\t\t\tbreak;\n+\t\t\t\t\t\t}\n+\t\t\t\t\t}\n+\n \t\t\t\t\t$this->messages->addMessage($message['sessionId'], $decodedMessage['to'], json_encode($decodedMessage));\n \n \t\t\t\t\tbreak;\ndiff --git a/src/utils/signaling.js b/src/utils/signaling.js\nindex 11e04207f..1cd0dafbf 100644\n--- a/src/utils/signaling.js\n+++ b/src/utils/signaling.js\n@@ -573,6 +573,7 @@ Signaling.Standalone.prototype.connect = function() {\n \t\t\tcb(data)\n \t\t}\n \t\tthis._trigger('onBeforeReceiveMessage', [data])\n+\t\tconst message = {}\n \t\tswitch (data.type) {\n \t\tcase 'hello':\n \t\t\tif (!id) {\n@@ -597,6 +598,12 @@ Signaling.Standalone.prototype.connect = function() {\n \t\t\tdata.message.data.from = data.message.sender.sessionid\n \t\t\tthis._trigger('message', [data.message.data])\n \t\t\tbreak\n+\t\tcase 'control':\n+\t\t\tmessage.type = 'control'\n+\t\t\tmessage.payload = data.control.data\n+\t\t\tmessage.from = data.control.sender.sessionid\n+\t\t\tthis._trigger('message', [message])\n+\t\t\tbreak\n \t\tdefault:\n \t\t\tif (!id) {\n \t\t\t\tconsole.error('Ignore unknown event', data)\n@@ -659,6 +666,21 @@ Signaling.Standalone.prototype.forceReconnect = function(newSession, flags) {\n }\n \n Signaling.Standalone.prototype.sendCallMessage = function(data) {\n+\tif (data.type === 'control') {\n+\t\tthis.doSend({\n+\t\t\t'type': 'control',\n+\t\t\t'control': {\n+\t\t\t\t'recipient': {\n+\t\t\t\t\t'type': 'session',\n+\t\t\t\t\t'sessionid': data.to,\n+\t\t\t\t},\n+\t\t\t\t'data': data.payload,\n+\t\t\t},\n+\t\t})\n+\n+\t\treturn\n+\t}\n+\n \tthis.doSend({\n \t\t'type': 'message',\n \t\t'message': {\n", 'commit_subject': 'Filter control messages not sent by moderators', 'repo_name': 'nextcloud/spreed', 'committer': {'name': 'Daniel Calviño Sánchez', 'email': 'bd7629e975cc0e00ea7cda4ab6a120f2ee42ac1b@gmail.com', 'time_sec': '1583502804', 'tz_offset': '60', 'date': {'seconds': '1583502804'}}, 'commit_tree': 'ea784e1f5da963539176ebb91c9ea71846950ceb', 'commit_message': 'Filter control messages not sent by moderators\n\nThe internal signaling server now filters signaling messages with type\n"control" that are not sent by logged in moderators (including the\nowner). This will be used, for example, to filter force mute messages\ndirectly in the server, without needing to rely on the clients filtering\nthem.\n\nThe external signaling server already supports control messages,\nalthough with a slightly different format (instead of being a subtype of\n"message" messages they are their own "control" message). Therefore,\ninternally the WebUI uses the same format as the internal signaling\nserver for control messages and formats them as needed when sending them\nto or receiving them from the external signaling server.\n\nSigned-off-by: Daniel Calviño Sánchez <bd7629e975cc0e00ea7cda4ab6a120f2ee42ac1b@gmail.com>\n', 'language_name': 'Shell', 'license': 'agpl-3.0', 'hunks': "@@ -573,6 +573,7 @@ Signaling.Standalone.prototype.connect = function() {\n \t\t\tcb(data)\n \t\t}\n \t\tthis._trigger('onBeforeReceiveMessage', [data])\n+\t\tconst message = {}\n \t\tswitch (data.type) {\n \t\tcase 'hello':\n \t\t\tif (!id) {\n@@ -597,6 +598,12 @@ Signaling.Standalone.prototype.connect = function() {\n \t\t\tdata.message.data.from = data.message.sender.sessionid\n \t\t\tthis._trigger('message', [data.message.data])\n \t\t\tbreak\n+\t\tcase 'control':\n+\t\t\tmessage.type = 'control'\n+\t\t\tmessage.payload = data.control.data\n+\t\t\tmessage.from = data.control.sender.sessionid\n+\t\t\tthis._trigger('message', [message])\n+\t\t\tbreak\n \t\tdefault:\n \t\t\tif (!id) {\n \t\t\t\tconsole.error('Ignore unknown event', data)\n@@ -659,6 +666,21 @@ Signaling.Standalone.prototype.forceReconnect = function(newSession, flags) {\n }\n \n Signaling.Standalone.prototype.sendCallMessage = function(data) {\n+\tif (data.type === 'control') {\n+\t\tthis.doSend({\n+\t\t\t'type': 'control',\n+\t\t\t'control': {\n+\t\t\t\t'recipient': {\n+\t\t\t\t\t'type': 'session',\n+\t\t\t\t\t'sessionid': data.to,\n+\t\t\t\t},\n+\t\t\t\t'data': data.payload,\n+\t\t\t},\n+\t\t})\n+\n+\t\treturn\n+\t}\n+\n \tthis.doSend({\n \t\t'type': 'message',\n \t\t'message': {\n", 'addition_count': 22, 'deletion_count': 0, 'src_file': 'a/src/utils/signaling.js', 'tgt_file': 'b/src/utils/signaling.js', 'file_extension': '.js', 'before_file': '/**\n * @copyright Copyright (c) 2019 Daniel Calviño Sánchez <danxuliu@gmail.com>\n *\n * @copyright Copyright (c) 2019 Ivan Sein <ivan@nextcloud.com>\n *\n * @copyright Copyright (c) 2019 Joachim Bauch <bauch@struktur.de>\n *\n * @copyright Copyright (c) 2019 Joas Schilling <coding@schilljs.com>\n *\n * @author Daniel Calviño Sánchez <danxuliu@gmail.com>\n *\n * @author Ivan Sein <ivan@nextcloud.com>\n *\n * @author Joachim Bauch <bauch@struktur.de>\n *\n * @author Joas Schilling <coding@schilljs.com>\n *\n * @license AGPL-3.0-or-later\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program.  If not, see <http://www.gnu.org/licenses/>.\n *\n */\n\nimport { pullSignalingMessages } from \'../services/signalingService.js\'\nimport { rejoinConversation } from \'../services/participantsService.js\'\nimport CancelableRequest from \'./cancelableRequest.js\'\nimport { EventBus } from \'../services/EventBus.js\'\nimport axios from \'@nextcloud/axios\'\nimport {\n\tgenerateOcsUrl,\n\tgenerateUrl,\n} from \'@nextcloud/router\'\nimport {\n\tshowError,\n\tshowWarning,\n\tTOAST_PERMANENT_TIMEOUT,\n} from \'@nextcloud/dialogs\'\n\nconst Signaling = {\n\tBase: {},\n\tInternal: {},\n\tStandalone: {},\n\n\t/**\n\t * Creates a connection to the signaling server\n\t *\n\t * @param {object} settings The signaling settings\n\t * @return {Standalone|Internal}\n\t */\n\tcreateConnection(settings) {\n\t\tif (!settings) {\n\t\t\tconsole.error(\'Signaling settings are not given\')\n\t\t}\n\n\t\tif (settings.signalingMode !== \'internal\') {\n\t\t\treturn new Signaling.Standalone(settings, settings.server)\n\t\t} else {\n\t\t\treturn new Signaling.Internal(settings)\n\t\t}\n\t},\n}\n\n/**\n * @param {object} settings The signaling settings\n */\nfunction Base(settings) {\n\tthis.settings = settings\n\tthis.sessionId = \'\'\n\tthis.currentRoomToken = null\n\tthis.currentCallToken = null\n\tthis.currentCallFlags = null\n\tthis.nextcloudSessionId = null\n\tthis.handlers = {}\n\tthis.features = {}\n\tthis._sendVideoIfAvailable = true\n\tthis.signalingConnectionTimeout = null\n\tthis.signalingConnectionWarning = null\n\tthis.signalingConnectionError = null\n}\n\nSignaling.Base = Base\nSignaling.Base.prototype.on = function(ev, handler) {\n\tif (!Object.prototype.hasOwnProperty.call(this.handlers, ev)) {\n\t\tthis.handlers[ev] = [handler]\n\t} else {\n\t\tthis.handlers[ev].push(handler)\n\t}\n\n\tlet servers = []\n\tswitch (ev) {\n\tcase \'stunservers\':\n\tcase \'turnservers\':\n\t\tservers = this.settings[ev] || []\n\t\tif (servers.length) {\n\t\t\thandler(servers)\n\t\t}\n\t\tbreak\n\t}\n}\n\nSignaling.Base.prototype.off = function(ev, handler) {\n\tif (!Object.prototype.hasOwnProperty.call(this.handlers, ev)) {\n\t\treturn\n\t}\n\n\tlet pos = this.handlers[ev].indexOf(handler)\n\twhile (pos !== -1) {\n\t\tthis.handlers[ev].splice(pos, 1)\n\t\tpos = this.handlers[ev].indexOf(handler)\n\t}\n}\n\nSignaling.Base.prototype._trigger = function(ev, args) {\n\tlet handlers = this.handlers[ev]\n\n\tif (handlers) {\n\t\thandlers = handlers.slice(0)\n\t\tfor (let i = 0, len = handlers.length; i < len; i++) {\n\t\t\tconst handler = handlers[i]\n\t\t\thandler.apply(handler, args)\n\t\t}\n\t}\n\n\t// Convert webrtc event names to kebab-case for "vue/custom-event-name-casing"\n\tconst kebabCase = string => string\n\t\t.replace(/([a-z])([A-Z])/g, \'$1-$2\')\n\t\t.replace(/[\\s_]+/g, \'-\')\n\t\t.toLowerCase()\n\tEventBus.$emit(\'signaling-\' + kebabCase(ev), args)\n}\n\nSignaling.Base.prototype.isNoMcuWarningEnabled = function() {\n\treturn !this.settings.hideWarning\n}\n\nSignaling.Base.prototype.getSessionId = function() {\n\treturn this.sessionId\n}\n\nSignaling.Base.prototype.getCurrentCallFlags = function() {\n\treturn this.currentCallFlags\n}\n\nSignaling.Base.prototype.disconnect = function() {\n\tthis.sessionId = \'\'\n\tthis.currentCallToken = null\n\tthis.currentCallFlags = null\n}\n\nSignaling.Base.prototype.hasFeature = function(feature) {\n\treturn this.features && this.features[feature]\n}\n\nSignaling.Base.prototype.emit = function(ev, data) {\n\tswitch (ev) {\n\tcase \'joinRoom\':\n\t\tthis.joinRoom(data)\n\t\tbreak\n\tcase \'joinCall\':\n\t\tthis.joinCall(data, arguments[2])\n\t\tbreak\n\tcase \'leaveRoom\':\n\t\tthis.leaveCurrentRoom()\n\t\tbreak\n\tcase \'leaveCall\':\n\t\tthis.leaveCurrentCall()\n\t\tbreak\n\tcase \'message\':\n\t\tthis.sendCallMessage(data)\n\t\tbreak\n\t}\n}\n\nSignaling.Base.prototype.leaveCurrentRoom = function() {\n\tif (this.currentRoomToken) {\n\t\tthis.leaveRoom(this.currentRoomToken)\n\t\tthis.currentRoomToken = null\n\t\tthis.nextcloudSessionId = null\n\t}\n}\n\nSignaling.Base.prototype.updateCurrentCallFlags = function(flags) {\n\treturn new Promise((resolve, reject) => {\n\t\tif (this.currentCallToken) {\n\t\t\tthis.updateCallFlags(this.currentCallToken, flags).then(() => { resolve() }).catch(reason => { reject(reason) })\n\t\t} else {\n\t\t\tresolve()\n\t\t}\n\t})\n}\n\nSignaling.Base.prototype.leaveCurrentCall = function() {\n\treturn new Promise((resolve, reject) => {\n\t\tif (this.currentCallToken) {\n\t\t\tthis.leaveCall(this.currentCallToken).then(() => { resolve() }).catch(reason => { reject(reason) })\n\t\t\tthis.currentCallToken = null\n\t\t\tthis.currentCallFlags = null\n\t\t} else {\n\t\t\tresolve()\n\t\t}\n\t})\n}\n\nSignaling.Base.prototype.joinRoom = function(token, sessionId) {\n\treturn new Promise((resolve, reject) => {\n\t\tconsole.debug(\'Joined\')\n\t\tthis.currentRoomToken = token\n\t\tthis.nextcloudSessionId = sessionId\n\t\tthis._trigger(\'joinRoom\', [token])\n\t\tresolve()\n\t\tif (this.currentCallToken === token) {\n\t\t\t// We were in this call before, join again.\n\t\t\tthis.joinCall(token, this.currentCallFlags)\n\t\t} else {\n\t\t\tthis.currentCallToken = null\n\t\t\tthis.currentCallFlags = null\n\t\t}\n\t\tthis._joinRoomSuccess(token, sessionId)\n\t})\n}\n\nSignaling.Base.prototype._leaveRoomSuccess = function(/* token */) {\n\t// Override in subclasses if necessary.\n}\n\nSignaling.Base.prototype.leaveRoom = function(token) {\n\tthis.leaveCurrentCall()\n\t\t.then(() => {\n\t\t\tthis._trigger(\'leaveRoom\', [token])\n\t\t\tthis._doLeaveRoom(token)\n\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tthis._leaveRoomSuccess(token)\n\t\t\t\tresolve()\n\t\t\t\t// We left the current room.\n\t\t\t\tif (token === this.currentRoomToken) {\n\t\t\t\t\tthis.currentRoomToken = null\n\t\t\t\t\tthis.nextcloudSessionId = null\n\t\t\t\t}\n\t\t\t})\n\t\t})\n}\n\nSignaling.Base.prototype.getSendVideoIfAvailable = function() {\n\treturn this._sendVideoIfAvailable\n}\n\nSignaling.Base.prototype.setSendVideoIfAvailable = function(sendVideoIfAvailable) {\n\tthis._sendVideoIfAvailable = sendVideoIfAvailable\n}\n\nSignaling.Base.prototype._joinCallSuccess = function(/* token */) {\n\t// Override in subclasses if necessary.\n}\n\nSignaling.Base.prototype.joinCall = function(token, flags, silent) {\n\treturn new Promise((resolve, reject) => {\n\t\tthis._trigger(\'beforeJoinCall\', [token])\n\n\t\taxios.post(generateOcsUrl(\'apps/spreed/api/v4/call/{token}\', { token }), {\n\t\t\tflags,\n\t\t\tsilent,\n\t\t})\n\t\t\t.then(function() {\n\t\t\t\tthis.currentCallToken = token\n\t\t\t\tthis.currentCallFlags = flags\n\t\t\t\tthis._trigger(\'joinCall\', [token])\n\t\t\t\tresolve()\n\t\t\t\tthis._joinCallSuccess(token)\n\t\t\t}.bind(this))\n\t\t\t.catch(function() {\n\t\t\t\treject(new Error())\n\t\t\t\t// Server maintenance, lobby kicked in, or room not found.\n\t\t\t\t// We first redirect to the conversation again and that\n\t\t\t\t// will then show the proper error message to the user.\n\t\t\t\twindow.location = generateUrl(\'call/\' + token)\n\t\t\t})\n\t})\n}\n\nSignaling.Base.prototype._leaveCallSuccess = function(/* token */) {\n\t// Override in subclasses if necessary.\n}\n\nSignaling.Base.prototype.updateCallFlags = function(token, flags) {\n\treturn new Promise((resolve, reject) => {\n\t\tif (!token) {\n\t\t\treject(new Error())\n\t\t\treturn\n\t\t}\n\n\t\taxios.put(generateOcsUrl(\'apps/spreed/api/v4/call/{token}\', { token }), {\n\t\t\tflags,\n\t\t})\n\t\t\t.then(function() {\n\t\t\t\tthis.currentCallFlags = flags\n\t\t\t\tthis._trigger(\'updateCallFlags\', [token, flags])\n\t\t\t\tresolve()\n\t\t\t}.bind(this))\n\t\t\t.catch(function() {\n\t\t\t\treject(new Error())\n\t\t\t})\n\t})\n}\n\nSignaling.Base.prototype.leaveCall = function(token, keepToken, all = false) {\n\treturn new Promise((resolve, reject) => {\n\t\tif (!token) {\n\t\t\treject(new Error())\n\t\t\treturn\n\t\t}\n\n\t\tthis._trigger(\'beforeLeaveCall\', [token, keepToken])\n\n\t\taxios.delete(generateOcsUrl(\'apps/spreed/api/v4/call/{token}\', { token }), {\n\t\t\tdata: {\n\t\t\t\tall,\n\t\t\t},\n\t\t})\n\t\t\t.then(function() {\n\t\t\t\tthis._trigger(\'leaveCall\', [token, keepToken])\n\t\t\t\tthis._leaveCallSuccess(token)\n\t\t\t\tresolve()\n\t\t\t\t// We left the current call.\n\t\t\t\tif (!keepToken && token === this.currentCallToken) {\n\t\t\t\t\tthis.currentCallToken = null\n\t\t\t\t\tthis.currentCallFlags = null\n\t\t\t\t}\n\t\t\t}.bind(this))\n\t\t\t.catch(function() {\n\t\t\t\tthis._trigger(\'leaveCall\', [token, keepToken])\n\t\t\t\treject(new Error())\n\t\t\t\t// We left the current call.\n\t\t\t\tif (!keepToken && token === this.currentCallToken) {\n\t\t\t\t\tthis.currentCallToken = null\n\t\t\t\t\tthis.currentCallFlags = null\n\t\t\t\t}\n\t\t\t}.bind(this))\n\t})\n}\n\n// Connection to the internal signaling server provided by the app.\n/**\n * @param {object} settings The signaling settings\n */\nfunction Internal(settings) {\n\tSignaling.Base.prototype.constructor.apply(this, arguments)\n\tthis.hideWarning = settings.hideWarning\n\tthis.spreedArrayConnection = []\n\n\tthis.pullMessageErrorToast = null\n\tthis.pullMessagesFails = 0\n\tthis.pullMessagesRequest = null\n\n\tthis.isSendingMessages = false\n\tthis.sendInterval = window.setInterval(function() {\n\t\tthis.sendPendingMessages()\n\t}.bind(this), 500)\n\n\tthis._joinCallAgainOnceDisconnected = false\n}\n\nInternal.prototype = new Signaling.Base()\nInternal.prototype.constructor = Internal\nSignaling.Internal = Internal\n\nSignaling.Internal.prototype.disconnect = function() {\n\tthis.spreedArrayConnection = []\n\tif (this.sendInterval) {\n\t\twindow.clearInterval(this.sendInterval)\n\t\tthis.sendInterval = null\n\t}\n\tSignaling.Base.prototype.disconnect.apply(this, arguments)\n}\n\nSignaling.Internal.prototype.on = function(ev/*, handler */) {\n\tSignaling.Base.prototype.on.apply(this, arguments)\n\n\tswitch (ev) {\n\tcase \'connect\':\n\t\t// A connection is established if we can perform a request\n\t\t// through it.\n\t\tthis._sendMessageWithCallback(ev)\n\t\tbreak\n\t}\n}\n\nSignaling.Internal.prototype.forceReconnect = function(newSession, flags) {\n\tif (newSession) {\n\t\tconsole.warn(\'Forced reconnects with a new session are not supported in the internal signaling; same session as before will be used\')\n\t}\n\n\tif (flags !== undefined) {\n\t\tthis.currentCallFlags = flags\n\t}\n\n\t// FIXME Naive reconnection routine; as the same session is kept peers\n\t// must be explicitly ended before the reconnection is forced.\n\tthis.leaveCall(this.currentCallToken, true).then(() => {\n\t\tthis._joinCallAgainOnceDisconnected = true\n\t})\n}\n\nSignaling.Internal.prototype._sendMessageWithCallback = function(ev) {\n\tconst message = [{\n\t\tev,\n\t}]\n\n\tthis._sendMessages(message)\n\t\t.then(function(result) {\n\t\t\tthis._trigger(ev, [result.data.ocs.data])\n\t\t}.bind(this))\n\t\t.catch(function(err) {\n\t\t\tconsole.error(err)\n\t\t\tshowError(t(\'spreed\', \'Sending signaling message has failed.\'))\n\t\t})\n}\n\nSignaling.Internal.prototype._sendMessages = function(messages) {\n\treturn axios.post(generateOcsUrl(\'apps/spreed/api/v3/signaling/{token}\', { token: this.currentRoomToken }), {\n\t\tmessages: JSON.stringify(messages),\n\t})\n}\n\nSignaling.Internal.prototype._joinRoomSuccess = function(token, sessionId) {\n\tthis._joinCallAgainOnceDisconnected = false\n\n\tthis.sessionId = sessionId\n\tthis._startPullingMessages()\n}\n\nSignaling.Internal.prototype._doLeaveRoom = function(token) {\n\tthis._joinCallAgainOnceDisconnected = false\n}\n\nSignaling.Internal.prototype.sendCallMessage = function(data) {\n\tif (data.type === \'answer\') {\n\t\tconsole.debug(\'ANSWER\', data)\n\t} else if (data.type === \'offer\') {\n\t\tconsole.debug(\'OFFER\', data)\n\t}\n\tthis.spreedArrayConnection.push({\n\t\tev: \'message\',\n\t\tfn: JSON.stringify(data),\n\t\tsessionId: this.sessionId,\n\t})\n}\n\n/**\n * @private\n */\nSignaling.Internal.prototype._startPullingMessages = function() {\n\tconst token = this.currentRoomToken\n\tif (!token) {\n\t\treturn\n\t}\n\n\t// Abort ongoing request\n\tif (this.pullMessagesRequest !== null) {\n\t\tthis.pullMessagesRequest(\'canceled\')\n\t}\n\n\t// Connect to the messages endpoint and pull for new messages\n\tconst { request, cancel } = CancelableRequest(pullSignalingMessages)\n\tthis.pullMessagesRequest = cancel\n\trequest(token)\n\t\t.then(function(result) {\n\t\t\tthis.pullMessagesFails = 0\n\t\t\tif (this.pullMessageErrorToast) {\n\t\t\t\tthis.pullMessageErrorToast.hideToast()\n\t\t\t\tthis.pullMessageErrorToast = null\n\t\t\t}\n\n\t\t\tresult.data.ocs.data.forEach(message => {\n\t\t\t\tlet localParticipant\n\n\t\t\t\tthis._trigger(\'onBeforeReceiveMessage\', [message])\n\t\t\t\tswitch (message.type) {\n\t\t\t\tcase \'usersInRoom\':\n\t\t\t\t\tthis._trigger(\'usersInRoom\', [message.data])\n\t\t\t\t\tthis._trigger(\'participantListChanged\')\n\n\t\t\t\t\tlocalParticipant = message.data.find(participant => participant.sessionId === this.sessionId)\n\t\t\t\t\tif (this._joinCallAgainOnceDisconnected && !localParticipant.inCall) {\n\t\t\t\t\t\tthis._joinCallAgainOnceDisconnected = false\n\t\t\t\t\t\tthis.joinCall(this.currentCallToken, this.currentCallFlags)\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak\n\t\t\t\tcase \'message\':\n\t\t\t\t\tif (typeof (message.data) === \'string\') {\n\t\t\t\t\t\tmessage.data = JSON.parse(message.data)\n\t\t\t\t\t}\n\t\t\t\t\tthis._trigger(\'message\', [message.data])\n\t\t\t\t\tbreak\n\t\t\t\tdefault:\n\t\t\t\t\tconsole.error(\'Unknown Signaling Message\', message)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tthis._trigger(\'onAfterReceiveMessage\', [message])\n\t\t\t})\n\t\t\tthis._startPullingMessages()\n\t\t}.bind(this))\n\t\t.catch(function(error) {\n\t\t\tif (token !== this.currentRoomToken) {\n\t\t\t\t// User navigated away in the meantime. Ignore\n\t\t\t} else if (axios.isCancel(error)) {\n\t\t\t\tconsole.debug(\'Pulling messages request was cancelled\')\n\t\t\t} else if (error?.response?.status === 409) {\n\t\t\t\t// Participant joined a second time and this session was killed\n\t\t\t\tconsole.error(\'Session was killed but the conversation still exists\')\n\t\t\t\tthis._trigger(\'pullMessagesStoppedOnFail\')\n\n\t\t\t\tEventBus.$emit(\'duplicate-session-detected\')\n\t\t\t} else if (error?.response?.status === 404 || error?.response?.status === 403) {\n\t\t\t\t// Conversation was deleted or the user was removed\n\t\t\t\tconsole.error(\'Conversation was not found anymore\')\n\t\t\t\tEventBus.$emit(\'deleted-session-detected\')\n\t\t\t} else if (token) {\n\t\t\t\tif (this.pullMessagesFails === 1) {\n\t\t\t\t\tthis.pullMessageErrorToast = showError(t(\'spreed\', \'Lost connection to signaling server. Trying to reconnect.\'), {\n\t\t\t\t\t\ttimeout: TOAST_PERMANENT_TIMEOUT,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tif (this.pullMessagesFails === 30) {\n\t\t\t\t\tif (this.pullMessageErrorToast) {\n\t\t\t\t\t\tthis.pullMessageErrorToast.hideToast()\n\t\t\t\t\t}\n\n\t\t\t\t\t// Giving up after 5 minutes\n\t\t\t\t\tthis.pullMessageErrorToast = showError(t(\'spreed\', \'Lost connection to signaling server. Try to reload the page manually.\'), {\n\t\t\t\t\t\ttimeout: TOAST_PERMANENT_TIMEOUT,\n\t\t\t\t\t})\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tthis.pullMessagesFails++\n\t\t\t\t// Retry to pull messages after 10 seconds\n\t\t\t\twindow.setTimeout(function() {\n\t\t\t\t\tthis._startPullingMessages()\n\t\t\t\t}.bind(this), 10000)\n\t\t\t}\n\t\t}.bind(this))\n}\n\n/**\n * @private\n */\nSignaling.Internal.prototype.sendPendingMessages = function() {\n\tif (!this.spreedArrayConnection.length || this.isSendingMessages) {\n\t\treturn\n\t}\n\n\tconst pendingMessagesLength = this.spreedArrayConnection.length\n\tthis.isSendingMessages = true\n\n\tthis._sendMessages(this.spreedArrayConnection).then(function(/* result */) {\n\t\tthis.spreedArrayConnection.splice(0, pendingMessagesLength)\n\t\tthis.isSendingMessages = false\n\t}.bind(this)).catch(function(/* xhr, textStatus, errorThrown */) {\n\t\tconsole.error(\'Sending pending signaling messages has failed.\')\n\t\t\tcb(data)\n\t\t}\n\t\tthis._trigger(\'onBeforeReceiveMessage\', [data])\n\t\tswitch (data.type) {\n\t\tcase \'hello\':\n\t\t\tif (!id) {\n */\nfunction Standalone(settings, urls) {\n\tSignaling.Base.prototype.constructor.apply(this, arguments)\n\tif (typeof (urls) === \'string\') {\n\t\turls = [urls]\n\t}\n\t// We can connect to any of the servers.\n\tconst idx = Math.floor(Math.random() * urls.length)\n\t// TODO(jojo): Try other server if connection fails.\n\tlet url = urls[idx]\n\t// Make sure we are using websocket urls.\n\tif (url.startsWith(\'https://\')) {\n\t\turl = \'wss://\' + url.slice(8)\n\t} else if (url.startsWith(\'http://\')) {\n\t\turl = \'ws://\' + url.slice(7)\n\t}\n\tif (url.endsWith(\'/\')) {\n\t\turl = url.slice(0, -1)\n\t\t\tdata.message.data.from = data.message.sender.sessionid\n\t\t\tthis._trigger(\'message\', [data.message.data])\n\t\t\tbreak\n\t\tdefault:\n\t\t\tif (!id) {\n\t\t\t\tconsole.error(\'Ignore unknown event\', data)\n\nStandalone.prototype = new Signaling.Base()\nStandalone.prototype.constructor = Standalone\nSignaling.Standalone = Standalone\n\nSignaling.Standalone.prototype.reconnect = function() {\n\tif (this.reconnectTimer) {\n\t\treturn\n\t}\n\n\t// Wiggle interval a little bit to prevent all clients from connecting\n\t// simultaneously in case the server connection is interrupted.\n\tconst interval = this.reconnectIntervalMs - (this.reconnectIntervalMs / 2) + (this.reconnectIntervalMs * Math.random())\n\tconsole.info(\'Reconnect in\', interval)\n\tthis.reconnected = true\n\tthis.reconnectTimer = window.setTimeout(function() {\n\t\tthis.reconnectTimer = null\n\t\tthis.connect()\n\t}.bind(this), interval)\n\tthis.reconnectIntervalMs = this.reconnectIntervalMs * 2\n\tif (this.reconnectIntervalMs > this.maxReconnectIntervalMs) {\n\t\tthis.reconnectIntervalMs = this.maxReconnectIntervalMs\n\t}\n\tif (this.socket) {\n\t\tthis.socket.close()\n\t\tthis.socket = null\n\t}\n}\n\nSignaling.Standalone.prototype.connect = function() {\n\tif (this.signalingConnectionError === null\n\t\t&& this.signalingConnectionWarning === null) {\n\t\tthis.signalingConnectionTimeout = setTimeout(() => {\n\t\t\tthis.signalingConnectionWarning = showWarning(t(\'spreed\', \'Establishing signaling connection is taking longer than expected …\'), {\n\t\t\t\ttimeout: TOAST_PERMANENT_TIMEOUT,\n\t\t\t})\n\t\t}, 2000)\n\t}\n\n\tconsole.debug(\'Connecting to \' + this.url + \' for \' + this.settings.token)\n\tthis.callbacks = {}\n\tthis.id = 1\n\tthis.pendingMessages = []\n\tthis.connected = false\n\tthis._forceReconnect = false\n\tthis._isRejoiningConversationWithNewSession = false\n\tthis.socket = new WebSocket(this.url)\n\twindow.signalingSocket = this.socket\n\tthis.socket.onopen = function(event) {\n\t\tconsole.debug(\'Connected\', event)\n\t\tif (this.signalingConnectionTimeout !== null) {\n\t\t\tclearTimeout(this.signalingConnectionTimeout)\n\t\t\tthis.signalingConnectionTimeout = null\n\t\t}\n\t\tif (this.signalingConnectionWarning !== null) {\n\t\t\tthis.signalingConnectionWarning.hideToast()\n}\n\nSignaling.Standalone.prototype.sendCallMessage = function(data) {\n\tthis.doSend({\n\t\t\'type\': \'message\',\n\t\t\'message\': {\n\t\t\t\ttimeout: TOAST_PERMANENT_TIMEOUT,\n\t\t\t})\n\t\t}\n\t\tthis.reconnect()\n\t}.bind(this)\n\tthis.socket.onclose = function(event) {\n\t\tconsole.debug(\'Close\', event)\n\t\tif (this.signalingConnectionTimeout !== null) {\n\t\t\tclearTimeout(this.signalingConnectionTimeout)\n\t\t\tthis.signalingConnectionTimeout = null\n\t\t}\n\t\tif (this.signalingConnectionWarning !== null) {\n\t\t\tthis.signalingConnectionWarning.hideToast()\n\t\t\tthis.signalingConnectionWarning = null\n\t\t}\n\t\tif (event.code === 1001 && this.signalingConnectionError !== null) {\n\t\t\tthis.signalingConnectionError.hideToast()\n\t\t\tthis.signalingConnectionError = null\n\t\t}\n\t\tif (this.socket && event.code !== 1001) {\n\t\t\tconsole.debug(\'Reconnecting socket as the connection was closed unexpected\')\n\t\t\tthis.reconnect()\n\t\t}\n\t}.bind(this)\n\tthis.socket.onmessage = function(event) {\n\t\tlet data = event.data\n\t\tif (typeof (data) === \'string\') {\n\t\t\tdata = JSON.parse(data)\n\t\t}\n\t\tif (OC.debug) {\n\t\t\tconsole.debug(\'Received\', data)\n\t\t}\n\t\tconst id = data.id\n\t\tif (id && Object.prototype.hasOwnProperty.call(this.callbacks, id)) {\n\t\t\tconst cb = this.callbacks[id]\n\t\t\tdelete this.callbacks[id]\n\t\t\tcb(data)\n\t\t}\n\t\tthis._trigger(\'onBeforeReceiveMessage\', [data])\n\t\tconst message = {}\n\t\tswitch (data.type) {\n\t\tcase \'welcome\':\n\t\t\tthis.welcomeReceived(data)\n\t\t\tbreak\n\t\tcase \'hello\':\n\t\t\tif (!id) {\n\t\t\t\t// Only process if not received as result of our "hello".\n\t\t\t\tthis.helloResponseReceived(data)\n\t\t\t}\n\t\t\tbreak\n\t\tcase \'room\':\n\t\t\tif (this.currentRoomToken && data.room.roomid !== this.currentRoomToken) {\n\t\t\t\tthis._trigger(\'roomChanged\', [this.currentRoomToken, data.room.roomid])\n\t\t\t\tthis.joinedUsers = {}\n\t\t\t\tthis.currentRoomToken = null\n\t\t\t\tthis.nextcloudSessionId = null\n\t\t\t} else {\n\t\t\t\t// TODO(fancycode): Only fetch properties of room that was modified.\n\t\t\t\tEventBus.$emit(\'should-refresh-conversations\')\n\t\t\t}\n\t\t\tbreak\n\t\tcase \'event\':\n\t\t\tthis.processEvent(data)\n\t\t\tbreak\n\t\tcase \'message\':\n\t\t\tdata.message.data.from = data.message.sender.sessionid\n\t\t\tthis._trigger(\'message\', [data.message.data])\n\t\t\tbreak\n\t\tcase \'control\':\n\t\t\tmessage.type = \'control\'\n\t\t\tmessage.payload = data.control.data\n\t\t\tmessage.from = data.control.sender.sessionid\n\t\t\tthis._trigger(\'message\', [message])\n\t\t\tbreak\n\t\tcase \'error\':\n\t\t\tswitch (data.error.code) {\n\t\t\tcase \'processing_failed\':\n\t\t\t\tconsole.error(\'An error occurred processing the signaling message, please ask your server administrator to check the log file\')\n\t\t\t\tbreak\n\t\t\tcase \'token_expired\':\n\t\t\t\tconsole.info(\'The signaling token is expired, need to update settings\')\n\t\t\t\tthis._trigger(\'updateSettings\')\n\t\t\t\tbreak\n\t\t\tdefault:\n\t\t\t\tconsole.error(\'Ignore unknown error\', data)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tbreak\n\t\tdefault:\n\t\t\tif (!id) {\n\t\t\t\tconsole.error(\'Ignore unknown event\', data)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tthis._trigger(\'onAfterReceiveMessage\', [data])\n\t}.bind(this)\n}\n\nSignaling.Standalone.prototype.welcomeReceived = function(data) {\n\tconsole.debug(\'Welcome received\', data)\n\tif (this.waitForWelcomeTimeout !== null) {\n\t\tclearTimeout(this.waitForWelcomeTimeout)\n\t\tthis.waitForWelcomeTimeout = null\n\t}\n\n\tthis.features = {}\n\tlet i\n\tif (data.welcome && data.welcome.features) {\n\t\tconst features = data.welcome.features\n\t\tfor (i = 0; i < features.length; i++) {\n\t\t\tthis.features[features[i]] = true\n\t\t}\n\t}\n\n\tthis.sendHello()\n}\n\nSignaling.Standalone.prototype.welcomeTimeout = function() {\n\tconsole.warn(\'No welcome received, assuming old-style signaling server\')\n\tthis.sendHello()\n}\n\nSignaling.Standalone.prototype.sendBye = function() {\n\tif (this.connected) {\n\t\tthis.doSend({\n\t\t\ttype: \'bye\',\n\t\t\tbye: {},\n\t\t})\n\t}\n\tthis.resumeId = null\n\tthis.signalingRoomJoined = null\n}\n\nSignaling.Standalone.prototype.disconnect = function() {\n\tthis.sendBye()\n\tif (this.socket) {\n\t\tthis.socket.close()\n\t\tthis.socket = null\n\t}\n\tSignaling.Base.prototype.disconnect.apply(this, arguments)\n}\n\nSignaling.Standalone.prototype.forceReconnect = function(newSession, flags) {\n\tif (flags !== undefined) {\n\t\tthis.currentCallFlags = flags\n\t}\n\n\tif (!this.connected) {\n\t\tif (!newSession) {\n\t\t\t// Not connected, will do reconnect anyway.\n\t\t\treturn\n\t\t}\n\n\t\tthis._forceReconnect = true\n\t\tthis.resumeId = null\n\t\tthis.signalingRoomJoined = null\n\t\treturn\n\t}\n\n\tthis._forceReconnect = false\n\tif (newSession) {\n\t\tif (this.currentCallToken) {\n\t\t\t// Mark this session as "no longer in the call".\n\t\t\tthis.leaveCall(this.currentCallToken, true)\n\t\t}\n\n\t\tthis._isRejoiningConversationWithNewSession = true\n\n\t\trejoinConversation(this.currentRoomToken)\n\t\t\t.then(response => {\n\t\t\t\tthis.nextcloudSessionId = response.data.ocs.data.sessionId\n\n\t\t\t\tthis.sendBye()\n\t\t\t\tif (this.socket) {\n\t\t\t\t\t// Trigger reconnect.\n\t\t\t\t\tthis.socket.close()\n\t\t\t\t}\n\t\t\t})\n\t} else if (this.socket) {\n\t\t// Trigger reconnect.\n\t\tthis.socket.close()\n\t}\n}\n\nSignaling.Standalone.prototype.sendCallMessage = function(data) {\n\tif (data.type === \'control\') {\n\t\tthis.doSend({\n\t\t\ttype: \'control\',\n\t\t\tcontrol: {\n\t\t\t\trecipient: {\n\t\t\t\t\ttype: \'session\',\n\t\t\t\t\tsessionid: data.to,\n\t\t\t\t},\n\t\t\t\tdata: data.payload,\n\t\t\t},\n\t\t})\n\n\t\treturn\n\t}\n\n\tthis.doSend({\n\t\ttype: \'message\',\n\t\tmessage: {\n\t\t\trecipient: {\n\t\t\t\ttype: \'session\',\n\t\t\t\tsessionid: data.to,\n\t\t\t},\n\t\t\tdata,\n\t\t},\n\t})\n}\n\nSignaling.Standalone.prototype.sendRoomMessage = function(data) {\n\tif (!this.currentCallToken) {\n\t\tconsole.warn(\'Not in a room, not sending room message\', data)\n\t\treturn\n\t}\n\n\tthis.doSend({\n\t\ttype: \'message\',\n\t\tmessage: {\n\t\t\trecipient: {\n\t\t\t\ttype: \'room\',\n\t\t\t},\n\t\t\tdata,\n\t\t},\n\t})\n}\n\nSignaling.Standalone.prototype.doSend = function(msg, callback) {\n\tif ((!this.connected && msg.type !== \'hello\') || this.socket === null) {\n\t\t// Defer sending any messages until the hello response has been\n\t\t// received and when the socket is open\n\t\tthis.pendingMessages.push([msg, callback])\n\t\treturn\n\t}\n\n\tif (callback) {\n\t\tconst id = this.id++\n\t\tthis.callbacks[id] = callback\n\t\tmsg.id = \'\' + id\n\t}\n\tif (OC.debug) {\n\t\tconsole.debug(\'Sending\', msg)\n\t}\n\tthis.socket.send(JSON.stringify(msg))\n}\n\nSignaling.Standalone.prototype.sendHello = function() {\n\tlet msg\n\tif (this.resumeId) {\n\t\tconsole.debug(\'Trying to resume session\', this.sessionId)\n\t\tmsg = {\n\t\t\ttype: \'hello\',\n\t\t\thello: {\n\t\t\t\tversion: \'1.0\',\n\t\t\t\tresumeid: this.resumeId,\n\t\t\t},\n\t\t}\n\t} else {\n\t\t// Already reconnected with a new session.\n\t\tthis._forceReconnect = false\n\t\tconst url = generateOcsUrl(\'apps/spreed/api/v3/signaling/backend\')\n\t\tlet helloVersion\n\t\tif (this.hasFeature(\'hello-v2\') && this.settings.helloAuthParams[\'2.0\']) {\n\t\t\thelloVersion = \'2.0\'\n\t\t} else {\n\t\t\thelloVersion = \'1.0\'\n\t\t}\n\t\tmsg = {\n\t\t\ttype: \'hello\',\n\t\t\thello: {\n\t\t\t\tversion: helloVersion,\n\t\t\t\tauth: {\n\t\t\t\t\turl,\n\t\t\t\t\tparams: this.settings.helloAuthParams[helloVersion],\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\tthis.doSend(msg, this.helloResponseReceived.bind(this))\n}\n\nSignaling.Standalone.prototype.helloResponseReceived = function(data) {\n\tconsole.debug(\'Hello response received\', data)\n\tif (data.type !== \'hello\') {\n\t\tif (this.resumeId) {\n\t\t\t// Resuming the session failed, reconnect as new session.\n\t\t\tthis.resumeId = \'\'\n\t\t\tthis.sendHello()\n\t\t\treturn\n\t\t}\n\n\t\tthis.helloResponseErrorCount++\n\n\t\tif (this.signalingConnectionError === null && this.helloResponseErrorCount < 5) {\n\t\t\tthis.signalingConnectionError = showError(t(\'spreed\', \'Failed to establish signaling connection. Retrying …\'), {\n\t\t\t\ttimeout: TOAST_PERMANENT_TIMEOUT,\n\t\t\t})\n\t\t} else if (this.helloResponseErrorCount === 5) {\n\t\t\t// Switch to a different message as several errors in a row in hello\n\t\t\t// responses indicate that the signaling server might be unable to\n\t\t\t// connect to Nextcloud.\n\t\t\tif (this.signalingConnectionError) {\n\t\t\t\tthis.signalingConnectionError.hideToast()\n\t\t\t}\n\t\t\tthis.signalingConnectionError = showError(t(\'spreed\', \'Failed to establish signaling connection. Something might be wrong in the signaling server configuration\'), {\n\t\t\t\ttimeout: TOAST_PERMANENT_TIMEOUT,\n\t\t\t})\n\t\t}\n\n\t\t// TODO(fancycode): How should this be handled better?\n\t\tconsole.error(\'Could not connect to server\', data)\n\t\tthis.reconnect()\n\t\treturn\n\t}\n\n\tthis.helloResponseErrorCount = 0\n\n\tif (this.signalingConnectionError !== null) {\n\t\tthis.signalingConnectionError.hideToast()\n\t\tthis.signalingConnectionError = null\n\t}\n\n\tconst resumedSession = !!this.resumeId\n\tthis.connected = true\n\tif (this._forceReconnect && resumedSession) {\n\t\tconsole.info(\'Perform pending forced reconnect\')\n\t\tthis.forceReconnect(true)\n\t\treturn\n\t}\n\tthis.sessionId = data.hello.sessionid\n\tthis.resumeId = data.hello.resumeid\n\tthis.features = {}\n\tlet i\n\tif (data.hello.server && data.hello.server.features) {\n\t\tconst features = data.hello.server.features\n\t\tfor (i = 0; i < features.length; i++) {\n\t\t\tthis.features[features[i]] = true\n\t\t}\n\t}\n\n\tif (!this.hasFeature(\'audio-video-permissions\') || !this.hasFeature(\'incall-all\')) {\n\t\tshowError(\n\t\t\tt(\'spreed\', \'The configured signaling server needs to be updated to be compatible with this version of Talk. Please contact your administrator.\'),\n\t\t\t{\n\t\t\t\ttimeout: TOAST_PERMANENT_TIMEOUT,\n\t\t\t}\n\t\t)\n\t\tconsole.error(\'The configured signaling server needs to be updated to be compatible with this version of Talk. Please contact your administrator.\')\n\t}\n\n\tconst messages = this.pendingMessages\n\tthis.pendingMessages = []\n\tfor (i = 0; i < messages.length; i++) {\n\t\tconst msg = messages[i][0]\n\t\tconst callback = messages[i][1]\n\t\tthis.doSend(msg, callback)\n\t}\n\n\tthis._trigger(\'connect\')\n\tif (!resumedSession && this.currentRoomToken && this.nextcloudSessionId) {\n\t\tthis.joinRoom(this.currentRoomToken, this.nextcloudSessionId)\n\t}\n}\n\nSignaling.Standalone.prototype.joinRoom = function(token, sessionId) {\n\tthis.ownSessionJoined = false\n\n\tif (!this.sessionId) {\n\t\tif (this._pendingJoinRoomPromise && this._pendingJoinRoomPromise.token === token) {\n\t\t\treturn this._pendingJoinRoomPromise\n\t\t}\n\n\t\tif (this._pendingJoinRoomPromise) {\n\t\t\tthis._pendingJoinRoomPromise.reject()\n\t\t}\n\n\t\tlet pendingJoinRoomPromiseResolve\n\t\tlet pendingJoinRoomPromiseReject\n\t\tthis._pendingJoinRoomPromise = new Promise((resolve, reject) => {\n\t\t\t// The Promise executor is run even before the Promise constructor\n\t\t\t// has finished, so "this._pendingJoinRoomPromise" is not available\n\t\t\t// yet.\n\t\t\tpendingJoinRoomPromiseResolve = resolve\n\t\t\tpendingJoinRoomPromiseReject = reject\n\t\t})\n\t\tthis._pendingJoinRoomPromise.resolve = pendingJoinRoomPromiseResolve\n\t\tthis._pendingJoinRoomPromise.reject = pendingJoinRoomPromiseReject\n\t\tthis._pendingJoinRoomPromise.token = token\n\n\t\t// If we would join without a connection to the signaling server here,\n\t\t// the room would be re-joined again in the "helloResponseReceived"\n\t\t// callback, leading to two entries for anonymous participants.\n\t\tconsole.info(\'Not connected to signaling server yet, defer joining room\', token)\n\t\tthis.currentRoomToken = token\n\t\tthis.nextcloudSessionId = sessionId\n\t\treturn this._pendingJoinRoomPromise\n\t}\n\n\tif (this._pendingJoinRoomPromise && this._pendingJoinRoomPromise.token !== token) {\n\t\tthis._pendingJoinRoomPromise.reject()\n\t\tdelete this._pendingJoinRoomPromise\n\t}\n\n\tif (!this._pendingJoinRoomPromise) {\n\t\treturn Signaling.Base.prototype.joinRoom.apply(this, arguments)\n\t}\n\n\tconst pendingJoinRoomPromise = this._pendingJoinRoomPromise\n\tdelete this._pendingJoinRoomPromise\n\n\tSignaling.Base.prototype.joinRoom.apply(this, arguments)\n\t\t.then(() => { pendingJoinRoomPromise.resolve() })\n\t\t.catch(reason => { pendingJoinRoomPromise.reject(reason) })\n\n\treturn pendingJoinRoomPromise\n}\n\nSignaling.Standalone.prototype._joinRoomSuccess = function(token, nextcloudSessionId) {\n\tif (!this.sessionId) {\n\t\tconsole.error(\'No hello response received yet, not joining room\', token)\n\t\treturn\n\t}\n\n\tconsole.debug(\'Join room\', token)\n\tthis.doSend({\n\t\ttype: \'room\',\n\t\troom: {\n\t\t\troomid: token,\n\t\t\t// Pass the Nextcloud session id to the signaling server. The\n\t\t\t// session id will be passed through to Nextcloud to check if\n\t\t\t// the (Nextcloud) user is allowed to join the room.\n\t\t\tsessionid: nextcloudSessionId,\n\t\t},\n\t}, function(data) {\n\t\tthis.joinResponseReceived(data, token)\n\t}.bind(this))\n}\n\nSignaling.Standalone.prototype.joinCall = function(token, flags) {\n\tif (this.signalingRoomJoined !== token) {\n\t\tconsole.debug(\'Not joined room yet, not joining call\', token)\n\n\t\tif (this.pendingJoinCall && this.pendingJoinCall.token === token) {\n\t\t\treturn this.pendingJoinCall.promise\n\t\t} else if (this.pendingJoinCall && this.pendingJoinCall.token !== token) {\n\t\t\tthis.pendingJoinCall.reject(new Error(\'Pending join call canceled for \' + this.pendingJoinCall.token))\n\t\t}\n\n\t\tconst promise = new Promise((resolve, reject) => {\n\t\t\tthis.pendingJoinCall = {\n\t\t\t\ttoken,\n\t\t\t\tflags,\n\t\t\t\tresolve,\n\t\t\t\treject,\n\t\t\t}\n\t\t})\n\n\t\tthis.pendingJoinCall.promise = promise\n\n\t\treturn this.pendingJoinCall.promise\n\t}\n\n\treturn Signaling.Base.prototype.joinCall.apply(this, arguments)\n}\n\nSignaling.Standalone.prototype.joinResponseReceived = function(data, token) {\n\tconsole.debug(\'Joined\', data, token)\n\tthis.signalingRoomJoined = token\n\tif (this.pendingJoinCall && token === this.pendingJoinCall.token) {\n\t\tconst pendingJoinCallResolve = this.pendingJoinCall.resolve\n\t\tconst pendingJoinCallReject = this.pendingJoinCall.reject\n\n\t\tthis.joinCall(this.pendingJoinCall.token, this.pendingJoinCall.flags).then(() => {\n\t\t\tpendingJoinCallResolve()\n\t\t}).catch(error => {\n\t\t\tpendingJoinCallReject(error)\n\t\t})\n\n\t\tthis.pendingJoinCall = null\n\t}\n\tif (this.roomCollection) {\n\t\t// The list of rooms is not fetched from the server. Update ping\n\t\t// of joined room so it gets sorted to the top.\n\t\tthis.roomCollection.forEach(function(room) {\n\t\t\tif (room.get(\'token\') === token) {\n\t\t\t\troom.set(\'lastPing\', (new Date()).getTime() / 1000)\n\t\t\t}\n\t\t})\n\t\tthis.roomCollection.sort()\n\t}\n}\n\nSignaling.Standalone.prototype._doLeaveRoom = function(token) {\n\tconsole.debug(\'Leave room\', token)\n\tthis.doSend({\n\t\ttype: \'room\',\n\t\troom: {\n\t\t\troomid: \'\',\n\t\t},\n\t}, function(data) {\n\t\tconsole.debug(\'Left\', data)\n\t\tthis.signalingRoomJoined = null\n\t\t// Any users we previously had in the room also "left" for us.\n\t\tconst leftUsers = Object.keys(this.joinedUsers)\n\t\tif (leftUsers.length) {\n\t\t\tthis._trigger(\'usersLeft\', [leftUsers])\n\t\t}\n\t\tthis.joinedUsers = {}\n\t}.bind(this))\n}\n\nSignaling.Standalone.prototype.processEvent = function(data) {\n\tswitch (data.event.target) {\n\tcase \'room\':\n\t\tthis.processRoomEvent(data)\n\t\tbreak\n\tcase \'roomlist\':\n\t\tthis.processRoomListEvent(data)\n\t\tbreak\n\tcase \'participants\':\n\t\tthis.processRoomParticipantsEvent(data)\n\t\tbreak\n\tdefault:\n\t\tconsole.error(\'Unsupported event target\', data)\n\t\tbreak\n\t}\n}\n\nSignaling.Standalone.prototype.processRoomEvent = function(data) {\n\tlet i\n\tlet joinedUsers = []\n\tlet leftSessionIds = []\n\tswitch (data.event.type) {\n\tcase \'join\':\n\t\tjoinedUsers = data.event.join || []\n\t\tif (joinedUsers.length) {\n\t\t\tconsole.debug(\'Users joined\', joinedUsers)\n\t\t\tlet leftUsers = {}\n\t\t\tif (this.reconnected) {\n\t\t\t\tthis.reconnected = false\n\t\t\t\t// The browser reconnected, some of the previous sessions\n\t\t\t\t// may now no longer exist.\n\t\t\t\tleftUsers = Object.assign({}, this.joinedUsers)\n\t\t\t}\n\n\t\t\tlet userListIsDirty = false\n\t\t\tfor (i = 0; i < joinedUsers.length; i++) {\n\t\t\t\tthis.joinedUsers[joinedUsers[i].sessionid] = true\n\t\t\t\tdelete leftUsers[joinedUsers[i].sessionid]\n\n\t\t\t\tif (this.settings.userId && joinedUsers[i].userid === this.settings.userId) {\n\t\t\t\t\tif (joinedUsers[i].sessionid === this.sessionId) {\n\t\t\t\t\t\t// We are ignoring joins before we found our own message,\n\t\t\t\t\t\t// as otherwise you get the warning for your own old session immediately\n\t\t\t\t\t\tthis.ownSessionJoined = true\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tuserListIsDirty = true\n\t\t\t\t}\n\t\t\t}\n\t\t\tleftUsers = Object.keys(leftUsers)\n\t\t\tif (leftUsers.length) {\n\t\t\t\tthis._trigger(\'usersLeft\', [leftUsers])\n\n\t\t\t\tfor (i = 0; i < leftUsers.length; i++) {\n\t\t\t\t\tdelete this.joinedUsers[leftUsers[i]]\n\n\t\t\t\t\tif (!this.settings.userId || leftUsers[i].userid !== this.settings.userId) {\n\t\t\t\t\t\tuserListIsDirty = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis._trigger(\'usersJoined\', [joinedUsers])\n\t\t\tif (userListIsDirty) {\n\t\t\t\tthis._trigger(\'participantListChanged\')\n\t\t\t}\n\t\t}\n\t\tbreak\n\tcase \'leave\':\n\t\tleftSessionIds = data.event.leave || []\n\t\tif (leftSessionIds.length) {\n\t\t\tconsole.debug(\'Users left\', leftSessionIds)\n\t\t\tfor (i = 0; i < leftSessionIds.length; i++) {\n\t\t\t\tdelete this.joinedUsers[leftSessionIds[i]]\n\t\t\t}\n\t\t\tthis._trigger(\'usersLeft\', [leftSessionIds])\n\t\t\tthis._trigger(\'participantListChanged\')\n\t\t}\n\t\tbreak\n\tcase \'message\':\n\t\tthis.processRoomMessageEvent(data.event.message.data)\n\t\tbreak\n\tdefault:\n\t\tconsole.error(\'Unknown room event\', data)\n\t\tbreak\n\t}\n}\n\nSignaling.Standalone.prototype.processRoomMessageEvent = function(data) {\n\tswitch (data.type) {\n\tcase \'chat\':\n\t\t// FIXME this is not listened to\n\t\tEventBus.$emit(\'should-refresh-chat-messages\')\n\t\tbreak\n\tdefault:\n\t\tconsole.error(\'Unknown room message event\', data)\n\t}\n}\n\nSignaling.Standalone.prototype.processRoomListEvent = function(data) {\n\tswitch (data.event.type) {\n\tcase \'update\':\n\t\tif (data.event.update.properties[\'participant-list\']) {\n\t\t\tconsole.debug(\'Room list event for participant list\', data)\n\t\t\tif (data.event.update.roomid === this.currentRoomToken) {\n\t\t\t\tthis._trigger(\'participantListChanged\')\n\t\t\t} else {\n\t\t\t\t// Participant list in another room changed, we don\'t really care\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\t// eslint-disable-next-line no-fallthrough\n\tcase \'disinvite\':\n\t\tif (data.event?.disinvite?.roomid === this.currentRoomToken) {\n\t\t\tif (this._isRejoiningConversationWithNewSession) {\n\t\t\t\tconsole.debug(\'Rejoining conversation with new session, "disinvite" message ignored\')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconsole.error(\'User or session was removed from the conversation, redirecting\')\n\t\t\tEventBus.$emit(\'deleted-session-detected\')\n\t\t\tbreak\n\t\t}\n\t\t// eslint-disable-next-line no-fallthrough\n\tdefault:\n\t\tconsole.debug(\'Room list event\', data)\n\t\tEventBus.$emit(\'should-refresh-conversations\')\n\t\tbreak\n\t}\n}\n\nSignaling.Standalone.prototype.processRoomParticipantsEvent = function(data) {\n\tswitch (data.event.type) {\n\tcase \'update\':\n\t\tif (data.event.update.all) {\n\t\t\t// With `"all": true`\n\t\t\tif (data.event.update.incall === 0) {\n\t\t\t\tthis._trigger(\'allUsersChangedInCallToDisconnected\')\n\t\t\t} else {\n\t\t\t\tconsole.error(\'Unknown room participant event\', data)\n\t\t\t}\n\t\t} else {\n\t\t\t// With updated user list\n\t\t\tthis._trigger(\'usersChanged\', [data.event.update.users || []])\n\t\t}\n\t\tthis._trigger(\'participantListChanged\')\n\t\tbreak\n\tcase \'flags\':\n\t\tthis._trigger(\'participantFlagsChanged\', [data.event.flags || []])\n\t\tbreak\n\tdefault:\n\t\tconsole.error(\'Unknown room participant event\', data)\n\t\tbreak\n\t}\n}\n\nSignaling.Standalone.prototype.requestOffer = function(sessionid, roomType, sid = undefined) {\n\tif (!this.hasFeature(\'mcu\')) {\n\t\tconsole.warn("Can\'t request an offer without a MCU.")\n\t\treturn\n\t}\n\n\tif (typeof (sessionid) !== \'string\') {\n\t\t// Got a user object.\n\t\tsessionid = sessionid.sessionId || sessionid.sessionid\n\t}\n\tconsole.debug(\'Request offer from\', sessionid, sid)\n\tthis.doSend({\n\t\ttype: \'message\',\n\t\tmessage: {\n\t\t\trecipient: {\n\t\t\t\ttype: \'session\',\n\t\t\t\tsessionid,\n\t\t\t},\n\t\t\tdata: {\n\t\t\t\ttype: \'requestoffer\',\n\t\t\t\troomType,\n\t\t\t\tsid,\n\t\t\t},\n\t\t},\n\t})\n}\n\nSignaling.Standalone.prototype.sendOffer = function(sessionid, roomType) {\n\t// TODO(jojo): This should go away and "requestOffer" should be used\n\t// instead by peers that want an offer by the MCU. See the calling\n\t// location for further details.\n\tif (!this.hasFeature(\'mcu\')) {\n\t\tconsole.warn("Can\'t send an offer without a MCU.")\n\t\treturn\n\t}\n\n\tif (typeof (sessionid) !== \'string\') {\n\t\t// Got a user object.\n\t\tsessionid = sessionid.sessionId || sessionid.sessionid\n\t}\n\tconsole.debug(\'Send offer to\', sessionid)\n\tthis.doSend({\n\t\ttype: \'message\',\n\t\tmessage: {\n\t\t\trecipient: {\n\t\t\t\ttype: \'session\',\n\t\t\t\tsessionid,\n\t\t\t},\n\t\t\tdata: {\n\t\t\t\ttype: \'sendoffer\',\n\t\t\t\troomType,\n\t\t\t},\n\t\t},\n\t})\n}\n\nexport default Signaling\n', 'message': 'Filter control messages not sent by moderators\n\nThe internal signaling server now filters signaling messages with type\n"control" that are not sent by logged in moderators (including the\nowner). This will be used, for example, to filter force mute messages\ndirectly in the server, without needing to rely on the clients filtering\nthem.\n\nThe external signaling server already supports control messages,\nalthough with a slightly different format (instead of being a subtype of\n"message" messages they are their own "control" message). Therefore,\ninternally the WebUI uses the same format as the internal signaling\nserver for control messages and formats them as needed when sending them\nto or receiving them from the external signaling server.\n\nSigned-off-by: Daniel Calviño Sánchez <bd7629e975cc0e00ea7cda4ab6a120f2ee42ac1b@gmail.com>\n', 'author_name': 'Daniel Calviño Sánchez', 'lang': 'js'}