Add Bird API integration with voice call and flash call support
- Implement Bird API client (`bird.php`) for handling HTTP requests to Bird services. - Add routes for voice and flash call management (`birdVoiceFlashCallsRoute.php`, `birdNumbersRoute.php`). - Introduce test cases for voice calls, flash calls, and numbers (`VoiceCallsApiTest.php`, `NumbersAndFlashCallsApiTest.php`). - Include configuration management classes and APIs for enabling the Bird module and managing API keys (`bird_c.php`). - Provide OpenAPI specifications for flash call endpoints (`bird-flash-calls.md`).
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
|
|||||||
|
# Voice Calls API
|
||||||
|
|
||||||
|
## Overview <a href="#overview" id="overview"></a>
|
||||||
|
|
||||||
|
Voice Calls API makes it possible to initiate, receive, and control voice calls using a REST API.
|
||||||
|
|
||||||
|
## API Access <a href="#api-access" id="api-access"></a>
|
||||||
|
|
||||||
|
The following API requests can only be made using a valid access key, and attached to an access role, with an access policy that at least specifies the permissions to the resources outlined in each section below. Learn more about [API access](https://docs.messagebird.com/api/api-access).
|
||||||
|
|
||||||
+355
@@ -80,8 +80,363 @@ tags:
|
|||||||
description: Department goals management
|
description: Department goals management
|
||||||
- name: Subusers
|
- name: Subusers
|
||||||
description: Subuser registration and setup
|
description: Subuser registration and setup
|
||||||
|
- name: Bird
|
||||||
|
description: Voice Calls via Bird
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
|
# Bird Voice Calls
|
||||||
|
/bird/voice/calls:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Bird
|
||||||
|
summary: Create/place a voice call via Bird
|
||||||
|
operationId: birdCreateVoiceCall
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: workspaceId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Workspace identifier
|
||||||
|
- in: query
|
||||||
|
name: channelId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Channel identifier
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
properties:
|
||||||
|
to:
|
||||||
|
type: string
|
||||||
|
description: E.164 phone number of the callee
|
||||||
|
example: "+4511122233"
|
||||||
|
from:
|
||||||
|
type: string
|
||||||
|
description: E.164 phone number of the caller (sender)
|
||||||
|
example: "+4599988877"
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Call created
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Bird
|
||||||
|
summary: List voice calls
|
||||||
|
operationId: birdListVoiceCalls
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: workspaceId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Workspace identifier
|
||||||
|
- in: query
|
||||||
|
name: channelId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Channel identifier
|
||||||
|
- in: query
|
||||||
|
name: page
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
required: false
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: A list of calls
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
|
||||||
|
/bird/voice/calls/{id}:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Bird
|
||||||
|
summary: Get a voice call by ID
|
||||||
|
operationId: birdGetVoiceCall
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: workspaceId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Workspace identifier
|
||||||
|
- in: query
|
||||||
|
name: channelId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Channel identifier
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Call details
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
|
||||||
|
/bird/voice/calls/{id}/hangup:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Bird
|
||||||
|
summary: Hang up a voice call by ID
|
||||||
|
operationId: birdHangupVoiceCall
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: workspaceId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Workspace identifier
|
||||||
|
- in: query
|
||||||
|
name: channelId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Channel identifier
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Hangup requested
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
|
||||||
|
# Bird Numbers
|
||||||
|
/bird/numbers:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Bird
|
||||||
|
summary: List your numbers
|
||||||
|
operationId: birdListNumbers
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: page
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
- in: query
|
||||||
|
name: limit
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: A list of numbers
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
|
||||||
|
# Bird Voice Flash Calling
|
||||||
|
/bird/voice/flash-calls:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Bird
|
||||||
|
summary: Create/place a flash call via Bird
|
||||||
|
operationId: birdCreateFlashCall
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: workspaceId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Workspace identifier
|
||||||
|
- in: query
|
||||||
|
name: channelId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Channel identifier
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Flash call created
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Bird
|
||||||
|
summary: List flash calls
|
||||||
|
operationId: birdListFlashCalls
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: workspaceId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Workspace identifier
|
||||||
|
- in: query
|
||||||
|
name: channelId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Channel identifier
|
||||||
|
- in: query
|
||||||
|
name: page
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
required: false
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: A list of flash calls
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
|
||||||
|
/bird/voice/flash-calls/{id}:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Bird
|
||||||
|
summary: Get a flash call by ID
|
||||||
|
operationId: birdGetFlashCall
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: workspaceId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Workspace identifier
|
||||||
|
- in: query
|
||||||
|
name: channelId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Channel identifier
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Flash call details
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Bird
|
||||||
|
summary: Complete/end a flash call by ID
|
||||||
|
description: Posts a completion/update payload to the flash call resource to finalize verification.
|
||||||
|
operationId: birdEndFlashCall
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: workspaceId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Workspace identifier
|
||||||
|
- in: query
|
||||||
|
name: channelId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Channel identifier
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Flash call completed
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
|
||||||
|
/bird/voice/flash-calls/end:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Bird
|
||||||
|
summary: Complete/end a flash call using from/to numbers
|
||||||
|
description: Ends an ongoing flash call by specifying the originating and destination numbers.
|
||||||
|
operationId: birdEndFlashCallByNumbers
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: workspaceId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Workspace identifier
|
||||||
|
- in: query
|
||||||
|
name: channelId
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
description: Bird Channel identifier
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
properties:
|
||||||
|
from:
|
||||||
|
type: string
|
||||||
|
description: E.164 formatted caller number
|
||||||
|
example: "+4599988877"
|
||||||
|
to:
|
||||||
|
type: string
|
||||||
|
description: E.164 formatted callee number
|
||||||
|
example: "+4511122233"
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Flash call completed (by numbers)
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
|
||||||
# Subusers (public registration + setup)
|
# Subusers (public registration + setup)
|
||||||
/subusers:
|
/subusers:
|
||||||
get:
|
get:
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace classes;
|
||||||
|
|
||||||
|
require_once WD . '/modules/bird/bird_c.php';
|
||||||
|
|
||||||
|
use bird\bird_c;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class bird
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Configuration of the Bird module
|
||||||
|
* @var bird_c|object
|
||||||
|
*/
|
||||||
|
public $config;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->config = new bird_c();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure module is enabled
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
function requireModuleEnabled(): void
|
||||||
|
{
|
||||||
|
if (!$this->config->enabled->isTrue()) {
|
||||||
|
throw new Exception('The bird module is not enabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure API key is present
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
function requireValidApiKey(): void
|
||||||
|
{
|
||||||
|
$k = $this->config->api_key->getVariableValue();
|
||||||
|
if ($k === null || $k === '') {
|
||||||
|
throw new Exception('Invalid API key defined in the config (bird_api_key_c)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure base URL is defined
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
function requireValidServerURL(): void
|
||||||
|
{
|
||||||
|
$u = $this->config->server_url->getVariableValue();
|
||||||
|
if ($u === null || $u === '') {
|
||||||
|
throw new Exception('Invalid server URL defined in the config (bird_server_url_c)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a request to Bird API
|
||||||
|
* @param string $endpoint e.g. "/v1/devices"
|
||||||
|
* @param array $data request body
|
||||||
|
* @param string $method HTTP method (currently only POST supported)
|
||||||
|
* @return object|array|null
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
function sendRequest(string $endpoint, array $data = [], string $method = 'POST'): object|array|null
|
||||||
|
{
|
||||||
|
$this->requireModuleEnabled();
|
||||||
|
$this->requireValidApiKey();
|
||||||
|
$this->requireValidServerURL();
|
||||||
|
return match (strtoupper($method)) {
|
||||||
|
'POST' => $this->sendPostRequest($endpoint, $data),
|
||||||
|
'GET' => $this->sendGetRequest($endpoint, $data),
|
||||||
|
default => throw new Exception('Invalid request method'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send POST request
|
||||||
|
* @param string $endpoint
|
||||||
|
* @param array $data
|
||||||
|
* @return array|object|null
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
function sendPostRequest(string $endpoint, array $data): array|object|null
|
||||||
|
{
|
||||||
|
$this->requireModuleEnabled();
|
||||||
|
$this->requireValidApiKey();
|
||||||
|
$this->requireValidServerURL();
|
||||||
|
|
||||||
|
$url = rtrim($this->config->server_url->getVariableValue(), '/') . $endpoint;
|
||||||
|
$headers = [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Authorization: Bearer ' . $this->config->api_key->getVariableValue(),
|
||||||
|
];
|
||||||
|
$body = json_encode($data);
|
||||||
|
|
||||||
|
$result = $this->doHttpRequest('POST', $url, $headers, $body);
|
||||||
|
$status = (int)($result['status_code'] ?? 0);
|
||||||
|
$response = $result['body'] ?? '';
|
||||||
|
if ($status >= 400) {
|
||||||
|
throw new Exception('Bird API request failed with status ' . $status);
|
||||||
|
}
|
||||||
|
if ($response === '' || $response === false || $response === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$decoded = json_decode($response);
|
||||||
|
return $decoded ?? $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Low-level HTTP transport (curl). Tests can override this to stub network.
|
||||||
|
* @param string $method
|
||||||
|
* @param string $url
|
||||||
|
* @param array $headers
|
||||||
|
* @param string $body
|
||||||
|
* @return array{status_code:int, body:string|false}
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function doHttpRequest(string $method, string $url, array $headers, string $body = ''): array
|
||||||
|
{
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
$upper = strtoupper($method);
|
||||||
|
if ($upper === 'POST') {
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||||
|
} else {
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $upper);
|
||||||
|
}
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||||
|
$resp = curl_exec($ch);
|
||||||
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
if (curl_errno($ch)) {
|
||||||
|
$err = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
throw new Exception('cURL error: ' . $err);
|
||||||
|
}
|
||||||
|
curl_close($ch);
|
||||||
|
return [
|
||||||
|
'status_code' => (int)$code,
|
||||||
|
'body' => $resp,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send GET request
|
||||||
|
* @param string $endpoint
|
||||||
|
* @param array $query
|
||||||
|
* @return array|object|null
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
function sendGetRequest(string $endpoint, array $query = []): array|object|null
|
||||||
|
{
|
||||||
|
$this->requireModuleEnabled();
|
||||||
|
$this->requireValidApiKey();
|
||||||
|
$this->requireValidServerURL();
|
||||||
|
|
||||||
|
$base = rtrim($this->config->server_url->getVariableValue(), '/');
|
||||||
|
$url = $base . $endpoint;
|
||||||
|
if (!empty($query)) {
|
||||||
|
$qs = http_build_query($query);
|
||||||
|
$url .= (str_contains($url, '?') ? '&' : '?') . $qs;
|
||||||
|
}
|
||||||
|
$headers = [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Authorization: Bearer ' . $this->config->api_key->getVariableValue(),
|
||||||
|
];
|
||||||
|
$result = $this->doHttpRequest('GET', $url, $headers);
|
||||||
|
$status = (int)($result['status_code'] ?? 0);
|
||||||
|
$response = $result['body'] ?? '';
|
||||||
|
if ($status >= 400) {
|
||||||
|
throw new Exception('Bird API request failed with status ' . $status);
|
||||||
|
}
|
||||||
|
if ($response === '' || $response === false || $response === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$decoded = json_decode($response);
|
||||||
|
return $decoded ?? $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Flash Calling API
|
||||||
|
|
||||||
|
This API enables users to leverage flash calls for quick and efficient number validation or authentication processes.
|
||||||
|
|
||||||
|
This API initiates a call to the destination number. If the call is answered, it will be immediately terminated.
|
||||||
|
|
||||||
|
## Create flash call
|
||||||
|
|
||||||
|
> Create a new channel flash call
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"openapi":"3.0.3","info":{"title":"Channels","version":"v1"},"tags":[],"servers":[{"url":"https://api.bird.com","description":"Production API"}],"security":[{"accessKey":[]}],"components":{"securitySchemes":{"accessKey":{"description":"Uses the Authorization header: 'AccessKey ' followed by your access key token (e.g., 'Authorization: AccessKey AbCdEfGhIjKlMnOpQrStUvWxYzAbCdEfGhIj')","scheme":"AccessKey","type":"http"}},"schemas":{"CreateFlashCall":{"type":"object","title":"ChannelFlashCallCreate","additionalProperties":false,"properties":{"from":{"type":"string"},"to":{"type":"string"},"ringTimeout":{"type":"integer","minimum":3,"maximum":120,"default":30}},"required":["to"]},"FlashCall":{"type":"object","title":"ChannelFlashCall","additionalProperties":false,"properties":{"id":{"$ref":"#/components/schemas/Id-2"},"channelId":{"$ref":"#/components/schemas/ChannelId"},"from":{"type":"string"},"to":{"type":"string"},"status":{"$ref":"#/components/schemas/Status-4"},"ringTimeout":{"type":"integer"},"attemptPrice":{"type":"object","properties":{"amount":{"type":"integer"},"exponent":{"type":"integer"},"currency":{"type":"string"}}},"connectionPrice":{"type":"object","properties":{"amount":{"type":"integer"},"exponent":{"type":"integer"},"currency":{"type":"string"}}},"reason":{"type":"string"},"duration":{"type":"integer"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"ringingAt":{"type":"string","format":"date-time"},"answeredAt":{"type":"string","format":"date-time"},"endedAt":{"type":"string","format":"date-time"}},"required":["id","channelId","from","to","status","createdAt","updatedAt"]},"Id-2":{"type":"string","format":"uuid"},"ChannelId":{"type":"string","format":"uuid"},"Status-4":{"type":"string","enum":["accepted","starting","ringing","ongoing","completed","no-answer","busy","failed","cancelled","scheduled"]},"RequestError":{"type":"object","properties":{"code":{"type":"string","description":"A unique code that identifies the error. This code can be used to programmatically identify the error.\n"},"message":{"type":"string","description":"A human-readable message that describes the error. An example is 'The requested resource does not exist: channel not found'.\n"}},"required":["code","message"]},"ValidationError":{"type":"object","properties":{"code":{"type":"string","description":"A unique code that identifies the error. This code can be used to programmatically identify the error.\n"},"message":{"type":"string","description":"A human-readable message that describes the error. An example is 'The requested resource does not exist: channel not found'.\n"},"details":{"type":"object","description":"Additional details about the error. This object can contain any additional information that may be useful for debugging.\n","additionalProperties":{"type":"array","items":{"type":"string"}}}},"required":["code","message"]}},"responses":{"requestError":{"description":"The request did not pass validation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestError"}}}},"validationError":{"description":"The request did not pass validation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}}}},"paths":{"/workspaces/{workspaceId}/channels/{channelId}/flashcalls":{"post":{"summary":"Create flash call","operationId":"createChannelFlashCall","description":"Create a new channel flash call","tags":["channel_flashcall"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFlashCall"}}}},"responses":{"202":{"description":"Flash Call was accepted for processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlashCall"}}}},"400":{"$ref":"#/components/responses/requestError"},"404":{"$ref":"#/components/responses/requestError"},"422":{"$ref":"#/components/responses/validationError"}}}}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
To terminate a flash call resource, or update the result of the verification, user can do a POST to a flash call resource, using this endpoint.<br>
|
||||||
|
|
||||||
|
## End flash call
|
||||||
|
|
||||||
|
> Completes the channel flash call
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"openapi":"3.0.3","info":{"title":"Channels","version":"v1"},"tags":[],"servers":[{"url":"https://api.bird.com","description":"Production API"}],"security":[{"accessKey":[]}],"components":{"securitySchemes":{"accessKey":{"description":"Uses the Authorization header: 'AccessKey ' followed by your access key token (e.g., 'Authorization: AccessKey AbCdEfGhIjKlMnOpQrStUvWxYzAbCdEfGhIj')","scheme":"AccessKey","type":"http"}},"schemas":{"HangupFlashCall":{"type":"object","title":"ChannelFlashCallHangup","additionalProperties":false,"properties":{"receivedCli":{"type":"string"},"result":{"type":"string","enum":["unknown","verified","canceled","timeout","wrong_cli"]}},"required":["result"]},"RequestError":{"type":"object","properties":{"code":{"type":"string","description":"A unique code that identifies the error. This code can be used to programmatically identify the error.\n"},"message":{"type":"string","description":"A human-readable message that describes the error. An example is 'The requested resource does not exist: channel not found'.\n"}},"required":["code","message"]},"ValidationError":{"type":"object","properties":{"code":{"type":"string","description":"A unique code that identifies the error. This code can be used to programmatically identify the error.\n"},"message":{"type":"string","description":"A human-readable message that describes the error. An example is 'The requested resource does not exist: channel not found'.\n"},"details":{"type":"object","description":"Additional details about the error. This object can contain any additional information that may be useful for debugging.\n","additionalProperties":{"type":"array","items":{"type":"string"}}}},"required":["code","message"]}},"responses":{"requestError":{"description":"The request did not pass validation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestError"}}}},"validationError":{"description":"The request did not pass validation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}}}},"paths":{"/workspaces/{workspaceId}/channels/{channelId}/flashcalls/{callId}":{"post":{"summary":"End flash call","operationId":"endChannelFlashCall","description":"Completes the channel flash call","tags":["channel_flashcall"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HangupFlashCall"}}}},"responses":{"202":{"description":"Hangup Flash Call was accepted for processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HangupFlashCall"}}}},"400":{"$ref":"#/components/responses/requestError"},"404":{"$ref":"#/components/responses/requestError"},"422":{"$ref":"#/components/responses/validationError"}}}}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
\
|
||||||
|
User can also chose to hangup calls, using a combination of From and To numbers:<br>
|
||||||
|
|
||||||
|
## End flash call
|
||||||
|
|
||||||
|
> Completes the channel flash call
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"openapi":"3.0.3","info":{"title":"Channels","version":"v1"},"tags":[],"servers":[{"url":"https://api.bird.com","description":"Production API"}],"security":[{"accessKey":[]}],"components":{"securitySchemes":{"accessKey":{"description":"Uses the Authorization header: 'AccessKey ' followed by your access key token (e.g., 'Authorization: AccessKey AbCdEfGhIjKlMnOpQrStUvWxYzAbCdEfGhIj')","scheme":"AccessKey","type":"http"}},"schemas":{"HangupFlashCallFromTo":{"anyOf":[{"$ref":"#/components/schemas/HangupFlashCall"},{"type":"object","properties":{"from":{"type":"string","description":"The phone number that initiated the call."},"to":{"type":"string","description":"The phone number that received the call."}},"required":["from","to"]}]},"HangupFlashCall":{"type":"object","title":"ChannelFlashCallHangup","additionalProperties":false,"properties":{"receivedCli":{"type":"string"},"result":{"type":"string","enum":["unknown","verified","canceled","timeout","wrong_cli"]}},"required":["result"]},"RequestError":{"type":"object","properties":{"code":{"type":"string","description":"A unique code that identifies the error. This code can be used to programmatically identify the error.\n"},"message":{"type":"string","description":"A human-readable message that describes the error. An example is 'The requested resource does not exist: channel not found'.\n"}},"required":["code","message"]},"ValidationError":{"type":"object","properties":{"code":{"type":"string","description":"A unique code that identifies the error. This code can be used to programmatically identify the error.\n"},"message":{"type":"string","description":"A human-readable message that describes the error. An example is 'The requested resource does not exist: channel not found'.\n"},"details":{"type":"object","description":"Additional details about the error. This object can contain any additional information that may be useful for debugging.\n","additionalProperties":{"type":"array","items":{"type":"string"}}}},"required":["code","message"]}},"responses":{"requestError":{"description":"The request did not pass validation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestError"}}}},"validationError":{"description":"The request did not pass validation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}}}},"paths":{"/workspaces/{workspaceId}/channels/{channelId}/flashcalls/hangup":{"post":{"summary":"End flash call","operationId":"endChannelFlashCallFromTo","description":"Completes the channel flash call","tags":["channel_flashcall"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HangupFlashCallFromTo"}}}},"responses":{"202":{"description":"Hangup Flash Call was accepted for processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HangupFlashCallFromTo"}}}},"400":{"$ref":"#/components/responses/requestError"},"404":{"$ref":"#/components/responses/requestError"},"422":{"$ref":"#/components/responses/validationError"}}}}}}
|
||||||
|
```
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace bird;
|
||||||
|
|
||||||
|
require_once WD . '/traits/module_config_t.php';
|
||||||
|
require_once WD . '/modules/bird/config/bird_enabled_c.php';
|
||||||
|
require_once WD . '/modules/bird/config/bird_api_key_c.php';
|
||||||
|
require_once WD . '/modules/bird/config/bird_server_url_c.php';
|
||||||
|
|
||||||
|
use bird\config\bird_enabled_c;
|
||||||
|
use bird\config\bird_api_key_c;
|
||||||
|
use bird\config\bird_server_url_c;
|
||||||
|
use traits\module_config_t;
|
||||||
|
|
||||||
|
class bird_c
|
||||||
|
{
|
||||||
|
use module_config_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the Bird module is enabled
|
||||||
|
* @var bird_enabled_c
|
||||||
|
*/
|
||||||
|
public bird_enabled_c $enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API key/token for Bird API (secret)
|
||||||
|
* @var bird_api_key_c
|
||||||
|
*/
|
||||||
|
public bird_api_key_c $api_key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base server URL for Bird API
|
||||||
|
* @var bird_server_url_c
|
||||||
|
*/
|
||||||
|
public bird_server_url_c $server_url;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->setupConfig('bird');
|
||||||
|
$this->allowUpdate([
|
||||||
|
bird_enabled_c::class,
|
||||||
|
bird_api_key_c::class,
|
||||||
|
bird_server_url_c::class,
|
||||||
|
]);
|
||||||
|
$this->enabled = new bird_enabled_c();
|
||||||
|
$this->api_key = new bird_api_key_c();
|
||||||
|
$this->server_url = new bird_server_url_c();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace bird\config;
|
||||||
|
|
||||||
|
require_once WD . '/traits/module_config_variable_t.php';
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use traits\module_config_variable;
|
||||||
|
|
||||||
|
class bird_api_key_c
|
||||||
|
{
|
||||||
|
use module_config_variable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
self::setupConfigVariable(
|
||||||
|
'bird',
|
||||||
|
'api_key',
|
||||||
|
'string',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
'The API key/token for authenticating against Bird API',
|
||||||
|
'sk_test_XXXXXXXXXXXXXXXXXXXXXXXX',
|
||||||
|
true,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace bird\config;
|
||||||
|
|
||||||
|
require_once WD . '/traits/module_config_variable_t.php';
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use traits\module_config_variable;
|
||||||
|
|
||||||
|
class bird_enabled_c
|
||||||
|
{
|
||||||
|
use module_config_variable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
self::setupConfigVariable(
|
||||||
|
'bird',
|
||||||
|
'enabled',
|
||||||
|
'bool',
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
'Whether the Bird module is enabled or not',
|
||||||
|
'true',
|
||||||
|
false,
|
||||||
|
'false'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace bird\config;
|
||||||
|
|
||||||
|
require_once WD . '/traits/module_config_variable_t.php';
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use traits\module_config_variable;
|
||||||
|
|
||||||
|
class bird_server_url_c
|
||||||
|
{
|
||||||
|
use module_config_variable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
self::setupConfigVariable(
|
||||||
|
'bird',
|
||||||
|
'server_url',
|
||||||
|
'string',
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
'The base URL for the Bird API',
|
||||||
|
'https://api.bird.com',
|
||||||
|
false,
|
||||||
|
'https://api.bird.com'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace routes;
|
||||||
|
|
||||||
|
use classes\bird;
|
||||||
|
use traits\route_t;
|
||||||
|
|
||||||
|
class birdNumbersRoute
|
||||||
|
{
|
||||||
|
use route_t;
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// List owned numbers
|
||||||
|
$this->get('/bird/numbers', function () {
|
||||||
|
global $response;
|
||||||
|
// Permission: list numbers via Bird
|
||||||
|
self::requirePermission('modules_bird_numbers_list');
|
||||||
|
$client = new bird();
|
||||||
|
$query = $this->getParametersAsArray();
|
||||||
|
$res = $client->sendGetRequest('/numbers', $query);
|
||||||
|
$response->success($res ?? []);
|
||||||
|
}, [
|
||||||
|
'modules_bird_numbers_list' => 'List your numbers via Bird',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace routes;
|
||||||
|
|
||||||
|
use classes\bird;
|
||||||
|
use traits\route_t;
|
||||||
|
|
||||||
|
class birdVoiceCallsRoute
|
||||||
|
{
|
||||||
|
use route_t;
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Create a voice call
|
||||||
|
$this->post('/bird/voice/calls', function () {
|
||||||
|
global $response;
|
||||||
|
// Permission: create/place a voice call via Bird
|
||||||
|
self::requirePermission('modules_bird_voice_calls_create');
|
||||||
|
$client = new bird();
|
||||||
|
// Require workspace/channel per Bird API docs
|
||||||
|
$ws = (string)($this->fromRequest('workspaceId') ?? '');
|
||||||
|
$ch = (string)($this->fromRequest('channelId') ?? '');
|
||||||
|
if ($ws === '' || $ch === '') {
|
||||||
|
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||||
|
}
|
||||||
|
$base = '/workspaces/' . rawurlencode($ws) . '/channels/' . rawurlencode($ch) . '/calls';
|
||||||
|
$payload = $this->getParametersAsArray();
|
||||||
|
unset($payload['workspaceId'], $payload['channelId']);
|
||||||
|
$res = $client->sendPostRequest($base, $payload);
|
||||||
|
$response->success($res ?? ['status' => 'ok']);
|
||||||
|
}, [
|
||||||
|
'modules_bird_voice_calls_create' => 'Create/place a voice call via Bird',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// List voice calls (passthrough of query filters)
|
||||||
|
$this->get('/bird/voice/calls', function () {
|
||||||
|
global $response;
|
||||||
|
// Permission: list voice calls via Bird
|
||||||
|
self::requirePermission('modules_bird_voice_calls_list');
|
||||||
|
$client = new bird();
|
||||||
|
$ws = (string)($this->fromQuery('workspaceId') ?? '');
|
||||||
|
$ch = (string)($this->fromQuery('channelId') ?? '');
|
||||||
|
if ($ws === '' || $ch === '') {
|
||||||
|
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||||
|
}
|
||||||
|
$base = '/workspaces/' . rawurlencode($ws) . '/channels/' . rawurlencode($ch) . '/calls';
|
||||||
|
$query = $this->getParametersAsArray();
|
||||||
|
unset($query['workspaceId'], $query['channelId']);
|
||||||
|
$res = $client->sendGetRequest($base, $query);
|
||||||
|
$response->success($res ?? []);
|
||||||
|
}, [
|
||||||
|
'modules_bird_voice_calls_list' => 'List voice calls via Bird',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get a specific call by ID
|
||||||
|
$this->get('/bird/voice/calls/{id}', function () {
|
||||||
|
global $response;
|
||||||
|
// Permission: get a specific voice call via Bird
|
||||||
|
self::requirePermission('modules_bird_voice_calls_get');
|
||||||
|
$client = new bird();
|
||||||
|
$id = (string)$this->fromRoute('id');
|
||||||
|
if ($id === null || $id === '') {
|
||||||
|
$response->error('Missing id', 400);
|
||||||
|
}
|
||||||
|
$ws = (string)($this->fromQuery('workspaceId') ?? '');
|
||||||
|
$ch = (string)($this->fromQuery('channelId') ?? '');
|
||||||
|
if ($ws === '' || $ch === '') {
|
||||||
|
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||||
|
}
|
||||||
|
$base = '/workspaces/' . rawurlencode($ws) . '/channels/' . rawurlencode($ch) . '/calls';
|
||||||
|
$res = $client->sendGetRequest($base . '/' . rawurlencode($id));
|
||||||
|
$response->success($res ?? []);
|
||||||
|
}, [
|
||||||
|
'modules_bird_voice_calls_get' => 'Get a voice call by ID via Bird',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Hang up an active call by ID
|
||||||
|
$this->post('/bird/voice/calls/{id}/hangup', function () {
|
||||||
|
global $response;
|
||||||
|
// Permission: hang up a specific voice call via Bird
|
||||||
|
self::requirePermission('modules_bird_voice_calls_hangup');
|
||||||
|
$client = new bird();
|
||||||
|
$id = (string)$this->fromRoute('id');
|
||||||
|
if ($id === null || $id === '') {
|
||||||
|
$response->error('Missing id', 400);
|
||||||
|
}
|
||||||
|
$ws = (string)($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId') ?? '');
|
||||||
|
$ch = (string)($this->fromRequest('channelId') ?? $this->fromQuery('channelId') ?? '');
|
||||||
|
if ($ws === '' || $ch === '') {
|
||||||
|
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||||
|
}
|
||||||
|
$base = '/workspaces/' . rawurlencode($ws) . '/channels/' . rawurlencode($ch) . '/calls';
|
||||||
|
$res = $client->sendPostRequest($base . '/' . rawurlencode($id) . '/hangup', []);
|
||||||
|
$response->success($res ?? ['status' => 'ok']);
|
||||||
|
}, [
|
||||||
|
'modules_bird_voice_calls_hangup' => 'Hang up a voice call by ID via Bird',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace routes;
|
||||||
|
|
||||||
|
use classes\bird;
|
||||||
|
use traits\route_t;
|
||||||
|
|
||||||
|
class birdVoiceFlashCallsRoute
|
||||||
|
{
|
||||||
|
use route_t;
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Create a flash call
|
||||||
|
$this->post('/bird/voice/flash-calls', function () {
|
||||||
|
global $response;
|
||||||
|
// Permission: create/place a flash call via Bird
|
||||||
|
self::requirePermission('modules_bird_voice_flash_calls_create');
|
||||||
|
$client = new bird();
|
||||||
|
// Require workspace/channel per Bird API docs
|
||||||
|
$ws = (string)($this->fromRequest('workspaceId') ?? '');
|
||||||
|
$ch = (string)($this->fromRequest('channelId') ?? '');
|
||||||
|
if ($ws === '' || $ch === '') {
|
||||||
|
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||||
|
}
|
||||||
|
$base = '/workspaces/' . rawurlencode($ws) . '/channels/' . rawurlencode($ch) . '/flashcalls';
|
||||||
|
$payload = $this->getParametersAsArray();
|
||||||
|
unset($payload['workspaceId'], $payload['channelId']);
|
||||||
|
$res = $client->sendPostRequest($base, $payload);
|
||||||
|
$response->success($res ?? ['status' => 'ok']);
|
||||||
|
}, [
|
||||||
|
'modules_bird_voice_flash_calls_create' => 'Create/place a flash call via Bird',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// List flash calls
|
||||||
|
$this->get('/bird/voice/flash-calls', function () {
|
||||||
|
global $response;
|
||||||
|
// Permission: list flash calls via Bird
|
||||||
|
self::requirePermission('modules_bird_voice_flash_calls_list');
|
||||||
|
$client = new bird();
|
||||||
|
$ws = (string)($this->fromQuery('workspaceId') ?? '');
|
||||||
|
$ch = (string)($this->fromQuery('channelId') ?? '');
|
||||||
|
if ($ws === '' || $ch === '') {
|
||||||
|
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||||
|
}
|
||||||
|
$base = '/workspaces/' . rawurlencode($ws) . '/channels/' . rawurlencode($ch) . '/flashcalls';
|
||||||
|
$query = $this->getParametersAsArray();
|
||||||
|
unset($query['workspaceId'], $query['channelId']);
|
||||||
|
$res = $client->sendGetRequest($base, $query);
|
||||||
|
$response->success($res ?? []);
|
||||||
|
}, [
|
||||||
|
'modules_bird_voice_flash_calls_list' => 'List flash calls via Bird',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get a specific flash call by ID
|
||||||
|
$this->get('/bird/voice/flash-calls/{id}', function () {
|
||||||
|
global $response;
|
||||||
|
// Permission: get a specific flash call via Bird
|
||||||
|
self::requirePermission('modules_bird_voice_flash_calls_get');
|
||||||
|
$client = new bird();
|
||||||
|
$id = (string)$this->fromRoute('id');
|
||||||
|
if ($id === null || $id === '') {
|
||||||
|
$response->error('Missing id', 400);
|
||||||
|
}
|
||||||
|
$ws = (string)($this->fromQuery('workspaceId') ?? '');
|
||||||
|
$ch = (string)($this->fromQuery('channelId') ?? '');
|
||||||
|
if ($ws === '' || $ch === '') {
|
||||||
|
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||||
|
}
|
||||||
|
$base = '/workspaces/' . rawurlencode($ws) . '/channels/' . rawurlencode($ch) . '/flashcalls';
|
||||||
|
$res = $client->sendGetRequest($base . '/' . rawurlencode($id));
|
||||||
|
$response->success($res ?? []);
|
||||||
|
}, [
|
||||||
|
'modules_bird_voice_flash_calls_get' => 'Get a flash call by ID via Bird',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Complete/end a flash call by ID (POST to the resource)
|
||||||
|
$this->post('/bird/voice/flash-calls/{id}', function () {
|
||||||
|
global $response;
|
||||||
|
// Permission: complete/end a flash call via Bird
|
||||||
|
self::requirePermission('modules_bird_voice_flash_calls_end');
|
||||||
|
$client = new bird();
|
||||||
|
$id = (string)$this->fromRoute('id');
|
||||||
|
if ($id === null || $id === '') {
|
||||||
|
$response->error('Missing id', 400);
|
||||||
|
}
|
||||||
|
// Forward any body fields (e.g., result/status) transparently
|
||||||
|
$ws = (string)($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId') ?? '');
|
||||||
|
$ch = (string)($this->fromRequest('channelId') ?? $this->fromQuery('channelId') ?? '');
|
||||||
|
if ($ws === '' || $ch === '') {
|
||||||
|
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||||
|
}
|
||||||
|
$base = '/workspaces/' . rawurlencode($ws) . '/channels/' . rawurlencode($ch) . '/flashcalls';
|
||||||
|
$payload = $this->getParametersAsArray();
|
||||||
|
unset($payload['workspaceId'], $payload['channelId']);
|
||||||
|
$res = $client->sendPostRequest($base . '/' . rawurlencode($id), $payload);
|
||||||
|
$response->success($res ?? ['status' => 'ok']);
|
||||||
|
}, [
|
||||||
|
'modules_bird_voice_flash_calls_end' => 'Complete/end a flash call by ID via Bird',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Complete/end a flash call by using from/to numbers
|
||||||
|
$this->post('/bird/voice/flash-calls/end', function () {
|
||||||
|
global $response;
|
||||||
|
// Permission: complete/end a flash call by numbers via Bird
|
||||||
|
self::requirePermission('modules_bird_voice_flash_calls_end_by_numbers');
|
||||||
|
$client = new bird();
|
||||||
|
$ws = (string)($this->fromRequest('workspaceId') ?? '');
|
||||||
|
$ch = (string)($this->fromRequest('channelId') ?? '');
|
||||||
|
if ($ws === '' || $ch === '') {
|
||||||
|
$response->error('Missing required parameters: workspaceId, channelId', 400);
|
||||||
|
}
|
||||||
|
$base = '/workspaces/' . rawurlencode($ws) . '/channels/' . rawurlencode($ch) . '/flashcalls';
|
||||||
|
$payload = $this->getParametersAsArray();
|
||||||
|
unset($payload['workspaceId'], $payload['channelId']);
|
||||||
|
// Basic validation hints: expect 'from' and 'to' but let Bird validate strictly
|
||||||
|
$res = $client->sendPostRequest($base . '/end', $payload);
|
||||||
|
$response->success($res ?? ['status' => 'ok']);
|
||||||
|
}, [
|
||||||
|
'modules_bird_voice_flash_calls_end_by_numbers' => 'Complete/end a flash call using from/to numbers via Bird',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
// Define app root for direct CLI execution
|
||||||
|
if (!defined('WD')) {
|
||||||
|
define('WD', dirname(__DIR__, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once WD . '/classes/bird.php';
|
||||||
|
|
||||||
|
function ok($message): void { echo "\n\033[32m✔ $message\033[0m\n"; }
|
||||||
|
function fail($message): void { echo "\n\033[31m✖ $message\033[0m\n"; }
|
||||||
|
|
||||||
|
// Minimal fakes to avoid DB and network
|
||||||
|
class FakeBoolVar { private bool $v; public function __construct(bool $v){$this->v=$v;} public function isTrue(): bool { return $this->v; } public function getVariableValue(): string { return $this->v ? 'true' : 'false'; } }
|
||||||
|
class FakeStringVar { private string $v; public function __construct(string $v){$this->v=$v;} public function getVariableValue(): string { return $this->v; } }
|
||||||
|
class FakeBirdConfig { public $enabled; public $api_key; public $server_url; }
|
||||||
|
|
||||||
|
class TestBird extends \classes\bird {
|
||||||
|
public function __construct(bool $enabled, string $api_key, string $server_url)
|
||||||
|
{
|
||||||
|
// Do not call parent constructor to avoid DB access
|
||||||
|
$cfg = new FakeBirdConfig();
|
||||||
|
$cfg->enabled = new FakeBoolVar($enabled);
|
||||||
|
$cfg->api_key = new FakeStringVar($api_key);
|
||||||
|
$cfg->server_url = new FakeStringVar($server_url);
|
||||||
|
$this->config = $cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doHttpRequest(string $method, string $url, array $headers, string $body = ''): array
|
||||||
|
{
|
||||||
|
return ['status_code' => 200, 'body' => '{}'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positive checks
|
||||||
|
$b = new TestBird(true, 'test_token_123', 'https://api.bird.com');
|
||||||
|
try { $b->requireModuleEnabled(); ok('Module enabled check passes when enabled=true'); } catch (\Exception $e) { fail('Module enabled should pass when enabled=true'); }
|
||||||
|
try { $b->requireValidApiKey(); ok('API key check passes when key provided'); } catch (\Exception $e) { fail('API key should be considered valid when provided'); }
|
||||||
|
try { $b->requireValidServerURL(); ok('Server URL check passes when URL provided'); } catch (\Exception $e) { fail('Server URL should be considered valid when provided'); }
|
||||||
|
|
||||||
|
// Negative checks
|
||||||
|
$thrown = false; try { (new TestBird(false, 'x', 'https://api.bird.com'))->requireModuleEnabled(); } catch (\Exception $e) { $thrown = true; }
|
||||||
|
if ($thrown) { ok('Module enabled guard throws when disabled'); } else { fail('Module enabled guard should throw when disabled'); }
|
||||||
|
|
||||||
|
$thrown = false; try { (new TestBird(true, '', 'https://api.bird.com'))->requireValidApiKey(); } catch (\Exception $e) { $thrown = true; }
|
||||||
|
if ($thrown) { ok('API key guard throws when key is empty'); } else { fail('API key guard should throw when key is empty'); }
|
||||||
|
|
||||||
|
$thrown = false; try { (new TestBird(true, 'x', ''))->requireValidServerURL(); } catch (\Exception $e) { $thrown = true; }
|
||||||
|
if ($thrown) { ok('Server URL guard throws when URL is empty'); } else { fail('Server URL guard should throw when URL is empty'); }
|
||||||
|
|
||||||
|
echo "\nBirdConfigTest completed.\n";
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
// Define app root for direct CLI execution
|
||||||
|
if (!defined('WD')) {
|
||||||
|
define('WD', dirname(__DIR__, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once WD . '/classes/bird.php';
|
||||||
|
|
||||||
|
function ok($message): void { echo "\n\033[32m✔ $message\033[0m\n"; }
|
||||||
|
function fail($message): void { echo "\n\033[31m✖ $message\033[0m\n"; }
|
||||||
|
|
||||||
|
class FakeBoolVar { private bool $v; public function __construct(bool $v){$this->v=$v;} public function isTrue(): bool { return $this->v; } }
|
||||||
|
class FakeStringVar { private string $v; public function __construct(string $v){$this->v=$v;} public function getVariableValue(): string { return $this->v; } }
|
||||||
|
class FakeBirdConfig { public $enabled; public $api_key; public $server_url; }
|
||||||
|
|
||||||
|
class InspectableBird extends \classes\bird
|
||||||
|
{
|
||||||
|
public array $last = [];
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// Inject fake config to avoid DB
|
||||||
|
$cfg = new FakeBirdConfig();
|
||||||
|
$cfg->enabled = new FakeBoolVar(true);
|
||||||
|
$cfg->api_key = new FakeStringVar('secret_token');
|
||||||
|
$cfg->server_url = new FakeStringVar('https://api.bird.com');
|
||||||
|
$this->config = $cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doHttpRequest(string $method, string $url, array $headers, string $body = ''): array
|
||||||
|
{
|
||||||
|
$this->last = compact('method', 'url', 'headers', 'body');
|
||||||
|
// Return 200 OK with empty JSON object
|
||||||
|
return ['status_code' => 200, 'body' => '{}'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$bird = new InspectableBird();
|
||||||
|
$payload = ['alpha' => 1, 'beta' => 'two'];
|
||||||
|
$bird->sendPostRequest('/v1/demo', $payload);
|
||||||
|
|
||||||
|
$expectedUrl = 'https://api.bird.com/v1/demo';
|
||||||
|
if (($bird->last['url'] ?? '') === $expectedUrl) { ok('URL is correctly composed with base + endpoint'); } else { fail('URL should be ' . $expectedUrl . ' but was ' . ($bird->last['url'] ?? '<none>')); }
|
||||||
|
|
||||||
|
$headers = $bird->last['headers'] ?? [];
|
||||||
|
$hasAuth = false; $hasJson = false;
|
||||||
|
foreach ($headers as $h) {
|
||||||
|
if (stripos($h, 'authorization:') === 0 && str_contains($h, 'Bearer secret_token')) { $hasAuth = true; }
|
||||||
|
if (strcasecmp($h, 'Content-Type: application/json') === 0) { $hasJson = true; }
|
||||||
|
}
|
||||||
|
if ($hasAuth) { ok('Authorization header includes Bearer token'); } else { fail('Missing or invalid Authorization header'); }
|
||||||
|
if ($hasJson) { ok('Content-Type header is application/json'); } else { fail('Missing Content-Type: application/json header'); }
|
||||||
|
|
||||||
|
$expectedBody = json_encode($payload);
|
||||||
|
if (($bird->last['body'] ?? '') === $expectedBody) { ok('Request body is JSON-encoded as expected'); } else { fail('Request body JSON mismatch'); }
|
||||||
|
|
||||||
|
echo "\nBirdHttpHeadersTest completed.\n";
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (!defined('WD')) { define('WD', dirname(__DIR__, 2)); }
|
||||||
|
|
||||||
|
require_once WD . '/classes/bird.php';
|
||||||
|
|
||||||
|
use classes\bird as BirdClient;
|
||||||
|
|
||||||
|
class DummyVar2
|
||||||
|
{
|
||||||
|
public function __construct(private mixed $value) {}
|
||||||
|
public function isTrue(): bool { return $this->value === true || $this->value === 'true' || $this->value === 1 || $this->value === '1'; }
|
||||||
|
public function getVariableValue(): mixed { return $this->value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class DummyConfig2
|
||||||
|
{
|
||||||
|
public DummyVar2 $enabled;
|
||||||
|
public DummyVar2 $api_key;
|
||||||
|
public DummyVar2 $server_url;
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->enabled = new DummyVar2('true');
|
||||||
|
$this->api_key = new DummyVar2('test_api_key');
|
||||||
|
$this->server_url = new DummyVar2('https://example.test');
|
||||||
|
}
|
||||||
|
public function getModuleName(): string { return 'bird'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeBird2 extends BirdClient
|
||||||
|
{
|
||||||
|
public array $last = [];
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// Bypass DB-backed config classes with our dummy config
|
||||||
|
$this->config = new DummyConfig2();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doHttpRequest(string $method, string $url, array $headers, string $body = ''): array
|
||||||
|
{
|
||||||
|
$this->last = compact('method','url','headers','body');
|
||||||
|
// Return a canned success response
|
||||||
|
$resp = ['ok' => true, 'id' => 'flash_123'];
|
||||||
|
return ['status_code' => 200, 'body' => json_encode($resp)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assert_true2($cond, $msg)
|
||||||
|
{
|
||||||
|
if ($cond) {
|
||||||
|
echo "✔ $msg\n";
|
||||||
|
} else {
|
||||||
|
echo "✘ $msg\n"; exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test: list numbers (GET /numbers)
|
||||||
|
$client = new FakeBird2();
|
||||||
|
$res = $client->sendGetRequest('/numbers', ['limit' => 10]);
|
||||||
|
assert_true2(is_object($res) || is_array($res), 'Numbers response is JSON-decodable');
|
||||||
|
assert_true2(str_starts_with($client->last['url'], 'https://example.test/numbers'), 'Numbers GET URL composed correctly');
|
||||||
|
assert_true2(str_contains($client->last['url'], 'limit=10'), 'Query string encoded correctly');
|
||||||
|
assert_true2($client->last['method'] === 'GET', 'HTTP method is GET (numbers)');
|
||||||
|
assert_true2(in_array('Authorization: Bearer test_api_key', $client->last['headers'], true), 'Authorization header is set (numbers)');
|
||||||
|
assert_true2(empty($client->last['body']), 'GET body is empty (numbers)');
|
||||||
|
|
||||||
|
// Test: create flash call (POST /voice/flash-calls)
|
||||||
|
$client2 = new FakeBird2();
|
||||||
|
$payload = [
|
||||||
|
'to' => '+4511122233',
|
||||||
|
'from' => '+4599988877',
|
||||||
|
'code' => '1234',
|
||||||
|
];
|
||||||
|
$res2 = $client2->sendPostRequest('/voice/flash-calls', $payload);
|
||||||
|
assert_true2(is_object($res2) || is_array($res2), 'Flash call response is JSON-decodable');
|
||||||
|
assert_true2(str_starts_with($client2->last['url'], 'https://example.test/voice/flash-calls'), 'Flash POST URL composed correctly');
|
||||||
|
assert_true2($client2->last['method'] === 'POST', 'HTTP method is POST (flash)');
|
||||||
|
assert_true2(in_array('Authorization: Bearer test_api_key', $client2->last['headers'], true), 'Authorization header is set (flash)');
|
||||||
|
assert_true2(json_decode($client2->last['body'], true)['code'] === '1234', 'Flash POST body encoded correctly');
|
||||||
|
|
||||||
|
// Test: list flash calls (GET /voice/flash-calls)
|
||||||
|
$client3 = new FakeBird2();
|
||||||
|
$client3->sendGetRequest('/voice/flash-calls', ['page' => 2]);
|
||||||
|
assert_true2(str_starts_with($client3->last['url'], 'https://example.test/voice/flash-calls'), 'Flash list GET URL composed correctly');
|
||||||
|
assert_true2(str_contains($client3->last['url'], 'page=2'), 'Flash list query encoded correctly');
|
||||||
|
assert_true2($client3->last['method'] === 'GET', 'HTTP method is GET (flash list)');
|
||||||
|
|
||||||
|
// Test: get flash call by id (GET /voice/flash-calls/{id})
|
||||||
|
$client4 = new FakeBird2();
|
||||||
|
$client4->sendGetRequest('/voice/flash-calls/flash_123');
|
||||||
|
assert_true2(str_ends_with($client4->last['url'], '/voice/flash-calls/flash_123'), 'Flash get-by-id URL composed correctly');
|
||||||
|
assert_true2($client4->last['method'] === 'GET', 'HTTP method is GET (flash by id)');
|
||||||
|
|
||||||
|
// Test: end flash call by id (POST /voice/flash-calls/{id})
|
||||||
|
$client5 = new FakeBird2();
|
||||||
|
$payloadEnd = [ 'result' => 'success' ];
|
||||||
|
$client5->sendPostRequest('/voice/flash-calls/flash_123', $payloadEnd);
|
||||||
|
assert_true2(str_ends_with($client5->last['url'], '/voice/flash-calls/flash_123'), 'Flash end-by-id URL composed correctly');
|
||||||
|
assert_true2($client5->last['method'] === 'POST', 'HTTP method is POST (flash end by id)');
|
||||||
|
assert_true2(json_decode($client5->last['body'], true)['result'] === 'success', 'Flash end-by-id body encoded correctly');
|
||||||
|
|
||||||
|
// Test: end flash call by numbers (POST /voice/flash-calls/end)
|
||||||
|
$client6 = new FakeBird2();
|
||||||
|
$payloadEndByNums = [ 'from' => '+4599988877', 'to' => '+4511122233' ];
|
||||||
|
$client6->sendPostRequest('/voice/flash-calls/end', $payloadEndByNums);
|
||||||
|
assert_true2(str_ends_with($client6->last['url'], '/voice/flash-calls/end'), 'Flash end-by-numbers URL composed correctly');
|
||||||
|
assert_true2($client6->last['method'] === 'POST', 'HTTP method is POST (flash end by numbers)');
|
||||||
|
assert_true2(json_decode($client6->last['body'], true)['from'] === '+4599988877', 'Flash end-by-numbers body encoded correctly');
|
||||||
|
|
||||||
|
echo "NumbersAndFlashCallsApiTest completed.\n";
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (!defined('WD')) { define('WD', dirname(__DIR__, 2)); }
|
||||||
|
|
||||||
|
require_once WD . '/classes/bird.php';
|
||||||
|
|
||||||
|
use classes\bird as BirdClient;
|
||||||
|
|
||||||
|
class DummyVar
|
||||||
|
{
|
||||||
|
public function __construct(private mixed $value) {}
|
||||||
|
public function isTrue(): bool { return $this->value === true || $this->value === 'true' || $this->value === 1 || $this->value === '1'; }
|
||||||
|
public function getVariableValue(): mixed { return $this->value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class DummyConfig
|
||||||
|
{
|
||||||
|
public DummyVar $enabled;
|
||||||
|
public DummyVar $api_key;
|
||||||
|
public DummyVar $server_url;
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->enabled = new DummyVar('true');
|
||||||
|
$this->api_key = new DummyVar('test_api_key');
|
||||||
|
$this->server_url = new DummyVar('https://example.test');
|
||||||
|
}
|
||||||
|
public function getModuleName(): string { return 'bird'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeBird extends BirdClient
|
||||||
|
{
|
||||||
|
public array $last = [];
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// Bypass DB-backed config classes with our dummy config
|
||||||
|
$this->config = new DummyConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doHttpRequest(string $method, string $url, array $headers, string $body = ''): array
|
||||||
|
{
|
||||||
|
$this->last = compact('method','url','headers','body');
|
||||||
|
// Return a canned success response
|
||||||
|
$resp = ['id' => 'call_123', 'status' => 'queued'];
|
||||||
|
return ['status_code' => 200, 'body' => json_encode($resp)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assert_true($cond, $msg)
|
||||||
|
{
|
||||||
|
if ($cond) {
|
||||||
|
echo "✔ $msg\n";
|
||||||
|
} else {
|
||||||
|
echo "✘ $msg\n"; exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test: create call (POST /voice/calls)
|
||||||
|
$client = new FakeBird();
|
||||||
|
$payload = [
|
||||||
|
'to' => '+4511122233',
|
||||||
|
'from' => '+4599988877',
|
||||||
|
'tts' => [ 'message' => 'Hello from test' ],
|
||||||
|
];
|
||||||
|
$res = $client->sendPostRequest('/voice/calls', $payload);
|
||||||
|
assert_true(is_object($res) || is_array($res), 'Response is JSON-decodable');
|
||||||
|
assert_true(str_starts_with($client->last['url'], 'https://example.test/voice/calls'), 'POST URL composed correctly');
|
||||||
|
assert_true($client->last['method'] === 'POST', 'HTTP method is POST');
|
||||||
|
assert_true(in_array('Authorization: Bearer test_api_key', $client->last['headers'], true), 'Authorization header is set');
|
||||||
|
assert_true(json_decode($client->last['body'], true)['to'] === '+4511122233', 'POST body encoded correctly');
|
||||||
|
|
||||||
|
// Test: get call (GET /voice/calls/{id})
|
||||||
|
$client2 = new FakeBird();
|
||||||
|
$res2 = $client2->sendGetRequest('/voice/calls/call_123');
|
||||||
|
assert_true(str_starts_with($client2->last['url'], 'https://example.test/voice/calls/call_123'), 'GET URL composed correctly');
|
||||||
|
assert_true($client2->last['method'] === 'GET', 'HTTP method is GET');
|
||||||
|
assert_true(empty($client2->last['body']), 'GET body is empty');
|
||||||
|
|
||||||
|
echo "VoiceCallsApiTest completed.\n";
|
||||||
Reference in New Issue
Block a user