7 Commits
v1.1.0 ... main

15 changed files with 1817 additions and 1052 deletions

View File

@@ -1,18 +1,18 @@
# React TeamSpeak5 RemoteApp API # React TeamSpeak5/6 RemoteApp API
[![npm](https://img.shields.io/npm/v/react-ts5-remote-app-api.svg)](https://www.npmjs.com/package/react-ts5-remote-app-api) ![downloads](https://img.shields.io/npm/dt/react-ts5-remote-app-api.svg) [![npm](https://img.shields.io/npm/v/react-teamspeak-remote-app-api.svg)](https://www.npmjs.com/package/react-teamspeak-remote-app-api) ![downloads](https://img.shields.io/npm/dt/react-teamspeak-remote-app-api.svg)
This is a ReactJS hook for the TeamSpeak5 RemoteApp API. This is a ReactJS hook for the TeamSpeak5/6 RemoteApp API.
It gathers all the events and methods from the API and makes them available as React states. It gathers all the events and methods from the API and makes them available as React states.
Please note that this is still a work in progress and not all events and methods are implemented yet. Please note that this is still a work in progress and not all events and methods are implemented yet.
Projects which are using this hook: [TS5 OBS Overlay](https://github.com/DerTyp7/ts5-obs-overlay) Projects which are using this hook: [TeamSpeak OBS Overlay](https://github.com/DerTyp7/teamspeak-obs-overlay)
## Table of Contents ## Table of Contents
- [React TeamSpeak5 RemoteApp API](#react-teamspeak5-remoteapp-api) - [React TeamSpeak5/6 RemoteApp API](#react-teamspeak56-remoteapp-api)
- [Table of Contents](#table-of-contents) - [Table of Contents](#table-of-contents)
- [Installation](#installation) - [Installation](#installation)
- [Usage](#usage) - [Usage](#usage)
@@ -26,13 +26,13 @@ Projects which are using this hook: [TS5 OBS Overlay](https://github.com/DerTyp7
## Installation ## Installation
```bash ```bash
npm install react-ts5-remote-app-api npm install react-teamspeak-remote-app-api
``` ```
## Usage ## Usage
```Typescript ```Typescript
import useTSRemoteApp, { IClient } from "react-ts5-remote-app-api"; import useTSRemoteApp, { IClient } from "react-teamspeak-remote-app-api";
export default function App() { export default function App() {
const { const {
@@ -57,7 +57,7 @@ export default function App() {
### Get all clients in the current channel ### Get all clients in the current channel
```Typescript ```Typescript
import useTSRemoteApp, { IClient } from "react-ts5-remote-app-api"; import useTSRemoteApp, { IClient } from "react-teamspeak-remote-app-api";
default function App() { default function App() {
const { clientsInChannel } = useTSRemoteApp({ const { clientsInChannel } = useTSRemoteApp({

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

2539
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,17 @@
{ {
"name": "react-ts5-remote-app-api", "name": "react-teamspeak-remote-app-api",
"version": "1.1.0", "version": "2.0.0",
"description": "React hook/api for the TeamSpeak5 remote app feature", "description": "React hook/api for the TeamSpeak5/6 remote app feature",
"main": "dist/cjs/index.js", "main": "dist/cjs/index.js",
"module": "dist/esm/index.js", "module": "dist/esm/index.js",
"source": "src/index.ts", "source": "src/index.ts",
"types": "dist/cjs/index.d.ts", "types": "dist/cjs/index.d.ts",
"scripts": { "scripts": {
"build": "npm run build:esm && npm run build:js", "build": "npm run build:esm && npm run build:js",
"build:esm": "tsc --project tsconfig.build.json --outDir dist/esm --module esnext", "build:esm": "tsc --project tsconfig.build.json --outDir dist/esm --module esnext && npm run alias:esm",
"build:js": "tsc --project tsconfig.build.json --outDir dist/cjs --module commonjs" "build:js": "tsc --project tsconfig.build.json --outDir dist/cjs --module commonjs && npm run alias:js",
"alias:esm": "tsc-alias --project tsconfig.build.json --outDir dist/esm",
"alias:js": "tsc-alias --project tsconfig.build.json --outDir dist/cjs"
}, },
"keywords": [ "keywords": [
"TeamSpeak5", "TeamSpeak5",
@@ -25,13 +27,13 @@
"author": "DerTyp7", "author": "DerTyp7",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/DerTyp7/react-ts5-remote-app-api" "url": "https://github.com/DerTyp7/react-teamspeak-remote-app-api"
}, },
"license": "ISC", "license": "ISC",
"peerDependencies": { "peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "@types/react": "^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" "react-dom": "^18.0.0 || ^19.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@types/react": { "@types/react": {
@@ -43,22 +45,24 @@
"src" "src"
], ],
"dependencies": { "dependencies": {
"@rollup/plugin-typescript": "^11.1.2", "@rollup/plugin-typescript": "^12.1.2",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0", "@types/node": "^22.10.10",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^18.0.0 || ^19.0.0",
"rollup-plugin-typescript-paths": "^1.4.0", "react-dom": "^18.0.0 || ^19.0.0",
"ts-node": "^10.9.1", "rollup-plugin-typescript-paths": "^1.5.0",
"tslib": "^2.6.0" "ts-node": "^10.9.2",
"tslib": "^2.8.1"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.15", "@types/react": "^19.0.8",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^19.0.3",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.21.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^8.21.0",
"@vitejs/plugin-react-swc": "^3.3.2", "@vitejs/plugin-react-swc": "^3.7.2",
"eslint": "^8.45.0", "eslint": "^9.18.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.3", "eslint-plugin-react-refresh": "^0.4.18",
"typescript": "^5.0.2" "tsc-alias": "^1.8.10",
"typescript": "^5.7.3"
} }
} }

View File

@@ -1,8 +1,8 @@
import { IAuthSenderPayload, IChannel, IClient, IConnection, ITS5ConnectionHandler, ITS5DataHandler, ITS5MessageHandler } from "../../interfaces/teamspeak"; import { IAuthSenderPayload, IChannel, IClient, IConnection, ITS5ConnectionHandler, ITS5DataHandler, ITS5MessageHandler } from "interfaces/teamspeak";
import { TS5DataHandler } from "./dataHandler"; import { TS5DataHandler } from "handlers/teamspeak/dataHandler";
import { TS5MessageHandler } from "./messageHandler"; import { TS5MessageHandler } from "handlers/teamspeak/messageHandler";
import Logger from "../../utils/logger"; import { ILogger } from "utils/logger";
import { ITSRemoteAppAuthPayloadOptions } from "../../interfaces/api"; import { ITSRemoteAppAuthPayloadOptions } from "interfaces/api";
// Establish connection to TS5 client // Establish connection to TS5 client
@@ -11,6 +11,7 @@ export class TS5ConnectionHandler implements ITS5ConnectionHandler {
ws: WebSocket; // Websocket connection to TS5 client ws: WebSocket; // Websocket connection to TS5 client
authenticated = false; // Is the connection authenticated? authenticated = false; // Is the connection authenticated?
remoteAppPort: number; // Port of TS5 client remoteAppPort: number; // Port of TS5 client
logger: ILogger; // Logger
authPayload: ITSRemoteAppAuthPayloadOptions; // Authentication payload authPayload: ITSRemoteAppAuthPayloadOptions; // Authentication payload
dataHandler: ITS5DataHandler; // Handles data/lists and states dataHandler: ITS5DataHandler; // Handles data/lists and states
messageHandler: ITS5MessageHandler; // Handles messages received from TS5 client messageHandler: ITS5MessageHandler; // Handles messages received from TS5 client
@@ -19,6 +20,7 @@ export class TS5ConnectionHandler implements ITS5ConnectionHandler {
// Port of TS5 client // Port of TS5 client
remoteAppPort: number, remoteAppPort: number,
authPayload: ITSRemoteAppAuthPayloadOptions, authPayload: ITSRemoteAppAuthPayloadOptions,
logger: ILogger,
// State setters for dataHandler // State setters for dataHandler
setConnections: React.Dispatch<React.SetStateAction<IConnection[]>>, setConnections: React.Dispatch<React.SetStateAction<IConnection[]>>,
setChannels: React.Dispatch<React.SetStateAction<IChannel[]>>, setChannels: React.Dispatch<React.SetStateAction<IChannel[]>>,
@@ -30,15 +32,16 @@ export class TS5ConnectionHandler implements ITS5ConnectionHandler {
// Create websocket connection to TS5 client // Create websocket connection to TS5 client
this.remoteAppPort = remoteAppPort; this.remoteAppPort = remoteAppPort;
this.authPayload = authPayload; this.authPayload = authPayload;
this.logger = logger;
this.ws = new WebSocket(`ws://localhost:${this.remoteAppPort}`); this.ws = new WebSocket(`ws://localhost:${this.remoteAppPort}`);
// Create dataHandler and messageHandler // Create dataHandler and messageHandler
this.dataHandler = new TS5DataHandler(setConnections, setChannels, setClients); this.dataHandler = new TS5DataHandler(setConnections, setChannels, setClients, logger);
this.messageHandler = new TS5MessageHandler(this.ws, this.dataHandler, setActiveConnectionStateId); this.messageHandler = new TS5MessageHandler(this.ws, this.dataHandler, setActiveConnectionStateId, logger);
} }
reconnect() { reconnect() {
Logger.log("Reconnecting...") this.logger.log("Reconnecting...")
this.ws.close(); this.ws.close();
this.ws = new WebSocket(`ws://localhost:${this.remoteAppPort}`); this.ws = new WebSocket(`ws://localhost:${this.remoteAppPort}`);
@@ -50,10 +53,10 @@ export class TS5ConnectionHandler implements ITS5ConnectionHandler {
// Connect to TS5 client // Connect to TS5 client
connect() { connect() {
Logger.log('Connecting to TS5 client...'); this.logger.log('Connecting to TS5 client...');
// Create authentication payload // Create authentication payload
const initalPayload: IAuthSenderPayload = { const initialPayload: IAuthSenderPayload = {
type: "auth", type: "auth",
payload: { payload: {
...this.authPayload, ...this.authPayload,
@@ -65,17 +68,17 @@ export class TS5ConnectionHandler implements ITS5ConnectionHandler {
this.ws.onopen = () => { this.ws.onopen = () => {
// Send authentication payload to TS5 client // Send authentication payload to TS5 client
this.ws.send(JSON.stringify(initalPayload)); this.ws.send(JSON.stringify(initialPayload));
Logger.wsSent(initalPayload); this.logger.wsSent(initialPayload);
}; };
this.ws.onclose = (event) => { this.ws.onclose = (event) => {
Logger.log("WebSocket connection closed", event); this.logger.log("WebSocket connection closed", event);
// If the connection was closed before authentication, remove the API key from local storage // If the connection was closed before authentication, remove the API key from local storage
// OBS weirdly caches the localstorage and is very stubborn about clearing it (even when clicken "Clear Cache") // OBS weirdly caches the localstorage and is very stubborn about clearing it (even when clicking "Clear Cache")
if (!this.authenticated) { if (!this.authenticated) {
Logger.log("WebSocket connection closed before authentication"); this.logger.log("WebSocket connection closed before authentication");
localStorage.removeItem("apiKey"); localStorage.removeItem("apiKey");
} }
@@ -89,7 +92,7 @@ export class TS5ConnectionHandler implements ITS5ConnectionHandler {
this.ws.onmessage = (event) => { this.ws.onmessage = (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
Logger.wsReceived(data) this.logger.wsReceived(data)
switch (data.type) { switch (data.type) {
case "auth": case "auth":
@@ -117,8 +120,11 @@ export class TS5ConnectionHandler implements ITS5ConnectionHandler {
case "channels": case "channels":
this.messageHandler.handleChannelsMessage(data); this.messageHandler.handleChannelsMessage(data);
break; break;
case "channelCreated":
this.messageHandler.handleChannelCreatedMessage(data);
break;
default: default:
Logger.log(`No handler for event type: ${data.type}`); this.logger.log(`No handler for event type: ${data.type}`);
break; break;
} }
}; };

View File

@@ -1,10 +1,10 @@
import Logger from "../..//utils/logger"; import { ILogger } from "utils/logger";
import { IConnection, IChannel, IClient, ITS5DataHandler } from "../../interfaces/teamspeak"; import { IConnection, IChannel, IClient, ITS5DataHandler } from "interfaces/teamspeak";
/** /**
* Handles data received from TS5 client (list of connections, channels and clients) * Handles data received from TS5 client (list of connections, channels and clients)
* Updates the states of App.tsx * Updates the states of useTSRemoteApp.tsx
*/ */
export class TS5DataHandler implements ITS5DataHandler { export class TS5DataHandler implements ITS5DataHandler {
// Local lists of connections, channels and clients // Local lists of connections, channels and clients
@@ -18,11 +18,14 @@ export class TS5DataHandler implements ITS5DataHandler {
setChannels: React.Dispatch<React.SetStateAction<IChannel[]>>; setChannels: React.Dispatch<React.SetStateAction<IChannel[]>>;
setClients: React.Dispatch<React.SetStateAction<IClient[]>>; setClients: React.Dispatch<React.SetStateAction<IClient[]>>;
logger: ILogger;
constructor( constructor(
// State setters for App.tsx // State setters for App.tsx
setConnections: React.Dispatch<React.SetStateAction<IConnection[]>>, setConnections: React.Dispatch<React.SetStateAction<IConnection[]>>,
setChannels: React.Dispatch<React.SetStateAction<IChannel[]>>, setChannels: React.Dispatch<React.SetStateAction<IChannel[]>>,
setClients: React.Dispatch<React.SetStateAction<IClient[]>> setClients: React.Dispatch<React.SetStateAction<IClient[]>>,
logger: ILogger,
) { ) {
this.setConnections = setConnections; this.setConnections = setConnections;
this.setChannels = setChannels; this.setChannels = setChannels;
@@ -31,6 +34,20 @@ export class TS5DataHandler implements ITS5DataHandler {
this.localConnections = []; this.localConnections = [];
this.localChannels = []; this.localChannels = [];
this.localClients = []; this.localClients = [];
this.logger = logger;
}
get connections(): IConnection[] {
return this.localConnections;
}
get channels(): IChannel[] {
return this.localChannels;
}
get clients(): IClient[] {
return this.localClients;
} }
// Update App.tsx states // Update App.tsx states
@@ -59,88 +76,88 @@ export class TS5DataHandler implements ITS5DataHandler {
// Add data to local lists and update states // Add data to local lists and update states
addConnection(connection: IConnection) { addConnection(connection: IConnection) {
Logger.log("Adding connection...", connection) this.logger.log("Adding connection...", connection)
const existingConnection: IConnection | undefined = this.localConnections.find((localConnection: IConnection) => localConnection.id === connection.id); const existingConnection: IConnection | undefined = this.localConnections.find((localConnection: IConnection) => localConnection.id === connection.id);
if (existingConnection == undefined) { if (existingConnection == undefined) {
this.localConnections.push(connection); this.localConnections.push(connection);
this.updateConnectionsState(); this.updateConnectionsState();
Logger.log("Connection added") this.logger.log("Connection added")
} else { } else {
Logger.log("Connection already exists") this.logger.log("Connection already exists")
} }
} }
addChannel(channel: IChannel) { addChannel(channel: IChannel) {
Logger.log("Adding channel...", channel) this.logger.log("Adding channel...", channel)
const existingChannel: IChannel | undefined = this.localChannels.find((localChannel: IChannel) => localChannel.id === channel.id && localChannel.connection.id === channel.connection.id); const existingChannel: IChannel | undefined = this.localChannels.find((localChannel: IChannel) => localChannel.id === channel.id && localChannel.connection.id === channel.connection.id);
if (existingChannel == undefined) { if (existingChannel == undefined) {
this.localChannels.push(channel); this.localChannels.push(channel);
this.updateChannelsState(); this.updateChannelsState();
Logger.log("Channel added") this.logger.log("Channel added")
} else { } else {
Logger.log("Channel already exists") this.logger.log("Channel already exists")
} }
} }
addClient(client: IClient) { addClient(client: IClient) {
Logger.log("Adding client...", client) this.logger.log("Adding client...", client)
const existingClient: IClient | undefined = this.localClients.find((localClient: IClient) => localClient.id === client.id && localClient.channel?.connection.id === client.channel?.connection.id); const existingClient: IClient | undefined = this.localClients.find((localClient: IClient) => localClient.id === client.id && localClient.channel?.connection.id === client.channel?.connection.id);
if (existingClient == undefined) { if (existingClient == undefined) {
this.localClients.push(client); this.localClients.push(client);
this.updateClientsState(); this.updateClientsState();
Logger.log("Client added") this.logger.log("Client added")
} else { } else {
Logger.log("Client already exists") this.logger.log("Client already exists")
} }
} }
// Update data in local lists and update states // Update data in local lists and update states
updateConnection(connection: IConnection) { updateConnection(connection: IConnection) {
Logger.log("Updating connection...", connection) this.logger.log("Updating connection...", connection)
const existingConnection: IConnection | undefined = this.localConnections.find((localConnection: IConnection) => localConnection.id === connection.id); const existingConnection: IConnection | undefined = this.localConnections.find((localConnection: IConnection) => localConnection.id === connection.id);
if (existingConnection !== undefined) { if (existingConnection !== undefined) {
this.localConnections[this.localConnections.indexOf(existingConnection)] = connection; this.localConnections[this.localConnections.indexOf(existingConnection)] = connection;
this.updateConnectionsState(); this.updateConnectionsState();
Logger.log("Connection updated") this.logger.log("Connection updated")
} else { } else {
Logger.log("Connection does not exist") this.logger.log("Connection does not exist")
} }
} }
updateChannel(channel: IChannel) { updateChannel(channel: IChannel) {
Logger.log("Updating channel...", channel) this.logger.log("Updating channel...", channel)
const existingChannel: IChannel | undefined = this.localChannels.find((localChannel: IChannel) => localChannel.id === channel.id && localChannel.connection.id === channel.connection.id); const existingChannel: IChannel | undefined = this.localChannels.find((localChannel: IChannel) => localChannel.id === channel.id && localChannel.connection.id === channel.connection.id);
if (existingChannel !== undefined) { if (existingChannel !== undefined) {
this.localChannels[this.localChannels.indexOf(existingChannel)] = channel; this.localChannels[this.localChannels.indexOf(existingChannel)] = channel;
this.updateChannelsState(); this.updateChannelsState();
Logger.log("Channel updated") this.logger.log("Channel updated")
} else { } else {
Logger.log("Channel does not exist") this.logger.log("Channel does not exist")
} }
} }
updateClient(client: IClient) { updateClient(client: IClient) {
Logger.log("Updating client...", client) this.logger.log("Updating client...", client)
const existingClient: IClient | undefined = this.localClients.find((localClient: IClient) => localClient.id === client.id && localClient.channel?.connection.id === client.channel?.connection.id); const existingClient: IClient | undefined = this.localClients.find((localClient: IClient) => localClient.id === client.id && localClient.channel?.connection.id === client.channel?.connection.id);
if (existingClient !== undefined) { if (existingClient !== undefined) {
this.localClients[this.localClients.indexOf(existingClient)] = client; this.localClients[this.localClients.indexOf(existingClient)] = client;
this.updateClientsState(); this.updateClientsState();
Logger.log("Client updated") this.logger.log("Client updated")
} else { } else {
Logger.log("Client does not exist") this.logger.log("Client does not exist")
} }
} }
// Remove data from local lists and update states // Remove data from local lists and update states
removeConnection(connection: IConnection) { removeConnection(connection: IConnection) {
Logger.log("Removing connection...", connection) this.logger.log("Removing connection...", connection)
const existingConnection: IConnection | undefined = this.localConnections.find((localConnection: IConnection) => localConnection.id === connection.id); const existingConnection: IConnection | undefined = this.localConnections.find((localConnection: IConnection) => localConnection.id === connection.id);
if (existingConnection !== undefined) { if (existingConnection !== undefined) {
@@ -153,14 +170,14 @@ export class TS5DataHandler implements ITS5DataHandler {
this.updateChannelsState(); this.updateChannelsState();
this.updateClientsState(); this.updateClientsState();
this.updateConnectionsState(); this.updateConnectionsState();
Logger.log("Connection removed") this.logger.log("Connection removed")
} else { } else {
Logger.log("Connection does not exist") this.logger.log("Connection does not exist")
} }
} }
removeChannel(channel: IChannel) { removeChannel(channel: IChannel) {
Logger.log("Removing channel...", channel) this.logger.log("Removing channel...", channel)
const existingChannel: IChannel | undefined = this.localChannels.find((localChannel: IChannel) => localChannel.id === channel.id && localChannel.connection.id === channel.connection.id); const existingChannel: IChannel | undefined = this.localChannels.find((localChannel: IChannel) => localChannel.id === channel.id && localChannel.connection.id === channel.connection.id);
if (existingChannel !== undefined) { if (existingChannel !== undefined) {
@@ -171,22 +188,22 @@ export class TS5DataHandler implements ITS5DataHandler {
this.updateClientsState(); this.updateClientsState();
this.updateChannelsState(); this.updateChannelsState();
Logger.log("Channel removed") this.logger.log("Channel removed")
} else { } else {
Logger.log("Channel does not exist") this.logger.log("Channel does not exist")
} }
} }
removeClient(client: IClient) { removeClient(client: IClient) {
Logger.log("Removing client...", client) this.logger.log("Removing client...", client)
const existingClient: IClient | undefined = this.localClients.find((localClient: IClient) => localClient.id === client.id && localClient.channel?.connection.id === client.channel?.connection.id); const existingClient: IClient | undefined = this.localClients.find((localClient: IClient) => localClient.id === client.id && localClient.channel?.connection.id === client.channel?.connection.id);
if (existingClient !== undefined) { if (existingClient !== undefined) {
this.localClients.splice(this.localClients.indexOf(existingClient), 1); this.localClients.splice(this.localClients.indexOf(existingClient), 1);
this.updateClientsState(); this.updateClientsState();
Logger.log("Client removed") this.logger.log("Client removed")
} else { } else {
Logger.log("Client does not exist") this.logger.log("Client does not exist")
} }
} }

View File

@@ -1,18 +1,21 @@
import Logger from "../../utils/logger"; import { ILogger } from "utils/logger";
import { IChannelInfos, IConnection, IChannel, IAuthMessage, IClientInfo, IClientMovedMessage, IClient, IClientPropertiesUpdatedMessage, ITalkStatusChangedMessage, IClientSelfPropertyUpdatedMessage, IServerPropertiesUpdatedMessage, IConnectStatusChangedMessage, IChannelsMessage, ITS5MessageHandler, ITS5DataHandler } from "../../interfaces/teamspeak"; import { IChannelInfos, IConnection, IChannel, IAuthMessage, IClientInfo, IClientMovedMessage, IClient, IClientPropertiesUpdatedMessage, ITalkStatusChangedMessage, IClientSelfPropertyUpdatedMessage, IServerPropertiesUpdatedMessage, IConnectStatusChangedMessage, IChannelsMessage, ITS5MessageHandler, ITS5DataHandler, IChannelCreatedMessage } from "interfaces/teamspeak";
import { log } from "console";
// Handle incoming messages from TS5 client // Handle incoming messages from TS5 client
export class TS5MessageHandler implements ITS5MessageHandler { export class TS5MessageHandler implements ITS5MessageHandler {
ws: WebSocket; ws: WebSocket;
dataHandler: ITS5DataHandler; dataHandler: ITS5DataHandler;
logger: ILogger;
setActiveConnectionStateId: React.Dispatch<React.SetStateAction<number>>; setActiveConnectionStateId: React.Dispatch<React.SetStateAction<number>>;
activeConnectionId = 0; activeConnectionId = 0;
constructor(ws: WebSocket, dataHandler: ITS5DataHandler, setActiveConnectionStateId: React.Dispatch<React.SetStateAction<number>>) { constructor(ws: WebSocket, dataHandler: ITS5DataHandler, setActiveConnectionStateId: React.Dispatch<React.SetStateAction<number>>, logger: ILogger) {
this.ws = ws; this.ws = ws;
this.dataHandler = dataHandler; this.dataHandler = dataHandler;
this.setActiveConnectionStateId = setActiveConnectionStateId; this.setActiveConnectionStateId = setActiveConnectionStateId;
this.logger = logger;
} }
setActiveConnection(connectionId: number) { setActiveConnection(connectionId: number) {
@@ -23,15 +26,13 @@ export class TS5MessageHandler implements ITS5MessageHandler {
parseChannelInfos(channelInfos: IChannelInfos, connection: IConnection) { parseChannelInfos(channelInfos: IChannelInfos, connection: IConnection) {
channelInfos.rootChannels.forEach((channel: IChannel) => { channelInfos.rootChannels.forEach((channel: IChannel) => {
this.dataHandler.addChannel({ ...channel, connection: connection }); this.dataHandler.addChannel({ ...channel, connection: connection });
if (channelInfos) {
if (channelInfos.subChannels !== null && channel.id in channelInfos.subChannels) {
channelInfos.subChannels[channel.id]?.forEach((subChannel: IChannel) => {
this.dataHandler.addChannel({ ...subChannel, connection: connection });
});
}
}
}); });
for (const key in channelInfos.subChannels) {
channelInfos.subChannels[key]?.forEach((subChannel: IChannel) => {
this.dataHandler.addChannel({ ...subChannel, connection: connection });
});
}
} }
// This message is sent by the TS5 server when the client is connected // This message is sent by the TS5 server when the client is connected
@@ -68,10 +69,9 @@ export class TS5MessageHandler implements ITS5MessageHandler {
handleClientMovedMessage(data: IClientMovedMessage) { handleClientMovedMessage(data: IClientMovedMessage) {
const client: IClient | undefined = this.dataHandler.getClientById(data.payload.clientId, data.payload.connectionId); const client: IClient | undefined = this.dataHandler.getClientById(data.payload.clientId, data.payload.connectionId);
//* This gets called when we are connecting to the server and the new clients get loaded //* This gets called when we are connecting to the server and the new clients get loaded
if (+data.payload.oldChannelId == 0) { // Create new client(when connecting to server) if (+data.payload.oldChannelId == 0) { // Create new client(when connecting to server)
//set timout to wait for channels to be created //set timeout to wait for channels to be created
setTimeout(() => { setTimeout(() => {
const newChannel = this.dataHandler.getChannelById(data.payload.newChannelId, data.payload.connectionId); const newChannel = this.dataHandler.getChannelById(data.payload.newChannelId, data.payload.connectionId);
if (newChannel !== undefined) { if (newChannel !== undefined) {
@@ -82,7 +82,7 @@ export class TS5MessageHandler implements ITS5MessageHandler {
channel: newChannel, channel: newChannel,
properties: data.payload.properties, properties: data.payload.properties,
}); });
Logger.ts(`New Client found (${data.payload.connectionId} - ${data.payload.clientId} - ${data.payload.properties.nickname})`) this.logger.ts(`New Client found (${data.payload.connectionId} - ${data.payload.clientId} - ${data.payload.properties.nickname})`)
} }
}, 2000); }, 2000);
@@ -90,7 +90,7 @@ export class TS5MessageHandler implements ITS5MessageHandler {
const newChannel: IChannel | undefined = this.dataHandler.getChannelById(data.payload.newChannelId, data.payload.connectionId); const newChannel: IChannel | undefined = this.dataHandler.getChannelById(data.payload.newChannelId, data.payload.connectionId);
if (newChannel === undefined || newChannel.id === 0) { if (newChannel === undefined || newChannel.id === 0) {
Logger.ts(`Client left (${data.payload.connectionId} - ${data.payload.clientId} - ${data.payload.properties.nickname})`) this.logger.ts(`Client left (${data.payload.connectionId} - ${data.payload.clientId} - ${data.payload.properties.nickname})`)
if (client !== undefined) { if (client !== undefined) {
this.dataHandler.removeClient(client); this.dataHandler.removeClient(client);
} }
@@ -98,7 +98,7 @@ export class TS5MessageHandler implements ITS5MessageHandler {
} }
if (client !== undefined) { // Client already exists if (client !== undefined) { // Client already exists
Logger.ts(`Client moved (${client.channel.connection.id} - ${client.id} - ${client.properties.nickname})`) this.logger.ts(`Client moved (${client.channel.connection.id} - ${client.id} - ${client.properties.nickname})`)
this.dataHandler.updateClient({ this.dataHandler.updateClient({
...client, ...client,
@@ -107,7 +107,7 @@ export class TS5MessageHandler implements ITS5MessageHandler {
} else { // Client does not exist } else { // Client does not exist
// Client joined // Client joined
Logger.ts(`Client joined (${data.payload.connectionId} - ${data.payload.clientId} - ${data.payload.properties.nickname})`) this.logger.ts(`Client joined (${data.payload.connectionId} - ${data.payload.clientId} - ${data.payload.properties.nickname})`)
this.dataHandler.addClient( this.dataHandler.addClient(
{ {
@@ -147,6 +147,7 @@ export class TS5MessageHandler implements ITS5MessageHandler {
// console.log(this.dataHandler.localClients) // console.log(this.dataHandler.localClients)
} }
handleClientSelfPropertyUpdatedMessage(data: IClientSelfPropertyUpdatedMessage) { handleClientSelfPropertyUpdatedMessage(data: IClientSelfPropertyUpdatedMessage) {
const connection: IConnection | undefined = this.dataHandler.getConnectionById(this.activeConnectionId); const connection: IConnection | undefined = this.dataHandler.getConnectionById(this.activeConnectionId);
@@ -194,4 +195,19 @@ export class TS5MessageHandler implements ITS5MessageHandler {
} }
}, 1000); }, 1000);
} }
handleChannelCreatedMessage(data: IChannelCreatedMessage) {
const connection = this.dataHandler.getConnectionById(data.payload.connectionId);
if (connection !== undefined) {
this.dataHandler.addChannel(
{
id: data.payload.channelId,
connection: connection,
order: data.payload.properties.order,
parentId: data.payload.parentId,
properties: data.payload.properties,
});
}
}
} }

View File

@@ -1,8 +1,9 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { TS5ConnectionHandler } from "../handlers/teamspeak/connectionHandler"; import { TS5ConnectionHandler } from "handlers/teamspeak/connectionHandler";
import { ITSRemoteAppOptions } from "../interfaces/api"; import { ITSRemoteAppOptions } from "interfaces/api";
import { IClient, IChannel, IConnection, ITS5ConnectionHandler } from "../interfaces/teamspeak"; import { IClient, IChannel, IConnection, ITS5ConnectionHandler } from "interfaces/teamspeak";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Logger from "utils/logger";
export default function useTSRemoteApp(options: ITSRemoteAppOptions) { export default function useTSRemoteApp(options: ITSRemoteAppOptions) {
const [clients, setClients] = useState<IClient[]>([]); const [clients, setClients] = useState<IClient[]>([]);
@@ -20,6 +21,7 @@ export default function useTSRemoteApp(options: ITSRemoteAppOptions) {
const tsConnection: ITS5ConnectionHandler = new TS5ConnectionHandler( const tsConnection: ITS5ConnectionHandler = new TS5ConnectionHandler(
options.remoteAppPort ?? 5899, options.remoteAppPort ?? 5899,
options.auth, options.auth,
new Logger(options.logging ?? false),
setConnections, setConnections,
setChannels, setChannels,
setClients, setClients,

View File

@@ -1,6 +1,6 @@
import useTSRemoteApp from "./hooks/useTSRemoteApp"; import useTSRemoteApp from "hooks/useTSRemoteApp";
export type { IChannel, IClient, IConnection, IAuthSenderPayload, IChannelInfos, IAuthMessage, IChannelProperties, IClientInfo, IServerProperties } from "./interfaces/teamspeak"; export type { IChannel, IClient, IConnection, IAuthSenderPayload, IChannelInfos, IAuthMessage, IChannelProperties, IClientInfo, IServerProperties } from "interfaces/teamspeak";
export default useTSRemoteApp; export default useTSRemoteApp;

View File

@@ -1,5 +1,6 @@
export interface ITSRemoteAppOptions { export interface ITSRemoteAppOptions {
remoteAppPort: number; remoteAppPort: number;
logging: boolean;
auth: ITSRemoteAppAuthPayloadOptions, auth: ITSRemoteAppAuthPayloadOptions,
} }

View File

@@ -1,10 +1,12 @@
import { ITSRemoteAppAuthPayloadOptions } from "./api"; import { ILogger } from "utils/logger";
import { ITSRemoteAppAuthPayloadOptions } from "interfaces/api";
// Classes // Classes
export interface ITS5ConnectionHandler { export interface ITS5ConnectionHandler {
ws: WebSocket; ws: WebSocket;
authenticated: boolean; authenticated: boolean;
remoteAppPort: number; remoteAppPort: number;
logger: ILogger;
authPayload: ITSRemoteAppAuthPayloadOptions; authPayload: ITSRemoteAppAuthPayloadOptions;
dataHandler: ITS5DataHandler; dataHandler: ITS5DataHandler;
messageHandler: ITS5MessageHandler; messageHandler: ITS5MessageHandler;
@@ -16,6 +18,7 @@ export interface ITS5DataHandler {
localConnections: IConnection[]; localConnections: IConnection[];
localChannels: IChannel[]; localChannels: IChannel[];
localClients: IClient[]; localClients: IClient[];
logger: ILogger;
setConnections: React.Dispatch<React.SetStateAction<IConnection[]>>; setConnections: React.Dispatch<React.SetStateAction<IConnection[]>>;
setChannels: React.Dispatch<React.SetStateAction<IChannel[]>>; setChannels: React.Dispatch<React.SetStateAction<IChannel[]>>;
setClients: React.Dispatch<React.SetStateAction<IClient[]>>; setClients: React.Dispatch<React.SetStateAction<IClient[]>>;
@@ -32,11 +35,16 @@ export interface ITS5DataHandler {
getConnectionById(id: number): IConnection | undefined; getConnectionById(id: number): IConnection | undefined;
getChannelById(id: number, connectionId: number): IChannel | undefined; getChannelById(id: number, connectionId: number): IChannel | undefined;
getClientById(id: number, connectionId: number): IClient | undefined; getClientById(id: number, connectionId: number): IClient | undefined;
readonly connections: IConnection[];
readonly channels: IChannel[];
readonly clients: IClient[];
} }
export interface ITS5MessageHandler { export interface ITS5MessageHandler {
ws: WebSocket; ws: WebSocket;
dataHandler: ITS5DataHandler; dataHandler: ITS5DataHandler;
logger: ILogger;
setActiveConnectionStateId: React.Dispatch<React.SetStateAction<number>>; setActiveConnectionStateId: React.Dispatch<React.SetStateAction<number>>;
activeConnectionId: number; activeConnectionId: number;
setActiveConnection(connectionId: number): void; setActiveConnection(connectionId: number): void;
@@ -49,6 +57,7 @@ export interface ITS5MessageHandler {
handleServerPropertiesUpdatedMessage(data: IServerPropertiesUpdatedMessage): void; handleServerPropertiesUpdatedMessage(data: IServerPropertiesUpdatedMessage): void;
handleConnectStatusChangedMessage(data: IConnectStatusChangedMessage): void; handleConnectStatusChangedMessage(data: IConnectStatusChangedMessage): void;
handleChannelsMessage(data: IChannelsMessage): void; handleChannelsMessage(data: IChannelsMessage): void;
handleChannelCreatedMessage(data: IChannelCreatedMessage): void;
} }
// Remote App // Remote App
@@ -341,4 +350,14 @@ export interface IChannelsMessage {
connectionId: number; connectionId: number;
info: IChannelInfos info: IChannelInfos
} }
}
export interface IChannelCreatedMessage {
type: "channelCreated";
payload: {
channelId: number;
connectionId: number;
parentId: string;
properties: IChannelProperties;
}
} }

View File

@@ -1,31 +1,53 @@
export default class Logger { export interface ILogger {
enabled: boolean;
log(message: string, data?: object | null): void;
warn(message: string, data?: object | null): void;
error(message: string, data?: object | null): void;
wsReceived(data: object, message?: string | undefined): void;
wsSent(data: object, message?: string | undefined): void;
ts(message: string, data?: object | null): void;
}
export default class Logger implements ILogger {
enabled: boolean;
constructor(enabled: boolean) {
this.enabled = enabled;
}
// Log message to the console // Log message to the console
public static log(message: string, data: object | null = null): void { public log(message: string, data: object | null = null): void {
if (!this.enabled) return;
console.log(`[Log] %c${message}`.trim(), "color: gray", data ?? ""); console.log(`[Log] %c${message}`.trim(), "color: gray", data ?? "");
} }
// Log warning to the console // Log warning to the console
public static warn(message: string, data: object | null = null): void { public warn(message: string, data: object | null = null): void {
if (!this.enabled) return;
console.warn(`%c${message}`.trim(), data ?? ""); console.warn(`%c${message}`.trim(), data ?? "");
} }
// Log error to the console // Log error to the console
public static error(message: string, data: object | null = null): void { public error(message: string, data: object | null = null): void {
if (!this.enabled) return;
console.error(`%c${message}`.trim(), data ?? ""); console.error(`%c${message}`.trim(), data ?? "");
} }
// Log message received from the websocket to the console // Log message received from the websocket to the console
public static wsReceived(data: object, message: string | undefined = undefined): void { public wsReceived(data: object, message: string | undefined = undefined): void {
if (!this.enabled) return;
console.log(`%c[WS Recieved] ${message ?? ""}`.trim(), "color: #8258c7", data); console.log(`%c[WS Recieved] ${message ?? ""}`.trim(), "color: #8258c7", data);
} }
// Log message sent to the websocket to the console // Log message sent to the websocket to the console
public static wsSent(data: object, message: string | undefined = undefined): void { public wsSent(data: object, message: string | undefined = undefined): void {
if (!this.enabled) return;
console.log(`%c[WS Sent] ${message ?? ""}`.trim(), "color: #4eb570", data); console.log(`%c[WS Sent] ${message ?? ""}`.trim(), "color: #4eb570", data);
} }
// Log message to the console with a timestamp // Log message to the console with a timestamp
public static ts(message: string, data: object | null = null): void { public ts(message: string, data: object | null = null): void {
if (!this.enabled) return;
console.log(`%c[TS] ${message}`.trim(), "color: #2e6bc7", data ?? ""); console.log(`%c[TS] ${message}`.trim(), "color: #2e6bc7", data ?? "");
} }
} }

View File

@@ -1 +1,5 @@
{ "extends": "./tsconfig.json", "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx"] } {
"extends": "./tsconfig.json",
"exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx"],
"compilerOptions": { "baseUrl": "./src" }
}

View File

@@ -9,7 +9,8 @@
"outDir": "dist", "outDir": "dist",
"strict": true, "strict": true,
"strictNullChecks": true, "strictNullChecks": true,
"target": "es5" "target": "es5",
"baseUrl": "./src"
}, },
"include": ["src"] "include": ["src"]
} }

View File

@@ -3,6 +3,7 @@
"composite": true, "composite": true,
"skipLibCheck": true, "skipLibCheck": true,
"module": "ESNext", "module": "ESNext",
"baseUrl": "./src",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true
}, },