175 lines
5.2 KiB
TypeScript
175 lines
5.2 KiB
TypeScript
import { createServer } from "node:http";
|
|
import { WebSocketServer } from "npm:ws";
|
|
import type {
|
|
WebSocket as WSWebSocket,
|
|
WebSocketServer as _WebSocketServer,
|
|
} from "npm:@types/ws";
|
|
import { authenticateUser, elevenLabsApiKey } from "./utils.ts";
|
|
import {
|
|
createFirstMessage,
|
|
createSystemPrompt,
|
|
getChatHistory,
|
|
getSupabaseClient,
|
|
} from "./supabase.ts";
|
|
import { SupabaseClient } from "@supabase/supabase-js";
|
|
import { isDev } from "./utils.ts";
|
|
import { connectToOpenAI } from "./models/openai.ts";
|
|
import { connectToGemini } from "./models/gemini.ts";
|
|
import { connectToElevenLabs } from "./models/elevenlabs.ts";
|
|
import { connectToHume } from "./models/hume.ts";
|
|
import { connectToGrok } from "./models/grok.ts";
|
|
|
|
const server = createServer();
|
|
|
|
const wss: _WebSocketServer = new WebSocketServer({ noServer: true,
|
|
perMessageDeflate: false,
|
|
});
|
|
|
|
wss.on('headers', (headers, req) => {
|
|
// You should NOT see any "Sec-WebSocket-Extensions" here
|
|
console.log('WS response headers :', headers);
|
|
});
|
|
|
|
wss.on("connection", async (ws: WSWebSocket, payload: IPayload) => {
|
|
const { user, supabase } = payload;
|
|
|
|
let connectionPcmFile: Deno.FsFile | null = null;
|
|
if (isDev) {
|
|
const filename = `debug_audio_${Date.now()}.pcm`;
|
|
connectionPcmFile = await Deno.open(filename, {
|
|
create: true,
|
|
write: true,
|
|
append: true,
|
|
});
|
|
}
|
|
|
|
const chatHistory = await getChatHistory(
|
|
supabase,
|
|
user.user_id,
|
|
user.personality?.key ?? null,
|
|
false,
|
|
);
|
|
const firstMessage = createFirstMessage(payload);
|
|
const systemPrompt = createSystemPrompt(chatHistory, payload);
|
|
|
|
const provider = user.personality?.provider;
|
|
|
|
// send user details to client
|
|
// when DEV_MODE is true, we send the default values 100, false, false
|
|
ws.send(
|
|
JSON.stringify({
|
|
type: "auth",
|
|
volume_control: user.device?.volume ?? 20,
|
|
is_ota: user.device?.is_ota ?? false,
|
|
is_reset: user.device?.is_reset ?? false,
|
|
pitch_factor: user.personality?.pitch_factor ?? 1,
|
|
}),
|
|
);
|
|
|
|
// Common close handler for cleanup
|
|
const closeHandler = async () => {
|
|
// Add any common cleanup logic here
|
|
};
|
|
|
|
// Common provider args
|
|
const providerArgs: ProviderArgs = {
|
|
ws,
|
|
payload,
|
|
connectionPcmFile,
|
|
firstMessage,
|
|
systemPrompt,
|
|
closeHandler,
|
|
};
|
|
|
|
switch (provider) {
|
|
case "openai":
|
|
await connectToOpenAI(providerArgs);
|
|
break;
|
|
case "gemini":
|
|
await connectToGemini(providerArgs);
|
|
break;
|
|
case "grok":
|
|
await connectToGrok(providerArgs);
|
|
break;
|
|
case "elevenlabs":
|
|
const agentId = user.personality?.oai_voice ?? "";
|
|
|
|
if (!elevenLabsApiKey) {
|
|
throw new Error("ELEVENLABS_API_KEY environment variable is required");
|
|
}
|
|
|
|
await connectToElevenLabs(
|
|
ws,
|
|
payload,
|
|
connectionPcmFile,
|
|
agentId,
|
|
elevenLabsApiKey,
|
|
closeHandler,
|
|
);
|
|
break;
|
|
case "hume":
|
|
await connectToHume(providerArgs);
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown provider: ${provider}`);
|
|
}
|
|
});
|
|
|
|
server.on("upgrade", async (req, socket, head) => {
|
|
console.log('foobar upgrade', req.headers);
|
|
let user: IUser;
|
|
let supabase: SupabaseClient;
|
|
let authToken: string;
|
|
try {
|
|
const {
|
|
authorization: authHeader,
|
|
"x-wifi-rssi": rssi,
|
|
"x-device-mac": deviceMac,
|
|
} = req.headers;
|
|
authToken = authHeader?.replace("Bearer ", "") ?? "";
|
|
const wifiStrength = parseInt(rssi as string); // Convert to number
|
|
|
|
// You can now use wifiStrength in your code
|
|
console.log("WiFi RSSI:", wifiStrength); // Will log something like -50
|
|
|
|
// Remove debug logging
|
|
if (!authToken) {
|
|
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
|
|
supabase = getSupabaseClient(authToken as string);
|
|
user = await authenticateUser(supabase, authToken as string);
|
|
|
|
// allow any mac address for dev
|
|
const expectedMac = user.device?.mac_address;
|
|
if (!isDev && deviceMac && deviceMac !== expectedMac) {
|
|
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
} catch (_e: any) {
|
|
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
|
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
wss.emit("connection", ws, {
|
|
user,
|
|
supabase,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
});
|
|
});
|
|
|
|
if (isDev) { // RUN WITH: deno run -A --env-file=.env main.ts
|
|
const HOST = Deno.env.get("HOST") || "0.0.0.0";
|
|
const PORT = Deno.env.get("PORT") || "8000";
|
|
server.listen(Number(PORT), HOST, () => {
|
|
console.log(`Audio capture server running on ws://${HOST}:${PORT}`);
|
|
});
|
|
} else {
|
|
server.listen(8080);
|
|
}
|