From 6a7ea0cd8d90686b08237b082baeb88635216457 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 9 Jan 2026 18:52:22 +0100 Subject: [PATCH] Updating node structure --- src/app/chat/[id]/page.tsx | 2 +- src/app/server/[ip]/page.tsx | 2 +- src/components/app/AppLayout.tsx | 2 +- src/hooks/use-node.ts | 17 +++ src/lib/auth.ts | 186 ------------------------------- src/lib/node.ts | 171 ++++++++++++++++++++++++++++ 6 files changed, 191 insertions(+), 189 deletions(-) create mode 100644 src/hooks/use-node.ts delete mode 100644 src/lib/auth.ts create mode 100644 src/lib/node.ts diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index 1920354..6816c58 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -5,7 +5,7 @@ import AppLayout from "@/components/app/AppLayout"; import { useParams, useRouter } from "next/navigation"; import useApp from "@/hooks/use-app"; import { useEffect, useRef, useState } from "react"; -import auth, { waitForOpen } from "@/lib/auth"; +import { NodeClient } from "@/lib/node"; import useAsync from "@/hooks/use-async"; import { Button } from "@/components/ui/button"; import useIndicators from "@/hooks/use-indicators"; diff --git a/src/app/server/[ip]/page.tsx b/src/app/server/[ip]/page.tsx index 9e3b012..366c5fc 100644 --- a/src/app/server/[ip]/page.tsx +++ b/src/app/server/[ip]/page.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react"; import { useParams, useSearchParams } from "next/navigation"; import AppLayout from "@/components/app/AppLayout"; -import auth from "@/lib/auth"; +import auth from "@/lib/node"; import useMessages from "@/hooks/use-messages"; import useApp from "@/hooks/use-app"; import { useEffectOnceWhenReady } from "@/hooks/use-once"; diff --git a/src/components/app/AppLayout.tsx b/src/components/app/AppLayout.tsx index 4717a75..18ab178 100644 --- a/src/components/app/AppLayout.tsx +++ b/src/components/app/AppLayout.tsx @@ -4,7 +4,7 @@ import React, { useEffect } from "react"; import { cn } from "@/lib/utils"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; -import auth from "@/lib/auth"; +import auth from "@/lib/node"; import { useIsMobile } from "@/hooks/is-mobile"; import App from "@/types/app"; import { Message } from "@/types/types"; diff --git a/src/hooks/use-node.ts b/src/hooks/use-node.ts new file mode 100644 index 0000000..5927384 --- /dev/null +++ b/src/hooks/use-node.ts @@ -0,0 +1,17 @@ +import { useEffect } from "react"; +import { MessageStore } from "./use-messages"; +import { NodeCallbacks, NodeClient } from "@/lib/node"; + +export default function useNode( + nodeId: string, + messageStore: MessageStore, + callbacks: NodeCallbacks +) { + useEffect(() => { + const client = new NodeClient(nodeId, messageStore, callbacks); + + client.connect(); + + return () => client.disconnect(); + }, [nodeId]); +} diff --git a/src/lib/auth.ts b/src/lib/auth.ts deleted file mode 100644 index 155c4b5..0000000 --- a/src/lib/auth.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { Message, Server } from "@/types/types"; -import { Dispatch, RefObject, SetStateAction } from "react"; -import { toast } from "sonner"; -import { MessageStore } from "@/hooks/use-messages"; -import { IndicatorContext, ServerMessage } from "@/types/protocol"; -import { authRelay } from "@/actions/auth"; -import { getServerById } from "@/actions/get-server"; -import { playPCM16 } from "./audio"; - -type AuthParams = { - id: string; - wsRef: RefObject; - setServer?: Dispatch>; - onNewMessage?: (m: Message) => void; - messageStore: MessageStore; - onIndicator?: (i: IndicatorContext) => void; - onVoice?: (userId: string, bytes: number[]) => void; - onVoiceJoin?: (userId: string, channelId: string, voiceId: number) => void; - onVoiceLeave?: (userId: string, channelId: string, voiceId: number) => void; - onSpeaking?: (voiceId: number) => void; -}; - -export default async function auth({ - id, - wsRef, - setServer, - onNewMessage, - messageStore, - onIndicator, - onVoiceJoin, - onVoiceLeave, - onSpeaking, -}: AuthParams) { - console.log("Authenticating with server id:", id); - - if (!id) return; - - const ip = (await getServerById(id)).address; - - console.log("Authenticating with server at:", ip); - const server_auth = await authRelay(id); - - var name = id; - - // Open the WebSocket connection - const ws = new WebSocket(`wss://${ip}`); - wsRef.current = ws; - - ws.onopen = () => { - console.log("Connected to WebSocket:", ip); - }; - - ws.onmessage = async (m) => { - if (m.data instanceof Blob) { - const buffer = await m.data.arrayBuffer(); - const bytes = new Uint8Array(buffer); - playPCM16(bytes); - if (onSpeaking) onSpeaking(new DataView(buffer).getUint16(0, true)); - return; - } - - var data = JSON.parse(m.data); - console.log("Message received:", data); - - if (data.error) { - toast.error(`Error from ${name} (${data.error})`, { - description: data.message, - }); - return; - } - - if (data.version) { - if (setServer) setServer({ channels: [], ...data }); - ws.send( - JSON.stringify({ - version: "0.0.1", - auth_token: server_auth, - }) - ); - name = data.name; - return; - } - - const msg = data as ServerMessage; - - switch (msg.type) { - case "authenticated": - if (onIndicator) msg.params.indicators.forEach(onIndicator); - if (onVoiceJoin) - Object.entries(msg.params.voice_chat).forEach(([channelId, users]) => - Object.entries(users).forEach(([userId, voiceId]) => - onVoiceJoin(userId, channelId, voiceId) - ) - ); - break; - - case "message_create": - if (onNewMessage) onNewMessage(msg.params as Message); - messageStore.addMessage(msg.params as Message); - break; - - case "message_delete": - messageStore.deleteMessage(msg.params.message_id); - break; - - case "message_update": - messageStore.editMessage(msg.params.message_id, msg.params.contents); - break; - case "chunk": - messageStore.insertMessages(msg.params); - break; - case "indicator": - if (!onIndicator) break; - onIndicator(msg.params); - break; - case "voice_join": - if (!onVoiceJoin) break; - onVoiceJoin( - msg.params.user_id, - msg.params.channel_id, - msg.params.voice_id - ); - break; - case "voice_leave": - if (!onVoiceLeave) break; - onVoiceLeave( - msg.params.user_id, - msg.params.channel_id, - msg.params.voice_id - ); - break; - } - }; - - ws.onerror = (err) => { - console.error("WebSocket error:", err); - - let description = "An unknown WebSocket error occurred."; - - if (err instanceof ErrorEvent) { - // Browser WebSocket errors show up here - description = - err.message || err.error?.message || "WebSocket connection failed."; - } else if (err instanceof Event) { - // Generic Event (no info) - description = "WebSocket encountered a network issue."; - } else if (err && typeof err === "object") { - // Custom data if something special got passed - description = JSON.stringify(err, null, 2); - } else { - description = String(err); - } - - toast.error(`WebSocket error (${name})`, { description }); - }; - - ws.onclose = () => { - console.log("WebSocket closed: ", ip); - }; - - // Cleanup on unmount or id change - return () => { - ws.close(); - }; -} - -export async function waitForOpen(ws: WebSocket | null): Promise { - if (ws?.readyState === WebSocket.OPEN) return Promise.resolve(); - - return new Promise((resolve, reject) => { - const onOpen = () => { - ws?.removeEventListener("open", onOpen); - ws?.removeEventListener("error", onError); - resolve(); - }; - - const onError = (e: Event) => { - ws?.removeEventListener("open", onOpen); - ws?.removeEventListener("error", onError); - reject(e); - }; - - ws?.addEventListener("open", onOpen); - ws?.addEventListener("error", onError); - }); -} diff --git a/src/lib/node.ts b/src/lib/node.ts new file mode 100644 index 0000000..4449643 --- /dev/null +++ b/src/lib/node.ts @@ -0,0 +1,171 @@ +import { Message, Server } from "@/types/types"; +import { toast } from "sonner"; +import { MessageStore } from "@/hooks/use-messages"; +import { IndicatorContext, ServerMessage } from "@/types/protocol"; +import { authRelay } from "@/actions/auth"; +import { getServerById } from "@/actions/get-server"; +import { playPCM16 } from "./audio"; + +export interface NodeCallbacks { + setServer?: (s: Server) => void; + onNewMessage?: (m: Message) => void; + onIndicator?: (i: IndicatorContext) => void; + onVoiceJoin?: (userId: string, channelId: string, voiceId: number) => void; + onVoiceLeave?: (userId: string, channelId: string, voiceId: number) => void; + onSpeaking?: (voiceId: number) => void; +}; + +export class NodeClient { + private ws: WebSocket | null = null; + private name: string; + private readonly serverId: string; + private readonly messageStore: MessageStore; + private readonly callbacks: NodeCallbacks; + + constructor( + serverId: string, + messageStore: MessageStore, + callbacks: NodeCallbacks = {} + ) { + this.serverId = serverId; + this.messageStore = messageStore; + this.callbacks = callbacks; + this.name = serverId; + } + + get socket() { + return this.ws; + } + + async connect() { + if (!this.serverId) return; + + const server = await getServerById(this.serverId); + const authToken = await authRelay(this.serverId); + + this.ws = new WebSocket(`wss://${server.address}`); + + this.ws.onopen = () => { + console.log("WebSocket connected:", server.address); + }; + + this.ws.onmessage = (e) => this.handleMessage(e, authToken); + this.ws.onerror = (e) => this.handleError(e); + this.ws.onclose = () => { + console.log("WebSocket closed:", server.address); + }; + } + + disconnect() { + this.ws?.close(); + this.ws = null; + } + + private async handleMessage(e: MessageEvent, authToken: string) { + // Voice packet + if (e.data instanceof Blob) { + const buffer = await e.data.arrayBuffer(); + const bytes = new Uint8Array(buffer); + + playPCM16(bytes); + + if (this.callbacks.onSpeaking) { + const voiceId = new DataView(buffer).getUint16(0, true); + this.callbacks.onSpeaking(voiceId); + } + return; + } + + const data = JSON.parse(e.data); + + if (data.error) { + toast.error(`Error from ${this.name} (${data.error})`, { + description: data.message, + }); + return; + } + + // Handshake + if (data.version) { + this.callbacks.setServer?.({ channels: [], ...data }); + + this.ws?.send( + JSON.stringify({ + version: "0.0.1", + auth_token: authToken, + }) + ); + + this.name = data.name; + return; + } + + this.handleServerMessage(data as ServerMessage); + } + + private handleServerMessage(msg: ServerMessage) { + switch (msg.type) { + case "authenticated": + msg.params.indicators.forEach((i) => this.callbacks.onIndicator?.(i)); + + Object.entries(msg.params.voice_chat).forEach(([channelId, users]) => + Object.entries(users).forEach(([userId, voiceId]) => + this.callbacks.onVoiceJoin?.(userId, channelId, voiceId) + ) + ); + break; + + case "message_create": + this.callbacks.onNewMessage?.(msg.params); + this.messageStore.addMessage(msg.params); + break; + + case "message_delete": + this.messageStore.deleteMessage(msg.params.message_id); + break; + + case "message_update": + this.messageStore.editMessage( + msg.params.message_id, + msg.params.contents + ); + break; + + case "chunk": + this.messageStore.insertMessages(msg.params); + break; + + case "indicator": + this.callbacks.onIndicator?.(msg.params); + break; + + case "voice_join": + this.callbacks.onVoiceJoin?.( + msg.params.user_id, + msg.params.channel_id, + msg.params.voice_id + ); + break; + + case "voice_leave": + this.callbacks.onVoiceLeave?.( + msg.params.user_id, + msg.params.channel_id, + msg.params.voice_id + ); + break; + } + } + + private handleError(err: Event) { + console.error("WebSocket error:", err); + + let description = "WebSocket connection failed."; + + if (err instanceof ErrorEvent) { + description = err.message || description; + } + + toast.error(`WebSocket error (${this.name})`, { description }); + } +}