Updating node structure
This commit is contained in:
@@ -5,7 +5,7 @@ import AppLayout from "@/components/app/AppLayout";
|
|||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import useApp from "@/hooks/use-app";
|
import useApp from "@/hooks/use-app";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import auth, { waitForOpen } from "@/lib/auth";
|
import { NodeClient } from "@/lib/node";
|
||||||
import useAsync from "@/hooks/use-async";
|
import useAsync from "@/hooks/use-async";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import useIndicators from "@/hooks/use-indicators";
|
import useIndicators from "@/hooks/use-indicators";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useParams, useSearchParams } from "next/navigation";
|
import { useParams, useSearchParams } from "next/navigation";
|
||||||
import AppLayout from "@/components/app/AppLayout";
|
import AppLayout from "@/components/app/AppLayout";
|
||||||
import auth from "@/lib/auth";
|
import auth from "@/lib/node";
|
||||||
import useMessages from "@/hooks/use-messages";
|
import useMessages from "@/hooks/use-messages";
|
||||||
import useApp from "@/hooks/use-app";
|
import useApp from "@/hooks/use-app";
|
||||||
import { useEffectOnceWhenReady } from "@/hooks/use-once";
|
import { useEffectOnceWhenReady } from "@/hooks/use-once";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React, { useEffect } from "react";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import auth from "@/lib/auth";
|
import auth from "@/lib/node";
|
||||||
import { useIsMobile } from "@/hooks/is-mobile";
|
import { useIsMobile } from "@/hooks/is-mobile";
|
||||||
import App from "@/types/app";
|
import App from "@/types/app";
|
||||||
import { Message } from "@/types/types";
|
import { Message } from "@/types/types";
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
}
|
||||||
-186
@@ -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<WebSocket | null>;
|
|
||||||
setServer?: Dispatch<SetStateAction<Server | undefined>>;
|
|
||||||
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<void> {
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+171
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user