[mirotalk] - #285 isolate mattermost integration from host auth systems
This commit is contained in:
@@ -158,6 +158,7 @@ MATTERMOST_SERVER_URL=YourMattermostServerUrl
|
||||
MATTERMOST_USERNAME=YourMattermostUsername
|
||||
MATTERMOST_PASSWORD=YourMattermostPassword
|
||||
MATTERMOST_TOKEN=YourMettarmostToken
|
||||
MATTERMOST_ROOM_TOKEN_EXPIRE=15m
|
||||
|
||||
# ChatGPT/OpenAI
|
||||
# 1. Goto https://platform.openai.com/
|
||||
|
||||
@@ -8,7 +8,7 @@ module.exports = {
|
||||
app: {
|
||||
language: 'en', // https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
|
||||
name: 'MiroTalk',
|
||||
title: '<h1>MiroTalk</h1><br />Free browser based Real-time video calls.<br />Simple, Secure, Fast.',
|
||||
title: '<h1>MiroTalk</h1>Free browser based Real-time video calls.<br />Simple, Secure, Fast.',
|
||||
description:
|
||||
'Start your next video call with a single click. No download, plug-in, or login is required. Just get straight to talking, messaging, and sharing your screen.',
|
||||
joinDescription: 'Pick a room name.<br />How about this one?',
|
||||
@@ -65,7 +65,7 @@ module.exports = {
|
||||
href="https://www.linkedin.com/in/miroslav-pejic-976a07101/" target="_blank">
|
||||
Miroslav Pejic
|
||||
</a>
|
||||
<br /><br />
|
||||
<br />
|
||||
Email:<a
|
||||
id="email-button"
|
||||
data-umami-event="Email button"
|
||||
|
||||
+127
-39
@@ -1,78 +1,166 @@
|
||||
'use strict';
|
||||
|
||||
const { Client4 } = require('@mattermost/client');
|
||||
|
||||
const { v4: uuidV4 } = require('uuid');
|
||||
|
||||
const TokenManager = require('./tokenManager');
|
||||
const Logger = require('./logs');
|
||||
|
||||
const log = new Logger('Mattermost');
|
||||
|
||||
class mattermost {
|
||||
constructor(app, mattermostCfg) {
|
||||
if (!this.isConfigValid(mattermostCfg)) return;
|
||||
class TokenService {
|
||||
constructor(secret, expiresIn, encryptionKey) {
|
||||
this.tokenManager = new TokenManager(secret, expiresIn, encryptionKey);
|
||||
}
|
||||
|
||||
this.app = app;
|
||||
this.disabled = mattermostCfg.api_disabled;
|
||||
this.token = mattermostCfg.token;
|
||||
this.serverUrl = mattermostCfg.server_url;
|
||||
this.username = mattermostCfg.username;
|
||||
this.password = mattermostCfg.password;
|
||||
createToken(payload) {
|
||||
return this.tokenManager.create(payload, true);
|
||||
}
|
||||
|
||||
decodeToken(token) {
|
||||
return this.tokenManager.decodePayload(token);
|
||||
}
|
||||
}
|
||||
|
||||
class MattermostService {
|
||||
constructor(config) {
|
||||
this.validateConfig(config);
|
||||
|
||||
this.token = config.token;
|
||||
this.serverUrl = config.server_url;
|
||||
this.username = config.username;
|
||||
this.password = config.password;
|
||||
this.disabledEndpoints = config.api_disabled || [];
|
||||
|
||||
this.client = new Client4();
|
||||
this.client.setUrl(this.serverUrl);
|
||||
this.authenticate();
|
||||
this.setupEventHandlers();
|
||||
|
||||
this.tokenService = new TokenService(
|
||||
this.token || 'fallback-secret-at-least-32-chars',
|
||||
config.roomTokenExpire || '15m',
|
||||
config.encryptionKey || 'fallback-encryption-key-32chars'
|
||||
);
|
||||
}
|
||||
|
||||
isConfigValid(config) {
|
||||
return config.enabled && config.server_url && config.token && config.username && config.password;
|
||||
validateConfig(config) {
|
||||
if (!config.enabled || !config.server_url || !config.token || !config.username || !config.password) {
|
||||
throw new Error('Invalid Mattermost configuration');
|
||||
}
|
||||
}
|
||||
|
||||
async authenticate() {
|
||||
try {
|
||||
const user = await this.client.login(this.username, this.password);
|
||||
log.debug('--------> Logged into Mattermost as', user.username);
|
||||
log.debug('Logged into Mattermost as', user.username);
|
||||
} catch (error) {
|
||||
log.error('Failed to log into Mattermost:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setupEventHandlers() {
|
||||
isEndpointDisabled(endpoint) {
|
||||
return this.disabledEndpoints.includes(endpoint);
|
||||
}
|
||||
|
||||
createMeetingToken(userId) {
|
||||
const payload = {
|
||||
userId,
|
||||
roomId: uuidV4(),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const token = this.tokenService.createToken(payload);
|
||||
return { token, payload };
|
||||
}
|
||||
|
||||
validateToken(token) {
|
||||
return this.tokenService.decodeToken(token);
|
||||
}
|
||||
|
||||
getMeetingURL(req, roomToken) {
|
||||
const host = req.headers.host;
|
||||
const protocol = host.includes('localhost') ? 'http' : 'https';
|
||||
return `${protocol}://${host}/mattermost/join/${encodeURIComponent(roomToken)}`;
|
||||
}
|
||||
}
|
||||
|
||||
class MattermostController {
|
||||
constructor(app, mattermostCfg, htmlInjector, clientHtml) {
|
||||
try {
|
||||
this.service = new MattermostService(mattermostCfg);
|
||||
} catch (error) {
|
||||
log.error('MattermostController disabled due to config error:', error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.htmlInjector = htmlInjector;
|
||||
this.clientHtml = clientHtml;
|
||||
this.app = app;
|
||||
this.token = mattermostCfg.token;
|
||||
|
||||
this.service.authenticate();
|
||||
this.setupRoutes();
|
||||
}
|
||||
|
||||
setupRoutes() {
|
||||
this.app.post('/mattermost', (req, res) => {
|
||||
// Check if endpoint allowed
|
||||
if (this.disabled.includes('mattermost')) {
|
||||
return res.end(
|
||||
'`This endpoint has been disabled`. Please contact the administrator for further information.',
|
||||
);
|
||||
if (this.service.isEndpointDisabled('mattermost')) {
|
||||
return res.end('`This endpoint has been disabled`. Please contact the administrator.');
|
||||
}
|
||||
|
||||
// Validate the token
|
||||
const { token, text, command, channel_id } = req.body;
|
||||
const { token, text, command, channel_id, user_id } = req.body;
|
||||
|
||||
if (token !== this.token) {
|
||||
log.error('Invalid token attempt', { token });
|
||||
return res.status(403).send('Invalid token');
|
||||
}
|
||||
|
||||
// Check if the command (slash-commands) or text (outgoing-webhook) matches "/p2p"
|
||||
if (command.trim() === '/p2p' || text.trim() === '/p2p') {
|
||||
const meetingUrl = this.getMeetingURL(req);
|
||||
return res.json({
|
||||
text: `Here is your meeting room: ${meetingUrl}`,
|
||||
channel_id: channel_id,
|
||||
});
|
||||
if (command?.trim() === '/p2p' || text?.trim() === '/p2p') {
|
||||
try {
|
||||
const { token: roomToken } = this.service.createMeetingToken(user_id);
|
||||
const meetingUrl = this.service.getMeetingURL(req, roomToken);
|
||||
|
||||
return res.json({
|
||||
response_type: 'in_channel',
|
||||
text: `🔗 [Click here to join your private meeting](${meetingUrl})`,
|
||||
channel_id,
|
||||
});
|
||||
} catch (error) {
|
||||
log.error('Token creation failed', error);
|
||||
return res.status(500).send('Error creating meeting');
|
||||
}
|
||||
}
|
||||
|
||||
// If the command is not recognized
|
||||
return res.status(404).send('Command not recognized');
|
||||
});
|
||||
}
|
||||
|
||||
getMeetingURL(req) {
|
||||
const host = req.headers.host;
|
||||
const protocol = host.includes('localhost') ? 'http' : 'https';
|
||||
return `${protocol}://${host}/join/${uuidV4()}`;
|
||||
this.app.get('/mattermost/join/:roomToken', (req, res) => {
|
||||
if (this.service.isEndpointDisabled('mattermost')) {
|
||||
return res.end('This endpoint has been disabled');
|
||||
}
|
||||
|
||||
const { roomToken } = req.params;
|
||||
|
||||
if (!roomToken) {
|
||||
return res.status(401).send('Token required');
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = this.service.validateToken(roomToken);
|
||||
log.debug('Decoded payload', payload);
|
||||
|
||||
if (!payload || !payload.userId || !payload.roomId) {
|
||||
log.error('Invalid or malformed token payload', payload);
|
||||
return res.status(400).send('Invalid token');
|
||||
}
|
||||
|
||||
return this.htmlInjector.injectHtml(this.clientHtml, res);
|
||||
} catch (error) {
|
||||
log.error('Token processing error', {
|
||||
error: error.message,
|
||||
token: roomToken.substring(0, 20) + '...',
|
||||
});
|
||||
return res.status(500).send('Error processing token');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = mattermost;
|
||||
module.exports = MattermostController;
|
||||
|
||||
+19
-12
@@ -45,7 +45,7 @@ dependencies: {
|
||||
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
|
||||
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
|
||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||
* @version 1.5.03
|
||||
* @version 1.5.04
|
||||
*
|
||||
*/
|
||||
|
||||
@@ -67,7 +67,7 @@ const app = express();
|
||||
const fs = require('fs');
|
||||
const checkXSS = require('./xss.js');
|
||||
const ServerApi = require('./api');
|
||||
const mattermostCli = require('./mattermost');
|
||||
const MattermostController = require('./mattermost');
|
||||
const Validate = require('./validate');
|
||||
const HtmlInjector = require('./htmlInjector');
|
||||
const Host = require('./host');
|
||||
@@ -272,6 +272,8 @@ const mattermostCfg = {
|
||||
username: process.env.MATTERMOST_USERNAME,
|
||||
password: process.env.MATTERMOST_PASSWORD,
|
||||
token: process.env.MATTERMOST_TOKEN,
|
||||
roomTokenExpire: process.env.MATTERMOST_ROOM_TOKEN_EXPIRE,
|
||||
encryptionKey: process.env.JWT_KEY,
|
||||
api_disabled: api_disabled,
|
||||
};
|
||||
|
||||
@@ -384,15 +386,20 @@ app.set('trust proxy', trustProxy); // Enables trust for proxy headers (e.g., X-
|
||||
app.use(helmet.noSniff()); // Enable content type sniffing prevention
|
||||
|
||||
// Use all static files from the public folder
|
||||
app.use(
|
||||
express.static(dir.public, {
|
||||
setHeaders: (res, filePath) => {
|
||||
if (filePath.endsWith('.js')) {
|
||||
res.setHeader('Content-Type', 'application/javascript');
|
||||
} //...
|
||||
},
|
||||
}),
|
||||
);
|
||||
const staticOptions = {
|
||||
setHeaders: (res, filePath) => {
|
||||
if (filePath.endsWith('.js')) {
|
||||
res.setHeader('Content-Type', 'application/javascript');
|
||||
}
|
||||
// Add other headers if needed...
|
||||
},
|
||||
};
|
||||
|
||||
// Serve static files from root (/)
|
||||
app.use(express.static(dir.public, staticOptions));
|
||||
|
||||
// Also serve the same files under /mattermost
|
||||
app.use('/mattermost', express.static(dir.public, staticOptions));
|
||||
|
||||
app.use(cors(corsOptions)); // Enable CORS with options
|
||||
app.use(compression()); // Compress all HTTP responses using GZip
|
||||
@@ -426,7 +433,7 @@ app.use((req, res, next) => {
|
||||
});
|
||||
|
||||
// Mattermost
|
||||
const mattermost = new mattermostCli(app, mattermostCfg);
|
||||
const mattermost = new MattermostController(app, mattermostCfg, htmlInjector, views.client);
|
||||
|
||||
// Remove trailing slashes in url handle bad requests
|
||||
app.use((err, req, res, next) => {
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
'use strict';
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
const CryptoJS = require('crypto-js');
|
||||
const Logger = require('./logs');
|
||||
|
||||
/**
|
||||
* Handles JWT signing and verification.
|
||||
*/
|
||||
class JWTService {
|
||||
/**
|
||||
* @param {string} secret - JWT secret key.
|
||||
* @param {string} algorithm - Signing algorithm (default: 'HS256').
|
||||
*/
|
||||
constructor(secret, algorithm = 'HS256') {
|
||||
this.secret = secret;
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a payload into a JWT token.
|
||||
* @param {Object} payload
|
||||
* @param {string|number} expiresIn - Expiration time (e.g., '1h', 3600).
|
||||
* @returns {string}
|
||||
*/
|
||||
sign(payload, expiresIn) {
|
||||
return jwt.sign(payload, this.secret, {
|
||||
expiresIn,
|
||||
algorithm: this.algorithm,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a JWT token.
|
||||
* @param {string} token
|
||||
* @param {boolean} [ignoreExpiration=false]
|
||||
* @returns {Object}
|
||||
*/
|
||||
verify(token, ignoreExpiration = false) {
|
||||
return jwt.verify(token, this.secret, {
|
||||
ignoreExpiration,
|
||||
algorithms: [this.algorithm],
|
||||
clockTolerance: 30,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a JWT token without verification.
|
||||
* @param {string} token
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
decode(token) {
|
||||
return jwt.decode(token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles AES encryption and decryption.
|
||||
*/
|
||||
class EncryptionService {
|
||||
/**
|
||||
* @param {string} encryptionKey - Secret key for encryption/decryption.
|
||||
*/
|
||||
constructor(encryptionKey) {
|
||||
this.encryptionKey = encryptionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a payload using AES.
|
||||
* @param {any} payload
|
||||
* @param {string} [type=typeof payload]
|
||||
* @returns {{ encrypted: string, type: string }}
|
||||
*/
|
||||
encrypt(payload, type = typeof payload) {
|
||||
const stringified = type === 'object' ? JSON.stringify(payload) : String(payload);
|
||||
return {
|
||||
encrypted: CryptoJS.AES.encrypt(stringified, this.encryptionKey).toString(),
|
||||
type,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts AES-encrypted data.
|
||||
* @param {{ encrypted: string, type: string }} encryptedPayload
|
||||
* @returns {any}
|
||||
*/
|
||||
decrypt(encryptedPayload) {
|
||||
const { encrypted, type } = encryptedPayload;
|
||||
const bytes = CryptoJS.AES.decrypt(encrypted, this.encryptionKey);
|
||||
const decrypted = bytes.toString(CryptoJS.enc.Utf8);
|
||||
|
||||
if (!decrypted) throw new Error('Decryption failed');
|
||||
|
||||
switch (type) {
|
||||
case 'object': return JSON.parse(decrypted);
|
||||
case 'number': return Number(decrypted);
|
||||
default: return decrypted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares payloads for token validation.
|
||||
*/
|
||||
class PayloadComparator {
|
||||
/**
|
||||
* Checks if actual matches expected payload.
|
||||
* @param {Object} expected
|
||||
* @param {Object} actual
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static match(expected, actual) {
|
||||
if (typeof expected === 'object') {
|
||||
return Object.keys(expected).every(key =>
|
||||
JSON.stringify(expected[key]) === JSON.stringify(actual[key])
|
||||
);
|
||||
}
|
||||
|
||||
const value = actual?.value ?? actual;
|
||||
return String(value) === String(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the difference between expected and actual payloads.
|
||||
* @param {Object} expected
|
||||
* @param {Object} actual
|
||||
* @returns {Object}
|
||||
*/
|
||||
static diff(expected, actual) {
|
||||
const diff = {};
|
||||
Object.keys(expected).forEach(key => {
|
||||
if (JSON.stringify(expected[key]) !== JSON.stringify(actual[key])) {
|
||||
diff[key] = { expected: expected[key], actual: actual[key] };
|
||||
}
|
||||
});
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TokenManager class that handles logs
|
||||
*/
|
||||
class TokenLogger {
|
||||
constructor(LoggerClass) {
|
||||
return new LoggerClass('TokenManager');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TokenManager class that handles token creation, validation, and decryption.
|
||||
*/
|
||||
class TokenManager {
|
||||
/**
|
||||
* @param {string} jwtSecret - Secret used for signing JWTs.
|
||||
* @param {string|number} defaultExpiry - Default token expiry (e.g., '24h').
|
||||
* @param {string} encryptionKey - Secret key for AES encryption.
|
||||
*/
|
||||
constructor(jwtSecret, defaultExpiry = 24, encryptionKey) {
|
||||
this.jwtService = new JWTService(jwtSecret);
|
||||
this.encryptionService = new EncryptionService(encryptionKey);
|
||||
this.logger = new TokenLogger(Logger);
|
||||
this.defaultExpiry = defaultExpiry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JWT token, optionally encrypting the payload.
|
||||
* @param {any} payload
|
||||
* @param {boolean} [encode=false] - Whether to encrypt the payload.
|
||||
* @param {string|number} [expiresIn=defaultExpiry]
|
||||
* @returns {string}
|
||||
*/
|
||||
create(payload, encode = false, expiresIn = this.defaultExpiry) {
|
||||
try {
|
||||
if (!payload && payload !== 0) throw new Error('Invalid payload');
|
||||
|
||||
const expiry = this._normalizeExpiry(expiresIn);
|
||||
const data = encode
|
||||
? this.encryptionService.encrypt(payload)
|
||||
: typeof payload === 'object' ? payload : { value: payload };
|
||||
|
||||
return this.jwtService.sign(data, expiry);
|
||||
} catch (error) {
|
||||
this.logger.error('Token creation failed', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a token and checks if payload matches.
|
||||
* @param {any} expectedPayload
|
||||
* @param {string} token
|
||||
* @param {boolean} [decode=false] - Whether to decrypt the token.
|
||||
* @param {boolean} [ignoreExpiry=false]
|
||||
* @returns {boolean}
|
||||
*/
|
||||
validate(expectedPayload, token, decode = false, ignoreExpiry = false) {
|
||||
try {
|
||||
const decoded = this.jwtService.verify(token, ignoreExpiry);
|
||||
|
||||
const payload = decode
|
||||
? this.encryptionService.decrypt(decoded)
|
||||
: decoded;
|
||||
|
||||
const isMatch = PayloadComparator.match(expectedPayload, payload);
|
||||
|
||||
if (!isMatch) {
|
||||
this.logger.debug('Payload mismatch', {
|
||||
expected: expectedPayload,
|
||||
actual: payload,
|
||||
diff: PayloadComparator.diff(expectedPayload, payload),
|
||||
});
|
||||
}
|
||||
|
||||
return isMatch;
|
||||
} catch (error) {
|
||||
this.logger.error('Token validation failed', { error: error.message });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes and decrypts token payload if necessary.
|
||||
* @param {string} token
|
||||
* @returns {any|null}
|
||||
*/
|
||||
decodePayload(token) {
|
||||
try {
|
||||
const decoded = this.jwtService.verify(token);
|
||||
return decoded?.encrypted
|
||||
? this.encryptionService.decrypt(decoded)
|
||||
: decoded;
|
||||
} catch (error) {
|
||||
this.logger.error('Decode failed', { error: error.message });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts raw payload from a token without verification.
|
||||
* @param {string} token
|
||||
* @returns {any|null}
|
||||
*/
|
||||
extractPayload(token) {
|
||||
try {
|
||||
const decoded = this.jwtService.decode(token);
|
||||
if (!decoded || decoded.encrypted) return null;
|
||||
|
||||
return decoded.payload ?? decoded.value ?? decoded;
|
||||
} catch (err) {
|
||||
this.logger.error('Payload extraction failed', { error: err.message });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes expiration format.
|
||||
* @private
|
||||
* @param {string|number} exp
|
||||
* @returns {string|number}
|
||||
*/
|
||||
_normalizeExpiry(exp) {
|
||||
if (typeof exp === 'number' || (typeof exp === 'string' && /^(\d+|\d+\.\d+)[smhd]?$/i.test(exp))) {
|
||||
return exp;
|
||||
}
|
||||
|
||||
this.logger.warn(`Invalid expiry "${exp}", defaulting to "24h"`);
|
||||
return '24h';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TokenManager;
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mirotalk",
|
||||
"version": "1.5.03",
|
||||
"version": "1.5.04",
|
||||
"description": "A free WebRTC browser-based video call",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
@@ -61,7 +61,7 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"nodemailer": "^6.10.1",
|
||||
"openai": "^4.95.0",
|
||||
"openai": "^4.95.1",
|
||||
"qs": "^6.14.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
|
||||
@@ -3780,6 +3780,18 @@ img.has-shadow {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#lastRoomContainer {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#lastRoom {
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* #roomName {
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ let brand = {
|
||||
},
|
||||
about: {
|
||||
imageUrl: '../images/mirotalk-logo.gif',
|
||||
title: 'WebRTC P2P v1.5.03',
|
||||
title: 'WebRTC P2P v1.5.04',
|
||||
html: `
|
||||
<button
|
||||
id="support-button"
|
||||
|
||||
+3
-3
@@ -15,7 +15,7 @@
|
||||
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
|
||||
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
|
||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||
* @version 1.5.03
|
||||
* @version 1.5.04
|
||||
*
|
||||
*/
|
||||
|
||||
@@ -940,7 +940,7 @@ function getRoomId() {
|
||||
let queryRoomId = filterXSS(qs.get('room'));
|
||||
|
||||
// skip /join/
|
||||
let roomId = queryRoomId ? queryRoomId : window.location.pathname.substring(6);
|
||||
let roomId = queryRoomId ? queryRoomId : window.location.pathname.split('/join/')[1];
|
||||
|
||||
// if not specified room id, create one random
|
||||
if (roomId == '') {
|
||||
@@ -11158,7 +11158,7 @@ function showAbout() {
|
||||
Swal.fire({
|
||||
background: swBg,
|
||||
position: 'center',
|
||||
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.5.03',
|
||||
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.5.04',
|
||||
imageUrl: brand.about?.imageUrl && brand.about.imageUrl.trim() !== '' ? brand.about.imageUrl : images.about,
|
||||
customClass: { image: 'img-about' },
|
||||
html: `
|
||||
|
||||
+1
-1
@@ -181,8 +181,8 @@ if (roomName) {
|
||||
const lastRoomContainer = document.getElementById('lastRoomContainer');
|
||||
const lastRoom = document.getElementById('lastRoom');
|
||||
const lastRoomName = window.localStorage.lastRoom ? window.localStorage.lastRoom : '';
|
||||
|
||||
if (lastRoomContainer && lastRoom && lastRoomName) {
|
||||
lastRoomContainer.style.display = 'inline-flex';
|
||||
lastRoom.setAttribute('href', '/join/' + lastRoomName);
|
||||
lastRoom.innerText = lastRoomName;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user