commit 9ba7e762361ed3acd5eee6c66d5aa9fec24833bc Author: Janis Date: Mon Jul 24 01:00:28 2023 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30bc162 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..054d599 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "tabWidth": 2, + "useTabs": false, + "printWidth": 120 +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e17088d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,103 @@ +{ + "name": "react-ts5-remote-app-api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "react-ts5-remote-app-api", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@types/react": "^18.2.15", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": "^5.1.6" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + }, + "node_modules/@types/react": { + "version": "18.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.15.tgz", + "integrity": "sha512-oEjE7TQt1fFTFSbf8kkNuc798ahTUzn3Le67/PWjE8MAfYAD/qB7O8hSTcromLFqHCt9bcdOg5GXMokzTjJ5SA==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..feec7b0 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "react-ts5-remote-app-api", + "version": "1.0.0", + "description": "React hook/api for the TeamSpeak5 remote app feature", + "main": "app.js", + "scripts": { + "build": "tsc" + }, + "keywords": [ + "TeamSpeak5", + "TS5", + "TeamSpeak", + "RemoteApp", + "API", + "Hook", + "React", + "ReactJS", + "Remote" + ], + "author": "DerTyp7", + "license": "ISC", + "dependencies": { + "@types/react": "^18.2.15", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": "^5.1.6" + } +} diff --git a/src/handlers/teamspeak/connectionHandler.ts b/src/handlers/teamspeak/connectionHandler.ts new file mode 100644 index 0000000..6d6ad6a --- /dev/null +++ b/src/handlers/teamspeak/connectionHandler.ts @@ -0,0 +1,132 @@ +import { IAuthSenderPayload, IChannel, IClient, IConnection, ITS5ConnectionHandler, ITS5DataHandler, ITS5MessageHandler } from "@interfaces/teamspeak"; +import { TS5DataHandler } from "./dataHandler"; +import { TS5MessageHandler } from "./messageHandler"; +import Logger from "@/utils/logger"; + + +// Establish connection to TS5 client +// Main class +export class TS5ConnectionHandler implements ITS5ConnectionHandler { + ws: WebSocket; // Websocket connection to TS5 client + authenticated = false; // Is the connection authenticated? + remoteAppPort: number; // Port of TS5 client + dataHandler: ITS5DataHandler; // Handles data/lists and states + messageHandler: ITS5MessageHandler; // Handles messages received from TS5 client + + constructor( + // Port of TS5 client + remoteAppPort: number, + + // State setters for dataHandler + setConnections: React.Dispatch>, + setChannels: React.Dispatch>, + setClients: React.Dispatch>, + setActiveConnectionStateId: React.Dispatch>, + ) { + + + // Create websocket connection to TS5 client + this.remoteAppPort = remoteAppPort; + this.ws = new WebSocket(`ws://localhost:${this.remoteAppPort}`); + + // Create dataHandler and messageHandler + this.dataHandler = new TS5DataHandler(setConnections, setChannels, setClients); + this.messageHandler = new TS5MessageHandler(this.ws, this.dataHandler, setActiveConnectionStateId); + } + + reconnect() { + Logger.log("Reconnecting...") + this.ws.close(); + + this.ws = new WebSocket(`ws://localhost:${this.remoteAppPort}`); + + this.dataHandler.clearAll(); + this.authenticated = false; + this.connect(); + } + + // Connect to TS5 client + connect() { + Logger.log('Connecting to TS5 client...'); + + // Create authentication payload + const initalPayload: IAuthSenderPayload = { + type: "auth", + payload: { + identifier: "de.tealfire.obs", + version: "1.1.3", + name: "TS5 OBS Overlay", + description: "A OBS overlay for TS5 by DerTyp876", + content: { + apiKey: localStorage.getItem("apiKey") ?? "", + }, + }, + }; + + this.ws.onopen = () => { + // Send authentication payload to TS5 client + this.ws.send(JSON.stringify(initalPayload)); + Logger.wsSent(initalPayload); + }; + + this.ws.onclose = (event) => { + Logger.log("WebSocket connection closed", event); + + // 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") + if (!this.authenticated) { + Logger.log("WebSocket connection closed before authentication"); + localStorage.removeItem("apiKey"); + } + + setTimeout(() => { + this.reconnect(); + }, 2000); + }; + + // Handle messages received from TS5 client + // See TS5MessageHandler class + this.ws.onmessage = (event) => { + const data = JSON.parse(event.data); + + Logger.wsReceived(data) + + switch (data.type) { + case "auth": + this.messageHandler.handleAuthMessage(data); + this.authenticated = true; + break; + case "clientMoved": + this.messageHandler.handleClientMovedMessage(data); + break; + case "clientPropertiesUpdated": + this.messageHandler.handleClientPropertiesUpdatedMessage(data); + break; + case "talkStatusChanged": + this.messageHandler.handleTalkStatusChangedMessage(data); + break; + case "serverPropertiesUpdated": + this.messageHandler.handleServerPropertiesUpdatedMessage(data); + break; + case "connectStatusChanged": + this.messageHandler.handleConnectStatusChangedMessage(data); + break; + case "clientSelfPropertyUpdated": + this.messageHandler.handleClientSelfPropertyUpdatedMessage(data); + break; + case "channels": + this.messageHandler.handleChannelsMessage(data); + break; + default: + Logger.log(`No handler for event type: ${data.type}`); + break; + } + }; + } +} + + + + + + diff --git a/src/handlers/teamspeak/dataHandler.ts b/src/handlers/teamspeak/dataHandler.ts new file mode 100644 index 0000000..1df8df7 --- /dev/null +++ b/src/handlers/teamspeak/dataHandler.ts @@ -0,0 +1,205 @@ +import Logger from "@/utils/logger"; +import { IConnection, IChannel, IClient, ITS5DataHandler } from "@interfaces/teamspeak"; + + +/** + * Handles data received from TS5 client (list of connections, channels and clients) + * Updates the states of App.tsx + */ +export class TS5DataHandler implements ITS5DataHandler { + // Local lists of connections, channels and clients + // These lists are used to keep track of the data, independent of the App.tsx state + localConnections: IConnection[]; + localChannels: IChannel[]; + localClients: IClient[]; + + // State setters for App.tsx + setConnections: React.Dispatch>; + setChannels: React.Dispatch>; + setClients: React.Dispatch>; + + constructor( + // State setters for App.tsx + setConnections: React.Dispatch>, + setChannels: React.Dispatch>, + setClients: React.Dispatch> + ) { + this.setConnections = setConnections; + this.setChannels = setChannels; + this.setClients = setClients; + + this.localConnections = []; + this.localChannels = []; + this.localClients = []; + } + + // Update App.tsx states + private updateConnectionsState() { + this.setConnections([...this.localConnections]); + } + + private updateChannelsState() { + this.setChannels([...this.localChannels]); + } + + private updateClientsState() { + this.setClients([...this.localClients]); + } + + // Clear all data + clearAll() { + this.localConnections = []; + this.localChannels = []; + this.localClients = []; + + this.updateConnectionsState(); + this.updateChannelsState(); + this.updateClientsState(); + } + + // Add data to local lists and update states + addConnection(connection: IConnection) { + Logger.log("Adding connection...", connection) + + const existingConnection: IConnection | undefined = this.localConnections.find((localConnection: IConnection) => localConnection.id === connection.id); + + if (existingConnection == undefined) { + this.localConnections.push(connection); + this.updateConnectionsState(); + Logger.log("Connection added") + } else { + Logger.log("Connection already exists") + } + } + + addChannel(channel: IChannel) { + Logger.log("Adding channel...", channel) + const existingChannel: IChannel | undefined = this.localChannels.find((localChannel: IChannel) => localChannel.id === channel.id && localChannel.connection.id === channel.connection.id); + + if (existingChannel == undefined) { + this.localChannels.push(channel); + this.updateChannelsState(); + Logger.log("Channel added") + } else { + Logger.log("Channel already exists") + } + } + + addClient(client: IClient) { + 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); + + if (existingClient == undefined) { + this.localClients.push(client); + this.updateClientsState(); + Logger.log("Client added") + } else { + Logger.log("Client already exists") + } + } + + // Update data in local lists and update states + updateConnection(connection: IConnection) { + Logger.log("Updating connection...", connection) + const existingConnection: IConnection | undefined = this.localConnections.find((localConnection: IConnection) => localConnection.id === connection.id); + + if (existingConnection !== undefined) { + this.localConnections[this.localConnections.indexOf(existingConnection)] = connection; + this.updateConnectionsState(); + Logger.log("Connection updated") + } else { + Logger.log("Connection does not exist") + } + } + + updateChannel(channel: IChannel) { + Logger.log("Updating channel...", channel) + const existingChannel: IChannel | undefined = this.localChannels.find((localChannel: IChannel) => localChannel.id === channel.id && localChannel.connection.id === channel.connection.id); + + if (existingChannel !== undefined) { + this.localChannels[this.localChannels.indexOf(existingChannel)] = channel; + this.updateChannelsState(); + Logger.log("Channel updated") + } else { + Logger.log("Channel does not exist") + } + } + + updateClient(client: IClient) { + 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); + + if (existingClient !== undefined) { + this.localClients[this.localClients.indexOf(existingClient)] = client; + this.updateClientsState(); + Logger.log("Client updated") + } else { + Logger.log("Client does not exist") + } + } + + // Remove data from local lists and update states + removeConnection(connection: IConnection) { + Logger.log("Removing connection...", connection) + const existingConnection: IConnection | undefined = this.localConnections.find((localConnection: IConnection) => localConnection.id === connection.id); + + if (existingConnection !== undefined) { + this.localConnections.splice(this.localConnections.indexOf(existingConnection), 1); + + // Remove all channels and clients associated with the connection + this.localChannels = this.localChannels.filter((localChannel: IChannel) => localChannel.connection.id !== connection.id); + this.localClients = this.localClients.filter((localClient: IClient) => localClient.channel?.connection.id !== connection.id); + + this.updateChannelsState(); + this.updateClientsState(); + this.updateConnectionsState(); + Logger.log("Connection removed") + } else { + Logger.log("Connection does not exist") + } + } + + removeChannel(channel: IChannel) { + Logger.log("Removing channel...", channel) + const existingChannel: IChannel | undefined = this.localChannels.find((localChannel: IChannel) => localChannel.id === channel.id && localChannel.connection.id === channel.connection.id); + + if (existingChannel !== undefined) { + this.localChannels.splice(this.localChannels.indexOf(existingChannel), 1); + + // Remove all clients associated with the channel + this.localClients = this.localClients.filter((localClient: IClient) => localClient.channel?.id !== channel.id); + + this.updateClientsState(); + this.updateChannelsState(); + Logger.log("Channel removed") + } else { + Logger.log("Channel does not exist") + } + } + + removeClient(client: IClient) { + 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); + + if (existingClient !== undefined) { + this.localClients.splice(this.localClients.indexOf(existingClient), 1); + this.updateClientsState(); + Logger.log("Client removed") + } else { + Logger.log("Client does not exist") + } + } + + // Helper functions + getConnectionById(id: number): IConnection | undefined { + return this.localConnections.find((connection: IConnection) => connection.id === id); + } + + getChannelById(id: number, connectionId: number): IChannel | undefined { + return this.localChannels.find((channel: IChannel) => channel.id === id && channel.connection?.id === connectionId); + } + + getClientById(id: number, connectionId: number): IClient | undefined { + return this.localClients.find((client: IClient) => client.id === id && client.channel?.connection.id === connectionId); + } +} \ No newline at end of file diff --git a/src/handlers/teamspeak/messageHandler.ts b/src/handlers/teamspeak/messageHandler.ts new file mode 100644 index 0000000..3e5dd41 --- /dev/null +++ b/src/handlers/teamspeak/messageHandler.ts @@ -0,0 +1,197 @@ +import Logger from "@/utils/logger"; +import { IChannelInfos, IConnection, IChannel, IAuthMessage, IClientInfo, IClientMovedMessage, IClient, IClientPropertiesUpdatedMessage, ITalkStatusChangedMessage, IClientSelfPropertyUpdatedMessage, IServerPropertiesUpdatedMessage, IConnectStatusChangedMessage, IChannelsMessage, ITS5MessageHandler, ITS5DataHandler } from "@interfaces/teamspeak"; + +// Handle incoming messages from TS5 client +export class TS5MessageHandler implements ITS5MessageHandler { + ws: WebSocket; + dataHandler: ITS5DataHandler; + + setActiveConnectionStateId: React.Dispatch>; + activeConnectionId = 0; + + constructor(ws: WebSocket, dataHandler: ITS5DataHandler, setActiveConnectionStateId: React.Dispatch>) { + this.ws = ws; + this.dataHandler = dataHandler; + this.setActiveConnectionStateId = setActiveConnectionStateId; + } + + setActiveConnection(connectionId: number) { + this.activeConnectionId = connectionId; + this.setActiveConnectionStateId(connectionId); + } + + parseChannelInfos(channelInfos: IChannelInfos, connection: IConnection) { + channelInfos.rootChannels.forEach((channel: IChannel) => { + 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 }); + }); + } + } + }); + } + + // This message is sent by the TS5 server when the client is connected + // It contains the initial data + handleAuthMessage(data: IAuthMessage) { + localStorage.setItem("apiKey", data.payload.apiKey); + + // Process auth payload and add initial data + data.payload.connections.forEach((connection: IConnection) => { + this.dataHandler.addConnection(connection); + + // Add channels + if (connection.channelInfos !== undefined) { + this.parseChannelInfos(connection.channelInfos, connection); + } + + // Add clients + connection.clientInfos?.forEach((clientInfo: IClientInfo) => { + const clientChannel: IChannel | undefined = this.dataHandler.getChannelById(clientInfo.channelId, connection.id); + + if (clientChannel !== undefined) { + this.dataHandler.addClient({ + id: clientInfo.id, + talkStatus: 0, + channel: { ...clientChannel, connection: connection }, + properties: clientInfo.properties, + }); + } + }); + }); + } + + // This message is sent by the TS5 server when a client moves a channel OR joins/leaves the server + handleClientMovedMessage(data: IClientMovedMessage) { + 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 + if (+data.payload.oldChannelId == 0) { // Create new client(when connecting to server) + //set timout to wait for channels to be created + setTimeout(() => { + const newChannel = this.dataHandler.getChannelById(data.payload.newChannelId, data.payload.connectionId); + if (newChannel !== undefined) { + this.dataHandler.addClient( + { + id: data.payload.clientId, + talkStatus: 0, + channel: newChannel, + properties: data.payload.properties, + }); + Logger.ts(`New Client found (${data.payload.connectionId} - ${data.payload.clientId} - ${data.payload.properties.nickname})`) + } + }, 2000); + + } else {//* This gets called when a client moves a channel OR joins/leaves the server + const newChannel: IChannel | undefined = this.dataHandler.getChannelById(data.payload.newChannelId, data.payload.connectionId); + + if (newChannel === undefined || newChannel.id === 0) { + Logger.ts(`Client left (${data.payload.connectionId} - ${data.payload.clientId} - ${data.payload.properties.nickname})`) + if (client !== undefined) { + this.dataHandler.removeClient(client); + } + return; + } + + if (client !== undefined) { // Client already exists + Logger.ts(`Client moved (${client.channel.connection.id} - ${client.id} - ${client.properties.nickname})`) + + this.dataHandler.updateClient({ + ...client, + channel: newChannel, + }); + + } else { // Client does not exist + // Client joined + Logger.ts(`Client joined (${data.payload.connectionId} - ${data.payload.clientId} - ${data.payload.properties.nickname})`) + + this.dataHandler.addClient( + { + id: data.payload.clientId, + talkStatus: 0, + channel: newChannel, + properties: data.payload.properties, + } + ); + } + } + } + + handleClientPropertiesUpdatedMessage(data: IClientPropertiesUpdatedMessage) { + const client: IClient | undefined = this.dataHandler.getClientById(data.payload.clientId, data.payload.connectionId); + + if (client !== undefined) { + this.dataHandler.updateClient({ + ...client, + properties: data.payload.properties, + }); + } + } + + handleTalkStatusChangedMessage(data: ITalkStatusChangedMessage) { + const client: IClient | undefined = this.dataHandler.getClientById(data.payload.clientId, data.payload.connectionId); + + if (client !== undefined) { + this.dataHandler.updateClient({ + ...client, + talkStatus: data.payload.status, + }); + } + + // console.log(this.dataHandler.localConnections) + // console.log(this.dataHandler.localChannels) + // console.log(this.dataHandler.localClients) + + } + handleClientSelfPropertyUpdatedMessage(data: IClientSelfPropertyUpdatedMessage) { + const connection: IConnection | undefined = this.dataHandler.getConnectionById(this.activeConnectionId); + + if (data.payload.flag == "inputHardware" || connection == undefined) { // sadly thats the only way to detect if a server is active or not + this.setActiveConnection(data.payload.connectionId); + } + } + + handleServerPropertiesUpdatedMessage(data: IServerPropertiesUpdatedMessage) { + const connection: IConnection | undefined = this.dataHandler.getConnectionById(data.payload.connectionId); + + if (connection !== undefined) { // Update existing connection + this.dataHandler.updateConnection({ + ...connection, + properties: data.payload.properties, + }); + } + } + + handleConnectStatusChangedMessage(data: IConnectStatusChangedMessage) { + if (data.payload.status === 0) { // Disconnected from server + const connection: IConnection | undefined = this.dataHandler.getConnectionById(data.payload.connectionId); + + if (connection !== undefined) { + this.dataHandler.removeConnection(connection); + } + } + + // Status 1-3 are the connection steps (connecting, authenticating, etc.) (i guess) + + if (data.payload.status === 4) { // Connected to server + this.dataHandler.addConnection({ + id: data.payload.connectionId, + clientId: data.payload.info.clientId, + }); + } + } + + handleChannelsMessage(data: IChannelsMessage) { + // Wait a bit for the connection to be added + setTimeout(() => { + const connection: IConnection | undefined = this.dataHandler.getConnectionById(data.payload.connectionId); + if (connection !== undefined) { + this.parseChannelInfos(data.payload.info, connection); + } + }, 1000); + } +} \ No newline at end of file diff --git a/src/hooks/useTSRemoteApp.tsx b/src/hooks/useTSRemoteApp.tsx new file mode 100644 index 0000000..5bd2c86 --- /dev/null +++ b/src/hooks/useTSRemoteApp.tsx @@ -0,0 +1,50 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { TS5ConnectionHandler } from "@/handlers/teamspeak/connectionHandler"; +import { IClient, IChannel, IConnection, ITS5ConnectionHandler } from "@/interfaces/teamspeak"; +import { useEffect, useState } from "react"; + +export default function useTSRemoteApp({ remoteAppPort = 5899 }: { remoteAppPort: number }) { + const [clients, setClients] = useState([]); + const [channels, setChannels] = useState([]); + const [connections, setConnections] = useState([]); + const [activeConnectionId, setActiveConnectionId] = useState(1); + + const [currentConnection, setCurrentConnection] = useState(undefined); + const [currentChannel, setCurrentChannel] = useState(undefined); + const [currentClient, setCurrentClient] = useState(undefined); + + useEffect(() => { + const tsConnection: ITS5ConnectionHandler = new TS5ConnectionHandler( + remoteAppPort, + setConnections, + setChannels, + setClients, + setActiveConnectionId + ); + tsConnection.connect(); + }, []); + + useEffect(() => { + const currentConnection = connections.find((connection) => connection.id === activeConnectionId); + setCurrentConnection(currentConnection); + + if (currentConnection) { + const currentClient = clients.find((client) => client.id === currentConnection.clientId); + setCurrentClient(currentClient); + if (currentClient) { + const currentChannel = channels.find((channel) => channel.id === currentClient.channel?.id); + setCurrentChannel(currentChannel); + } + } + }, [clients, channels, connections, activeConnectionId]); + + return { + clients, + channels, + connections, + activeConnectionId, + currentConnection, + currentChannel, + currentClient, + }; +} diff --git a/src/interfaces/teamspeak.ts b/src/interfaces/teamspeak.ts new file mode 100644 index 0000000..5f50673 --- /dev/null +++ b/src/interfaces/teamspeak.ts @@ -0,0 +1,341 @@ +// Classes +export interface ITS5ConnectionHandler { + ws: WebSocket; + authenticated: boolean; + remoteAppPort: number; + dataHandler: ITS5DataHandler; + messageHandler: ITS5MessageHandler; + reconnect(): void; + connect(): void; +} + +export interface ITS5DataHandler { + localConnections: IConnection[]; + localChannels: IChannel[]; + localClients: IClient[]; + setConnections: React.Dispatch>; + setChannels: React.Dispatch>; + setClients: React.Dispatch>; + clearAll(): void; + addConnection(connection: IConnection): void; + addChannel(channel: IChannel): void; + addClient(client: IClient): void; + updateConnection(connection: IConnection): void; + updateChannel(channel: IChannel): void; + updateClient(client: IClient): void; + removeConnection(connection: IConnection): void; + removeChannel(channel: IChannel): void; + removeClient(client: IClient): void; + getConnectionById(id: number): IConnection | undefined; + getChannelById(id: number, connectionId: number): IChannel | undefined; + getClientById(id: number, connectionId: number): IClient | undefined; +} + +export interface ITS5MessageHandler { + ws: WebSocket; + dataHandler: ITS5DataHandler; + setActiveConnectionStateId: React.Dispatch>; + activeConnectionId: number; + setActiveConnection(connectionId: number): void; + parseChannelInfos(channelInfos: IChannelInfos, connection: IConnection): void; + handleAuthMessage(data: IAuthMessage): void; + handleClientMovedMessage(data: IClientMovedMessage): void; + handleClientPropertiesUpdatedMessage(data: IClientPropertiesUpdatedMessage): void; + handleTalkStatusChangedMessage(data: ITalkStatusChangedMessage): void; + handleClientSelfPropertyUpdatedMessage(data: IClientSelfPropertyUpdatedMessage): void; + handleServerPropertiesUpdatedMessage(data: IServerPropertiesUpdatedMessage): void; + handleConnectStatusChangedMessage(data: IConnectStatusChangedMessage): void; + handleChannelsMessage(data: IChannelsMessage): void; +} + +// Remote App +export interface IAuthSenderPayload { + type: "auth"; + payload: { + identifier: string; + version: string; + name: string; + description: string; + content: { + apiKey: string; + }; + }; +} + +export interface IClient { + id: number; + talkStatus: number; + channel: IChannel; + properties: IClientProperties; +} + +export interface IChannel { + id: number; + connection: IConnection; + order: string; + parentId: string; + properties: IChannelProperties; +} + +export interface IConnection { + channelInfos?: IChannelInfos; + clientId: number; + clientInfos?: IClientInfo[]; + id: number; + properties?: IServerProperties; +} + + +export interface IChannelProperties { + bannerGfxUrl: string; + bannerMode: number; + codec: number; + codecIsUnencrypted: boolean; + codecLatencyFactor: number; + codecQuality: number; + deleteDelay: number; + description: string; + flagAreSubscribed: boolean; + flagDefault: boolean; + flagMaxclientsUnlimited: boolean; + flagMaxfamilyclientsInherited: boolean; + flagMaxfamilyclientsUnlimited: boolean; + flagPassword: boolean; + flagPermanent: boolean; + flagSemiPermanent: boolean; + forcedSilence: boolean; + iconId: number; + maxclients: number; + maxfamilyclients: number; + name: string; + namePhonetic: string; + neededTalkPower: number; + order: string; + permissionHints: number; + storageQuota: number; + topic: string; + uniqueIdentifier: string; +} + + +export interface IChannelInfos { + rootChannels: IChannel[]; + subChannels: { [key: number]: IChannel[] }; +} + +export interface IClientInfo { + channelId: number; + id: number; + properties: IClientProperties; +} + +export interface IClientProperties { + away: boolean; + awayMessage: string; + badges: string; + channelGroupId: string; + channelGroupInheritedChannelId: string; + country: string; + created: number; + databaseId: string; + defaultChannel: string; + defaultChannelPassword: string; + defaultToken: string; + description: string; + flagAvatar: string; + flagTalking: boolean; + iconId: number; + idleTime: number; + inputDeactivated: boolean; + inputHardware: boolean; + inputMuted: boolean; + integrations: string; + isChannelCommander: boolean; + isMuted: boolean; + isPrioritySpeaker: boolean; + isRecording: boolean; + isTalker: boolean; + lastConnected: number; + metaData: string; + monthBytesDownloaded: number; + monthBytesUploaded: number; + myteamspeakAvatar: string; + myteamspeakId: string; + neededServerQueryViewPower: number; + nickname: string; + nicknamePhonetic: string; + outputHardware: boolean; + outputMuted: boolean; + outputOnlyMuted: boolean; + permissionHints: number; + platform: string; + serverGroups: string; + serverPassword: string; + signedBadges: string; + talkPower: number; + talkRequest: number; + talkRequestMsg: string; + totalBytesDownloaded: number; + totalBytesUploaded: number; + totalConnections: number; + type: number; + uniqueIdentifier: string; + unreadMessages: number; + userTag: string; + version: string; + volumeModificator: number; +} + +export interface IServerProperties { + antiFloodPointsNeededCommandBlock: number; + antiFloodPointsNeededIpBlock: number; + antiFloodPointsNeededPluginBlock: number; + antiFloodPointsTickReduce: number; + askForPrivilegeKeyAfterNickname: boolean; + askForPrivilegeKeyForChannelCreation: boolean; + askForPrivilegeKeyForModify: boolean; + awayMessage: string; + badges: string; + channelGroupId: string; + channelGroupInheritedChannelId: string; + clientType: number; + connectionBandwidthReceived: number; + connectionBandwidthSent: number; + connectionClientIp: string; + connectionConnectedTime: number; + connectionFiletransferBandwidthReceived: number; + connectionFiletransferBandwidthSent: number; + connectionPacketloss: number; + connectionPing: number; + connectionPacketsReceived: number; + connectionPacketsSent: number; + connectionPort: number; + connectionQueryBandwidthReceived: number; + connectionQueryBandwidthSent: number; + connectionServerIp: string; + connectionServerPort: number; + connectionThrottleBandwidthReceived: number; + connectionThrottleBandwidthSent: number; + country: string; + created: number; + defaultChannel: string; + defaultChannelPassword: string; + defaultServerGroup: string; + defaultToken: string; + flagAvatar: string; + iconId: number; + inputHardware: boolean; + inputMuted: boolean; + isChannelCommander: boolean; + isMuted: boolean; + isPrioritySpeaker: boolean; + isRecording: boolean; + isTalker: boolean; + isTts: boolean; + metaData: string; + monthBytesDownloaded: number; + monthBytesUploaded: number; + myteamspeakAvatar: string; + myteamspeakId: string; + neededServerQueryViewPower: number; + nickname: string; + nicknamePhonetic: string; + outputHardware: boolean; + outputMuted: boolean; + outputOnlyMuted: boolean; + permissionHints: number; + platform: string; + serverPassword: string; + signedBadges: string; + talkPower: number; + talkRequest: number; + talkRequestMsg: string; + totalBytesDownloaded: number; + totalBytesUploaded: number; + totalConnections: number; + type: number; + uniqueIdentifier: string; + unreadMessages: number; + userTag: string; + version: string; + volumeModificator: number; +} + + +export interface IClientPropertiesUpdatedMessage { + type: "clientPropertiesUpdated"; + payload: { + clientId: number; + connectionId: number; + properties: IClientProperties; + }; +} + +export interface IClientMovedMessage { + type: "clientMoved"; + payload: { + properties: IClientProperties; + clientId: number; + connectionId: number; + newChannelId: number; + oldChannelId: number; + type: number; + visibility: number; + }; +} + +export interface ITalkStatusChangedMessage { + type: "talkStatusChanged"; + payload: { + clientId: number; + connectionId: number; + isWhisper: boolean; + status: number; + }; +} + +export interface IClientSelfPropertyUpdatedMessage { + type: "clientSelfPropertyUpdated"; + payload: { + connectionId: number; + flag: string; + newValue: boolean; + oldValue: boolean; + }; +} + +export interface IAuthMessage { + type: "auth"; + payload: { + apiKey: string; + connections: IConnection[]; + }; +} + +export interface IServerPropertiesUpdatedMessage { + type: "serverPropertiesUpdated"; + payload: { + connectionId: number; + properties: IServerProperties; + }; +} + +export interface IConnectStatusChangedMessage { + type: "connectStatusChanged"; + payload: { + connectionId: number; + error: number; + info: { + clientId: number; + } + status: number; + }; +} + +export interface IChannelsMessage { + type: "channels"; + payload: { + connectionId: number; + info: IChannelInfos + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bb4321b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,31 @@ +{ + "include": ["src"], + "exclude": ["dist", "node_modules"], + "compilerOptions": { + "module": "esnext", + "lib": ["dom", "esnext"], + "importHelpers": true, + "declaration": true, + "sourceMap": true, + "rootDir": "./src", + "outDir": "./dist/esm", + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "moduleResolution": "node", + "jsx": "react", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@assets/*": ["src/assets/*"], + "@utils/*": ["src/utils/*"], + "@interfaces/*": ["src/interfaces/*"], + "@handlers/*": ["src/handlers/*"] + } + } +}