feat: add Plivo telephony provider support (#245)

* Add Plivo telephony provider support

* add Plivo telephony UI, fix audio config, and improve inbound call handling

---------

Co-authored-by: Dilip Tiwari <digitalapache20@gmail.com>
Co-authored-by: Sabiha Khan <sabihak89@gmail.com>
Co-authored-by: Abhishek <abhishek@a6k.me>
This commit is contained in:
dilipevents2007-cpu 2026-04-25 20:41:46 +05:30 committed by GitHub
parent 3e3773f400
commit 2218ba8ad9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1123 additions and 13 deletions

View file

@ -12,6 +12,8 @@ import type {
AriConfigurationResponse,
CloudonixConfigurationRequest,
CloudonixConfigurationResponse,
PlivoConfigurationRequest,
PlivoConfigurationResponse,
TelephonyConfigurationResponse,
TelnyxConfigurationRequest,
TelnyxConfigurationResponse,
@ -49,6 +51,9 @@ interface TelephonyConfigForm {
private_key?: string;
api_key?: string;
api_secret?: string;
// Plivo fields
plivo_auth_id?: string;
plivo_auth_token?: string;
// Vobiz fields
auth_id?: string;
vobiz_auth_token?: string;
@ -146,6 +151,13 @@ export default function ConfigureTelephonyPage() {
setValue("auth_id", response.data.vobiz.auth_id);
setValue("vobiz_auth_token", response.data.vobiz.auth_token);
setValue("from_numbers", response.data.vobiz.from_numbers?.length > 0 ? response.data.vobiz.from_numbers : [""]);
} else if ((response.data as TelephonyConfigurationResponse)?.plivo) {
const plivoConfig = (response.data as TelephonyConfigurationResponse).plivo as PlivoConfigurationResponse;
setHasExistingConfig(true);
setValue("provider", "plivo");
setValue("plivo_auth_id", plivoConfig.auth_id);
setValue("plivo_auth_token", plivoConfig.auth_token);
setValue("from_numbers", plivoConfig.from_numbers?.length > 0 ? plivoConfig.from_numbers : [""]);
} else if ((response.data as TelephonyConfigurationResponse)?.cloudonix) {
const cloudonixConfig = (response.data as TelephonyConfigurationResponse).cloudonix as CloudonixConfigurationResponse;
setHasExistingConfig(true);
@ -192,6 +204,7 @@ export default function ConfigureTelephonyPage() {
// Build the request body based on provider
let requestBody:
| TwilioConfigurationRequest
| PlivoConfigurationRequest
| VonageConfigurationRequest
| VobizConfigurationRequest
| CloudonixConfigurationRequest
@ -214,7 +227,7 @@ export default function ConfigureTelephonyPage() {
let pattern: RegExp;
let formatMessage: string;
if (data.provider === "twilio" || data.provider === "telnyx") {
if (data.provider === "twilio" || data.provider === "telnyx" || data.provider === "plivo") {
pattern = twilioPattern;
formatMessage = "with + prefix (e.g., +1234567890)";
} else if (data.provider === "cloudonix") {
@ -259,6 +272,13 @@ export default function ConfigureTelephonyPage() {
auth_id: data.auth_id,
auth_token: data.vobiz_auth_token,
} as VobizConfigurationRequest;
} else if (data.provider === "plivo") {
requestBody = {
provider: data.provider,
from_numbers: filteredNumbers,
auth_id: data.plivo_auth_id!,
auth_token: data.plivo_auth_token!,
} as PlivoConfigurationRequest;
} else if (data.provider === "telnyx") {
requestBody = {
provider: data.provider,
@ -335,6 +355,8 @@ export default function ConfigureTelephonyPage() {
? "Vonage"
: selectedProvider === "vobiz"
? "Vobiz"
: selectedProvider === "plivo"
? "Plivo"
: selectedProvider === "telnyx"
? "Telnyx"
: selectedProvider === "ari"
@ -385,6 +407,12 @@ export default function ConfigureTelephonyPage() {
</a>{" "}
for developer documentation and API reference.
</>
) : selectedProvider === "plivo" ? (
<>
Plivo is a cloud communications platform providing voice and messaging
APIs. Use Plivo to build voice applications with real-time audio streaming
and global telephony coverage.
</>
) : selectedProvider === "vobiz" ? (
<>
Vobiz is a telephony provider. Visit their documentation
@ -438,6 +466,25 @@ export default function ConfigureTelephonyPage() {
</p>
</div>
</div>
) : selectedProvider === "plivo" ? (
<div className="space-y-4 text-sm">
<div>
<h4 className="font-semibold mb-2">Getting Started with Plivo:</h4>
<ol className="list-decimal list-inside space-y-1 text-muted-foreground">
<li>Sign up at <a href="https://www.plivo.com" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">plivo.com</a> and go to the Console Dashboard</li>
<li>Find your Auth ID and Auth Token on the Dashboard overview page</li>
<li>Purchase a phone number under Phone Numbers &gt; Buy Numbers</li>
<li>Create an XML Application under Voice &gt; XML Applications</li>
<li>Enter your Auth ID, Auth Token, and phone numbers below</li>
</ol>
</div>
<div className="bg-muted border border-border rounded p-3">
<p className="text-sm">
<strong>Note:</strong> Plivo uses XML-based call control with bidirectional
audio streaming. Phone numbers should be in E.164 format with + prefix (e.g., +1234567890).
</p>
</div>
</div>
) : selectedProvider === "twilio" || selectedProvider === "vonage" ? (
<div className="aspect-video">
<iframe
@ -518,6 +565,7 @@ export default function ConfigureTelephonyPage() {
<SelectContent>
<SelectItem value="twilio">Twilio</SelectItem>
<SelectItem value="vonage">Vonage</SelectItem>
<SelectItem value="plivo">Plivo</SelectItem>
<SelectItem value="vobiz">Vobiz</SelectItem>
<SelectItem value="telnyx">Telnyx</SelectItem>
<SelectItem value="cloudonix">Cloudonix</SelectItem>
@ -795,6 +843,92 @@ export default function ConfigureTelephonyPage() {
</>
)}
{/* Plivo-specific fields */}
{selectedProvider === "plivo" && (
<>
<div className="space-y-2">
<Label htmlFor="plivo_auth_id">Auth ID</Label>
<Input
id="plivo_auth_id"
placeholder="MAxxxxxxxxxxxxxxxxxxxxx"
{...register("plivo_auth_id", {
required: selectedProvider === "plivo" ? "Auth ID is required" : false,
})}
/>
{errors.plivo_auth_id && (
<p className="text-sm text-red-500">
{errors.plivo_auth_id.message}
</p>
)}
<p className="text-xs text-muted-foreground">
Found on your Plivo Console Dashboard
</p>
</div>
<div className="space-y-2">
<Label htmlFor="plivo_auth_token">Auth Token</Label>
<Input
id="plivo_auth_token"
type="password"
autoComplete="current-password"
placeholder={
hasExistingConfig
? "Leave masked to keep existing"
: "Enter your auth token"
}
{...register("plivo_auth_token", {
required: selectedProvider === "plivo" && !hasExistingConfig
? "Auth token is required"
: false,
})}
/>
{errors.plivo_auth_token && (
<p className="text-sm text-red-500">
{errors.plivo_auth_token.message}
</p>
)}
</div>
<div className="space-y-2">
<Label>CLI Phone Numbers</Label>
{fromNumbers.map((number, index) => (
<div key={index} className="flex gap-2">
<Input
autoComplete="tel"
placeholder="+1234567890"
value={number}
onChange={(e) => updatePhoneNumber(index, e.target.value)}
/>
{fromNumbers.length > 1 && (
<Button
type="button"
variant="outline"
size="icon"
onClick={() => removePhoneNumber(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
))}
<Button
type="button"
variant="outline"
size="sm"
onClick={addPhoneNumber}
>
<Plus className="h-4 w-4 mr-2" />
Add Phone Number
</Button>
{fromNumbers.some(n => n.trim() !== "" && !/^\+[1-9]\d{1,14}$/.test(n)) && (
<p className="text-sm text-red-500">
Enter valid phone numbers with country code (e.g., +1234567890)
</p>
)}
</div>
</>
)}
{/* Telnyx-specific fields */}
{selectedProvider === "telnyx" && (
<>

View file

@ -57,7 +57,7 @@ export const PhoneCallDialog = ({
try {
const configResponse = await getTelephonyConfigurationApiV1OrganizationsTelephonyConfigGet({});
if (configResponse.error || (!configResponse.data?.twilio && !configResponse.data?.vonage && !configResponse.data?.vobiz && !configResponse.data?.cloudonix && !configResponse.data?.ari && !configResponse.data?.telnyx)) {
if (configResponse.error || (!configResponse.data?.twilio && !configResponse.data?.vonage && !configResponse.data?.vobiz && !configResponse.data?.cloudonix && !configResponse.data?.ari && !configResponse.data?.telnyx && !configResponse.data?.plivo)) {
setNeedsConfiguration(true);
} else {
setNeedsConfiguration(false);

View file

@ -735,6 +735,60 @@ export type CloudonixConfigurationResponse = {
from_numbers: Array<string>;
};
/**
* PlivoConfigurationRequest
*
* Request schema for Plivo configuration.
*/
export type PlivoConfigurationRequest = {
/**
* Provider
*/
provider?: string;
/**
* Auth Id
*
* Plivo Auth ID
*/
auth_id: string;
/**
* Auth Token
*
* Plivo Auth Token
*/
auth_token: string;
/**
* From Numbers
*
* List of Plivo phone numbers
*/
from_numbers: Array<string>;
};
/**
* PlivoConfigurationResponse
*
* Response schema for Plivo configuration with masked sensitive fields.
*/
export type PlivoConfigurationResponse = {
/**
* Provider
*/
provider: string;
/**
* Auth Id
*/
auth_id: string;
/**
* Auth Token
*/
auth_token: string;
/**
* From Numbers
*/
from_numbers: Array<string>;
};
/**
* CreateAPIKeyRequest
*/
@ -2880,6 +2934,7 @@ export type SuperuserWorkflowRunsListResponse = {
*/
export type TelephonyConfigurationResponse = {
twilio?: TwilioConfigurationResponse | null;
plivo?: PlivoConfigurationResponse | null;
vonage?: VonageConfigurationResponse | null;
vobiz?: VobizConfigurationResponse | null;
cloudonix?: CloudonixConfigurationResponse | null;
@ -7344,7 +7399,7 @@ export type SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData
/**
* Request
*/
body: TwilioConfigurationRequest | VonageConfigurationRequest | VobizConfigurationRequest | CloudonixConfigurationRequest | AriConfigurationRequest | TelnyxConfigurationRequest;
body: TwilioConfigurationRequest | PlivoConfigurationRequest | VonageConfigurationRequest | VobizConfigurationRequest | CloudonixConfigurationRequest | AriConfigurationRequest | TelnyxConfigurationRequest;
headers?: {
/**
* Authorization

View file

@ -10,7 +10,8 @@ export const WORKFLOW_RUN_MODES = {
WEBRTC: 'webrtc',
SMALL_WEBRTC: 'smallwebrtc',
ARI: 'ari',
TELNYX: 'telnyx'
TELNYX: 'telnyx',
PLIVO: 'plivo'
} as const;
export type WorkflowRunMode = typeof WORKFLOW_RUN_MODES[keyof typeof WORKFLOW_RUN_MODES];