feat: add cloudonix outbound telephony (#101)

Co-authored-by: Sabiha Khan <sabihak89@gmail.com>
This commit is contained in:
Nir Simionovich 2026-01-03 08:32:21 +02:00 committed by GitHub
parent a33fa6cffe
commit 90b690efff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1080 additions and 47 deletions

View file

@ -45,6 +45,8 @@ ENV NEXT_PUBLIC_CHATWOOT_URL="https://chat.dograh.com"
ENV NEXT_PUBLIC_CHATWOOT_TOKEN="3fkFx2mCEjNHjM9gaNc4A82X"
# Build the application with standalone mode
# Increase Node.js heap size to prevent out-of-memory errors during build
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN npm run build && \
rm -rf /tmp/* /root/.npm /root/.next/cache

View file

@ -1,11 +1,19 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { getTelephonyConfigurationApiV1OrganizationsTelephonyConfigGet, saveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPost } from "@/client/sdk.gen";
import type { TwilioConfigurationRequest, VobizConfigurationRequest,VonageConfigurationRequest } from "@/client/types.gen";
import type {
CloudonixConfigurationRequest,
CloudonixConfigurationResponse,
TelephonyConfigurationResponse,
TwilioConfigurationRequest,
VobizConfigurationRequest,
VonageConfigurationRequest
} from "@/client/types.gen";
import { Button } from "@/components/ui/button";
import {
Card,
@ -39,19 +47,22 @@ interface TelephonyConfigForm {
// Vobiz fields
auth_id?: string;
vobiz_auth_token?: string;
// Cloudonix fields
bearer_token?: string;
domain_id?: string;
// Common field
from_number: string;
}
export default function ConfigureTelephonyPage() {
const router = useRouter();
const searchParams = useSearchParams();
const { user, getAccessToken, loading: authLoading } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const [hasExistingConfig, setHasExistingConfig] = useState(false);
// Clean up any stale pointer-events from dialogs that weren't properly closed before navigation
useEffect(() => {
document.body.style.pointerEvents = '';
}, []);
// Get returnTo parameter from URL
const returnTo = searchParams.get("returnTo") || "/workflow";
const {
register,
@ -109,6 +120,15 @@ export default function ConfigureTelephonyPage() {
if (response.data.vobiz.from_numbers?.length > 0) {
setValue("from_number", response.data.vobiz.from_numbers[0]);
}
} else if ((response.data as TelephonyConfigurationResponse)?.cloudonix) {
const cloudonixConfig = (response.data as TelephonyConfigurationResponse).cloudonix as CloudonixConfigurationResponse;
setHasExistingConfig(true);
setValue("provider", "cloudonix");
setValue("bearer_token", cloudonixConfig.bearer_token);
setValue("domain_id", cloudonixConfig.domain_id);
if (cloudonixConfig.from_numbers?.length > 0) {
setValue("from_number", cloudonixConfig.from_numbers[0]);
}
}
}
} catch (error) {
@ -126,7 +146,11 @@ export default function ConfigureTelephonyPage() {
const accessToken = await getAccessToken();
// Build the request body based on provider
let requestBody: TwilioConfigurationRequest | VonageConfigurationRequest | VobizConfigurationRequest;
let requestBody:
| TwilioConfigurationRequest
| VonageConfigurationRequest
| VobizConfigurationRequest
| CloudonixConfigurationRequest;
if (data.provider === "twilio") {
requestBody = {
@ -144,18 +168,26 @@ export default function ConfigureTelephonyPage() {
api_key: data.api_key || undefined,
api_secret: data.api_secret || undefined,
} as VonageConfigurationRequest;
} else {
} else if (data.provider === "vobiz") {
requestBody = {
provider: data.provider,
from_numbers: [data.from_number],
auth_id: data.auth_id,
auth_token: data.vobiz_auth_token,
} as VobizConfigurationRequest;
} else {
// Cloudonix
requestBody = {
provider: data.provider,
from_numbers: data.from_number ? [data.from_number] : [],
bearer_token: data.bearer_token!,
domain_id: data.domain_id!,
} as CloudonixConfigurationRequest;
}
const response = await saveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPost({
headers: { Authorization: `Bearer ${accessToken}` },
body: requestBody,
body: requestBody
});
if (response.error) {
@ -166,6 +198,9 @@ export default function ConfigureTelephonyPage() {
}
toast.success("Telephony configuration saved successfully");
// Redirect back to the page that sent us here
router.push(returnTo);
} catch (error) {
toast.error(
error instanceof Error
@ -178,40 +213,127 @@ export default function ConfigureTelephonyPage() {
};
return (
<div className="container mx-auto p-6 space-y-6">
<div>
<div className="min-h-screen bg-gray-50">
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-2">Configure Telephony</h1>
<p className="text-muted-foreground">
<p className="text-gray-600 mb-6">
Set up your telephony provider to make phone calls
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<Card className="h-full">
<CardHeader>
<CardTitle>
{selectedProvider === "twilio" ? "Twilio" : "Vonage"} Setup Guide
{selectedProvider === "twilio"
? "Twilio"
: selectedProvider === "vonage"
? "Vonage"
: selectedProvider === "vobiz"
? "Vobiz"
: "Cloudonix"}{" "}
Setup Guide
</CardTitle>
<CardDescription>
Watch this video to learn how to setup {selectedProvider === "twilio" ? "Twilio" : "Vonage"}
{selectedProvider === "cloudonix" ? (
<>
Cloudonix is an AI Connectivity platform, enabling you to connect Dograh to any SIP product or SIP Telephony Provider.<br/><br/>
<iframe
style={{ border: 0 }}
width="100%"
height="450"
src="https://www.youtube.com/embed/qLKX0F99jpU?si=a_sF9ijSJdV4OdG-"
allowFullScreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
/><br/><br/>
Visit{" "}
<a
href="https://cockpit.cloudonix.io/onboarding?affiliate=DOGRAH"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
https://cloudonix.com
</a>{" "}
for more information about Cloudonix services and pricing.Visit{" "}
<a
href="https://developers.cloudonix.com"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
https://developers.cloudonix.com
</a>{" "}
for developer documentation and API reference.
</>
) : selectedProvider === "vobiz" ? (
<>
Vobiz is a telephony provider. Visit their documentation
for setup instructions.
</>
) : (
<>
Watch this video to learn how to setup{" "}
{selectedProvider === "twilio" ? "Twilio" : "Vonage"}
</>
)}
</CardDescription>
</CardHeader>
<CardContent>
<div className="aspect-video">
<iframe
style={{ border: 0 }}
width="100%"
height="100%"
src={
selectedProvider === "twilio"
? "https://www.tella.tv/video/cmgbvzkrt00jk0clacu16blm3/embed?b=0&title=1&a=1&loop=0&t=0&muted=0&wt=0"
: "https://www.tella.tv/video/configuring-telephony-on-dograh-with-vonage-3wvo/embed?b=0&title=1&a=1&loop=0&t=0&muted=0&wt=0"
}
allowFullScreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
/>
</div>
{selectedProvider === "twilio" || selectedProvider === "vonage" ? (
<div className="aspect-video">
<iframe
style={{ border: 0 }}
width="100%"
height="100%"
src={
selectedProvider === "twilio"
? "https://www.tella.tv/video/cmgbvzkrt00jk0clacu16blm3/embed?b=0&title=1&a=1&loop=0&t=0&muted=0&wt=0"
: "https://www.tella.tv/video/configuring-telephony-on-dograh-with-vonage-3wvo/embed?b=0&title=1&a=1&loop=0&t=0&muted=0&wt=0"
}
allowFullScreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
/>
</div>
) : selectedProvider === "vobiz" ? (
<div className="space-y-4 text-sm">
<div>
<h4 className="font-semibold mb-2">Getting Started with Vobiz:</h4>
<ol className="list-decimal list-inside space-y-1 text-gray-600">
<li>Sign up for a Vobiz account</li>
<li>Get your Auth ID from the Vobiz dashboard</li>
<li>Generate an Auth Token</li>
<li>Configure phone numbers in your Vobiz account</li>
<li>Enter your credentials below</li>
</ol>
</div>
<div className="bg-blue-50 border border-blue-200 rounded p-3">
<p className="text-sm text-blue-800">
<strong>Note:</strong> Vobiz provides cloud-based telephony services
with global reach and competitive pricing.
</p>
</div>
</div>
) : (
<div className="space-y-4 text-sm">
<div>
<h4 className="font-semibold mb-2">Getting Started with Cloudonix:</h4>
<ol className="list-decimal list-inside space-y-1 text-gray-600">
<li>Sign up for a Cloudonix account at https://cloudonix.com</li>
<li>Create an <i>API token</i> for your Cloudonix domain</li>
<li>Configure your Cloudoinx <i>API Token</i> and <i>Cloudonix Domain Name</i> in Dograh</li>
<li>Configure an optional outbound phone number for your Dograh agent</li>
</ol>
</div>
<div className="bg-blue-50 border border-blue-200 rounded p-3">
<p className="text-sm text-blue-800">
<strong>Note:</strong> Cloudonix uses Bearer token
authentication and is fully TwiML-compatible for voice
applications.
</p>
</div>
</div>
)}
</CardContent>
</Card>
</div>
@ -239,10 +361,11 @@ export default function ConfigureTelephonyPage() {
<SelectItem value="twilio">Twilio</SelectItem>
<SelectItem value="vonage">Vonage</SelectItem>
<SelectItem value="vobiz">Vobiz</SelectItem>
<SelectItem value="cloudonix">Cloudonix</SelectItem>
</SelectContent>
</Select>
{hasExistingConfig && (
<p className="text-sm text-yellow-600 dark:text-yellow-500">
<p className="text-sm text-amber-600">
Switching providers will require entering new credentials
</p>
)}
@ -404,7 +527,7 @@ export default function ConfigureTelephonyPage() {
<Label htmlFor="auth_id">Auth ID</Label>
<Input
id="auth_id"
placeholder="MA_XXXXXXXX"
placeholder="MA_SYQRLN1K"
{...register("auth_id", {
required: selectedProvider === "vobiz" ? "Auth ID is required" : false,
})}
@ -445,13 +568,13 @@ export default function ConfigureTelephonyPage() {
<Input
id="from_number"
autoComplete="tel"
placeholder="918071387428 (E.164 without + prefix)"
placeholder="14155551234 (no + prefix for Vobiz)"
{...register("from_number", {
required: "Phone number is required",
pattern: {
value: /^[1-9]\d{1,14}$/,
message:
"Enter a valid phone number without + prefix (e.g., 918071387428)",
"Enter a valid phone number without + prefix (e.g., 14155551234)",
},
})}
/>
@ -464,6 +587,82 @@ export default function ConfigureTelephonyPage() {
</>
)}
{/* Cloudonix-specific fields */}
{selectedProvider === "cloudonix" && (
<>
<div className="space-y-2">
<Label htmlFor="bearer_token">Domain API Token (eg. XI-....)</Label>
<Input
id="bearer_token"
type="password"
autoComplete="current-password"
placeholder={
hasExistingConfig
? "Leave masked to keep existing"
: "Enter your bearer token"
}
{...register("bearer_token", {
required:
selectedProvider === "cloudonix" && !hasExistingConfig
? "Domain API token is required"
: false,
})}
/>
{errors.bearer_token && (
<p className="text-sm text-red-500">
{errors.bearer_token.message}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="domain_id">Domain Name or UUID</Label>
<Input
id="domain_id"
placeholder="your-domain-id"
{...register("domain_id", {
required:
selectedProvider === "cloudonix"
? "Domain Name or UUID is required"
: false,
})}
/>
{errors.domain_id && (
<p className="text-sm text-red-500">
{errors.domain_id.message}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="from_number">
From Phone Number (Optional)
</Label>
<Input
id="from_number"
autoComplete="tel"
placeholder="+1234567890"
{...register("from_number", {
pattern: {
value: /^\+?[1-9]\d{1,14}$/,
message:
"Enter a valid phone number (e.g., +1234567890)",
},
})}
/>
{errors.from_number && (
<p className="text-sm text-red-500">
{errors.from_number.message}
</p>
)}
<p className="text-xs text-gray-500">
Phone numbers can be fetched from Cloudonix DNIDs if not
specified
</p>
</div>
</>
)}
<div className="pt-4">
<Button
type="submit"
@ -478,6 +677,7 @@ export default function ConfigureTelephonyPage() {
</form>
</div>
</div>
</div>
</div>
);

View file

@ -60,7 +60,7 @@ export const PhoneCallDialog = ({
headers: { 'Authorization': `Bearer ${accessToken}` },
});
if (configResponse.error || (!configResponse.data?.twilio && !configResponse.data?.vonage && !configResponse.data?.vobiz)) {
if (configResponse.error || (!configResponse.data?.twilio && !configResponse.data?.vonage && !configResponse.data?.vobiz && !configResponse.data?.cloudonix)) {
setNeedsConfiguration(true);
} else {
setNeedsConfiguration(false);

View file

@ -1,9 +1,8 @@
// This file is auto-generated by @hey-api/openapi-ts
import { type ClientOptions as DefaultClientOptions, type Config, createClient, createConfig } from '@hey-api/client-fetch';
import { createClientConfig } from '../lib/apiClient';
import type { ClientOptions } from './types.gen';
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from '@hey-api/client-fetch';
import { createClientConfig } from '../lib/apiClient';
/**
* The `createClientConfig()` function will be called on client initialization
@ -17,4 +16,4 @@ export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> =
export const client = createClient(createClientConfig(createConfig<ClientOptions>({
baseUrl: 'http://127.0.0.1:8000'
})));
})));

View file

@ -1,3 +1,3 @@
// This file is auto-generated by @hey-api/openapi-ts
export * from './sdk.gen';
export * from './types.gen';
export * from './sdk.gen';

File diff suppressed because one or more lines are too long

View file

@ -81,6 +81,35 @@ export type CampaignsResponse = {
campaigns: Array<CampaignResponse>;
};
/**
* Request schema for Cloudonix configuration.
*/
export type CloudonixConfigurationRequest = {
provider?: string;
/**
* Cloudonix API Bearer Token
*/
bearer_token: string;
/**
* Cloudonix Domain ID
*/
domain_id: string;
/**
* List of Cloudonix phone numbers (optional)
*/
from_numbers?: Array<string>;
};
/**
* Response schema for Cloudonix configuration with masked sensitive fields.
*/
export type CloudonixConfigurationResponse = {
provider: string;
bearer_token: string;
domain_id: string;
from_numbers: Array<string>;
};
export type CreateApiKeyRequest = {
name: string;
};
@ -544,6 +573,7 @@ export type TelephonyConfigurationResponse = {
twilio?: TwilioConfigurationResponse | null;
vonage?: VonageConfigurationResponse | null;
vobiz?: VobizConfigurationResponse | null;
cloudonix?: CloudonixConfigurationResponse | null;
};
export type TestSessionResponse = {
@ -1093,6 +1123,35 @@ export type HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdP
200: unknown;
};
export type HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostData = {
body?: never;
path: {
workflow_run_id: number;
};
query?: never;
url: '/api/v1/telephony/cloudonix/status-callback/{workflow_run_id}';
};
export type HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostError = HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostErrors[keyof HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostErrors];
export type HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type OfferApiV1PipecatRtcOfferPostData = {
body: RtcOfferRequest;
headers?: {
@ -2799,7 +2858,7 @@ export type GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetRespons
export type GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse = GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponses[keyof GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponses];
export type SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData = {
body: TwilioConfigurationRequest | VonageConfigurationRequest | VobizConfigurationRequest;
body: TwilioConfigurationRequest | VonageConfigurationRequest | VobizConfigurationRequest | CloudonixConfigurationRequest;
headers?: {
authorization?: string | null;
};
@ -3872,4 +3931,4 @@ export type HealthApiV1HealthGetResponses = {
export type ClientOptions = {
baseUrl: 'http://127.0.0.1:8000' | (string & {});
};
};

View file

@ -3,6 +3,12 @@
* These modes determine how a workflow run is executed
*/
export const WORKFLOW_RUN_MODES = {
TWILIO: 'twilio',
VONAGE: 'vonage',
VOBIZ: 'vobiz',
CLOUDONIX: 'cloudonix',
STASIS: 'stasis',
WEBRTC: 'webrtc',
SMALL_WEBRTC: 'smallwebrtc',
} as const;