[mirotalk] - #285 refactoring

This commit is contained in:
Miroslav Pejic
2025-04-19 21:27:16 +02:00
parent 4cb3ac2270
commit ed2645852c
5 changed files with 119 additions and 71 deletions
+101 -54
View File
@@ -6,6 +6,7 @@ const TokenManager = require('./tokenManager');
const Logger = require('./logs');
const log = new Logger('Mattermost');
// Token operations
class TokenService {
constructor(secret, expiresIn, encryptionKey) {
this.tokenManager = new TokenManager(secret, expiresIn, encryptionKey);
@@ -20,33 +21,16 @@ class TokenService {
}
}
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 || [];
// Mattermost authentication
class MattermostAuthService {
constructor({ serverUrl, username, password }) {
this.client = new Client4();
this.client.setUrl(this.serverUrl);
this.tokenService = new TokenService(
this.token || 'fallback-secret-at-least-32-chars',
config.roomTokenExpire || '15m',
config.encryptionKey || 'fallback-encryption-key-32chars'
);
this.client.setUrl(serverUrl);
this.username = username;
this.password = password;
}
validateConfig(config) {
if (!config.enabled || !config.server_url || !config.token || !config.username || !config.password) {
throw new Error('Invalid Mattermost configuration');
}
}
async authenticate() {
async login() {
try {
const user = await this.client.login(this.username, this.password);
log.debug('Logged into Mattermost as', user.username);
@@ -54,77 +38,122 @@ class MattermostService {
log.error('Failed to log into Mattermost:', error);
}
}
}
// Meeting-related operations
class MeetingService {
constructor(tokenService, serverUrl, disabledEndpoints = [], secure = false) {
this.tokenService = tokenService;
this.serverUrl = serverUrl;
this.disabledEndpoints = disabledEndpoints;
this.secure = secure;
}
isEndpointDisabled(endpoint) {
return this.disabledEndpoints.includes(endpoint);
}
createMeetingToken(userId) {
const payload = {
createTokenPayload(userId) {
return {
userId,
roomId: uuidV4(),
timestamp: Date.now(),
};
const token = this.tokenService.createToken(payload);
return { token, payload };
}
validateToken(token) {
createMeetingURL(req) {
return `${this.getBaseURL(req)}/join/${uuidV4()}`;
}
createSecureMeetingURL(req, userId) {
const payload = this.createTokenPayload(userId);
const token = this.tokenService.createToken(payload);
return {
url: `${this.getBaseURL(req)}/mattermost/join/${encodeURIComponent(token)}`,
payload,
};
}
decodeToken(token) {
return this.tokenService.decodeToken(token);
}
getMeetingURL(req, roomToken) {
getBaseURL(req) {
const host = req.headers.host;
const protocol = host.includes('localhost') ? 'http' : 'https';
return `${protocol}://${host}/mattermost/join/${encodeURIComponent(roomToken)}`;
return `${protocol}://${host}`;
}
}
// Just handles routing and delegates everything
class MattermostController {
constructor(app, mattermostCfg, htmlInjector, clientHtml) {
constructor(app, config, htmlInjector, clientHtml) {
try {
this.service = new MattermostService(mattermostCfg);
this.validateConfig(config);
const tokenService = new TokenService(
config.token || 'fallback-secret-at-least-32-chars',
config.roomTokenExpire || '15m',
config.encryptionKey || 'fallback-encryption-key-32chars'
);
this.authService = new MattermostAuthService({
serverUrl: config.server_url,
username: config.username,
password: config.password,
});
this.meetingService = new MeetingService(
tokenService,
config.server_url,
config.api_disabled,
config.security
);
this.token = config.token;
this.app = app;
this.htmlInjector = htmlInjector;
this.clientHtml = clientHtml;
this.authService.login();
this.setupRoutes();
} 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();
validateConfig(cfg) {
if (!cfg.enabled || !cfg.server_url || !cfg.token || !cfg.username || !cfg.password) {
throw new Error('Invalid Mattermost configuration');
}
}
setupRoutes() {
this.app.post('/mattermost', (req, res) => {
if (this.service.isEndpointDisabled('mattermost')) {
if (this.meetingService.isEndpointDisabled('mattermost')) {
return res.end('`This endpoint has been disabled`. Please contact the administrator.');
}
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');
}
if (command?.trim() === '/p2p' || text?.trim() === '/p2p') {
if (this.isP2PCommand(command, text)) {
try {
const { token: roomToken } = this.service.createMeetingToken(user_id);
const meetingUrl = this.service.getMeetingURL(req, roomToken);
const meetingUrl = this.generateMeetingUrl(req, user_id);
const message = this.getMeetingResponseMessage(meetingUrl);
return res.json({
response_type: 'in_channel',
text: `🔗 [Click here to join your private meeting](${meetingUrl})`,
text: message,
channel_id,
});
} catch (error) {
log.error('Token creation failed', error);
return res.status(500).send('Error creating meeting');
log.error('Meeting creation failed', { error, user_id });
return res.status(500).json({ error: 'Failed to create meeting' });
}
}
@@ -132,7 +161,7 @@ class MattermostController {
});
this.app.get('/mattermost/join/:roomToken', (req, res) => {
if (this.service.isEndpointDisabled('mattermost')) {
if (this.meetingService.isEndpointDisabled('mattermost')) {
return res.end('This endpoint has been disabled');
}
@@ -143,14 +172,13 @@ class MattermostController {
}
try {
const payload = this.service.validateToken(roomToken);
log.debug('Decoded payload', payload);
const payload = this.meetingService.decodeToken(roomToken);
if (!payload || !payload.userId || !payload.roomId) {
log.error('Invalid or malformed token payload', payload);
return res.status(400).send('Invalid token');
}
log.debug('Decoded payload', payload);
return this.htmlInjector.injectHtml(this.clientHtml, res);
} catch (error) {
log.error('Token processing error', {
@@ -161,6 +189,25 @@ class MattermostController {
}
});
}
isP2PCommand(command, text) {
const normalizedCommand = command?.trim();
const normalizedText = text?.trim();
return normalizedCommand === '/p2p' || normalizedText === '/p2p';
}
generateMeetingUrl(req, userId) {
if (this.meetingService.secure) {
return this.meetingService.createSecureMeetingURL(req, userId).url;
}
return this.meetingService.createMeetingURL(req);
}
getMeetingResponseMessage(meetingUrl) {
return this.meetingService.secure
? `🔒 [Join your secure private meeting](${meetingUrl})`
: `🌐 Join meeting: ${meetingUrl}`;
}
}
module.exports = MattermostController;
+14 -13
View File
@@ -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.04
* @version 1.5.05
*
*/
@@ -265,18 +265,6 @@ if (configChatGPT.enabled) {
}
}
// Mattermost config
const mattermostCfg = {
enabled: getEnvBoolean(process.env.MATTERMOST_ENABLED),
server_url: process.env.MATTERMOST_SERVER_URL,
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,
};
// IP Whitelist
const ipWhitelist = {
enabled: getEnvBoolean(process.env.IP_WHITELIST_ENABLED),
@@ -350,6 +338,19 @@ function OIDCAuth(req, res, next) {
}
}
// Mattermost config
const mattermostCfg = {
enabled: getEnvBoolean(process.env.MATTERMOST_ENABLED),
server_url: process.env.MATTERMOST_SERVER_URL,
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,
security: (hostCfg.protected || OIDC.enabled),
api_disabled: api_disabled,
};
// stats configuration
const statsData = {
enabled: process.env.STATS_ENABLED ? getEnvBoolean(process.env.STATS_ENABLED) : true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mirotalk",
"version": "1.5.04",
"version": "1.5.05",
"description": "A free WebRTC browser-based video call",
"main": "server.js",
"scripts": {
+1 -1
View File
@@ -73,7 +73,7 @@ let brand = {
},
about: {
imageUrl: '../images/mirotalk-logo.gif',
title: 'WebRTC P2P v1.5.04',
title: 'WebRTC P2P v1.5.05',
html: `
<button
id="support-button"
+2 -2
View File
@@ -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.04
* @version 1.5.05
*
*/
@@ -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.04',
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.5.05',
imageUrl: brand.about?.imageUrl && brand.about.imageUrl.trim() !== '' ? brand.about.imageUrl : images.about,
customClass: { image: 'img-about' },
html: `