Refactor Bird API routes to support fallback workspace and channel configuration, centralize helper methods, and update OpenAPI spec to reflect optional parameters.

This commit is contained in:
Jeppe Bundgaard
2026-03-04 10:42:33 +01:00
parent 98203282ba
commit cbac9181f6
7 changed files with 175 additions and 141 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ class bird
{
public const TEST_OUTBOUND_NUMBER_RAW = '+45 42 33 11 28';
public const TEST_OUTBOUND_NUMBER_E164 = '+4542331128';
private const ALLOWED_HANGUP_CAUSES = ['rejected', 'busy'];
private const ALLOWED_HANGUP_CAUSES = ['rejected', 'busy', 'completed'];
/**
* Configuration of the Bird module
+2 -32
View File
@@ -3,42 +3,12 @@
namespace routes;
use classes\bird;
use traits\bird_route_helpers_t;
use traits\route_t;
class birdNumbersRoute
{
use route_t;
private function normalizeOptionalString(mixed $value): string
{
if (!is_scalar($value)) {
return '';
}
$normalized = trim((string)$value);
if ($normalized === '') {
return '';
}
$lower = strtolower($normalized);
if ($lower === 'undefined' || $lower === 'null') {
return '';
}
return $normalized;
}
private function getConfiguredWorkspaceId(bird $client): string
{
$workspaceConfig = null;
if (property_exists($client->config, 'workspaceId')) {
$workspaceConfig = $client->config->workspaceId;
} elseif (property_exists($client->config, 'workplaceId')) {
// Backward compatibility with existing config key naming.
$workspaceConfig = $client->config->workplaceId;
}
if (is_object($workspaceConfig) && method_exists($workspaceConfig, 'getVariableValue')) {
return $this->normalizeOptionalString($workspaceConfig->getVariableValue());
}
return '';
}
use route_t, bird_route_helpers_t;
public function run(): void
{
@@ -3,54 +3,12 @@
namespace routes;
use classes\bird;
use traits\bird_route_helpers_t;
use traits\route_t;
class birdVoiceCallsRoute
{
use route_t;
private function normalizeOptionalString(mixed $value): string
{
if (!is_scalar($value)) {
return '';
}
$normalized = trim((string)$value);
if ($normalized === '') {
return '';
}
$lower = strtolower($normalized);
if ($lower === 'undefined' || $lower === 'null') {
return '';
}
return $normalized;
}
private function getConfiguredWorkspaceId(bird $client): string
{
$workspaceConfig = null;
if (property_exists($client->config, 'workspaceId')) {
$workspaceConfig = $client->config->workspaceId;
} elseif (property_exists($client->config, 'workplaceId')) {
// Backward compatibility with existing config key naming.
$workspaceConfig = $client->config->workplaceId;
}
if (is_object($workspaceConfig) && method_exists($workspaceConfig, 'getVariableValue')) {
return $this->normalizeOptionalString($workspaceConfig->getVariableValue());
}
return '';
}
private function getConfiguredChannelId(bird $client): string
{
$channelConfig = null;
if (property_exists($client->config, 'channelId')) {
$channelConfig = $client->config->channelId;
}
if (is_object($channelConfig) && method_exists($channelConfig, 'getVariableValue')) {
return $this->normalizeOptionalString($channelConfig->getVariableValue());
}
return '';
}
use route_t, bird_route_helpers_t;
public function run(): void
{
@@ -61,8 +19,14 @@ class birdVoiceCallsRoute
self::requirePermission('modules_bird_voice_calls_create');
$client = new bird();
// Require workspace/channel per Bird API docs
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId'));
$ch = $this->normalizeOptionalString($this->fromRequest('channelId'));
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
if ($ws === '') {
$ws = $this->getConfiguredWorkspaceId($client);
}
if ($ch === '') {
$ch = $this->getConfiguredChannelId($client);
}
if ($ws === '' || $ch === '') {
$response->error('Missing required parameters: workspaceId, channelId', 400);
}
@@ -78,10 +42,16 @@ class birdVoiceCallsRoute
$this->get('/bird/voice/calls', function () {
global $response;
// Permission: list voice calls via Bird
self::requirePermission('modules_bird_voice_calls_list');
//self::requirePermission('modules_bird_voice_calls_list');
$client = new bird();
$ws = $this->normalizeOptionalString($this->fromQuery('workspaceId'));
$ch = $this->normalizeOptionalString($this->fromQuery('channelId'));
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
if ($ws === '') {
$ws = $this->getConfiguredWorkspaceId($client);
}
if ($ch === '') {
$ch = $this->getConfiguredChannelId($client);
}
if ($ws === '' || $ch === '') {
$response->error('Missing required parameters: workspaceId, channelId', 400);
}
@@ -103,8 +73,14 @@ class birdVoiceCallsRoute
if ($id === null || $id === '') {
$response->error('Missing id', 400);
}
$ws = $this->normalizeOptionalString($this->fromQuery('workspaceId'));
$ch = $this->normalizeOptionalString($this->fromQuery('channelId'));
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
if ($ws === '') {
$ws = $this->getConfiguredWorkspaceId($client);
}
if ($ch === '') {
$ch = $this->getConfiguredChannelId($client);
}
if ($ws === '' || $ch === '') {
$response->error('Missing required parameters: workspaceId, channelId', 400);
}
@@ -126,6 +102,12 @@ class birdVoiceCallsRoute
}
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
if ($ws === '') {
$ws = $this->getConfiguredWorkspaceId($client);
}
if ($ch === '') {
$ch = $this->getConfiguredChannelId($client);
}
if ($ws === '' || $ch === '') {
$response->error('Missing required parameters: workspaceId, channelId', 400);
}
@@ -3,11 +3,12 @@
namespace routes;
use classes\bird;
use traits\bird_route_helpers_t;
use traits\route_t;
class birdVoiceFlashCallsRoute
{
use route_t;
use route_t, bird_route_helpers_t;
public function run(): void
{
@@ -18,8 +19,14 @@ class birdVoiceFlashCallsRoute
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') ?? '');
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
if ($ws === '') {
$ws = $this->getConfiguredWorkspaceId($client);
}
if ($ch === '') {
$ch = $this->getConfiguredChannelId($client);
}
if ($ws === '' || $ch === '') {
$response->error('Missing required parameters: workspaceId, channelId', 400);
}
@@ -38,8 +45,14 @@ class birdVoiceFlashCallsRoute
// 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') ?? '');
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
if ($ws === '') {
$ws = $this->getConfiguredWorkspaceId($client);
}
if ($ch === '') {
$ch = $this->getConfiguredChannelId($client);
}
if ($ws === '' || $ch === '') {
$response->error('Missing required parameters: workspaceId, channelId', 400);
}
@@ -62,8 +75,14 @@ class birdVoiceFlashCallsRoute
if ($id === null || $id === '') {
$response->error('Missing id', 400);
}
$ws = (string)($this->fromQuery('workspaceId') ?? '');
$ch = (string)($this->fromQuery('channelId') ?? '');
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
if ($ws === '') {
$ws = $this->getConfiguredWorkspaceId($client);
}
if ($ch === '') {
$ch = $this->getConfiguredChannelId($client);
}
if ($ws === '' || $ch === '') {
$response->error('Missing required parameters: workspaceId, channelId', 400);
}
@@ -85,8 +104,14 @@ class birdVoiceFlashCallsRoute
$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') ?? '');
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
if ($ws === '') {
$ws = $this->getConfiguredWorkspaceId($client);
}
if ($ch === '') {
$ch = $this->getConfiguredChannelId($client);
}
if ($ws === '' || $ch === '') {
$response->error('Missing required parameters: workspaceId, channelId', 400);
}
@@ -105,8 +130,14 @@ class birdVoiceFlashCallsRoute
// 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') ?? '');
$ws = $this->normalizeOptionalString($this->fromRequest('workspaceId') ?? $this->fromQuery('workspaceId'));
$ch = $this->normalizeOptionalString($this->fromRequest('channelId') ?? $this->fromQuery('channelId'));
if ($ws === '') {
$ws = $this->getConfiguredWorkspaceId($client);
}
if ($ch === '') {
$ch = $this->getConfiguredChannelId($client);
}
if ($ws === '' || $ch === '') {
$response->error('Missing required parameters: workspaceId, channelId', 400);
}
@@ -55,24 +55,24 @@ function assert_true($cond, $msg)
}
}
// Test: create call (POST /voice/calls)
// Test: create call (POST /workspaces/{ws}/channels/{ch}/calls)
$client = new FakeBird();
$payload = [
'to' => '+4511122233',
'from' => '+4599988877',
'tts' => [ 'message' => 'Hello from test' ],
];
$res = $client->sendPostRequest('/voice/calls', $payload);
$res = $client->createVoiceCall('ws_123', 'ch_123', $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(str_contains($client->last['url'], '/workspaces/ws_123/channels/ch_123/calls'), 'POST URL composed correctly');
assert_true($client->last['method'] === 'POST', 'HTTP method is POST');
assert_true(in_array('Authorization: AccessKey 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})
// Test: get call (GET /workspaces/{ws}/channels/{ch}/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');
$res2 = $client2->getVoiceCall('ws_123', 'ch_123', 'call_123');
assert_true(str_contains($client2->last['url'], '/workspaces/ws_123/channels/ch_123/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');
@@ -0,0 +1,51 @@
<?php
namespace traits;
use classes\bird;
trait bird_route_helpers_t
{
private function normalizeOptionalString(mixed $value): string
{
if (!is_scalar($value)) {
return '';
}
$normalized = trim((string)$value);
if ($normalized === '') {
return '';
}
$lower = strtolower($normalized);
if ($lower === 'undefined' || $lower === 'null') {
return '';
}
return $normalized;
}
private function getConfiguredWorkspaceId(bird $client): string
{
$workspaceConfig = null;
if (property_exists($client->config, 'workspaceId')) {
$workspaceConfig = $client->config->workspaceId;
} elseif (property_exists($client->config, 'workplaceId')) {
// Backward compatibility with existing config key naming.
$workspaceConfig = $client->config->workplaceId;
}
if (is_object($workspaceConfig) && method_exists($workspaceConfig, 'getVariableValue')) {
return $this->normalizeOptionalString($workspaceConfig->getVariableValue());
}
return '';
}
private function getConfiguredChannelId(bird $client): string
{
$channelConfig = null;
if (property_exists($client->config, 'channelId')) {
$channelConfig = $client->config->channelId;
}
if (is_object($channelConfig) && method_exists($channelConfig, 'getVariableValue')) {
return $this->normalizeOptionalString($channelConfig->getVariableValue());
}
return '';
}
}