mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-14 11:53:49 +00:00
Client remote control (#930)
--------- Co-authored-by: kellhogs <fabianpos99@gmail.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me> Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
70e8fbf327
commit
a15eb0db4c
@@ -14,7 +14,7 @@
|
||||
* Payload: adds necessary payload for the request to be completed
|
||||
*/
|
||||
|
||||
import { LogOrigin } from 'ontime-types';
|
||||
import { Client, LogOrigin } from 'ontime-types';
|
||||
|
||||
import { WebSocket, WebSocketServer } from 'ws';
|
||||
import type { Server } from 'http';
|
||||
@@ -24,6 +24,7 @@ import { IAdapter } from './IAdapter.js';
|
||||
import { eventStore } from '../stores/EventStore.js';
|
||||
import { logger } from '../classes/Logger.js';
|
||||
import { dispatchFromAdapter } from '../api-integration/integration.controller.js';
|
||||
import { generateId } from 'ontime-utils';
|
||||
|
||||
let instance: SocketServer | null = null;
|
||||
|
||||
@@ -31,7 +32,7 @@ export class SocketServer implements IAdapter {
|
||||
private readonly MAX_PAYLOAD = 1024 * 256; // 256Kb
|
||||
|
||||
private wss: WebSocketServer | null;
|
||||
private readonly clientIds: Set<string>;
|
||||
private readonly clients: Map<string, Client>;
|
||||
|
||||
constructor() {
|
||||
if (instance) {
|
||||
@@ -40,7 +41,7 @@ export class SocketServer implements IAdapter {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton
|
||||
instance = this;
|
||||
this.clientIds = new Set<string>();
|
||||
this.clients = new Map<string, Client>();
|
||||
this.wss = null;
|
||||
}
|
||||
|
||||
@@ -48,9 +49,32 @@ export class SocketServer implements IAdapter {
|
||||
this.wss = new WebSocketServer({ path: '/ws', server, maxPayload: this.MAX_PAYLOAD });
|
||||
|
||||
this.wss.on('connection', (ws) => {
|
||||
let clientId = getRandomName();
|
||||
this.clientIds.add(clientId);
|
||||
logger.info(LogOrigin.Client, `${this.clientIds.size} Connections with new: ${clientId}`);
|
||||
const clientId = generateId();
|
||||
|
||||
this.clients.set(clientId, {
|
||||
type: 'unknown',
|
||||
identify: false,
|
||||
name: getRandomName(),
|
||||
path: '',
|
||||
});
|
||||
|
||||
logger.info(LogOrigin.Client, `${this.clients.size} Connections with new: ${clientId}`);
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'client-id',
|
||||
payload: clientId,
|
||||
}),
|
||||
);
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'client-name',
|
||||
payload: this.clients.get(clientId).name,
|
||||
}),
|
||||
);
|
||||
|
||||
this.sendClientList();
|
||||
|
||||
// send store payload on connect
|
||||
ws.send(
|
||||
@@ -60,18 +84,12 @@ export class SocketServer implements IAdapter {
|
||||
}),
|
||||
);
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'client-name',
|
||||
payload: clientId,
|
||||
}),
|
||||
);
|
||||
|
||||
ws.on('error', console.error);
|
||||
|
||||
ws.on('close', () => {
|
||||
this.clientIds.delete(clientId);
|
||||
logger.info(LogOrigin.Client, `${this.clientIds.size} Connections with disconnected: ${clientId}`);
|
||||
this.clients.delete(clientId);
|
||||
logger.info(LogOrigin.Client, `${this.clients.size} Connections with disconnected: ${clientId}`);
|
||||
this.sendClientList();
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
@@ -84,26 +102,44 @@ export class SocketServer implements IAdapter {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'client-name',
|
||||
payload: clientId,
|
||||
payload: this.clients.get(clientId).name,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'set-client-type') {
|
||||
if (payload && typeof payload == 'string') {
|
||||
const previousData = this.clients.get(clientId);
|
||||
this.clients.set(clientId, { ...previousData, type: payload });
|
||||
}
|
||||
this.sendClientList();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'set-client-path') {
|
||||
if (payload && typeof payload == 'string') {
|
||||
const previousData = this.clients.get(clientId);
|
||||
previousData.path = payload;
|
||||
this.clients.set(clientId, previousData);
|
||||
}
|
||||
this.sendClientList();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'set-client-name') {
|
||||
if (payload) {
|
||||
const previousName = clientId;
|
||||
clientId = payload;
|
||||
this.clientIds.delete(previousName);
|
||||
this.clientIds.add(clientId);
|
||||
logger.info(LogOrigin.Client, `Client ${previousName} renamed to ${clientId}`);
|
||||
const previousData = this.clients.get(clientId);
|
||||
logger.info(LogOrigin.Client, `Client ${previousData.name} renamed to ${payload}`);
|
||||
this.clients.set(clientId, { ...previousData, name: payload });
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'client-name',
|
||||
payload: this.clients.get(clientId).name,
|
||||
}),
|
||||
);
|
||||
}
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'client-name',
|
||||
payload: clientId,
|
||||
}),
|
||||
);
|
||||
this.sendClientList();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -135,17 +171,58 @@ export class SocketServer implements IAdapter {
|
||||
});
|
||||
}
|
||||
|
||||
// message is any serializable value
|
||||
sendAsJson(message: unknown) {
|
||||
this.wss?.clients.forEach((client) => {
|
||||
try {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(message));
|
||||
}
|
||||
} catch (_) {
|
||||
/** We do not handle this error */
|
||||
}
|
||||
private sendClientList(): void {
|
||||
const payload = Object.fromEntries(this.clients.entries());
|
||||
this.sendAsJson({ type: 'client-list', payload });
|
||||
}
|
||||
|
||||
public getClientList(): string[] {
|
||||
return Array.from(this.clients.keys());
|
||||
}
|
||||
|
||||
public renameClient(target: string, name: string) {
|
||||
const previousData = this.clients.get(target);
|
||||
if (!previousData) {
|
||||
throw new Error(`Client "${target}" not found`);
|
||||
}
|
||||
logger.info(LogOrigin.Client, `Client ${previousData.name} renamed to ${name}`);
|
||||
this.clients.set(target, { ...previousData, name });
|
||||
this.sendAsJson({
|
||||
type: 'client-rename',
|
||||
payload: { name, target },
|
||||
});
|
||||
this.sendClientList();
|
||||
}
|
||||
|
||||
public redirectClient(target: string, path: string) {
|
||||
const previousData = this.clients.get(target);
|
||||
if (!previousData) {
|
||||
throw new Error(`Client "${target}" not found`);
|
||||
}
|
||||
this.sendAsJson({ type: 'client-redirect', payload: { target, path } });
|
||||
}
|
||||
|
||||
public identifyClient(target: string, identify: boolean) {
|
||||
const previousData = this.clients.get(target);
|
||||
if (!previousData) {
|
||||
throw new Error(`Client "${target}" not found`);
|
||||
}
|
||||
this.clients.set(target, { ...previousData, identify });
|
||||
this.sendClientList();
|
||||
}
|
||||
|
||||
// message is any serializable value
|
||||
public sendAsJson(message: unknown) {
|
||||
try {
|
||||
const stringifiedMessage = JSON.stringify(message);
|
||||
this.wss?.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(stringifiedMessage);
|
||||
}
|
||||
});
|
||||
} catch (_) {
|
||||
/** We do not handle this error */
|
||||
}
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { eventStore } from '../stores/EventStore.js';
|
||||
import * as assert from '../utils/assert.js';
|
||||
import { isEmptyObject } from '../utils/parserUtils.js';
|
||||
import { parseProperty, updateEvent } from './integration.utils.js';
|
||||
import { socket } from '../adapters/WebsocketAdapter.js';
|
||||
import { throttle } from '../utils/throttle.js';
|
||||
import { willCauseRegeneration } from '../services/rundown-service/rundownCacheUtils.js';
|
||||
|
||||
@@ -240,6 +241,33 @@ const actionHandlers: Record<string, ActionHandler> = {
|
||||
}
|
||||
throw new Error('No matching method provided');
|
||||
},
|
||||
/* Client */
|
||||
client: (payload) => {
|
||||
assert.isObject(payload);
|
||||
if (!('target' in payload) || typeof payload.target != 'string') {
|
||||
throw new Error('No or invalid client target');
|
||||
}
|
||||
|
||||
if ('rename' in payload && typeof payload.rename == 'string') {
|
||||
const { target, rename } = payload;
|
||||
socket.renameClient(target, rename);
|
||||
return { payload: 'success' };
|
||||
}
|
||||
|
||||
if ('redirect' in payload && typeof payload.redirect == 'string') {
|
||||
const { target, redirect } = payload;
|
||||
socket.redirectClient(target, redirect);
|
||||
return { payload: 'success' };
|
||||
}
|
||||
|
||||
if ('identify' in payload && typeof payload.identify == 'boolean') {
|
||||
const { target, identify } = payload;
|
||||
socket.identifyClient(target, identify);
|
||||
return { payload: 'success' };
|
||||
}
|
||||
|
||||
throw new Error('No matching method provided');
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user