Merge branch 'main' of https://github.com/slopus/happy-server
This commit is contained in:
commit
ae583ffaff
6
.env.dev
6
.env.dev
@ -18,3 +18,9 @@ S3_ACCESS_KEY=minioadmin
|
|||||||
S3_SECRET_KEY=minioadmin
|
S3_SECRET_KEY=minioadmin
|
||||||
S3_BUCKET=happy
|
S3_BUCKET=happy
|
||||||
S3_PUBLIC_URL=http://localhost:9000/happy
|
S3_PUBLIC_URL=http://localhost:9000/happy
|
||||||
|
|
||||||
|
# --- Voice Feature ---
|
||||||
|
# 11Labs API for voice conversations (required for voice feature)
|
||||||
|
# Secret - congiured in .env, not checked in
|
||||||
|
|
||||||
|
NODE_ENV=development
|
||||||
|
@ -13,6 +13,7 @@ import { startSocket } from "./socket";
|
|||||||
import { machinesRoutes } from "./routes/machinesRoutes";
|
import { machinesRoutes } from "./routes/machinesRoutes";
|
||||||
import { devRoutes } from "./routes/devRoutes";
|
import { devRoutes } from "./routes/devRoutes";
|
||||||
import { versionRoutes } from "./routes/versionRoutes";
|
import { versionRoutes } from "./routes/versionRoutes";
|
||||||
|
import { voiceRoutes } from "./routes/voiceRoutes";
|
||||||
import { enableMonitoring } from "./utils/enableMonitoring";
|
import { enableMonitoring } from "./utils/enableMonitoring";
|
||||||
import { enableErrorHandlers } from "./utils/enableErrorHandlers";
|
import { enableErrorHandlers } from "./utils/enableErrorHandlers";
|
||||||
import { enableAuthentication } from "./utils/enableAuthentication";
|
import { enableAuthentication } from "./utils/enableAuthentication";
|
||||||
@ -55,6 +56,7 @@ export async function startApi(eventRouter: EventRouter) {
|
|||||||
machinesRoutes(typed, eventRouter);
|
machinesRoutes(typed, eventRouter);
|
||||||
devRoutes(typed);
|
devRoutes(typed);
|
||||||
versionRoutes(typed);
|
versionRoutes(typed);
|
||||||
|
voiceRoutes(typed);
|
||||||
|
|
||||||
// Start HTTP
|
// Start HTTP
|
||||||
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3005;
|
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3005;
|
||||||
|
113
sources/app/api/routes/voiceRoutes.ts
Normal file
113
sources/app/api/routes/voiceRoutes.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { type Fastify } from "../types";
|
||||||
|
import { log } from "@/utils/log";
|
||||||
|
|
||||||
|
export function voiceRoutes(app: Fastify) {
|
||||||
|
app.post('/v1/voice/token', {
|
||||||
|
preHandler: app.authenticate,
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
agentId: z.string(),
|
||||||
|
revenueCatPublicKey: z.string().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
allowed: z.boolean(),
|
||||||
|
token: z.string().optional(),
|
||||||
|
agentId: z.string().optional()
|
||||||
|
}),
|
||||||
|
400: z.object({
|
||||||
|
allowed: z.boolean(),
|
||||||
|
error: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, async (request, reply) => {
|
||||||
|
const userId = request.userId; // CUID from JWT
|
||||||
|
const { agentId, revenueCatPublicKey } = request.body;
|
||||||
|
|
||||||
|
log({ module: 'voice' }, `Voice token request from user ${userId}`);
|
||||||
|
|
||||||
|
const isDevelopment = process.env.NODE_ENV === 'development' || process.env.ENV === 'dev';
|
||||||
|
|
||||||
|
// Production requires RevenueCat key
|
||||||
|
if (!isDevelopment && !revenueCatPublicKey) {
|
||||||
|
log({ module: 'voice' }, 'Production environment requires RevenueCat public key');
|
||||||
|
return reply.code(400).send({
|
||||||
|
allowed: false,
|
||||||
|
error: 'RevenueCat public key required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check subscription in production
|
||||||
|
if (!isDevelopment && revenueCatPublicKey) {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.revenuecat.com/v1/subscribers/${userId}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${revenueCatPublicKey}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
log({ module: 'voice' }, `RevenueCat check failed for user ${userId}: ${response.status}`);
|
||||||
|
return reply.send({
|
||||||
|
allowed: false,
|
||||||
|
agentId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json() as any;
|
||||||
|
const proEntitlement = data.subscriber?.entitlements?.active?.pro;
|
||||||
|
|
||||||
|
if (!proEntitlement) {
|
||||||
|
log({ module: 'voice' }, `User ${userId} does not have active subscription`);
|
||||||
|
return reply.send({
|
||||||
|
allowed: false,
|
||||||
|
agentId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if 11Labs API key is configured
|
||||||
|
const elevenLabsApiKey = process.env.ELEVENLABS_API_KEY;
|
||||||
|
if (!elevenLabsApiKey) {
|
||||||
|
log({ module: 'voice' }, 'Missing 11Labs API key');
|
||||||
|
return reply.code(400).send({ allowed: false, error: 'Missing 11Labs API key on the server' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 11Labs conversation token
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.elevenlabs.io/v1/convai/conversation/token?agent_id=${agentId}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'xi-api-key': elevenLabsApiKey,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
log({ module: 'voice' }, `Failed to get 11Labs token for user ${userId}`);
|
||||||
|
return reply.code(400).send({
|
||||||
|
allowed: false,
|
||||||
|
error: `Failed to get 11Labs token for user ${userId}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json() as any;
|
||||||
|
console.log(data);
|
||||||
|
const token = data.token;
|
||||||
|
|
||||||
|
log({ module: 'voice' }, `Voice token issued for user ${userId}`);
|
||||||
|
return reply.send({
|
||||||
|
allowed: true,
|
||||||
|
token,
|
||||||
|
agentId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user