├── src ├── Version.php ├── LaML.php ├── LaML │ ├── FaxResponse.php │ ├── MessagingResponse.php │ ├── MessageResponse.php │ ├── Voice │ │ └── Connect.php │ └── VoiceResponse.php ├── Rest │ ├── Api │ │ ├── V2010 │ │ │ ├── Account │ │ │ │ ├── CallInstance.php │ │ │ │ └── CallList.php │ │ │ └── AccountContext.php │ │ └── V2010.php │ ├── Api.php │ └── Client.php ├── Util │ ├── BladeMethod.php │ └── Events.php ├── Relay │ ├── Calling │ │ ├── TapType.php │ │ ├── RecordType.php │ │ ├── SendDigitsState.php │ │ ├── Event.php │ │ ├── Results │ │ │ ├── PlayPauseResult.php │ │ │ ├── PlayResumeResult.php │ │ │ ├── PlayVolumeResult.php │ │ │ ├── PromptVolumeResult.php │ │ │ ├── PlayResult.php │ │ │ ├── AnswerResult.php │ │ │ ├── SendDigitsResult.php │ │ │ ├── DisconnectResult.php │ │ │ ├── DialResult.php │ │ │ ├── StopResult.php │ │ │ ├── ConnectResult.php │ │ │ ├── HangupResult.php │ │ │ ├── DetectResult.php │ │ │ ├── BaseResult.php │ │ │ ├── RecordResult.php │ │ │ ├── TapResult.php │ │ │ ├── PromptResult.php │ │ │ └── FaxResult.php │ │ ├── PromptType.php │ │ ├── TapState.php │ │ ├── DetectType.php │ │ ├── FaxState.php │ │ ├── Actions │ │ │ ├── ConnectAction.php │ │ │ ├── SendDigitsAction.php │ │ │ ├── FaxAction.php │ │ │ ├── DetectAction.php │ │ │ ├── RecordAction.php │ │ │ ├── TapAction.php │ │ │ ├── PromptAction.php │ │ │ ├── BaseAction.php │ │ │ └── PlayAction.php │ │ ├── PlayState.php │ │ ├── RecordState.php │ │ ├── PlayType.php │ │ ├── ConnectState.php │ │ ├── PromptState.php │ │ ├── DisconnectReason.php │ │ ├── Components │ │ │ ├── FaxReceive.php │ │ │ ├── Await.php │ │ │ ├── FaxSend.php │ │ │ ├── Answer.php │ │ │ ├── Disconnect.php │ │ │ ├── Dial.php │ │ │ ├── BaseFax.php │ │ │ ├── SendDigits.php │ │ │ ├── Hangup.php │ │ │ ├── Play.php │ │ │ ├── Record.php │ │ │ ├── Connect.php │ │ │ ├── Tap.php │ │ │ ├── Controllable.php │ │ │ ├── Prompt.php │ │ │ ├── BaseComponent.php │ │ │ └── Detect.php │ │ ├── DetectState.php │ │ ├── Blocker.php │ │ ├── CallState.php │ │ ├── Notification.php │ │ ├── Method.php │ │ └── Calling.php │ ├── Constants.php │ ├── Messaging │ │ ├── Notification.php │ │ ├── MessageState.php │ │ ├── SendResult.php │ │ ├── Message.php │ │ └── Messaging.php │ ├── Tasking │ │ └── Tasking.php │ ├── Task.php │ ├── BaseRelay.php │ ├── BroadcastHandler.php │ ├── Setup.php │ ├── Connection.php │ └── Consumer.php ├── Messages │ ├── Execute.php │ ├── Subscription.php │ ├── BaseMessage.php │ └── Connect.php ├── Log.php ├── Handler.php ├── Twiml.php └── functions.php ├── docker-dev ├── AUTHORS.md ├── .gitignore ├── provisioning ├── docker-compose.yml └── Dockerfile ├── examples ├── laml │ ├── voice-response.php │ ├── generate-laml.php │ ├── create-sms.php │ ├── create-call.php │ ├── create-fax.php │ ├── rest-client.php │ ├── ai.php │ └── create-call-amd.php └── relay │ ├── create-task.php │ ├── handle-messages.php │ ├── handle-tasks.php │ ├── handle-calls.php │ ├── detect-digits.php │ ├── play-audio.php │ ├── send-digits.php │ ├── detect-answering-machine.php │ ├── call-connect.php │ ├── send-sms.php │ ├── send-fax.php │ └── prompt-pin.php ├── tests ├── bootstrap.php ├── laml │ ├── VoiceResponseTest.php │ ├── IntegrationTest.php │ ├── ClientTest.php │ └── LaMLTest.php ├── relay │ ├── Calling │ │ ├── CallingTest.php │ │ ├── BlockerTest.php │ │ ├── BaseActionCase.php │ │ ├── CallSendDigitsTest.php │ │ ├── CallTapTest.php │ │ └── CallRecordTest.php │ ├── Tasking │ │ ├── TaskingTest.php │ │ └── TaskTest.php │ ├── BaseRelayCase.php │ ├── Messaging │ │ └── MessagingTest.php │ ├── RelayClientTest.php │ └── SetupTest.php ├── fixtures │ ├── get_fax │ └── list_faxes ├── MessagesTest.php └── HandlerTest.php ├── phpunit.xml ├── .github └── workflows │ └── build-test.yml ├── LICENSE ├── composer.json ├── README.md └── CHANGELOG.md /src/Version.php: -------------------------------------------------------------------------------- 1 | nest(new AI($attributes)); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Relay/Calling/RecordType.php: -------------------------------------------------------------------------------- 1 | name = $name; 10 | $this->payload = $payload; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Relay/Calling/Results/PlayPauseResult.php: -------------------------------------------------------------------------------- 1 | successful = $successful; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Relay/Calling/Results/PlayResumeResult.php: -------------------------------------------------------------------------------- 1 | successful = $successful; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Relay/Calling/Results/PlayVolumeResult.php: -------------------------------------------------------------------------------- 1 | successful = $successful; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/laml/voice-response.php: -------------------------------------------------------------------------------- 1 | say('Hello'); 6 | $response->play('https://cdn.signalwire.com/default-music/welcome.mp3', ['loop' => 5]); 7 | print $response; 8 | -------------------------------------------------------------------------------- /src/Relay/Calling/Results/PromptVolumeResult.php: -------------------------------------------------------------------------------- 1 | successful = $successful; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Relay/Calling/PromptType.php: -------------------------------------------------------------------------------- 1 | say("Welcome to SignalWire!"); 6 | $response->play("https://cdn.signalwire.com/default-music/welcome.mp3", array("loop" => 5)); 7 | 8 | echo $response; 9 | -------------------------------------------------------------------------------- /src/Relay/Calling/DetectType.php: -------------------------------------------------------------------------------- 1 | component); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/Relay/Messaging/Notification.php: -------------------------------------------------------------------------------- 1 | setMode('once') 6 | ->enableRequestMatchers(array('method', 'url', 'host')); 7 | \VCR\VCR::turnOn(); 8 | 9 | \SignalWire\Log::getLogger()->pushHandler(new \Monolog\Handler\NullHandler()); 10 | -------------------------------------------------------------------------------- /src/Relay/Calling/Actions/SendDigitsAction.php: -------------------------------------------------------------------------------- 1 | component); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/Relay/Calling/Results/SendDigitsResult.php: -------------------------------------------------------------------------------- 1 | buildRequest(array( 10 | 'method' => $this->method, 11 | 'params' => $params 12 | )); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Relay/Calling/RecordState.php: -------------------------------------------------------------------------------- 1 | buildRequest(array( 10 | 'method' => $this->method, 11 | 'params' => $params 12 | )); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Relay/Calling/ConnectState.php: -------------------------------------------------------------------------------- 1 | component); 11 | } 12 | 13 | public function stop() { 14 | return $this->component->stop(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/Relay/Calling/Results/DialResult.php: -------------------------------------------------------------------------------- 1 | component->call; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Relay/Calling/Results/StopResult.php: -------------------------------------------------------------------------------- 1 | code = $result->code; 12 | $this->message = $result->message; 13 | $this->successful = $this->code === '200'; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Relay/Calling/Actions/DetectAction.php: -------------------------------------------------------------------------------- 1 | component); 11 | } 12 | 13 | public function stop() { 14 | return $this->component->stop(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/Relay/Calling/Results/ConnectResult.php: -------------------------------------------------------------------------------- 1 | component->call->peer; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Relay/Calling/Results/HangupResult.php: -------------------------------------------------------------------------------- 1 | component->reason; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | tests 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Relay/Calling/DisconnectReason.php: -------------------------------------------------------------------------------- 1 | nest(new Voice\Connect($attributes)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Relay/Calling/Components/FaxReceive.php: -------------------------------------------------------------------------------- 1 | $this->call->nodeId, 13 | 'call_id' => $this->call->id, 14 | 'control_id' => $this->controlId 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/laml/create-sms.php: -------------------------------------------------------------------------------- 1 | "example.signalwire.com")); 6 | 7 | $message = $client->messages 8 | ->create("+1+++", // to 9 | array("from" => "+1+++", "body" => "Hello World!") 10 | ); 11 | 12 | print($message->sid); 13 | ?> -------------------------------------------------------------------------------- /provisioning/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.0 2 | 3 | RUN apt-get update && apt-get install -y libxml2-dev git 4 | 5 | RUN docker-php-ext-install soap 6 | 7 | RUN curl -sS https://getcomposer.org/installer | php \ 8 | && mv composer.phar /usr/local/bin/ \ 9 | && ln -s /usr/local/bin/composer.phar /usr/local/bin/composer 10 | 11 | COPY . /app 12 | WORKDIR /app 13 | 14 | RUN composer install --prefer-source --no-interaction 15 | 16 | ENV PATH="~/.composer/vendor/bin:./vendor/bin:${PATH}" 17 | -------------------------------------------------------------------------------- /src/Relay/Tasking/Tasking.php: -------------------------------------------------------------------------------- 1 | context}"); 11 | Handler::trigger($this->client->relayProtocol, $notification->message, $this->_ctxReceiveUniqueId($notification->context)); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/Relay/Calling/Actions/RecordAction.php: -------------------------------------------------------------------------------- 1 | component); 11 | } 12 | 13 | public function getUrl() { 14 | return $this->component->url; 15 | } 16 | 17 | public function stop() { 18 | return $this->component->stop(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/Relay/Calling/Actions/TapAction.php: -------------------------------------------------------------------------------- 1 | component); 11 | } 12 | 13 | public function getSourceDevice() { 14 | return $this->component->getSourceDevice(); 15 | } 16 | 17 | public function stop() { 18 | return $this->component->stop(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/Relay/Calling/DetectState.php: -------------------------------------------------------------------------------- 1 | component->type; 15 | } 16 | 17 | public function getResult() { 18 | return $this->component->result; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/Relay/Calling/Blocker.php: -------------------------------------------------------------------------------- 1 | eventType = $eventType; 14 | $this->controlId = $controlId; 15 | 16 | $this->promise = new Promise(function (callable $resolve) { 17 | $this->resolve = $resolve; 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/laml/create-call.php: -------------------------------------------------------------------------------- 1 | "example.signalwire.com")); 6 | 7 | $call = $client->calls 8 | ->create("+1+++", // to 9 | "+1+++", // from 10 | array("url" => "http://your-application.com/docs/voice.xml") 11 | ); 12 | 13 | print($call->sid); 14 | ?> 15 | -------------------------------------------------------------------------------- /src/Rest/Api/V2010/AccountContext.php: -------------------------------------------------------------------------------- 1 | _calls) { 11 | $this->_calls = new \SignalWire\Rest\Api\V2010\Account\CallList( 12 | $this->version, 13 | $this->solution['sid'] 14 | ); 15 | } 16 | 17 | return $this->_calls; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Relay/Calling/Results/BaseResult.php: -------------------------------------------------------------------------------- 1 | component = $component; 13 | } 14 | 15 | public function isSuccessful() { 16 | return $this->component->successful; 17 | } 18 | 19 | public function getEvent() { 20 | return $this->component->event; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/laml/create-fax.php: -------------------------------------------------------------------------------- 1 | "example.signalwire.com")); 6 | 7 | $fax = $client->fax->v1->faxes 8 | ->create("+1+++", // to 9 | "https://example.com/fax.pdf", // mediaUrl 10 | array("from" => "+1+++") 11 | ); 12 | 13 | print($fax->sid); 14 | ?> -------------------------------------------------------------------------------- /src/Rest/Api/V2010.php: -------------------------------------------------------------------------------- 1 | _account) { 11 | $this->_account = new \SignalWire\Rest\Api\V2010\AccountContext( 12 | $this, 13 | $this->domain->getClient()->getAccountSid() 14 | ); 15 | } 16 | return $this->_account; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Relay/Calling/CallState.php: -------------------------------------------------------------------------------- 1 | baseUrl = "https://$domain"; 11 | } 12 | 13 | /** 14 | * @return V2010 Version v2010 of api 15 | */ 16 | protected function getV2010(): \Twilio\Rest\Api\V2010 { 17 | if (!$this->_v2010) { 18 | $this->_v2010 = new \SignalWire\Rest\Api\V2010($this); 19 | } 20 | return $this->_v2010; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Relay/Calling/Results/RecordResult.php: -------------------------------------------------------------------------------- 1 | component->url; 15 | } 16 | 17 | public function getDuration() { 18 | return $this->component->duration; 19 | } 20 | 21 | public function getSize() { 22 | return $this->component->size; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Relay/Calling/Results/TapResult.php: -------------------------------------------------------------------------------- 1 | component->tap; 15 | } 16 | 17 | public function getSourceDevice() { 18 | return $this->component->getSourceDevice(); 19 | } 20 | 21 | public function getDestinationDevice() { 22 | return $this->component->device; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Relay/Messaging/MessageState.php: -------------------------------------------------------------------------------- 1 | say('Hello'); 10 | $response->play('https://cdn.signalwire.com/default-music/welcome.mp3', ['loop' => 5]); 11 | 12 | $this->assertEquals($response->__toString(), "\nHellohttps://cdn.signalwire.com/default-music/welcome.mp3\n"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/laml/rest-client.php: -------------------------------------------------------------------------------- 1 | .signalwire.com"; 5 | $project = ""; 6 | $token = ""; 7 | if (empty($project) || empty($token)) { 8 | throw new \Exception('Set your SignalWire project and token before run the example.'); 9 | } 10 | 11 | $client = new SignalWire\Rest\Client($project, $token, array( 12 | "signalwireSpaceUrl" => $space_url 13 | )); 14 | 15 | $calls = $client->calls->read(); 16 | echo "Total calls: " . count($calls) . chr(10); 17 | 18 | $messages = $client->messages->read(); 19 | echo "Total messages: " . count($messages) . chr(10); 20 | -------------------------------------------------------------------------------- /src/Relay/Calling/Actions/PromptAction.php: -------------------------------------------------------------------------------- 1 | component); 12 | } 13 | 14 | public function stop() { 15 | return $this->component->stop(); 16 | } 17 | 18 | public function volume($value) { 19 | return $this->component->volume($value)->then(function($result) { 20 | return new PromptVolumeResult($result); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/relay/create-task.php: -------------------------------------------------------------------------------- 1 | 'value', 19 | 'data' => 'random stuff' 20 | ]; 21 | $success = $task->deliver($context, $data); 22 | -------------------------------------------------------------------------------- /src/Relay/Calling/Notification.php: -------------------------------------------------------------------------------- 1 | successful = isset($result->code) && $result->code === '200'; 14 | $this->messageId = isset($result->message_id) ? $result->message_id : null; 15 | } 16 | 17 | public function isSuccessful() { 18 | return $this->successful; 19 | } 20 | 21 | public function getEvent() { 22 | return $this->event; 23 | } 24 | 25 | public function getMessageId() { 26 | return $this->messageId; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Relay/Calling/Results/PromptResult.php: -------------------------------------------------------------------------------- 1 | component->type; 15 | } 16 | 17 | public function getResult() { 18 | return $this->component->input; 19 | } 20 | 21 | public function getTerminator() { 22 | return $this->component->terminator; 23 | } 24 | 25 | public function getConfidence() { 26 | return $this->component->confidence; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/Messages/BaseMessage.php: -------------------------------------------------------------------------------- 1 | id) { 12 | $this->id = Uuid::uuid4()->toString(); 13 | } 14 | 15 | $this->request = array_merge( 16 | array('jsonrpc' => '2.0', 'id' => $this->id), 17 | $params 18 | ); 19 | } 20 | 21 | public function toJson(Bool $pretty = false){ 22 | return $pretty ? json_encode($this->request, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES) : json_encode($this->request, JSON_UNESCAPED_SLASHES); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Relay/Calling/Method.php: -------------------------------------------------------------------------------- 1 | component = $component; 13 | } 14 | 15 | abstract function getResult(); 16 | 17 | public function getControlId() { 18 | return $this->component->controlId; 19 | } 20 | 21 | public function getPayload() { 22 | return $this->component->payload; 23 | } 24 | 25 | public function isCompleted() { 26 | return $this->component->completed; 27 | } 28 | 29 | public function getState() { 30 | return $this->component->state; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Util/Events.php: -------------------------------------------------------------------------------- 1 | _mockResponse([$response]); 12 | 13 | $callback = function() {}; 14 | $this->client->calling->onReceive(['c1', 'c2'], $callback)->done(function() { 15 | $this->assertTrue(Handler::isQueued('relay-proto', 'calling.ctxReceive.c1')); 16 | $this->assertTrue(Handler::isQueued('relay-proto', 'calling.ctxReceive.c2')); 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Relay/Calling/Results/FaxResult.php: -------------------------------------------------------------------------------- 1 | component->direction; 15 | } 16 | 17 | public function getIdentity() { 18 | return $this->component->identity; 19 | } 20 | 21 | public function getRemoteIdentity() { 22 | return $this->component->remoteIdentity; 23 | } 24 | 25 | public function getDocument() { 26 | return $this->component->document; 27 | } 28 | 29 | public function getPages() { 30 | return $this->component->pages; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tests/relay/Tasking/TaskingTest.php: -------------------------------------------------------------------------------- 1 | _mockResponse([$response]); 12 | 13 | $callback = function() {}; 14 | $this->client->tasking->onReceive(['home', 'office'], $callback)->done(function() { 15 | $this->assertTrue(Handler::isQueued('relay-proto', 'tasking.ctxReceive.home')); 16 | $this->assertTrue(Handler::isQueued('relay-proto', 'tasking.ctxReceive.office')); 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/laml/ai.php: -------------------------------------------------------------------------------- 1 | connect(); 6 | 7 | $ai = $conn->ai(); 8 | $ai->setEngine('gcloud'); 9 | $p1 = $ai->prompt('prompt1'); 10 | $p1->setTemperature(0.2); 11 | $ai->postPrompt('prompt2'); 12 | 13 | $swaig = $ai->swaig(); 14 | $swaig->defaults([ 'webHookURL' => "https://user:pass@server.com/commands.cgi"]); 15 | 16 | $fn = $swaig->function(); 17 | $fn->setName('fn1'); 18 | $fn->setArgument('no argument'); 19 | $fn->setPurpose('to do something'); 20 | 21 | $fn = $swaig->function(); 22 | $fn->setName('fn2'); 23 | $fn->setArgument('no argument'); 24 | $fn->setPurpose('to do something'); 25 | $fn->addMetaData("AAA", "111"); 26 | $fn->addMetaData("BBB", "222"); 27 | 28 | print $response; -------------------------------------------------------------------------------- /src/Relay/Calling/Components/Await.php: -------------------------------------------------------------------------------- 1 | controlId = $call->tag; 17 | } 18 | 19 | public function payload() { 20 | return null; 21 | } 22 | 23 | public function notificationHandler($params) { 24 | if ($this->_hasBlocker() && in_array($params->call_state, $this->_eventsToWait)) { 25 | $this->completed = true; 26 | $this->successful = true; 27 | $this->event = new Event($params->call_state, $params); 28 | ($this->blocker->resolve)(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/relay/handle-messages.php: -------------------------------------------------------------------------------- 1 | project = isset($_ENV['PROJECT']) ? $_ENV['PROJECT'] : ''; 15 | $this->token = isset($_ENV['TOKEN']) ? $_ENV['TOKEN'] : ''; 16 | } 17 | 18 | public function onIncomingMessage($message): Coroutine { 19 | yield; 20 | Log::info("Message received on context: {$message->context}, from: {$message->from} to: {$message->to}"); 21 | print_r($message); 22 | } 23 | 24 | public function teardown(): Coroutine { 25 | yield; 26 | Log::info('Consumer teardown. Cleanup..'); 27 | } 28 | } 29 | 30 | $consumer = new CustomConsumer(); 31 | $consumer->run(); 32 | -------------------------------------------------------------------------------- /examples/relay/handle-tasks.php: -------------------------------------------------------------------------------- 1 | project = isset($_ENV['PROJECT']) ? $_ENV['PROJECT'] : ''; 18 | $this->token = isset($_ENV['TOKEN']) ? $_ENV['TOKEN'] : ''; 19 | } 20 | 21 | public function onTask($task): Coroutine { 22 | yield; 23 | Log::info('Inbound task payload: '); 24 | print_r($task); 25 | } 26 | 27 | public function teardown(): Coroutine { 28 | yield; 29 | Log::info('Consumer teardown. Cleanup..'); 30 | } 31 | } 32 | 33 | $consumer = new CustomConsumer(); 34 | $consumer->run(); 35 | -------------------------------------------------------------------------------- /src/Messages/Connect.php: -------------------------------------------------------------------------------- 1 | array( 14 | 'major' => self::VERSION_MAJOR, 15 | 'minor' => self::VERSION_MINOR, 16 | 'revision' => self::VERSION_REVISION 17 | ), 18 | 'authentication' => array( 19 | 'project' => $project, 20 | 'token' => $token 21 | ), 22 | 'agent' => 'PHP SDK/' . \SignalWire\VERSION 23 | ); 24 | if ($sessionid) { 25 | $params['sessionid'] = $sessionid; 26 | } 27 | 28 | $this->buildRequest(array( 29 | 'method' => $this->method, 30 | 'params' => $params 31 | )); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/laml/create-call-amd.php: -------------------------------------------------------------------------------- 1 | "example.signalwire.com")); 6 | 7 | $call = $client->calls 8 | ->create("+1+++", // to 9 | "+1+++", // from 10 | array( 11 | "url" => "http://your-application.com/docs/voice.xml", 12 | "asyncAmd" => "true", 13 | "AsyncAmdStatusCallback" => "http://your-application.com/docs/voice.xml/api/test", 14 | "AsyncAmdStatusMethod" => "POST", 15 | "MachineDetection" => "DetectMessageEnd", 16 | "AsyncAmdPartialResults" => "true" 17 | ) 18 | ); 19 | 20 | print($call->sid); 21 | ?> 22 | -------------------------------------------------------------------------------- /tests/relay/Calling/BlockerTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($blocker->controlId, 'control-id'); 17 | } 18 | 19 | public function testBlockerExposeEventType(): void { 20 | $blocker = new Blocker('event', 'control-id'); 21 | $this->assertEquals($blocker->eventType, 'event'); 22 | } 23 | 24 | public function testBlockerResolve(): void { 25 | $blocker = new Blocker('event', 'control-id'); 26 | ($blocker->resolve)('done'); 27 | $blocker->promise->done(function($res) { 28 | $this->assertEquals($res, 'done'); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/relay/handle-calls.php: -------------------------------------------------------------------------------- 1 | project = isset($_ENV['PROJECT']) ? $_ENV['PROJECT'] : ''; 15 | $this->token = isset($_ENV['TOKEN']) ? $_ENV['TOKEN'] : ''; 16 | } 17 | 18 | public function onIncomingCall($call): Coroutine { 19 | Log::info("Incoming call on context: {$call->context}, from: {$call->from} to: {$call->to}"); 20 | // Do something with the call.. 21 | 22 | yield $call->answer(); 23 | 24 | // then hangup.. 25 | yield $call->hangup(); 26 | } 27 | 28 | public function teardown(): Coroutine { 29 | yield; 30 | Log::info('Consumer teardown. Cleanup..'); 31 | } 32 | } 33 | 34 | $consumer = new CustomConsumer(); 35 | $consumer->run(); 36 | -------------------------------------------------------------------------------- /src/Relay/Task.php: -------------------------------------------------------------------------------- 1 | host = Constants::Host; 16 | $this->project = $project; 17 | $this->token = $token; 18 | $this->_httpClient = new Client(['timeout' => 5]); 19 | } 20 | 21 | public function deliver(string $context, $message) { 22 | $params = [ 'context' => $context, 'message' => $message ]; 23 | try { 24 | $uri = "https://{$this->host}/api/relay/rest/tasks"; 25 | $response = $this->_httpClient->request('POST', $uri, [ 26 | 'auth' => [$this->project, $this->token], 27 | 'json' => $params 28 | ]); 29 | return $response->getStatusCode() === 204; 30 | } catch (\Throwable $th) { 31 | echo PHP_EOL . $th->getMessage() . PHP_EOL; 32 | return false; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Relay/Calling/Components/FaxSend.php: -------------------------------------------------------------------------------- 1 | _document = $document; 18 | $this->_identity = $identity; 19 | $this->_header = $header; 20 | } 21 | 22 | public function payload() { 23 | $payload = [ 24 | 'node_id' => $this->call->nodeId, 25 | 'call_id' => $this->call->id, 26 | 'control_id' => $this->controlId, 27 | 'document' => $this->_document 28 | ]; 29 | if ($this->_identity) { 30 | $payload['identity'] = $this->_identity; 31 | } 32 | if ($this->_header) { 33 | $payload['header_info'] = $this->_header; 34 | } 35 | return $payload; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Relay/Calling/Actions/PlayAction.php: -------------------------------------------------------------------------------- 1 | component); 14 | } 15 | 16 | public function stop() { 17 | return $this->component->stop(); 18 | } 19 | 20 | public function pause() { 21 | return $this->component->pause()->then(function($result) { 22 | return new PlayPauseResult($result); 23 | }); 24 | } 25 | 26 | public function volume($value) { 27 | return $this->component->volume($value)->then(function($result) { 28 | return new PlayVolumeResult($result); 29 | }); 30 | } 31 | 32 | public function resume() { 33 | return $this->component->resume()->then(function($result) { 34 | return new PlayResumeResult($result); 35 | }); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | php-versions: ["8.0", "8.1"] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Setup PHP 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: ${{ matrix.php-versions }} 24 | 25 | - name: Get composer cache directory 26 | id: composer-cache 27 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 28 | 29 | - name: Cache dependencies 30 | uses: actions/cache@v2 31 | with: 32 | path: ${{ steps.composer-cache.outputs.dir }} 33 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 34 | restore-keys: ${{ runner.os }}-composer- 35 | 36 | - name: Install dependencies 37 | run: composer install --prefer-source --no-interaction 38 | 39 | - name: Test with phpunit 40 | run: vendor/bin/phpunit tests 41 | -------------------------------------------------------------------------------- /src/Relay/Calling/Components/Answer.php: -------------------------------------------------------------------------------- 1 | controlId = $call->tag; 19 | } 20 | 21 | public function payload() { 22 | return [ 23 | 'node_id' => $this->call->nodeId, 24 | 'call_id' => $this->call->id 25 | ]; 26 | } 27 | 28 | public function notificationHandler($params) { 29 | if ($params->call_state === CallState::Answered) { 30 | $this->completed = true; 31 | $this->successful = true; 32 | $this->event = new Event($params->call_state, $params); 33 | } 34 | 35 | if ($this->_hasBlocker() && in_array($params->call_state, $this->_eventsToWait)) { 36 | ($this->blocker->resolve)(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/laml/IntegrationTest.php: -------------------------------------------------------------------------------- 1 | sid = "my-signalwire-sid"; 14 | $this->token = "my-signalwire-token"; 15 | $this->domain = 'example.signalwire.com'; 16 | putenv("SIGNALWIRE_API_HOSTNAME=$this->domain"); 17 | 18 | $this->client = new SignalWire\Rest\Client($this->sid, $this->token); 19 | 20 | \VCR\VCR::turnOn(); 21 | } 22 | 23 | protected function tearDown(): void { 24 | \VCR\VCR::eject(); 25 | \VCR\VCR::turnOff(); 26 | } 27 | 28 | public function testFaxList(): void { 29 | \VCR\VCR::insertCassette('list_faxes'); 30 | 31 | $faxes = $this->client->fax->v1->faxes->read(); 32 | $this->assertEquals(count($faxes), 7); 33 | } 34 | 35 | public function testGetFax(): void { 36 | \VCR\VCR::insertCassette('get_fax'); 37 | 38 | $fax = $this->client->fax->v1->faxes("831455c6-574e-4d8b-b6ee-2418140bf4cd")->fetch(); 39 | $this->assertEquals($fax->to, '+15556677888'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2021 SignalWire 4 | Copyright (c) 2022 SignalWire Community 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /tests/fixtures/get_fax: -------------------------------------------------------------------------------- 1 | 2 | - 3 | request: 4 | method: GET 5 | url: 'https://example.signalwire.com/2010-04-01/Accounts/my-signalwire-sid/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd' 6 | headers: 7 | Accept-Charset: utf-8 8 | Accept: application/json 9 | response: 10 | status: 11 | http_version: '1.1' 12 | code: '200' 13 | message: OK 14 | headers: 15 | Server: nginx 16 | body: '{"account_sid":"my-signalwire-sid","api_version":"v1","date_created":"2019-01-04T16:28:33Z","date_updated":"2019-01-04T16:29:20Z","direction":"outbound","from":"+14043287382","media_url":"https://s3.us-east-2.amazonaws.com/signalwire-assets/faxes/20190104162834-831455c6-574e-4d8b-b6ee-2418140bf4cd.tiff","media_sid":"aff0684c-3445-49bc-802b-3a0a488139f5","num_pages":1,"price":0.0105,"price_unit":"USD","quality":"fine","sid":"831455c6-574e-4d8b-b6ee-2418140bf4cd","status":"delivered","to":"+15556677888","duration":41,"links":{"media":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd/Media"},"url":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd"}' 17 | - 18 | 19 | -------------------------------------------------------------------------------- /examples/relay/detect-digits.php: -------------------------------------------------------------------------------- 1 | project = isset($_ENV['PROJECT']) ? $_ENV['PROJECT'] : ''; 15 | $this->token = isset($_ENV['TOKEN']) ? $_ENV['TOKEN'] : ''; 16 | } 17 | 18 | public function onIncomingCall($call): Coroutine { 19 | Log::info("Incoming call on context: {$call->context}, from: {$call->from} to: {$call->to}"); 20 | 21 | yield $call->answer(); 22 | 23 | $call->on('detect.update', function ($call, $params) { 24 | print_r($params); 25 | }); 26 | 27 | $result = yield $call->detectDigit(['digits' => '123']); 28 | Log::info('isSuccessful: ' . $result->isSuccessful()); 29 | Log::info('getResult: ' . $result->getResult()); 30 | yield $call->hangup(); 31 | } 32 | 33 | public function teardown(): Coroutine { 34 | yield; 35 | Log::info('Consumer teardown. Cleanup..'); 36 | } 37 | } 38 | 39 | $consumer = new CustomConsumer(); 40 | $consumer->run(); 41 | -------------------------------------------------------------------------------- /src/Relay/Calling/Components/Disconnect.php: -------------------------------------------------------------------------------- 1 | controlId = $call->tag; 19 | } 20 | 21 | public function payload() { 22 | return [ 23 | 'node_id' => $this->call->nodeId, 24 | 'call_id' => $this->call->id 25 | ]; 26 | } 27 | 28 | public function notificationHandler($params) { 29 | $this->state = $params->connect_state; 30 | 31 | $this->completed = $this->state !== ConnectState::Connecting; 32 | if ($this->completed) { 33 | $this->successful = $this->state === ConnectState::Disconnected; 34 | $this->event = new Event($params->connect_state, $params); 35 | } 36 | 37 | if ($this->_hasBlocker() && in_array($this->state, $this->_eventsToWait)) { 38 | ($this->blocker->resolve)(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Relay/Calling/Components/Dial.php: -------------------------------------------------------------------------------- 1 | controlId = $call->tag; 19 | } 20 | 21 | public function payload() { 22 | return [ 23 | 'tag' => $this->call->tag, 24 | 'device' => $this->call->getDevice() 25 | ]; 26 | } 27 | 28 | public function notificationHandler($params) { 29 | $this->state = $params->call_state; 30 | 31 | $this->completed = in_array($this->state, [CallState::Answered, CallState::Ending, CallState::Ended]); 32 | if ($this->completed) { 33 | $this->successful = $this->state === CallState::Answered; 34 | $this->event = new Event($params->call_state, $params); 35 | } 36 | 37 | if ($this->_hasBlocker() && in_array($this->state, $this->_eventsToWait)) { 38 | ($this->blocker->resolve)(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Relay/Calling/Components/BaseFax.php: -------------------------------------------------------------------------------- 1 | fax; 20 | $this->state = $fax->type; 21 | 22 | $this->completed = $this->state !== FaxState::Page; 23 | if ($this->completed) { 24 | if (isset($fax->params->success) && $fax->params->success) { 25 | $this->successful = true; 26 | $this->direction = $fax->params->direction; 27 | $this->identity = $fax->params->identity; 28 | $this->remoteIdentity = $fax->params->remote_identity; 29 | $this->document = $fax->params->document; 30 | $this->pages = $fax->params->pages; 31 | } 32 | $this->event = new Event($this->state, $fax); 33 | } 34 | 35 | if ($this->_hasBlocker() && in_array($this->state, $this->_eventsToWait)) { 36 | ($this->blocker->resolve)(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Relay/Calling/Components/SendDigits.php: -------------------------------------------------------------------------------- 1 | digits = $digits; 20 | } 21 | 22 | public function payload() { 23 | return [ 24 | 'node_id' => $this->call->nodeId, 25 | 'call_id' => $this->call->id, 26 | 'control_id' => $this->controlId, 27 | 'digits' => $this->digits 28 | ]; 29 | } 30 | 31 | public function notificationHandler($params) { 32 | $this->state = $params->state; 33 | 34 | $this->completed = $this->state === SendDigitsState::Finished; 35 | $this->successful = $this->completed; 36 | $this->event = new Event($params->state, $params); 37 | 38 | if ($this->_hasBlocker() && in_array($this->state, $this->_eventsToWait)) { 39 | ($this->blocker->resolve)(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "signalwire-community/signalwire", 3 | "description": "Client library for connecting to SignalWire.", 4 | "type": "library", 5 | "keywords": [ 6 | "voip", 7 | "sms", 8 | "mms", 9 | "voice", 10 | "voicemail", 11 | "video", 12 | "fax", 13 | "api", 14 | "ivr", 15 | "iot", 16 | "freeswitch", 17 | "signalwire", 18 | "relay", 19 | "laml" 20 | ], 21 | "homepage": "https://github.com/signalwire-community/signalwire-php", 22 | "license": "MIT", 23 | "autoload": { 24 | "psr-4": { 25 | "SignalWire\\": "src/" 26 | }, 27 | "files": [ "src/functions.php", "src/Version.php" ] 28 | }, 29 | "authors": [ 30 | { 31 | "name": "SignalWire Team", 32 | "email": "open.source@signalwire.com" 33 | } 34 | ], 35 | "repositories": [ 36 | { 37 | "type": "vcs", 38 | "url": "https://github.com/danieleds/php-vcr" 39 | } 40 | ], 41 | "require": { 42 | "php": ">=8.0", 43 | "twilio/sdk": "6.33.0", 44 | "ratchet/pawl": "^0.4.1", 45 | "monolog/monolog": "^1.24 || ^2.0", 46 | "ramsey/uuid": "^3.8 || ^4.0", 47 | "recoil/react": "1.0.3", 48 | "guzzlehttp/guzzle": ">=6.0" 49 | }, 50 | "require-dev": { 51 | "phpunit/phpunit": "^8", 52 | "php-vcr/php-vcr": "dev-master" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/relay/play-audio.php: -------------------------------------------------------------------------------- 1 | project = isset($_ENV['PROJECT']) ? $_ENV['PROJECT'] : ''; 14 | $this->token = isset($_ENV['TOKEN']) ? $_ENV['TOKEN'] : ''; 15 | } 16 | 17 | public function ready(): Coroutine { 18 | $params = ['type' => 'phone', 'from' => '+1xxx', 'to' => '+1yyy']; 19 | Log::info('Trying to dial: ' . $params['to']); 20 | $dialResult = yield $this->client->calling->dial($params); 21 | if (!$dialResult->isSuccessful()) { 22 | Log::warning('Outbound call failed or not answered.'); 23 | return; 24 | } 25 | $call = $dialResult->getCall(); 26 | $call->on('play.stateChange', function ($call, $params) { 27 | Log::info('play.stateChange: ' . $params->state); 28 | }); 29 | 30 | Log::info('Trying to play audio..'); 31 | yield $call->playAudio('https://cdn.signalwire.com/default-music/welcome.mp3'); 32 | yield $call->hangup(); 33 | } 34 | 35 | public function teardown(): Coroutine { 36 | yield; 37 | Log::info('Consumer teardown. Cleanup..'); 38 | } 39 | } 40 | 41 | $consumer = new CustomConsumer(); 42 | $consumer->run(); 43 | -------------------------------------------------------------------------------- /examples/relay/send-digits.php: -------------------------------------------------------------------------------- 1 | project = isset($_ENV['PROJECT']) ? $_ENV['PROJECT'] : ''; 15 | $this->token = isset($_ENV['TOKEN']) ? $_ENV['TOKEN'] : ''; 16 | } 17 | 18 | public function ready(): Coroutine { 19 | $params = ['type' => 'phone', 'from' => '+1xxx', 'to' => '+1yyy']; 20 | Log::info('Trying to dial: ' . $params['to']); 21 | $dialResult = yield $this->client->calling->dial($params); 22 | if (!$dialResult->isSuccessful()) { 23 | Log::warning('Outbound call failed or not answered.'); 24 | return; 25 | } 26 | $call = $dialResult->getCall(); 27 | Log::info('Sending digits..'); 28 | $result = yield $call->sendDigits('1w2w3w4w5w6'); 29 | if ($result->isSuccessful()) { 30 | Log::error('Digits sent successfully!'); 31 | } else { 32 | Log::error('Error sending digits!'); 33 | } 34 | yield $call->hangup(); 35 | } 36 | 37 | public function teardown(): Coroutine { 38 | yield; 39 | Log::info('Consumer teardown. Cleanup..'); 40 | } 41 | } 42 | 43 | $consumer = new CustomConsumer(); 44 | $consumer->run(); 45 | -------------------------------------------------------------------------------- /examples/relay/detect-answering-machine.php: -------------------------------------------------------------------------------- 1 | project = isset($_ENV['PROJECT']) ? $_ENV['PROJECT'] : ''; 15 | $this->token = isset($_ENV['TOKEN']) ? $_ENV['TOKEN'] : ''; 16 | } 17 | 18 | public function ready(): Coroutine { 19 | $params = ['type' => 'phone', 'from' => '+1xxx', 'to' => '+1yyy']; 20 | Log::info('Trying to dial: ' . $params['to']); 21 | $dialResult = yield $this->client->calling->dial($params); 22 | if (!$dialResult->isSuccessful()) { 23 | Log::warning('Outbound call failed or not answered.'); 24 | return; 25 | } 26 | $call = $dialResult->getCall(); 27 | Log::info('Start AMD..'); 28 | $result = yield $call->amd(); 29 | 30 | Log::info('isSuccessful: ' . $result->isSuccessful()); 31 | Log::info('getType: ' . $result->getType()); 32 | Log::info('getResult: ' . $result->getResult()); 33 | 34 | yield $call->hangup(); 35 | } 36 | 37 | public function teardown(): Coroutine { 38 | yield; 39 | Log::info('Consumer teardown. Cleanup..'); 40 | } 41 | } 42 | 43 | $consumer = new CustomConsumer(); 44 | $consumer->run(); 45 | -------------------------------------------------------------------------------- /src/Relay/Calling/Components/Hangup.php: -------------------------------------------------------------------------------- 1 | controlId = $call->tag; 20 | $this->reason = $reason; 21 | } 22 | 23 | public function payload() { 24 | return [ 25 | 'node_id' => $this->call->nodeId, 26 | 'call_id' => $this->call->id, 27 | 'reason' => $this->reason 28 | ]; 29 | } 30 | 31 | public function notificationHandler($params) { 32 | $this->state = $params->call_state; 33 | 34 | $this->completed = $this->state === CallState::Ended; 35 | if ($this->completed) { 36 | $this->successful = true; 37 | $this->event = new Event($params->call_state, $params); 38 | 39 | if (isset($params->end_reason)) { 40 | $this->reason = $params->end_reason; 41 | } 42 | } 43 | 44 | if ($this->_hasBlocker() && in_array($this->state, $this->_eventsToWait)) { 45 | ($this->blocker->resolve)(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/relay/call-connect.php: -------------------------------------------------------------------------------- 1 | project = isset($_ENV['PROJECT']) ? $_ENV['PROJECT'] : ''; 15 | $this->token = isset($_ENV['TOKEN']) ? $_ENV['TOKEN'] : ''; 16 | } 17 | 18 | public function onIncomingCall($call): Coroutine { 19 | Log::info("Incoming call on context: {$call->context}, from: {$call->from} to: {$call->to}"); 20 | yield $call->answer(); 21 | $devices = [ 22 | ['type' => 'phone', 'to' => '+1xxx'] 23 | ]; 24 | $result = yield $call->connect([ 25 | 'devices' => $devices 26 | ]); 27 | 28 | // For demonstration only: we disconnect the legs as soon as they have been connected. 29 | if ($result->isSuccessful()) { 30 | Log::info("Legs have been connected... now disconnect!"); 31 | $disResult = yield $call->disconnect(); 32 | } 33 | 34 | // Hangup the inbound leg, the remote leg is still active 35 | yield $call->hangup(); 36 | } 37 | 38 | public function teardown(): Coroutine { 39 | yield; 40 | Log::info('Consumer teardown. Cleanup..'); 41 | } 42 | } 43 | 44 | $consumer = new CustomConsumer(); 45 | $consumer->run(); 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SignalWire PHP 2 | 3 | 4 | ![Packagist Version](https://img.shields.io/packagist/v/signalwire-community/signalwire.svg?color=brightgreen) 5 | 6 | The Relay SDK for PHP enables PHP developers to connect and use SignalWire's Relay APIs within their own PHP code. Our Relay SDK allows developers to build or add robust and innovative communication services to their applications. 7 | 8 | > ⚠️ Disclaimer: 9 | > 10 | > The libraries in this repository are NOT supported by SignalWire. 11 | 12 | ## Getting Started 13 | 14 | Read the implementation documentation, guides and API Reference at the [Relay SDK for PHP Documentation](https://signalwire-community.github.io/docs/php/) site. 15 | 16 | --- 17 | 18 | ## Contributing 19 | 20 | If you'd like to contribute, feel free to visit our [Slack channel](https://signalwire.community/) and read our developer section to get the code running in your local environment. 21 | 22 | ## Developers 23 | 24 | To setup the dev environment follow these steps: 25 | 26 | 1. Fork this repository and clone it. 27 | 2. Create a new branch from `master` for your change. 28 | 3. Make changes! 29 | 30 | ## Versioning 31 | 32 | Relay SDK for PHP follows Semantic Versioning 2.0 as defined at . 33 | 34 | ## License 35 | 36 | Relay SDK for PHP is free software, and may be redistributed under the terms specified in the [MIT-LICENSE](https://github.com/signalwire-community/signalwire-php/blob/master/LICENSE) file. 37 | -------------------------------------------------------------------------------- /tests/relay/Tasking/TaskTest.php: -------------------------------------------------------------------------------- 1 | task = new Task('project', 'token'); 19 | } 20 | 21 | public function tearDown(): void { 22 | unset($this->task); 23 | } 24 | 25 | private function _mockResponse($responses) { 26 | $mock = new MockHandler($responses); 27 | 28 | $handlerStack = HandlerStack::create($mock); 29 | $this->task->_httpClient = new Client(['handler' => $handlerStack]); 30 | } 31 | 32 | public function testDeliverWithSuccess(): void { 33 | $this->_mockResponse([ 34 | new Response(204) 35 | ]); 36 | 37 | $success = $this->task->deliver('context', ['key' => 'value']); 38 | 39 | $this->assertTrue($success); 40 | } 41 | 42 | public function testDeliverWithException(): void { 43 | $this->_mockResponse([ 44 | new ClientException('POST 400 Bad Request', new Request('POST', '/api/relay/rest/tasks'), new Response()) 45 | ]); 46 | 47 | $success = $this->task->deliver('context', ['key' => 'value']); 48 | 49 | $this->assertFalse($success); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Relay/Messaging/Message.php: -------------------------------------------------------------------------------- 1 | message_id)) { 20 | $this->id = $options->message_id; 21 | } 22 | if (isset($options->message_state)) { 23 | $this->state = $options->message_state; 24 | } 25 | if (isset($options->context)) { 26 | $this->context = $options->context; 27 | } 28 | if (isset($options->from_number)) { 29 | $this->from = $options->from_number; 30 | } 31 | if (isset($options->to_number)) { 32 | $this->to = $options->to_number; 33 | } 34 | if (isset($options->body)) { 35 | $this->body = $options->body; 36 | } 37 | if (isset($options->direction)) { 38 | $this->direction = $options->direction; 39 | } 40 | if (isset($options->media)) { 41 | $this->media = $options->media; 42 | } 43 | if (isset($options->segments)) { 44 | $this->segments = $options->segments; 45 | } 46 | if (isset($options->tags)) { 47 | $this->tags = $options->tags; 48 | } 49 | if (isset($options->reason)) { 50 | $this->reason = $options->reason; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Relay/Calling/Components/Play.php: -------------------------------------------------------------------------------- 1 | _play = $play; 22 | $this->_volume = (float)$volume; 23 | } 24 | 25 | public function payload() { 26 | $tmp = [ 27 | 'node_id' => $this->call->nodeId, 28 | 'call_id' => $this->call->id, 29 | 'control_id' => $this->controlId, 30 | 'play' => $this->_play 31 | ]; 32 | if ($this->_volume !== 0.0) { 33 | $tmp['volume'] = $this->_volume; 34 | } 35 | return $tmp; 36 | } 37 | 38 | public function notificationHandler($params) { 39 | $this->state = $params->state; 40 | 41 | $this->completed = $this->state !== PlayState::Playing; 42 | if ($this->completed) { 43 | $this->successful = $this->state === PlayState::Finished; 44 | $this->event = new Event($params->state, $params); 45 | } 46 | 47 | if ($this->_hasBlocker() && in_array($this->state, $this->_eventsToWait)) { 48 | ($this->blocker->resolve)(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Relay/BaseRelay.php: -------------------------------------------------------------------------------- 1 | client = $client; 15 | $this->service = strtolower((new \ReflectionClass($this))->getShortName()); 16 | } 17 | 18 | public function onReceive(Array $contexts, Callable $handler) { 19 | return Setup::receive($this->client, $contexts)->then(function($success) use ($contexts, $handler) { 20 | if ($success) { 21 | foreach ($contexts as $context) { 22 | Handler::register($this->client->relayProtocol, $handler, $this->_ctxReceiveUniqueId($context)); 23 | } 24 | } 25 | }); 26 | } 27 | 28 | public function onStateChange(Array $contexts, Callable $handler) { 29 | return Setup::receive($this->client, $contexts)->then(function($success) use ($contexts, $handler) { 30 | if ($success) { 31 | foreach ($contexts as $context) { 32 | Handler::register($this->client->relayProtocol, $handler, $this->_ctxStateUniqueId($context)); 33 | } 34 | } 35 | }); 36 | } 37 | 38 | protected function _ctxReceiveUniqueId(String $context) { 39 | return "{$this->service}.ctxReceive.{$context}"; 40 | } 41 | 42 | protected function _ctxStateUniqueId(String $context) { 43 | return "{$this->service}.ctxState.{$context}"; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Relay/Calling/Components/Record.php: -------------------------------------------------------------------------------- 1 | _record = $record; 25 | } 26 | 27 | public function payload() { 28 | return [ 29 | 'node_id' => $this->call->nodeId, 30 | 'call_id' => $this->call->id, 31 | 'control_id' => $this->controlId, 32 | 'record' => $this->_record 33 | ]; 34 | } 35 | 36 | public function notificationHandler($params) { 37 | $this->state = $params->state; 38 | 39 | $this->completed = $this->state !== RecordState::Recording; 40 | if ($this->completed) { 41 | $this->successful = $this->state === RecordState::Finished; 42 | $this->event = new Event($params->state, $params); 43 | $this->url = $params->url; 44 | $this->duration = $params->duration; 45 | $this->size = $params->size; 46 | } 47 | 48 | if ($this->_hasBlocker() && in_array($this->state, $this->_eventsToWait)) { 49 | ($this->blocker->resolve)(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /examples/relay/send-sms.php: -------------------------------------------------------------------------------- 1 | project = isset($_ENV['PROJECT']) ? $_ENV['PROJECT'] : ''; 18 | $this->token = isset($_ENV['TOKEN']) ? $_ENV['TOKEN'] : ''; 19 | } 20 | 21 | // Once the Consumer is ready send an SMS 22 | public function ready(): Coroutine { 23 | $params = [ 24 | 'context' => 'office', 25 | 'from' => '+1xxx', 26 | 'to' => '+1yyy', 27 | 'body' => 'Welcome at SignalWire!' 28 | ]; 29 | Log::info('Sending SMS..'); 30 | $result = yield $this->client->messaging->send($params); 31 | if ($result->isSuccessful()) { 32 | Log::info('SMS queued successfully!'); 33 | } else { 34 | Log::warning('Error sending SMS!'); 35 | } 36 | } 37 | 38 | // Keep track of your SMS state changes 39 | public function onMessageStateChange($message): Coroutine { 40 | yield; 41 | Log::info("Message {$message->id} state: {$message->state}."); 42 | print_r($message); 43 | } 44 | 45 | public function teardown(): Coroutine { 46 | yield; 47 | Log::info('Consumer teardown. Cleanup..'); 48 | } 49 | } 50 | 51 | $consumer = new CustomConsumer(); 52 | $consumer->run(); 53 | -------------------------------------------------------------------------------- /src/Relay/Calling/Components/Connect.php: -------------------------------------------------------------------------------- 1 | controlId = $call->tag; 22 | $this->_devices = $devices; 23 | $this->_ringback = $ringback; 24 | } 25 | 26 | public function payload() { 27 | $tmp = [ 28 | 'node_id' => $this->call->nodeId, 29 | 'call_id' => $this->call->id, 30 | 'devices' => $this->_devices 31 | ]; 32 | if (is_array($this->_ringback) && count($this->_ringback) > 0) { 33 | $tmp['ringback'] = $this->_ringback; 34 | } 35 | return $tmp; 36 | } 37 | 38 | public function notificationHandler($params) { 39 | $this->state = $params->connect_state; 40 | 41 | $this->completed = $this->state !== ConnectState::Connecting; 42 | if ($this->completed) { 43 | $this->successful = $this->state === ConnectState::Connected; 44 | $this->event = new Event($params->connect_state, $params); 45 | } 46 | 47 | if ($this->_hasBlocker() && in_array($this->state, $this->_eventsToWait)) { 48 | ($this->blocker->resolve)(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/relay/send-fax.php: -------------------------------------------------------------------------------- 1 | project = isset($_ENV['PROJECT']) ? $_ENV['PROJECT'] : ''; 15 | $this->token = isset($_ENV['TOKEN']) ? $_ENV['TOKEN'] : ''; 16 | } 17 | 18 | public function ready(): Coroutine { 19 | $params = ['type' => 'phone', 'from' => '+1xxx', 'to' => '+1yyy']; 20 | Log::info('Trying to dial: ' . $params['to']); 21 | $dialResult = yield $this->client->calling->dial($params); 22 | if (!$dialResult->isSuccessful()) { 23 | Log::warning('Outbound call failed or not answered.'); 24 | return; 25 | } 26 | $call = $dialResult->getCall(); 27 | 28 | $pdfDocument = 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf'; 29 | $result = yield $call->faxSend($pdfDocument); 30 | Log::info('isSuccessful: ' . $result->isSuccessful()); 31 | Log::info('getDirection: ' . $result->getDirection()); 32 | Log::info('getIdentity: ' . $result->getIdentity()); 33 | Log::info('getRemoteIdentity: ' . $result->getRemoteIdentity()); 34 | Log::info('getDocument: ' . $result->getDocument()); 35 | Log::info('getPages: ' . $result->getPages()); 36 | 37 | yield $call->hangup(); 38 | } 39 | 40 | public function teardown(): Coroutine { 41 | yield; 42 | Log::info('Consumer teardown. Cleanup..'); 43 | } 44 | } 45 | 46 | $consumer = new CustomConsumer(); 47 | $consumer->run(); 48 | -------------------------------------------------------------------------------- /examples/relay/prompt-pin.php: -------------------------------------------------------------------------------- 1 | project = isset($_ENV['PROJECT']) ? $_ENV['PROJECT'] : ''; 14 | $this->token = isset($_ENV['TOKEN']) ? $_ENV['TOKEN'] : ''; 15 | } 16 | 17 | public function ready(): Coroutine { 18 | $params = ['type' => 'phone', 'from' => '+1xxx', 'to' => '+1yyy']; 19 | Log::info('Trying to dial: ' . $params['to']); 20 | $dialResult = yield $this->client->calling->dial($params); 21 | if (!$dialResult->isSuccessful()) { 22 | Log::warning('Outbound call failed or not answered.'); 23 | return; 24 | } 25 | $call = $dialResult->getCall(); 26 | $promptParams = [ 27 | 'type' => 'digits', 28 | 'digits_max' => '4', 29 | 'digits_terminators' => '#', 30 | 'text' => 'Welcome at SignalWire. Please, enter your PIN and then # to proceed' 31 | ]; 32 | $promptResult = yield $call->promptTTS($promptParams); 33 | $pin = $promptResult->getResult(); 34 | Log::info('PIN: ' . $pin); 35 | if ($pin === '1234') { 36 | yield $call->playTTS(['text' => 'You entered the proper PIN. Thank you!']); 37 | } else { 38 | yield $call->playTTS(['text' => 'Unknown PIN.']); 39 | } 40 | yield $call->hangup(); 41 | } 42 | 43 | public function teardown(): Coroutine { 44 | yield; 45 | Log::info('Consumer teardown. Cleanup..'); 46 | } 47 | } 48 | 49 | $consumer = new CustomConsumer(); 50 | $consumer->run(); 51 | -------------------------------------------------------------------------------- /src/Relay/BroadcastHandler.php: -------------------------------------------------------------------------------- 1 | relayProtocol !== $notification->protocol) { 12 | Log::debug('Broadcast protocol mismatch.'); 13 | return; 14 | } 15 | 16 | switch ($notification->event) { 17 | case 'queuing.relay.tasks': 18 | $client->getTasking()->notificationHandler($notification->params); 19 | break; 20 | case 'queuing.relay.messaging': 21 | $client->getMessaging()->notificationHandler($notification->params); 22 | break; 23 | case 'queuing.relay.events': 24 | self::switchEventType($client, $notification->params->event_type, $notification->params); 25 | break; 26 | default: 27 | Log::warning("Unknown notification event: {$notification->event}"); 28 | break; 29 | } 30 | } 31 | 32 | static function switchEventType(Client $client, string $eventType, $params) { 33 | switch ($eventType) { 34 | case CallNotification::State: 35 | case CallNotification::Receive: 36 | case CallNotification::Connect: 37 | case CallNotification::Record: 38 | case CallNotification::Play: 39 | case CallNotification::Collect: 40 | case CallNotification::Fax: 41 | case CallNotification::Detect: 42 | case CallNotification::Tap: 43 | case CallNotification::SendDigits: 44 | $client->getCalling()->notificationHandler($params); 45 | break; 46 | default: 47 | Log::warning("Unknown notification type: {$eventType}"); 48 | break; 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/Relay/Messaging/Messaging.php: -------------------------------------------------------------------------------- 1 | params->event_type = $notification->event_type; 13 | $message = new Message($notification->params); 14 | switch ($notification->event_type) 15 | { 16 | case Notification::State: 17 | Log::info("Relay message '{$message->id}' changes state to '{$message->state}'"); 18 | Handler::trigger($this->client->relayProtocol, $message, $this->_ctxStateUniqueId($message->context)); 19 | break; 20 | case Notification::Receive: 21 | Log::info("New Relay {$message->direction} message in context '{$message->context}'"); 22 | Handler::trigger($this->client->relayProtocol, $message, $this->_ctxReceiveUniqueId($message->context)); 23 | break; 24 | } 25 | } 26 | 27 | public function send(Array $params) { 28 | $params['from_number'] = isset($params['from']) ? $params['from'] : ''; 29 | $params['to_number'] = isset($params['to']) ? $params['to'] : ''; 30 | unset($params['from'], $params['to']); 31 | $msg = new Execute([ 32 | 'protocol' => $this->client->relayProtocol, 33 | 'method' => 'messaging.send', 34 | 'params' => $params 35 | ]); 36 | return $this->client->execute($msg)->then(function($response) { 37 | Log::info($response->result->message); 38 | return new SendResult($response->result); 39 | }, function ($error) { 40 | Log::error("Messaging send error: {$error->message}. [code: {$error->code}]"); 41 | return new SendResult($error); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Relay/Calling/Components/Tap.php: -------------------------------------------------------------------------------- 1 | _tap = $tap; 24 | $this->_device = $device; 25 | } 26 | 27 | public function payload() { 28 | $this->_tap['params'] = (object) $this->_tap['params']; 29 | $this->_device['params'] = (object) $this->_device['params']; 30 | return [ 31 | 'node_id' => $this->call->nodeId, 32 | 'call_id' => $this->call->id, 33 | 'control_id' => $this->controlId, 34 | 'tap' => $this->_tap, 35 | 'device' => $this->_device 36 | ]; 37 | } 38 | 39 | public function getSourceDevice() { 40 | if ($this->_executeResult && isset($this->_executeResult->source_device)) { 41 | return $this->_executeResult->source_device; 42 | } 43 | return null; 44 | } 45 | 46 | public function notificationHandler($params) { 47 | $this->tap = $params->tap; 48 | $this->device = $params->device; 49 | $this->state = $params->state; 50 | 51 | $this->completed = $this->state === TapState::Finished; 52 | if ($this->completed) { 53 | $this->successful = true; 54 | $this->event = new Event($params->state, $params); 55 | } 56 | 57 | if ($this->_hasBlocker() && in_array($this->state, $this->_eventsToWait)) { 58 | ($this->blocker->resolve)(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/relay/BaseRelayCase.php: -------------------------------------------------------------------------------- 1 | mockUuid(); 13 | $this->client = new Client(array('project' => 'project', 'token' => 'token')); 14 | $this->client->relayProtocol = 'relay-proto'; 15 | } 16 | 17 | public function tearDown(): void { 18 | unset($this->client); 19 | \Ramsey\Uuid\Uuid::setFactory(new \Ramsey\Uuid\UuidFactory()); 20 | SignalWire\Handler::clear(); 21 | } 22 | 23 | protected function mockUuid() { 24 | $factory = $this->createMock(\Ramsey\Uuid\UuidFactoryInterface::class); 25 | $factory->method('uuid4') 26 | ->will($this->returnValue(\Ramsey\Uuid\Uuid::fromString(self::UUID))); 27 | \Ramsey\Uuid\Uuid::setFactory($factory); 28 | } 29 | 30 | protected function _mockResponse($responses, $requests = []) { 31 | $stub = $this->createMock(SignalWire\Relay\Connection::class, ['send']); 32 | if (!is_array($responses)) { 33 | $responses = [$responses]; 34 | } 35 | foreach ($responses as $i => $r) { 36 | if (isset($requests[$i])) { 37 | $stub->expects($this->at($i)) 38 | ->method('send') 39 | ->with($requests[$i]) 40 | ->will($this->returnValue(\React\Promise\resolve($r))); 41 | } else { 42 | $stub->expects($this->at($i)) 43 | ->method('send') 44 | ->will($this->returnValue(\React\Promise\resolve($r))); 45 | } 46 | } 47 | 48 | $this->client->connection = $stub; 49 | } 50 | 51 | protected function _mockSendNotToBeCalled() { 52 | $stub = $this->createMock(SignalWire\Relay\Connection::class, ['send']); 53 | $stub->expects($this->never())->method('send'); 54 | $this->client->connection = $stub; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/Relay/Calling/Components/Controllable.php: -------------------------------------------------------------------------------- 1 | _execute("{$this->method}.stop")->then(function ($result) { 12 | if ($result->code !== '200') { 13 | $this->terminate(); 14 | } 15 | return new StopResult($result); 16 | }); 17 | } 18 | 19 | public function pause() { 20 | return $this->_execute("{$this->method}.pause")->then(function($result) { 21 | return $result->code === '200'; 22 | }); 23 | } 24 | 25 | public function resume() { 26 | return $this->_execute("{$this->method}.resume")->then(function($result) { 27 | return $result->code === '200'; 28 | }); 29 | } 30 | 31 | public function volume($value) { 32 | $msg = new Execute([ 33 | 'protocol' => $this->call->relayInstance->client->relayProtocol, 34 | 'method' => "{$this->method}.volume", 35 | 'params' => [ 36 | 'node_id' => $this->call->nodeId, 37 | 'call_id' => $this->call->id, 38 | 'control_id' => $this->controlId, 39 | 'volume' => (float)$value 40 | ] 41 | ]); 42 | 43 | return $this->call->_execute($msg)->then(function() { 44 | return true; 45 | }, function() { 46 | return false; 47 | }); 48 | } 49 | 50 | private function _execute(string $method) { 51 | $msg = new Execute([ 52 | 'protocol' => $this->call->relayInstance->client->relayProtocol, 53 | 'method' => $method, 54 | 'params' => [ 55 | 'node_id' => $this->call->nodeId, 56 | 'call_id' => $this->call->id, 57 | 'control_id' => $this->controlId 58 | ] 59 | ]); 60 | 61 | return $this->call->_execute($msg)->otherwise(function($error) { 62 | return (object)[ 63 | 'code' => $error->getCode(), 64 | 'message' => $error->getMessage() 65 | ]; 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/relay/Calling/BaseActionCase.php: -------------------------------------------------------------------------------- 1 | setUpClient(); 17 | $this->setUpCall(); 18 | } 19 | 20 | public function tearDown(): void { 21 | unset($this->client, $this->call); 22 | parent::tearDown(); 23 | } 24 | 25 | protected function setUpClient() { 26 | $this->client->connection = $this->createMock(SignalWire\Relay\Connection::class, ['send']); 27 | $this->client->relayProtocol = 'signalwire_calling_proto'; 28 | } 29 | 30 | protected function setUpCall() { 31 | $this->calling = new Calling($this->client); 32 | 33 | $options = (object)[ 34 | 'device' => (object)[ 35 | 'type' => 'phone', 36 | 'params' => (object)['from_number' => '234', 'to_number' => '456', 'timeout' => 20] 37 | ] 38 | ]; 39 | $this->call = new Call($this->calling, $options); 40 | } 41 | 42 | protected function _setCallReady() { 43 | $this->call->id = 'call-id'; 44 | $this->call->nodeId = 'node-id'; 45 | } 46 | 47 | protected function _mockSuccessResponse($msg, $success = null) { 48 | if (is_null($success)) { 49 | $success = json_decode('{"result":{"code":"200","message":"message","control_id":"' . self::UUID . '"}}'); 50 | } 51 | $this->client->connection->expects($this->once())->method('send')->with($msg)->willReturn(\React\Promise\resolve($success)); 52 | } 53 | 54 | protected function _mockFailResponse($msg, $fail = null) { 55 | if (is_null($fail)) { 56 | $fail = json_decode('{"result":{"code":"400","message":"some error","control_id":"' . self::UUID . '"}}'); 57 | } 58 | $this->client->connection->expects($this->once())->method('send')->with($msg)->willReturn(\React\Promise\reject($fail)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Log.php: -------------------------------------------------------------------------------- 1 | setFormatter($formatter); 37 | 38 | $logger = new Logger('SignalWire'); 39 | $logger->pushHandler($streamHandler); 40 | self::$instance = $logger; 41 | } 42 | 43 | public static function debug($message, array $context = []){ 44 | self::getLogger()->debug($message, $context); 45 | } 46 | 47 | public static function info($message, array $context = []){ 48 | self::getLogger()->info($message, $context); 49 | } 50 | 51 | public static function notice($message, array $context = []){ 52 | self::getLogger()->notice($message, $context); 53 | } 54 | 55 | public static function warning($message, array $context = []){ 56 | self::getLogger()->warning($message, $context); 57 | } 58 | 59 | public static function error($message, array $context = []){ 60 | self::getLogger()->error($message, $context); 61 | } 62 | 63 | // public static function critical($message, array $context = []){ 64 | // self::getLogger()->Critical($message, $context); 65 | // } 66 | 67 | // public static function alert($message, array $context = []){ 68 | // self::getLogger()->Alert($message, $context); 69 | // } 70 | 71 | // public static function emergency($message, array $context = []){ 72 | // self::getLogger()->Emergency($message, $context); 73 | // } 74 | } 75 | -------------------------------------------------------------------------------- /src/Relay/Calling/Components/Prompt.php: -------------------------------------------------------------------------------- 1 | _collect = $collect; 28 | $this->_play = $play; 29 | $this->_volume = (float)$volume; 30 | } 31 | 32 | public function payload() { 33 | $tmp = [ 34 | 'node_id' => $this->call->nodeId, 35 | 'call_id' => $this->call->id, 36 | 'control_id' => $this->controlId, 37 | 'play' => $this->_play, 38 | 'collect' => $this->_collect 39 | ]; 40 | if ($this->_volume !== 0.0) { 41 | $tmp['volume'] = $this->_volume; 42 | } 43 | return $tmp; 44 | } 45 | 46 | public function notificationHandler($params) { 47 | $this->completed = true; 48 | 49 | $this->type = $params->result->type; 50 | $this->event = new Event($this->type, $params->result); 51 | switch ($this->type) { 52 | case PromptState::Digit: 53 | $this->state = 'successful'; 54 | $this->successful = true; 55 | $this->input = $params->result->params->digits; 56 | $this->terminator = $params->result->params->terminator; 57 | break; 58 | case PromptState::Speech: 59 | $this->state = 'successful'; 60 | $this->successful = true; 61 | $this->input = $params->result->params->text; 62 | $this->confidence = $params->result->params->confidence; 63 | break; 64 | default: 65 | $this->state = $this->type; 66 | $this->successful = false; 67 | } 68 | 69 | if ($this->_hasBlocker() && in_array($this->type, $this->_eventsToWait)) { 70 | ($this->blocker->resolve)(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Relay/Setup.php: -------------------------------------------------------------------------------- 1 | relayProtocol) { 18 | $split = explode('_', $client->relayProtocol); 19 | if (isset($split[1]) && $split[1] === $client->signature) { 20 | $params->protocol = $client->relayProtocol; 21 | } 22 | } 23 | $msg = new Execute(array( 24 | 'protocol' => self::Protocol, 25 | 'method' => self::Method, 26 | 'params' => $params 27 | )); 28 | return $client->execute($msg)->then(function ($response) use ($client) { 29 | return $client->subscribe($response->result->protocol, self::Channels)->then(function ($response) { 30 | return $response->protocol; 31 | }, function ($error) use ($client) { 32 | Log::error("Setup error: {$error->message}. [code: {$error->code}]"); 33 | $client->eventLoop->stop(); 34 | }); 35 | }, function($error) use ($client) { 36 | Log::error("Setup error: {$error->message}. [code: {$error->code}]"); 37 | $client->eventLoop->stop(); 38 | }); 39 | } 40 | 41 | static function receive(Client $client, $newContexts) { 42 | $newContexts = array_filter((array)$newContexts); 43 | if (!count($newContexts)) { 44 | Log::error("One or more contexts are required."); 45 | return \React\Promise\resolve(false); 46 | } 47 | $contexts = array_diff($newContexts, $client->contexts); 48 | if (!count($contexts)) { 49 | return \React\Promise\resolve(true); 50 | } 51 | $msg = new Execute([ 52 | 'protocol' => $client->relayProtocol, 53 | 'method' => self::Receive, 54 | 'params' => ['contexts' => $contexts] 55 | ]); 56 | return $client->execute($msg)->then(function ($response) use ($client, $contexts) { 57 | $client->contexts = array_merge($client->contexts, $contexts); 58 | Log::info($response->result->message); 59 | return true; 60 | }, function ($error) { 61 | Log::error("Receive error: {$error->message}. [code: {$error->code}]"); 62 | return false; 63 | }); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /tests/laml/ClientTest.php: -------------------------------------------------------------------------------- 1 | sid, $this->token); 24 | $this->assertEquals($client->api->baseUrl, "https://$domain"); 25 | } 26 | 27 | public function testRestEndpointWithEnvSpaceUrl(): void { 28 | $domain = 'space.signalwire.com'; 29 | $_ENV[Client::ENV_SW_SPACE] = $domain; 30 | 31 | $client = new Client($this->sid, $this->token); 32 | $this->assertEquals($client->api->baseUrl, "https://$domain"); 33 | } 34 | 35 | public function testRestEndpointWithPutEnvHostname(): void { 36 | $domain = 'test.signalwire.com'; 37 | putenv(Client::ENV_SW_HOSTNAME . "=$domain"); 38 | 39 | $client = new Client($this->sid, $this->token); 40 | $this->assertEquals($client->api->baseUrl, "https://$domain"); 41 | } 42 | 43 | public function testRestEndpointWithPutEnvSpaceUrl(): void { 44 | $domain = 'space.signalwire.com'; 45 | putenv(Client::ENV_SW_SPACE . "=$domain"); 46 | 47 | $client = new Client($this->sid, $this->token); 48 | $this->assertEquals($client->api->baseUrl, "https://$domain"); 49 | } 50 | 51 | public function testThrowExceptionWithoutHostname(): void { 52 | $this->expectException(Exception::class); 53 | $client = new Client($this->sid, $this->token); 54 | 55 | $this->expectException(Exception::class); 56 | $client = new Client($this->sid, $this->token, array('test' => 'fake')); 57 | 58 | $this->expectException(Exception::class); 59 | $client = new Client($this->sid, $this->token, array('signalwireSpaceUrl' => '')); 60 | } 61 | 62 | public function testTNoExceptionIfSetInConstructor(): void { 63 | $domain = 'constructor.signalwire.com'; 64 | $opts = array( 65 | 'signalwireSpaceUrl' => $domain 66 | ); 67 | $client = new Client($this->sid, $this->token, $opts); 68 | $this->assertEquals($client->api->baseUrl, "https://$domain"); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/MessagesTest.php: -------------------------------------------------------------------------------- 1 | id.'","method":"blade.connect","params":{"version":{"major":2,"minor":1,"revision":0},"authentication":{"project":"project","token":"token"},"agent":"PHP SDK/'.\SignalWire\VERSION.'"}}'; 15 | $this->assertEquals($msg->toJson(), $json); 16 | } 17 | 18 | public function testBladeConnectWithSessionId(): void { 19 | $msg = new Connect('project', 'token', 'sessId'); 20 | $json = '{"jsonrpc":"2.0","id":"'.$msg->id.'","method":"blade.connect","params":{"version":{"major":2,"minor":1,"revision":0},"authentication":{"project":"project","token":"token"},"agent":"PHP SDK/'.\SignalWire\VERSION.'","sessionid":"sessId"}}'; 21 | $this->assertEquals($msg->toJson(), $json); 22 | } 23 | 24 | public function testBladeExecuteRequest(): void { 25 | $params = array( 26 | 'key' => 'value', 27 | 'nested' => array('service' => 'test') 28 | ); 29 | $msg = new Execute($params); 30 | $json = '{"jsonrpc":"2.0","id":"'.$msg->id.'","method":"blade.execute","params":{"key":"value","nested":{"service":"test"}}}'; 31 | $this->assertEquals($msg->toJson(), $json); 32 | 33 | $params = array( 34 | 'key' => 'value', 35 | 'params' => array('channels' => array('test', 'test1', 'test2')), 36 | ); 37 | $msg = new Execute($params); 38 | $json = '{"jsonrpc":"2.0","id":"'.$msg->id.'","method":"blade.execute","params":{"key":"value","params":{"channels":["test","test1","test2"]}}}'; 39 | $this->assertEquals($msg->toJson(), $json); 40 | } 41 | 42 | public function testBladeSubscription(): void { 43 | $params = array( 44 | 'command' => 'add', 45 | 'protocol' => 'test', 46 | 'channels' => array('c1', 'c2', 'c3') 47 | ); 48 | $msg = new Subscription($params); 49 | $json = '{"jsonrpc":"2.0","id":"'.$msg->id.'","method":"blade.subscription","params":{"command":"add","protocol":"test","channels":["c1","c2","c3"]}}'; 50 | $this->assertEquals($msg->toJson(), $json); 51 | 52 | $params = array( 53 | 'command' => 'remove', 54 | 'protocol' => 'test', 55 | 'channels' => array('c1', 'c2', 'c3') 56 | ); 57 | $msg = new Subscription($params); 58 | $json = '{"jsonrpc":"2.0","id":"'.$msg->id.'","method":"blade.subscription","params":{"command":"remove","protocol":"test","channels":["c1","c2","c3"]}}'; 59 | $this->assertEquals($msg->toJson(), $json); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Rest/Client.php: -------------------------------------------------------------------------------- 1 | _getHost($options); 16 | $this->_api = new Api($this, $domain); 17 | } 18 | 19 | public function getSignalwireDomain() { 20 | return $this->_api->baseUrl; 21 | } 22 | 23 | protected function getFax(): \Twilio\Rest\Fax { 24 | if (!$this->_fax) { 25 | $this->_fax = new \SignalWire\Rest\Fax($this); 26 | } 27 | return $this->_fax; 28 | } 29 | 30 | private function _getHost(Array $options = array()) { 31 | if (array_key_exists("signalwireSpaceUrl", $options) && trim($options["signalwireSpaceUrl"]) !== "") { 32 | return trim($options["signalwireSpaceUrl"]); 33 | } elseif ($this->_checkEnv(self::ENV_SW_SPACE)) { 34 | return $this->_checkEnv(self::ENV_SW_SPACE); 35 | } elseif ($this->_checkEnv(self::ENV_SW_HOSTNAME)) { 36 | return $this->_checkEnv(self::ENV_SW_HOSTNAME); 37 | } 38 | 39 | throw new \Exception("SignalWire Space URL is not configured.\nEnter your SignalWire Space domain via the SIGNALWIRE_SPACE_URL or SIGNALWIRE_API_HOSTNAME environment variables, or specifying the property \"signalwireSpaceUrl\" in the init options."); 40 | } 41 | 42 | private function _checkEnv(String $key) { 43 | if (isset($_ENV[$key]) && trim($_ENV[$key]) !== "") { 44 | return trim($_ENV[$key]); 45 | } elseif (getenv($key) !== false) { 46 | return getenv($key); 47 | } 48 | return false; 49 | } 50 | 51 | protected function getCalls(): \Twilio\Rest\Api\V2010\Account\CallList { 52 | return $this->_api->v2010->account->calls; 53 | } 54 | } 55 | 56 | class Fax extends \Twilio\Rest\Fax { 57 | public function __construct(Client $client) { 58 | parent::__construct($client); 59 | $this->baseUrl = $client->getSignalwireDomain(); 60 | } 61 | 62 | protected function getV1(): \Twilio\Rest\Fax\V1 { 63 | if (!$this->_v1) { 64 | $this->_v1 = new \SignalWire\Rest\V1($this); 65 | } 66 | return $this->_v1; 67 | } 68 | 69 | } 70 | 71 | class V1 extends \Twilio\Rest\Fax\V1 { 72 | protected $_faxes = null; 73 | /** 74 | * Construct the V1 version of Fax 75 | * 76 | * @param \Twilio\Domain $domain Domain that contains the version 77 | * @return \Twilio\Rest\Fax\V1 V1 version of Fax 78 | */ 79 | public function __construct(Fax $domain) { 80 | parent::__construct($domain); 81 | $this->version = '2010-04-01/Accounts/' . $domain->client->username; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Handler.php: -------------------------------------------------------------------------------- 1 | $handler){ 35 | if ($handler === $callable) { 36 | unset(self::$queue[$event][$index]); 37 | } 38 | } 39 | } elseif (isset(self::$queue[$event])) { 40 | self::$queue[$event] = array(); 41 | } 42 | if (!count(self::$queue[$event])) { 43 | unset(self::$queue[$event]); 44 | } 45 | return true; 46 | } 47 | 48 | static public function deRegisterAll(String $evt){ 49 | $find = self::_cleanEventName($evt, ""); 50 | foreach (self::$queue as $event => $callbacks){ 51 | if (strpos($event, $find) === 0) { 52 | unset(self::$queue[$event]); 53 | } 54 | } 55 | } 56 | 57 | static public function trigger(String $evt, $params, String $uniqueId = self::GLOBAL){ 58 | if (!self::isQueued($evt, $uniqueId)) { 59 | return false; 60 | } 61 | $event = self::_cleanEventName($evt, $uniqueId); 62 | if (isset(self::$queue[$event])) { 63 | foreach (self::$queue[$event] as $callable){ 64 | $callable($params); 65 | } 66 | } 67 | return true; 68 | } 69 | 70 | static public function isQueued(String $evt, String $uniqueId = self::GLOBAL){ 71 | $event = self::_cleanEventName($evt, $uniqueId); 72 | return array_key_exists($event, self::$queue) && count(self::$queue[$event]) > 0; 73 | } 74 | 75 | static public function clear(){ 76 | self::$queue = array(); 77 | } 78 | 79 | static public function queueCount(String $evt, String $uniqueId = self::GLOBAL){ 80 | if (!self::isQueued($evt, $uniqueId)) { 81 | return 0; 82 | } 83 | $event = self::_cleanEventName($evt, $uniqueId); 84 | return count(self::$queue[$event]); 85 | } 86 | 87 | static private function _cleanEventName($event, $uniqueId) { 88 | return trim($event) . "|" . trim($uniqueId); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Rest/Api/V2010/Account/CallList.php: -------------------------------------------------------------------------------- 1 | $to, 23 | 'From' => $from, 24 | 'Url' => $options['url'], 25 | 'Twiml' => $options['twiml'], 26 | 'ApplicationSid' => $options['applicationSid'], 27 | 'Method' => $options['method'], 28 | 'FallbackUrl' => $options['fallbackUrl'], 29 | 'FallbackMethod' => $options['fallbackMethod'], 30 | 'StatusCallback' => $options['statusCallback'], 31 | 'StatusCallbackEvent' => Serialize::map($options['statusCallbackEvent'], function ($e) { 32 | return $e; 33 | }), 34 | 'StatusCallbackMethod' => $options['statusCallbackMethod'], 35 | 'SendDigits' => $options['sendDigits'], 36 | 'Timeout' => $options['timeout'], 37 | 'Record' => Serialize::booleanToString($options['record']), 38 | 'RecordingChannels' => $options['recordingChannels'], 39 | 'RecordingStatusCallback' => $options['recordingStatusCallback'], 40 | 'RecordingStatusCallbackMethod' => $options['recordingStatusCallbackMethod'], 41 | 'SipAuthUsername' => $options['sipAuthUsername'], 42 | 'SipAuthPassword' => $options['sipAuthPassword'], 43 | 'MachineDetection' => $options['machineDetection'], 44 | 'MachineDetectionTimeout' => $options['machineDetectionTimeout'], 45 | 'RecordingStatusCallbackEvent' => Serialize::map($options['recordingStatusCallbackEvent'], function ($e) { 46 | return $e; 47 | }), 48 | 'Trim' => $options['trim'], 49 | 'CallerId' => $options['callerId'], 50 | 'MachineDetectionSpeechThreshold' => $options['machineDetectionSpeechThreshold'], 51 | 'MachineDetectionSpeechEndThreshold' => $options['machineDetectionSpeechEndThreshold'], 52 | 'MachineDetectionSilenceTimeout' => $options['machineDetectionSilenceTimeout'], 53 | 'AsyncAmd' => $options['asyncAmd'], 54 | 'AsyncAmdStatusCallback' => $options['asyncAmdStatusCallback'], 55 | 'AsyncAmdStatusCallbackMethod' => $options['asyncAmdStatusCallbackMethod'], 56 | 'AsyncAmdPartialResults' => $options['asyncAmdPartialResults'], 57 | 'Byoc' => $options['byoc'], 58 | 'CallReason' => $options['callReason'], 59 | 'CallToken' => $options['callToken'], 60 | 'RecordingTrack' => $options['recordingTrack'], 61 | 'TimeLimit' => $options['timeLimit'], 62 | ]); 63 | 64 | $payload = $this->version->create('POST', $this->uri, [], $data); 65 | 66 | return new \SignalWire\Rest\Api\V2010\Account\CallInstance( 67 | $this->version, 68 | $payload, 69 | $this->solution['accountSid'] 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/relay/Calling/CallSendDigitsTest.php: -------------------------------------------------------------------------------- 1 | _setCallReady(); 22 | } 23 | 24 | public function testSendDigitsSuccess(): void { 25 | $msg = $this->_sendDigitsMsg(); 26 | $this->_mockSuccessResponse($msg, self::$success); 27 | $this->call->sendDigits('1234')->done(function($result) { 28 | $this->assertInstanceOf('SignalWire\Relay\Calling\Results\SendDigitsResult', $result); 29 | $this->assertTrue($result->isSuccessful()); 30 | $this->assertObjectHasAttribute('state', $result->getEvent()->payload); 31 | }); 32 | $this->calling->notificationHandler(self::$notificationFinished); 33 | } 34 | 35 | public function testSendDigitsFail(): void { 36 | $msg = $this->_sendDigitsMsg(); 37 | $this->_mockFailResponse($msg, self::$fail); 38 | $this->call->sendDigits('1234')->done(function($result) { 39 | $this->assertInstanceOf('SignalWire\Relay\Calling\Results\SendDigitsResult', $result); 40 | $this->assertFalse($result->isSuccessful()); 41 | }); 42 | } 43 | 44 | public function testSendDigitsAsyncSuccess(): void { 45 | $msg = $this->_sendDigitsMsg(); 46 | $this->_mockSuccessResponse($msg, self::$success); 47 | $this->call->sendDigitsAsync('1234')->done(function($action) { 48 | $this->assertInstanceOf('SignalWire\Relay\Calling\Actions\SendDigitsAction', $action); 49 | $this->assertInstanceOf('SignalWire\Relay\Calling\Results\SendDigitsResult', $action->getResult()); 50 | $this->assertFalse($action->isCompleted()); 51 | $this->calling->notificationHandler(self::$notificationFinished); 52 | $this->assertTrue($action->isCompleted()); 53 | }); 54 | } 55 | 56 | public function testSendDigitsAsyncFail(): void { 57 | $msg = $this->_sendDigitsMsg(); 58 | $this->_mockFailResponse($msg, self::$fail); 59 | $this->call->sendDigitsAsync('1234')->done(function($action) { 60 | $this->assertInstanceOf('SignalWire\Relay\Calling\Actions\SendDigitsAction', $action); 61 | $this->assertTrue($action->isCompleted()); 62 | $this->assertEquals($action->getState(), 'failed'); 63 | }); 64 | } 65 | 66 | private function _sendDigitsMsg() { 67 | return new Execute([ 68 | 'protocol' => 'signalwire_calling_proto', 69 | 'method' => 'calling.send_digits', 70 | 'params' => [ 71 | 'call_id' => 'call-id', 72 | 'node_id' => 'node-id', 73 | 'control_id' => self::UUID, 74 | 'digits' => '1234' 75 | ] 76 | ]); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Relay/Calling/Components/BaseComponent.php: -------------------------------------------------------------------------------- 1 | call = $call; 52 | $this->controlId = Uuid::uuid4()->toString(); 53 | } 54 | 55 | /** 56 | * Payload sent to Relay in requests 57 | * 58 | */ 59 | abstract function payload(); 60 | 61 | /** 62 | * Handle Relay notification to update the component 63 | * 64 | * @param params Relay notification params 65 | */ 66 | abstract function notificationHandler($params); 67 | 68 | public function execute() { 69 | if ($this->call->ended) { 70 | $this->terminate(); 71 | return \React\Promise\resolve(); 72 | } 73 | if ($this->method === null) { 74 | return \React\Promise\resolve(); 75 | } 76 | $msg = new Execute([ 77 | 'protocol' => $this->call->relayInstance->client->relayProtocol, 78 | 'method' => $this->method, 79 | 'params' => $this->payload() 80 | ]); 81 | 82 | return $this->call->_execute($msg)->then(function($result) { 83 | $this->_executeResult = $result; 84 | 85 | return $this->_executeResult; 86 | }, function($error) { 87 | $this->terminate(); 88 | }); 89 | } 90 | 91 | public function _waitFor(...$events) { 92 | $this->_eventsToWait = $events; 93 | $this->blocker = new Blocker($this->eventType, $this->controlId); 94 | 95 | return $this->execute()->then(function() { 96 | return $this->blocker->promise; 97 | }); 98 | } 99 | 100 | public function terminate($params = null) { 101 | $this->completed = true; 102 | $this->successful = false; 103 | $this->state = 'failed'; 104 | if ($params && isset($params->call_state)) { 105 | $this->event = new Event($params->call_state, $params); 106 | } 107 | if ($this->_hasBlocker()) { 108 | ($this->blocker->resolve)(); 109 | } 110 | } 111 | 112 | protected function _hasBlocker() { 113 | return $this->blocker instanceof Blocker; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Relay/Calling/Components/Detect.php: -------------------------------------------------------------------------------- 1 | _detect = $detect; 31 | $this->_timeout = $timeout; 32 | $this->_waitForBeep = $waitForBeep; 33 | } 34 | 35 | public function payload() { 36 | $this->_detect['params'] = (object) $this->_detect['params']; 37 | $payload = [ 38 | 'node_id' => $this->call->nodeId, 39 | 'call_id' => $this->call->id, 40 | 'control_id' => $this->controlId, 41 | 'detect' => $this->_detect 42 | ]; 43 | if ($this->_timeout) { 44 | $payload['timeout'] = $this->_timeout; 45 | } 46 | return $payload; 47 | } 48 | 49 | public function notificationHandler($params) { 50 | $detect = $params->detect; 51 | $this->type = $detect->type; 52 | $this->state = $detect->params->event; 53 | 54 | $finishedEvents = [DetectState::Finished, DetectState::Error]; 55 | if (in_array($this->state, $finishedEvents)) { 56 | return $this->_complete($detect); 57 | } 58 | 59 | if (!$this->_hasBlocker()) { 60 | array_push($this->_events, $detect->params->event); 61 | return; 62 | } 63 | 64 | if ($this->type === DetectType::Digit) { 65 | return $this->_complete($detect); 66 | } 67 | 68 | if ($this->_waitingForReady) { 69 | if ($this->state === DetectState::Ready) { 70 | return $this->_complete($detect); 71 | } 72 | return; 73 | } 74 | 75 | if ($this->_waitForBeep && $this->state === DetectState::Machine) { 76 | $this->_waitingForReady = true; 77 | return; 78 | } 79 | 80 | if (in_array($this->state, $this->_eventsToWait)) { 81 | return $this->_complete($detect); 82 | } 83 | } 84 | 85 | private function _complete($detect) { 86 | $this->completed = true; 87 | $this->event = new Event($this->state, $detect); 88 | 89 | if ($this->_hasBlocker()) { 90 | // force READY/NOT_READY to MACHINE 91 | if (in_array($this->state, [DetectState::Ready, DetectState::NotReady])) { 92 | $this->result = DetectState::Machine; 93 | } elseif (!in_array($this->state, [DetectState::Finished, DetectState::Error])) { 94 | $this->result = $this->state; 95 | } 96 | $this->successful = !in_array($this->state, [DetectState::Finished, DetectState::Error]); 97 | ($this->blocker->resolve)(); 98 | } else { 99 | $this->result = join(',', $this->_events); 100 | $this->successful = $this->state !== DetectState::Error; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Relay/Connection.php: -------------------------------------------------------------------------------- 1 | client = $client; 21 | } 22 | 23 | public function connect() { 24 | $host = \SignalWire\checkWebSocketHost($this->client->host); 25 | Log::debug("Connecting to: $host"); 26 | 27 | $connector = new Connector($this->client->eventLoop); 28 | $connector($host)->done( 29 | function(WebSocket $webSocket) { 30 | $this->_ws = $webSocket; 31 | $this->_ws->on('message', function($msg) { 32 | Log::debug("RECV " . $msg->getPayload()); 33 | $obj = json_decode($msg->getPayload()); 34 | if (!is_object($obj) || !isset($obj->id)) { 35 | return; 36 | } 37 | if (Handler::trigger($obj->id, $obj) === false) { 38 | Handler::trigger(Events::SocketMessage, $obj, $this->client->uuid); 39 | } 40 | }); 41 | 42 | $this->_ws->on('close', function($code = null, $reason = null) { 43 | $this->_connected = false; 44 | if ($this->_keepAliveTimer) { 45 | $this->client->eventLoop->cancelTimer($this->_keepAliveTimer); 46 | } 47 | $param = array('code' => $code, 'reason' => $reason); 48 | Handler::trigger(Events::SocketClose, $param, $this->client->uuid); 49 | }); 50 | 51 | Handler::trigger(Events::SocketOpen, null, $this->client->uuid); 52 | 53 | $this->_keepAlive(); 54 | }, 55 | function(\Exception $error) { 56 | Handler::trigger(Events::SocketError, $error, $this->client->uuid); 57 | } 58 | ); 59 | } 60 | 61 | public function close() { 62 | if (isset($this->_ws)) { 63 | $this->_ws->close(); 64 | unset($this->_ws); 65 | } elseif ($this->_connectorTimer) { 66 | $this->client->eventLoop->cancelTimer($this->_connectorTimer); 67 | } else { 68 | $this->_connectorTimer = $this->client->eventLoop->addTimer(1, [$this, 'close']); 69 | } 70 | } 71 | 72 | public function send(BaseMessage $msg) { 73 | $promise = new \React\Promise\Promise(function (callable $resolve, callable $reject) use ($msg) { 74 | $callback = function($msg) use ($resolve, $reject) { 75 | if (isset($msg->error)) { 76 | return $reject($msg->error); 77 | } 78 | if (isset($msg->result->result->code) && $msg->result->result->code !== "200") { 79 | return $reject($msg->result); 80 | } 81 | $resolve($msg->result); 82 | }; 83 | 84 | Handler::registerOnce($msg->id, $callback); 85 | }); 86 | 87 | Log::debug("SEND {$msg->toJson()}"); 88 | $this->_ws->send($msg->toJson()); 89 | 90 | return $promise; 91 | } 92 | 93 | private function _keepAlive() { 94 | $this->_connected = true; 95 | 96 | $this->_ws->on('pong', function() { 97 | $this->_connected = true; 98 | }); 99 | 100 | $this->_keepAliveTimer = $this->client->eventLoop->addPeriodicTimer(self::PING_INTERVAL, function () { 101 | if ($this->_connected) { 102 | $this->_connected = false; 103 | $this->_ws->send(new Frame('', true, Frame::OP_PING)); 104 | } else { 105 | $this->_ws->close(); 106 | } 107 | }); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/relay/Calling/CallTapTest.php: -------------------------------------------------------------------------------- 1 | 'audio']; 19 | self::$device = ['type' => 'rtp', 'addr' => '127.0.0.1', 'port' => 1234]; 20 | } 21 | 22 | protected function setUp(): void { 23 | parent::setUp(); 24 | 25 | $this->_setCallReady(); 26 | } 27 | 28 | public function testTapSuccess(): void { 29 | $msg = $this->_tapMsg(); 30 | $this->_mockSuccessResponse($msg, self::$success); 31 | $this->call->tap(self::$tap, self::$device)->done(function($result) { 32 | $this->assertInstanceOf('SignalWire\Relay\Calling\Results\TapResult', $result); 33 | $this->assertTrue($result->isSuccessful()); 34 | $this->assertEquals($result->getTap(), json_decode('{"type":"audio","params":{"direction":"listen"}}')); 35 | $this->assertEquals($result->getSourceDevice(), self::$success->result->source_device); 36 | $this->assertEquals($result->getDestinationDevice(), json_decode('{"type":"rtp","params":{"addr":"127.0.0.1","port":"1234","codec":"PCMU","ptime":"20"}}')); 37 | $this->assertObjectHasAttribute('tap', $result->getEvent()->payload); 38 | $this->assertObjectHasAttribute('device', $result->getEvent()->payload); 39 | }); 40 | $this->calling->notificationHandler(self::$notificationFinished); 41 | } 42 | 43 | public function testTapFail(): void { 44 | $msg = $this->_tapMsg(); 45 | $this->_mockFailResponse($msg, self::$fail); 46 | $this->call->tap(self::$tap, self::$device)->done(function($result) { 47 | $this->assertInstanceOf('SignalWire\Relay\Calling\Results\TapResult', $result); 48 | $this->assertFalse($result->isSuccessful()); 49 | }); 50 | } 51 | 52 | public function testTapAsyncSuccess(): void { 53 | $msg = $this->_tapMsg(); 54 | $this->_mockSuccessResponse($msg, self::$success); 55 | $this->call->tapAsync(self::$tap, self::$device)->done(function($action) { 56 | $this->assertInstanceOf('SignalWire\Relay\Calling\Actions\TapAction', $action); 57 | $this->assertInstanceOf('SignalWire\Relay\Calling\Results\TapResult', $action->getResult()); 58 | $this->assertFalse($action->isCompleted()); 59 | $this->calling->notificationHandler(self::$notificationFinished); 60 | $this->assertTrue($action->isCompleted()); 61 | }); 62 | } 63 | 64 | public function testTapAsyncFail(): void { 65 | $msg = $this->_tapMsg(); 66 | $this->_mockFailResponse($msg, self::$fail); 67 | $this->call->tapAsync(self::$tap, self::$device)->done(function($action) { 68 | $this->assertInstanceOf('SignalWire\Relay\Calling\Actions\TapAction', $action); 69 | $this->assertTrue($action->isCompleted()); 70 | $this->assertEquals($action->getState(), 'failed'); 71 | }); 72 | } 73 | 74 | private function _tapMsg() { 75 | return new Execute([ 76 | 'protocol' => 'signalwire_calling_proto', 77 | 'method' => 'calling.tap', 78 | 'params' => [ 79 | 'call_id' => 'call-id', 80 | 'node_id' => 'node-id', 81 | 'control_id' => self::UUID, 82 | 'tap' => ['type' => 'audio', 'params' => new \stdClass], 83 | 'device' => ['type' => 'rtp', 'params' => (object)['addr' => '127.0.0.1', 'port' => 1234]] 84 | ] 85 | ]); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/relay/Messaging/MessagingTest.php: -------------------------------------------------------------------------------- 1 | _mockResponse([$response]); 13 | 14 | $mock = $this->getMockBuilder(\stdClass::class)->setMethods(['foo'])->getMock(); 15 | $mock->expects($this->once())->method('foo'); 16 | 17 | $this->client->messaging->onReceive(['home', 'office'], [$mock, 'foo'])->done(function() { 18 | $this->assertTrue(Handler::isQueued('relay-proto', 'messaging.ctxReceive.home')); 19 | $this->assertTrue(Handler::isQueued('relay-proto', 'messaging.ctxReceive.office')); 20 | 21 | $msg = json_decode('{"jsonrpc":"2.0","id":"req-uuid","method":"blade.broadcast","params":{"broadcaster_nodeid":"uuid","protocol":"relay-proto","channel":"notifications","event":"queuing.relay.messaging","params":{"event_type":"messaging.receive","space_id":"uuid","project_id":"uuid","context":"home","params":{"message_id":"id","context":"home","direction":"inbound","tags":["message","inbound","SMS","home","+1xxx","+1yyy","relay-client"],"from_number":"+1xxx","to_number":"+1yyy","body":"Welcome at SignalWire!","media":[],"segments":1,"message_state":"received"}}}}'); 22 | Handler::trigger(Events::SocketMessage, $msg, $this->client->uuid); 23 | }); 24 | } 25 | 26 | public function testOnStateChange(): void { 27 | $response = json_decode('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"code":"200","message":"Receiving all inbound related to the requested relay contexts"}}'); 28 | $this->_mockResponse([$response]); 29 | 30 | $mock = $this->getMockBuilder(\stdClass::class)->setMethods(['foo'])->getMock(); 31 | $mock->expects($this->once())->method('foo'); 32 | 33 | $this->client->messaging->onStateChange(['home', 'office'], [$mock, 'foo'])->done(function() { 34 | $this->assertTrue(Handler::isQueued('relay-proto', 'messaging.ctxState.home')); 35 | $this->assertTrue(Handler::isQueued('relay-proto', 'messaging.ctxState.office')); 36 | 37 | $msg = json_decode('{"jsonrpc":"2.0","id":"req-id","method":"blade.broadcast","params":{"broadcaster_nodeid":"uuid","protocol":"relay-proto","channel":"notifications","event":"queuing.relay.messaging","params":{"event_type":"messaging.state","space_id":"uuid","project_id":"uuid","context":"office","params":{"message_id":"224d1192-b266-4ca2-bd8e-48c64a44d830","context":"office","direction":"outbound","tags":["message","outbound","SMS","office","relay-client"],"from_number":"+1xxx","to_number":"+1yyy","body":"Welcome at SignalWire!","media":[],"segments":1,"message_state":"queued"}}}}'); 38 | Handler::trigger(Events::SocketMessage, $msg, $this->client->uuid); 39 | }); 40 | } 41 | 42 | public function testSendWithSuccess(): void { 43 | $response = json_decode('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"message":"Message accepted","code":"200","message_id":"2c0e265d-4597-470e-9d5d-00581e0874a2"}}'); 44 | $this->_mockResponse([$response]); 45 | 46 | $params = [ 'context' => 'office', 'from' => '8992222222', 'to' => '8991111111', 'body' => 'Hello' ]; 47 | $this->client->messaging->send($params)->done(function($result) { 48 | $this->assertInstanceOf('SignalWire\Relay\Messaging\SendResult', $result); 49 | $this->assertTrue($result->successful); 50 | $this->assertEquals($result->getMessageId(), '2c0e265d-4597-470e-9d5d-00581e0874a2'); 51 | $this->assertTrue($result->isSuccessful()); 52 | }); 53 | } 54 | 55 | public function testSendWithFailure(): void { 56 | $response = json_decode('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"message":"Some error","code":"400"}}'); 57 | $this->_mockResponse([$response]); 58 | 59 | $params = [ 'context' => 'office', 'from' => '8992222222', 'to' => '8991111111', 'body' => 'Hello' ]; 60 | $this->client->messaging->send($params)->done(function($result) { 61 | $this->assertInstanceOf('SignalWire\Relay\Messaging\SendResult', $result); 62 | $this->assertFalse($result->successful); 63 | $this->assertFalse($result->isSuccessful()); 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 5 | 6 | ## [2.3.10] - 2022-01-10 7 | ### Fixed 8 | - Fixed `SignalWire\LaML\MessageResponse` namespace and introduced `SignalWire\LaML\MessagingResponse` for better compatibility. 9 | 10 | ## [2.3.9] - 2021-09-23 11 | ### Updated 12 | - Update compatibility SDK versions. 13 | - Add requirement to PHP `^7`. 14 | 15 | ## [2.3.8] - 2020-10-14 16 | ### Updated 17 | - Relax Guzzle version requirements and allow Guzzle 7. 18 | 19 | ## [2.3.7] - 2020-09-05 20 | ### Updated 21 | - Updated Twilio version to `6.10.4` 22 | - Added new test file and examples 23 | - Added VoiceResponse, FaxResponse and MessageResponse 24 | 25 | ## [2.3.6] - 2020-04-23 26 | ### Changed 27 | - Loosened the version requirements for `ramsey/uuid`. Allowed versions are `^3.8 || ^4.0` 28 | 29 | ## [2.3.5] - 2020-03-11 30 | ### Fixed 31 | - Handle Blade timeout response and randomize reconnection attempts. 32 | 33 | ## [2.3.4] - 2020-01-31 34 | ### Changed 35 | - Loosened the version requirements for `monolog/monolog`. Allowed versions are `^1.24 || ^2.0` 36 | 37 | ## [2.3.3] - 2020-01-09 38 | ### Fixed 39 | - LaML engine 40 | 41 | ## [2.3.2] - 2019-12-16 42 | ### Added 43 | - Call `disconnect()` method. 44 | 45 | ### Fixed 46 | - Set `peer` property on the connected Call [#101](https://github.com/signalwire/signalwire-php/issues/101) 47 | 48 | ## [2.3.1] - 2019-11-04 49 | ### Fixed 50 | - Reconnect and restore previous protocol issue. 51 | 52 | ## [2.3.0] - 2019-10-22 53 | ### Added 54 | - Add `getUrl()` method to `RecordAction` object. 55 | - Add methods to `pause` and `resume` a PlayAction. 56 | - Ability to set volume playback on `play` and `prompt` methods, or through the asynchronous `PlayAction` and `PromptAction` objects. 57 | - Add `playRingtone` and `playRingtoneAsync` methods to simplify play a ringtone. 58 | - Add `promptRingtone` and `promptRingtoneAsync` methods to simplify play a ringtone. 59 | - Support `ringback` option on `connect` and `connectAsync` methods. 60 | 61 | ## [2.2.0] - 2019-09-09 62 | ### Changed 63 | - Minor change at the lower level APIs: using `calling.` instead of `call.` prefix for calling methods. 64 | - Flattened parameters for _record_, _play_, _prompt_, _detect_ and _tap_ calling methods. 65 | 66 | ### Added 67 | - New methods to perform answering machine detection: `amd` (alias to `detectAnsweringMachine`) and `amdAsync` (alias to `detectAnsweringMachineAsync`). 68 | 69 | ### Deprecated 70 | - Deprecated the following methods on Call: `detectHuman`, `detectHumanAsync`, `detectMachine`, `detectMachineAsync`. 71 | 72 | ### Added 73 | - Methods to send digits on a Call: `sendDigits`, `sendDigitsAsync`. 74 | 75 | ## [2.1.0] - 2019-07-30 76 | ### Added 77 | - Create your own Relay Tasks and enable `onTask` method on RelayConsumer to receive/handle them. 78 | - Methods to start a detector on a Call: `detect`, `detectAsync`, `detectHuman`, `detectHumanAsync`, `detectMachine`, `detectMachineAsync`, `detectFax`, `detectFaxAsync`, `detectDigit`, `detectDigitAsync` 79 | - Methods to tap media in a Call: `tap` and `tapAsync` 80 | - Support for Relay Messaging 81 | 82 | ### Fixed 83 | - Possible issue on WebSocket reconnect due to a race condition on the EventLoop. 84 | 85 | ## [2.0.0] - 2019-07-16 86 | ### Added 87 | - Add support for faxing. New call methods: `faxReceive`, `faxReceiveAsync`, `faxSend`, `faxSendAsync`. 88 | 89 | ## [2.0.0-RC1] - 2019-07-10 90 | ### Added 91 | - Released new Relay Client interface. 92 | - Add RelayConsumer. 93 | - Handle SIGINT/SIGTERM signals. 94 | - Add Relay calling `waitFor`, `waitForRinging`, `waitForAnswered`, `waitForEnding`, `waitForEnded` methods. 95 | ### Fixed 96 | - Default React EventLoop 97 | 98 | ## [1.4.1] 99 | ### Fixed 100 | - Fix bug handling connect notifications. 101 | 102 | ## [1.4.0] 103 | ### Added 104 | - Ability to set a custom `\React\EventLoop` in RelayClient. 105 | 106 | ## [1.3.0] 107 | ### Added 108 | - Call `connect()` method. 109 | - Call `record()` method. 110 | - Call `playMedia()`, `playAudio()`, `playTTS()`, `playSilence()` methods. 111 | - Call `playMediaAndCollect()`, `playAudioAndCollect()`, `playTTSAndCollect()`, `playSilenceAndCollect()` methods. 112 | - Expose Call `play.*`, `record.*`, `collect` events. 113 | 114 | ## [1.2.1] 115 | ### Fixed 116 | - Add websocket host protocol and path automatically. 117 | 118 | ## [1.2.0] 119 | ### Added 120 | - Relay SDK to connect and use SignalWire's Relay APIs. 121 | 122 | ## [1.1.1] 123 | ### Added 124 | - Ability to set SignalWire Space URL in `SignalWire\Rest\Client` constructor via `signalwireSpaceUrl` key. 125 | - Support SIGNALWIRE_SPACE_URL env variable. 126 | 127 | ## [1.1.0] 128 | ### Added 129 | - Fax support 130 | 131 | ## [1.0.0] 132 | 133 | Initial release 134 | 135 | 142 | -------------------------------------------------------------------------------- /tests/HandlerTest.php: -------------------------------------------------------------------------------- 1 | mock = $this->getMockBuilder(self::class) 15 | ->enableProxyingToOriginalMethods() 16 | ->getMock(); 17 | } 18 | 19 | protected function tearDown(): void { 20 | SignalWire\Handler::clear(); 21 | } 22 | 23 | // register() 24 | public function testRegisterWithoutUniqueId(): void { 25 | SignalWire\Handler::register(self::EVENT_NAME, [$this->mock, 'noop']); 26 | 27 | $this->assertTrue(SignalWire\Handler::isQueued(self::EVENT_NAME)); 28 | $this->assertFalse(SignalWire\Handler::isQueued(self::EVENT_NAME, self::UNIQUE_ID)); 29 | $this->assertEquals(SignalWire\Handler::queueCount(self::EVENT_NAME), 1); 30 | } 31 | 32 | public function testRegisterWithUniqueId(): void { 33 | SignalWire\Handler::register(self::EVENT_NAME, [$this->mock, 'noop'], self::UNIQUE_ID); 34 | 35 | $this->assertTrue(SignalWire\Handler::isQueued(self::EVENT_NAME, self::UNIQUE_ID)); 36 | $this->assertFalse(SignalWire\Handler::isQueued(self::EVENT_NAME)); 37 | $this->assertEquals(SignalWire\Handler::queueCount(self::EVENT_NAME, self::UNIQUE_ID), 1); 38 | } 39 | 40 | // registerOnce() 41 | public function testRegisterOnceWithoutUniqueId(): void { 42 | SignalWire\Handler::registerOnce(self::EVENT_NAME, [$this->mock, 'noop']); 43 | 44 | $this->assertTrue(SignalWire\Handler::isQueued(self::EVENT_NAME)); 45 | $this->assertFalse(SignalWire\Handler::isQueued(self::EVENT_NAME, self::UNIQUE_ID)); 46 | $this->assertEquals(SignalWire\Handler::queueCount(self::EVENT_NAME), 1); 47 | 48 | $this->mock->expects($this->exactly(1))->method('noop')->with('once'); 49 | 50 | SignalWire\Handler::trigger(self::EVENT_NAME, 'once'); 51 | SignalWire\Handler::trigger(self::EVENT_NAME, 'once'); 52 | 53 | $this->assertFalse(SignalWire\Handler::isQueued(self::EVENT_NAME)); 54 | } 55 | 56 | public function testRegisterOnceWithUniqueId(): void { 57 | SignalWire\Handler::registerOnce(self::EVENT_NAME, [$this->mock, 'noop'], self::UNIQUE_ID); 58 | 59 | $this->assertTrue(SignalWire\Handler::isQueued(self::EVENT_NAME, self::UNIQUE_ID)); 60 | $this->assertFalse(SignalWire\Handler::isQueued(self::EVENT_NAME)); 61 | $this->assertEquals(SignalWire\Handler::queueCount(self::EVENT_NAME, self::UNIQUE_ID), 1); 62 | 63 | $this->mock->expects($this->exactly(1))->method('noop')->with('once'); 64 | 65 | SignalWire\Handler::trigger(self::EVENT_NAME, 'once', self::UNIQUE_ID); 66 | SignalWire\Handler::trigger(self::EVENT_NAME, 'once', self::UNIQUE_ID); 67 | 68 | $this->assertFalse(SignalWire\Handler::isQueued(self::EVENT_NAME, self::UNIQUE_ID)); 69 | } 70 | 71 | // deRegister() 72 | public function testDeRegisterWithoutUniqueId(): void { 73 | SignalWire\Handler::register(self::EVENT_NAME, [$this->mock, 'noop']); 74 | 75 | $this->assertTrue(SignalWire\Handler::isQueued(self::EVENT_NAME)); 76 | SignalWire\Handler::deRegister(self::EVENT_NAME, [$this->mock, 'noop']); 77 | $this->assertFalse(SignalWire\Handler::isQueued(self::EVENT_NAME)); 78 | } 79 | 80 | public function testDeRegisterWithUniqueId(): void { 81 | SignalWire\Handler::register(self::EVENT_NAME, [$this->mock, 'noop'], self::UNIQUE_ID); 82 | 83 | $this->assertTrue(SignalWire\Handler::isQueued(self::EVENT_NAME, self::UNIQUE_ID)); 84 | SignalWire\Handler::deRegister(self::EVENT_NAME, [$this->mock, 'noop'], self::UNIQUE_ID); 85 | $this->assertFalse(SignalWire\Handler::isQueued(self::EVENT_NAME, self::UNIQUE_ID)); 86 | } 87 | 88 | // deRegisterAll() 89 | public function testDeRegisterAllWithoutUniqueId(): void { 90 | SignalWire\Handler::register(self::EVENT_NAME, [$this->mock, 'noop']); 91 | SignalWire\Handler::register(self::EVENT_NAME, [$this->mock, 'noop'], self::UNIQUE_ID); 92 | 93 | SignalWire\Handler::deRegisterAll(self::EVENT_NAME); 94 | $this->assertFalse(SignalWire\Handler::isQueued(self::EVENT_NAME)); 95 | $this->assertFalse(SignalWire\Handler::isQueued(self::EVENT_NAME, self::UNIQUE_ID)); 96 | } 97 | 98 | // trigger() 99 | public function testTriggerWithoutUniqueId(): void { 100 | SignalWire\Handler::register(self::EVENT_NAME, [$this->mock, 'noop']); 101 | 102 | $this->mock->expects($this->exactly(2))->method('noop')->with('hello'); 103 | 104 | SignalWire\Handler::trigger(self::EVENT_NAME, 'hello'); 105 | SignalWire\Handler::trigger(self::EVENT_NAME, 'hello'); 106 | } 107 | 108 | public function testTriggerWithUniqueId(): void { 109 | SignalWire\Handler::register(self::EVENT_NAME, [$this->mock, 'noop'], self::UNIQUE_ID); 110 | 111 | $this->mock->expects($this->exactly(2))->method('noop')->with('unique'); 112 | 113 | SignalWire\Handler::trigger(self::EVENT_NAME, 'unique', self::UNIQUE_ID); 114 | SignalWire\Handler::trigger(self::EVENT_NAME, 'unique', self::UNIQUE_ID); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/relay/RelayClientTest.php: -------------------------------------------------------------------------------- 1 | _mockResponse(json_decode('{"protocol":"proto","command":"add","subscribe_channels":["c1","c2"]}')); 15 | 16 | $this->client->subscribe('proto', array('c1', 'c2')); 17 | 18 | $this->assertArrayHasKey('protoc1', $this->client->subscriptions); 19 | $this->assertArrayHasKey('protoc2', $this->client->subscriptions); 20 | } 21 | 22 | public function testSubscribeWithFailedResponse(): void { 23 | $this->_mockResponse(json_decode('{"protocol":"proto","command":"add","failed_channels":["c1","c2"]}')); 24 | $this->client->subscribe('proto', array('c1', 'c2')); 25 | 26 | $this->assertCount(0, $this->client->subscriptions); 27 | } 28 | 29 | public function testSubscribeWithBothResponse(): void { 30 | $this->_mockResponse(json_decode('{"protocol":"proto","command":"add","subscribe_channels":["c1"],"failed_channels":["c2"]}')); 31 | $this->client->subscribe('proto', array('c1', 'c2')); 32 | 33 | $this->assertArrayHasKey('protoc1', $this->client->subscriptions); 34 | $this->assertArrayNotHasKey('protoc2', $this->client->subscriptions); 35 | } 36 | 37 | public function testSubscribeWithHandler(): void { 38 | $this->_mockResponse(json_decode('{"protocol":"proto","command":"add","subscribe_channels":["notifications"]}')); 39 | $fn = function($data) {}; 40 | $this->client->subscribe('proto', array('notifications'), $fn); 41 | 42 | $this->assertArrayHasKey('protonotifications', $this->client->subscriptions); 43 | $this->assertTrue(SignalWire\Handler::isQueued('proto', 'notifications')); 44 | $this->assertEquals(SignalWire\Handler::queueCount('proto', 'notifications'), 1); 45 | } 46 | 47 | public function testCallingProperty(): void { 48 | $this->assertInstanceOf('SignalWire\Relay\Calling\Calling', $this->client->calling); 49 | } 50 | 51 | public function testTaskingProperty(): void { 52 | $this->assertInstanceOf('SignalWire\Relay\Tasking\Tasking', $this->client->tasking); 53 | } 54 | 55 | public function testMessagingProperty(): void { 56 | $this->assertInstanceOf('SignalWire\Relay\Messaging\Messaging', $this->client->messaging); 57 | } 58 | 59 | public function testOnSocketOpenWithSuccess(): void { 60 | $mockOnReady = $this->getMockBuilder(\stdClass::class) 61 | ->setMethods(['__invoke']) 62 | ->getMock(); 63 | $mockOnReady->expects($this->once())->method('__invoke'); 64 | $this->client->on('signalwire.ready', $mockOnReady); 65 | 66 | $requests = [ 67 | new Connect('project', 'token'), 68 | new Execute(['protocol' => 'signalwire', 'method' => 'setup', 'params' => new \stdClass]), 69 | new Subscription([ 70 | 'command' => 'add', 71 | 'protocol' => 'signalwire_service_random_uuid', 72 | 'channels' => ['notifications'] 73 | ]) 74 | ]; 75 | $responses = [ 76 | json_decode('{"session_restored":false,"sessionid":"bfb34f66-3caf-45a9-8a4b-a74bbd3d0b28","nodeid":"uuid","master_nodeid":"uuid","authorization":{"project":"uuid","expires_at":null,"scopes":["calling","messaging","tasking"],"signature":"uuid-signature"},"routes":[],"protocols":[],"subscriptions":[],"authorities":[],"authorizations":[],"accesses":[],"protocols_uncertified":["signalwire"]}'), 77 | json_decode('{"result":{"protocol":"signalwire_service_random_uuid"}}'), 78 | json_decode('{"command":"add","failed_channels":[],"protocol":"signalwire_service_random_uuid","subscribe_channels":["notifications"]}') 79 | ]; 80 | $this->_mockResponse($responses, $requests); 81 | 82 | Handler::trigger(Events::SocketOpen, null, $this->client->uuid); 83 | $this->assertEquals($this->client->sessionid, 'bfb34f66-3caf-45a9-8a4b-a74bbd3d0b28'); 84 | $this->assertEquals($this->client->nodeid, 'uuid'); 85 | $this->assertEquals($this->client->signature, 'uuid-signature'); 86 | $this->assertEquals($this->client->relayProtocol, 'signalwire_service_random_uuid'); 87 | } 88 | 89 | public function testOnSocketOpenOnTimeout(): void { 90 | $mockOnReady = $this->getMockBuilder(\stdClass::class) 91 | ->setMethods(['__invoke']) 92 | ->getMock(); 93 | $mockOnReady->expects($this->never())->method('__invoke'); 94 | $this->client->on('signalwire.ready', $mockOnReady); 95 | 96 | 97 | $stub = $this->createMock(SignalWire\Relay\Connection::class, ['send']); 98 | $stub->expects($this->once()) 99 | ->method('send') 100 | ->with(new Connect('project', 'token')) 101 | ->will($this->returnValue(\React\Promise\reject(json_decode('{"code":-32000,"message":"Timeout"}')))); 102 | $this->client->connection = $stub; 103 | Handler::trigger(Events::SocketOpen, null, $this->client->uuid); 104 | $this->assertNull($this->client->connection); 105 | $this->assertTrue($this->client->idle); 106 | $this->assertTrue($this->client->autoReconnect); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Relay/Consumer.php: -------------------------------------------------------------------------------- 1 | setup(); 74 | $this->_checkRequirements(); 75 | 76 | if (!($this->loop instanceof LoopInterface)) { 77 | $this->loop = ReactFactory::create(); 78 | } 79 | $this->_kernel = ReactKernel::create($this->loop); 80 | $this->_kernel->execute([$this, '_init']); 81 | $this->loop->run(); 82 | ReactKernel::start(function() { 83 | yield $this->teardown(); 84 | }); 85 | } 86 | 87 | public function _init(): Coroutine { 88 | $this->client = new Client([ 89 | 'host' => $this->host, 90 | 'project' => $this->project, 91 | 'token' => $this->token, 92 | 'eventLoop' => yield \Recoil\Recoil::eventLoop() 93 | ]); 94 | 95 | $this->client->on('signalwire.ready', yield Recoil::callback(function($client) { 96 | try { 97 | $success = yield Setup::receive($client, $this->contexts); 98 | if ($success) { 99 | yield $this->_registerCallingContexts(); 100 | yield $this->_registerTaskingContexts(); 101 | yield $this->_registerMessagingContexts(); 102 | yield $this->ready(); 103 | } 104 | } catch (\Throwable $th) { 105 | Log::error($th->getMessage()); 106 | throw $th; 107 | } 108 | })); 109 | 110 | yield $this->client->connect(); 111 | } 112 | 113 | private function _registerCallingContexts(): Coroutine { 114 | $callback = yield Recoil::callback(function ($call) { 115 | try { 116 | yield $this->onIncomingCall($call); 117 | } catch (\Throwable $error) { 118 | echo PHP_EOL; 119 | echo PHP_EOL . $error->getMessage(); 120 | echo PHP_EOL . $error->getTraceAsString() . PHP_EOL; 121 | } 122 | }); 123 | 124 | yield $this->client->calling->onReceive($this->contexts, $callback); 125 | } 126 | 127 | private function _registerTaskingContexts(): Coroutine { 128 | $callback = yield Recoil::callback(function ($message) { 129 | try { 130 | yield $this->onTask($message); 131 | } catch (\Throwable $error) { 132 | echo PHP_EOL; 133 | echo PHP_EOL . $error->getMessage(); 134 | echo PHP_EOL . $error->getTraceAsString() . PHP_EOL; 135 | } 136 | }); 137 | 138 | yield $this->client->tasking->onReceive($this->contexts, $callback); 139 | } 140 | 141 | private function _registerMessagingContexts(): Coroutine { 142 | $receiveCallback = yield Recoil::callback(function ($message) { 143 | try { 144 | yield $this->onIncomingMessage($message); 145 | } catch (\Throwable $error) { 146 | echo PHP_EOL; 147 | echo PHP_EOL . $error->getMessage(); 148 | echo PHP_EOL . $error->getTraceAsString() . PHP_EOL; 149 | } 150 | }); 151 | 152 | $changeStateCallback = yield Recoil::callback(function ($message) { 153 | try { 154 | yield $this->onMessageStateChange($message); 155 | } catch (\Throwable $error) { 156 | echo PHP_EOL; 157 | echo PHP_EOL . $error->getMessage(); 158 | echo PHP_EOL . $error->getTraceAsString() . PHP_EOL; 159 | } 160 | }); 161 | 162 | yield $this->client->messaging->onReceive($this->contexts, $receiveCallback); 163 | yield $this->client->messaging->onStateChange($this->contexts, $changeStateCallback); 164 | } 165 | 166 | private function _checkRequirements() { 167 | if (!isset($this->project)) { 168 | throw new \InvalidArgumentException(get_class($this) . ' must have a $project.'); 169 | } 170 | if (!isset($this->token)) { 171 | throw new \InvalidArgumentException(get_class($this) . ' must have a $token.'); 172 | } 173 | if (!isset($this->contexts) || !count($this->contexts)) { 174 | throw new \InvalidArgumentException(get_class($this) . ' must have one or more $contexts.'); 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Twiml.php: -------------------------------------------------------------------------------- 1 | element = $arg; 31 | break; 32 | case $arg === null: 33 | $this->element = new \SimpleXMLElement(''); 34 | break; 35 | case \is_array($arg): 36 | $this->element = new \SimpleXMLElement(''); 37 | foreach ($arg as $name => $value) { 38 | $this->element->addAttribute($name, $value); 39 | } 40 | break; 41 | default: 42 | throw new TwimlException('Invalid argument'); 43 | } 44 | } 45 | 46 | /** 47 | * Converts method calls into Twiml verbs. 48 | * 49 | * A basic example: 50 | * 51 | * .. code-block:: php 52 | * 53 | * php> print $this->say('hello'); 54 | * hello 55 | * 56 | * An example with attributes: 57 | * 58 | * .. code-block:: php 59 | * 60 | * print $this->say('hello', array('voice' => 'woman')); 61 | * hello 62 | * 63 | * You could even just pass in an attributes array, omitting the noun: 64 | * 65 | * .. code-block:: php 66 | * 67 | * print $this->gather(array('timeout' => '20')); 68 | * 69 | * 70 | * @param string $verb The Twiml verb. 71 | * @param mixed[] $args 72 | * @return self 73 | * :param string $verb: The Twiml verb. 74 | * :param array $args: 75 | * - (noun string) 76 | * - (noun string, attributes array) 77 | * - (attributes array) 78 | * 79 | * :return: A SimpleXmlElement 80 | * :rtype: SimpleXmlElement 81 | */ 82 | public function __call($verb, array $args) { 83 | list($noun, $attrs) = $args + array('', array()); 84 | if (\is_array($noun)) { 85 | list($attrs, $noun) = array($noun, ''); 86 | } 87 | /* addChild does not escape XML, while addAttribute does. This means if 88 | * you pass unescaped ampersands ("&") to addChild, you will generate 89 | * an error. 90 | * 91 | * Some inexperienced developers will pass in unescaped ampersands, and 92 | * we want to make their code work, by escaping the ampersands for them 93 | * before passing the string to addChild. (with htmlentities) 94 | * 95 | * However other people will know what to do, and their code 96 | * already escapes ampersands before passing them to addChild. We don't 97 | * want to break their existing code by turning their &'s into 98 | * & 99 | * 100 | * We also want to use numeric entities, not named entities so that we 101 | * are fully compatible with XML 102 | * 103 | * The following lines accomplish the desired behavior. 104 | */ 105 | $decoded = \html_entity_decode($noun, ENT_COMPAT, 'UTF-8'); 106 | $normalized = \htmlspecialchars($decoded, ENT_COMPAT, 'UTF-8', false); 107 | $hasNoun = \is_scalar($noun) && \strlen($noun); 108 | $child = $hasNoun 109 | ? $this->element->addChild(\ucfirst($verb), $normalized) 110 | : $this->element->addChild(\ucfirst($verb)); 111 | 112 | if (\is_array($attrs)) { 113 | foreach ($attrs as $name => $value) { 114 | /* Note that addAttribute escapes raw ampersands by default, so we 115 | * haven't touched its implementation. So this is the matrix for 116 | * addAttribute: 117 | * 118 | * & turns into & 119 | * & turns into & 120 | */ 121 | if (\is_bool($value)) { 122 | $value = ($value === true) ? 'true' : 'false'; 123 | } 124 | $child->addAttribute($name, $value); 125 | } 126 | } 127 | return new static($child); 128 | } 129 | 130 | /** 131 | * Returns the object as XML. 132 | * 133 | * :return: The response as an XML string 134 | * :rtype: string 135 | */ 136 | public function __toString() { 137 | $xml = $this->element->asXML(); 138 | return (string)\str_replace( 139 | '', 140 | '', $xml); 141 | } 142 | } -------------------------------------------------------------------------------- /src/Relay/Calling/Calling.php: -------------------------------------------------------------------------------- 1 | params->event_type = $notification->event_type; 14 | switch ($notification->event_type) 15 | { 16 | case Notification::State: 17 | $this->_onState($notification->params); 18 | break; 19 | case Notification::Connect: 20 | $this->_onConnect($notification->params); 21 | break; 22 | case Notification::Record: 23 | $this->_onRecord($notification->params); 24 | break; 25 | case Notification::Play: 26 | $this->_onPlay($notification->params); 27 | break; 28 | case Notification::Collect: 29 | $this->_onCollect($notification->params); 30 | break; 31 | case Notification::Fax: 32 | $this->_onFax($notification->params); 33 | break; 34 | case Notification::Detect: 35 | $this->_onDetect($notification->params); 36 | break; 37 | case Notification::Tap: 38 | $this->_onTap($notification->params); 39 | break; 40 | case Notification::SendDigits: 41 | $this->_onSendDigits($notification->params); 42 | break; 43 | case Notification::Receive: 44 | $call = new Call($this, $notification->params); 45 | Handler::trigger($this->client->relayProtocol, $call, $this->_ctxReceiveUniqueId($call->context)); 46 | break; 47 | } 48 | } 49 | 50 | public function newCall(Array $params) { 51 | return new Call($this, $this->_buildDevice($params)); 52 | } 53 | 54 | public function dial(Array $params) { 55 | $call = new Call($this, $this->_buildDevice($params)); 56 | return $call->dial(); 57 | } 58 | 59 | public function addCall(Call $call) { 60 | array_push($this->_calls, $call); 61 | } 62 | 63 | public function removeCall(Call $call) { 64 | foreach ($this->_calls as $index => $c) { 65 | if ($c->id === $call->id) { 66 | array_splice($this->_calls, $index, 1); 67 | return; 68 | } 69 | } 70 | } 71 | 72 | public function getCallById(String $callId) { 73 | foreach ($this->_calls as $call) { 74 | if ($call->id === $callId) { 75 | return $call; 76 | } 77 | } 78 | return false; 79 | } 80 | 81 | public function getCallByTag(String $tag) { 82 | foreach ($this->_calls as $call) { 83 | if ($call->tag === $tag) { 84 | return $call; 85 | } 86 | } 87 | return false; 88 | } 89 | 90 | private function _onState($params) { 91 | $call = $this->getCallById($params->call_id); 92 | if (!$call && isset($params->tag)) { 93 | $call = $this->getCallByTag($params->tag); 94 | } 95 | if ($call) { 96 | if (!$call->id && isset($params->call_id) && isset($params->node_id)) { 97 | $call->id = $params->call_id; 98 | $call->nodeId = $params->node_id; 99 | } 100 | $call->_stateChange($params); 101 | } elseif (isset($params->call_id) && isset($params->peer)) { 102 | $call = new Call($this, $params); 103 | } else { 104 | Log::error('Unknown call', (array)$params); 105 | } 106 | } 107 | 108 | private function _onRecord($params) { 109 | $call = $this->getCallById($params->call_id); 110 | if ($call) { 111 | $call->_recordChange($params); 112 | } 113 | } 114 | 115 | private function _onPlay($params) { 116 | $call = $this->getCallById($params->call_id); 117 | if ($call) { 118 | $call->_playChange($params); 119 | } 120 | } 121 | 122 | private function _onCollect($params) { 123 | $call = $this->getCallById($params->call_id); 124 | if ($call) { 125 | $call->_collectChange($params); 126 | } 127 | } 128 | 129 | private function _onFax($params) { 130 | $call = $this->getCallById($params->call_id); 131 | if ($call) { 132 | $call->_faxChange($params); 133 | } 134 | } 135 | 136 | private function _onDetect($params) { 137 | $call = $this->getCallById($params->call_id); 138 | if ($call) { 139 | $call->_detectChange($params); 140 | } 141 | } 142 | 143 | private function _onTap($params) { 144 | $call = $this->getCallById($params->call_id); 145 | if ($call) { 146 | $call->_tapChange($params); 147 | } 148 | } 149 | 150 | private function _onConnect($params) { 151 | $call = $this->getCallById($params->call_id); 152 | if ($call) { 153 | $call->_connectChange($params); 154 | } 155 | } 156 | 157 | private function _onSendDigits($params) { 158 | $call = $this->getCallById($params->call_id); 159 | if ($call) { 160 | $call->_sendDigitsChange($params); 161 | } 162 | } 163 | 164 | private function _buildDevice(Array $params) { 165 | return (object)[ 166 | 'device' => (object)[ 167 | 'type' => $params['type'], 168 | 'params' => (object)[ 169 | 'from_number' => isset($params['from']) ? $params['from'] : null, 170 | 'to_number' => isset($params['to']) ? $params['to'] : null, 171 | 'timeout' => isset($params['timeout']) ? $params['timeout'] : 30 172 | ] 173 | ] 174 | ]; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /tests/fixtures/list_faxes: -------------------------------------------------------------------------------- 1 | 2 | - 3 | request: 4 | method: GET 5 | url: 'https://example.signalwire.com/2010-04-01/Accounts/my-signalwire-sid/Faxes' 6 | headers: 7 | Accept-Charset: utf-8 8 | Accept: application/json 9 | response: 10 | status: 11 | http_version: '1.1' 12 | code: '200' 13 | message: OK 14 | headers: 15 | Server: nginx 16 | body: '{"uri":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes?Page=0\u0026PageSize=50","first_page_uri":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes?Page=0\u0026PageSize=50","next_page_uri":null,"previous_page_uri":null,"page":0,"page_size":50,"faxes":[{"account_sid":"my-signalwire-sid","api_version":"v1","date_created":"2019-01-07T16:51:22Z","date_updated":"2019-01-07T16:52:00Z","direction":"outbound","from":"+15556677999","media_url":"https://s3.us-east-2.amazonaws.com/signalwire-assets/faxes/20190107165123-dd3e1ac4-50c9-4241-933a-5d4e9a2baf31.tiff","media_sid":"ceab8d12-359b-4c0c-86fc-75e5d500a1c0","num_pages":1,"price":0.0105,"price_unit":"USD","quality":"fine","sid":"dd3e1ac4-50c9-4241-933a-5d4e9a2baf31","status":"delivered","to":"+15556677888","duration":34,"links":{"media":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes/dd3e1ac4-50c9-4241-933a-5d4e9a2baf31/Media"},"url":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes/dd3e1ac4-50c9-4241-933a-5d4e9a2baf31"},{"account_sid":"my-signalwire-sid","api_version":"v1","date_created":"2019-01-07T16:46:42Z","date_updated":"2019-01-07T16:47:11Z","direction":"outbound","from":"+15556677999","media_url":"https://s3.us-east-2.amazonaws.com/signalwire-assets/faxes/20190107164643-8dc7f5aa-c9f9-44cf-817b-fcd0ccc801db.tiff","media_sid":"7ad80d05-8dfe-44bf-9a51-8ecae439e05e","num_pages":1,"price":0.0105,"price_unit":"USD","quality":"fine","sid":"8dc7f5aa-c9f9-44cf-817b-fcd0ccc801db","status":"delivered","to":"+15556677888","duration":26,"links":{"media":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes/8dc7f5aa-c9f9-44cf-817b-fcd0ccc801db/Media"},"url":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes/8dc7f5aa-c9f9-44cf-817b-fcd0ccc801db"},{"account_sid":"my-signalwire-sid","api_version":"v1","date_created":"2019-01-07T16:44:43Z","date_updated":"2019-01-07T16:45:23Z","direction":"outbound","from":"+15556677999","media_url":"https://s3.us-east-2.amazonaws.com/signalwire-assets/faxes/20190107164443-ab77c13e-13a6-475e-bf8a-e21d57060537.tiff","media_sid":"5d3a0dd4-7061-461d-a274-f68d7e6e940c","num_pages":1,"price":0.0105,"price_unit":"USD","quality":"fine","sid":"ab77c13e-13a6-475e-bf8a-e21d57060537","status":"delivered","to":"+15556677888","duration":34,"links":{"media":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes/ab77c13e-13a6-475e-bf8a-e21d57060537/Media"},"url":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes/ab77c13e-13a6-475e-bf8a-e21d57060537"},{"account_sid":"my-signalwire-sid","api_version":"v1","date_created":"2019-01-05T10:56:25Z","date_updated":"2019-01-05T10:57:11Z","direction":"outbound","from":"+15556677999","media_url":"https://s3.us-east-2.amazonaws.com/signalwire-assets/faxes/20190105105625-2b7a9801-1739-410e-961b-9d589d4a76e5.tiff","media_sid":"464c5e5d-e87b-4673-939f-383b6cc61f51","num_pages":1,"price":0.0105,"price_unit":"USD","quality":"fine","sid":"2b7a9801-1739-410e-961b-9d589d4a76e5","status":"delivered","to":"+15556677888","duration":40,"links":{"media":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes/2b7a9801-1739-410e-961b-9d589d4a76e5/Media"},"url":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes/2b7a9801-1739-410e-961b-9d589d4a76e5"},{"account_sid":"my-signalwire-sid","api_version":"v1","date_created":"2019-01-04T16:28:33Z","date_updated":"2019-01-04T16:29:20Z","direction":"outbound","from":"+15556677999","media_url":"https://s3.us-east-2.amazonaws.com/signalwire-assets/faxes/20190104162834-831455c6-574e-4d8b-b6ee-2418140bf4cd.tiff","media_sid":"aff0684c-3445-49bc-802b-3a0a488139f5","num_pages":1,"price":0.0105,"price_unit":"USD","quality":"fine","sid":"831455c6-574e-4d8b-b6ee-2418140bf4cd","status":"delivered","to":"+15556677888","duration":41,"links":{"media":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd/Media"},"url":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes/831455c6-574e-4d8b-b6ee-2418140bf4cd"},{"account_sid":"my-signalwire-sid","api_version":"v1","date_created":"2019-01-04T16:05:18Z","date_updated":"2019-01-04T16:05:45Z","direction":"outbound","from":"+15556677999","media_url":"https://s3.us-east-2.amazonaws.com/signalwire-assets/faxes/20190104160520-5ed234e3-0e6b-4c49-869a-6c0ef3c30884.tiff","media_sid":"77643eca-a413-48c7-ad34-6e703fc77ca7","num_pages":0,"price":0.0105,"price_unit":"USD","quality":"fine","sid":"5ed234e3-0e6b-4c49-869a-6c0ef3c30884","status":"failed","to":"+15556677888","duration":17,"links":{"media":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes/5ed234e3-0e6b-4c49-869a-6c0ef3c30884/Media"},"url":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes/5ed234e3-0e6b-4c49-869a-6c0ef3c30884"},{"account_sid":"my-signalwire-sid","api_version":"v1","date_created":"2019-01-04T16:03:11Z","date_updated":"2019-01-04T16:03:11Z","direction":"outbound","from":"+15556677999","media_url":null,"media_sid":"a9d56213-1ec6-4618-adac-969d4d11c09a","num_pages":null,"price":null,"price_unit":"USD","quality":"fine","sid":"ce501cac-3144-4540-a6c7-a1c7963501f7","status":"failed","to":"+15556677888","duration":0,"links":{"media":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes/ce501cac-3144-4540-a6c7-a1c7963501f7/Media"},"url":"/api/laml/2010-04-01/Accounts/my-signalwire-sid/Faxes/ce501cac-3144-4540-a6c7-a1c7963501f7"}]}' 17 | - 18 | 19 | -------------------------------------------------------------------------------- /tests/relay/SetupTest.php: -------------------------------------------------------------------------------- 1 | _mockResponse([$responseProto, $responseSubscr]); 14 | 15 | Setup::protocol($this->client)->then(function (String $protocol) { 16 | $this->assertEquals('signalwire_calling_proto', $protocol); 17 | $this->assertArrayHasKey('signalwire_calling_protonotifications', $this->client->subscriptions); 18 | }); 19 | } 20 | 21 | public function testProtocolSetupAfterReconnectWithSameSignature(): void { 22 | $responseProto = json_decode('{"requester_nodeid":"ad490dc4-550a-4742-929d-b86fdf8958ef","responder_nodeid":"b0007713-071d-45f9-88aa-302d14e1251c","result":{"protocol":"signalwire_calling_proto"}}'); 23 | $responseSubscr = json_decode('{"protocol":"signalwire_calling_proto","command":"add","subscribe_channels":["notifications"]}'); 24 | $requestProto = new Execute([ 25 | 'protocol' => 'signalwire', 'method' => 'setup', 'params' => (object)[ 'protocol' => 'signalwire_signature_uuid_uuid' ] 26 | ]); 27 | $this->_mockResponse([$responseProto, $responseSubscr], [$requestProto]); 28 | $this->client->signature = 'signature'; 29 | $this->client->relayProtocol = 'signalwire_signature_uuid_uuid'; 30 | Setup::protocol($this->client)->then(function (String $protocol) { 31 | $this->assertEquals('signalwire_calling_proto', $protocol); 32 | $this->assertArrayHasKey('signalwire_calling_protonotifications', $this->client->subscriptions); 33 | }); 34 | } 35 | 36 | public function testProtocolSetupAfterReconnectWithDifferentSignature(): void { 37 | $responseProto = json_decode('{"requester_nodeid":"ad490dc4-550a-4742-929d-b86fdf8958ef","responder_nodeid":"b0007713-071d-45f9-88aa-302d14e1251c","result":{"protocol":"signalwire_calling_proto"}}'); 38 | $responseSubscr = json_decode('{"protocol":"signalwire_calling_proto","command":"add","subscribe_channels":["notifications"]}'); 39 | $requestProto = new Execute([ 40 | 'protocol' => 'signalwire', 'method' => 'setup', 'params' => (object)[] 41 | ]); 42 | $this->_mockResponse([$responseProto, $responseSubscr], [$requestProto]); 43 | $this->client->signature = 'another-signature'; 44 | $this->client->relayProtocol = 'signalwire_signature_uuid_uuid'; 45 | Setup::protocol($this->client)->then(function (String $protocol) { 46 | $this->assertEquals('signalwire_calling_proto', $protocol); 47 | $this->assertArrayHasKey('signalwire_calling_protonotifications', $this->client->subscriptions); 48 | }); 49 | } 50 | 51 | public function testReceiveWithInvalidData(): void { 52 | $this->_mockSendNotToBeCalled(); 53 | 54 | Setup::receive($this->client, '')->done(function ($success) { 55 | $this->assertFalse($success); 56 | }); 57 | 58 | Setup::receive($this->client, [])->done(function ($success) { 59 | $this->assertFalse($success); 60 | }); 61 | 62 | Setup::receive($this->client, [''])->done(function ($success) { 63 | $this->assertFalse($success); 64 | }); 65 | } 66 | 67 | public function testReceiveWithString(): void { 68 | $response = json_decode('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"code":"200","message":"Receiving all inbound related to the requested relay contexts"}}'); 69 | $this->_mockResponse([$response]); 70 | 71 | Setup::receive($this->client, 'test')->done(function ($success) { 72 | $this->assertTrue($success); 73 | $this->assertEquals(['test'], $this->client->contexts); 74 | }); 75 | } 76 | 77 | public function testReceiveWithArray(): void { 78 | $response = json_decode('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"code":"200","message":"Receiving all inbound related to the requested relay contexts"}}'); 79 | $this->_mockResponse([$response]); 80 | 81 | Setup::receive($this->client, ['test1', 'test2'])->done(function ($success) { 82 | $this->assertTrue($success); 83 | $this->assertEquals(['test1', 'test2'], $this->client->contexts); 84 | }); 85 | } 86 | 87 | public function testReceiveContextAlreadyRegistered(): void { 88 | $this->_mockSendNotToBeCalled(); 89 | 90 | $this->client->contexts = ['exists']; 91 | Setup::receive($this->client, 'exists')->done(function ($success) { 92 | $this->assertTrue($success); 93 | $this->assertEquals(['exists'], $this->client->contexts); 94 | }); 95 | } 96 | 97 | public function testReceiveMixedContextsAlreadyRegisteredAndNot(): void { 98 | $response = json_decode('{"requester_nodeid":"uuid","responder_nodeid":"uuid","result":{"code":"200","message":"Receiving all inbound related to the requested relay contexts"}}'); 99 | $this->_mockResponse([$response]); 100 | 101 | $this->client->contexts = ['exists']; 102 | 103 | Setup::receive($this->client, ['exists', 'home', 'office'])->done(function ($success) { 104 | $this->assertTrue($success); 105 | $this->assertEquals(['exists', 'home', 'office'], $this->client->contexts); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/relay/Calling/CallRecordTest.php: -------------------------------------------------------------------------------- 1 | _setCallReady(); 22 | } 23 | 24 | public function testRecordSuccess(): void { 25 | $msg = $this->_recordMsg(); 26 | $this->_mockSuccessResponse($msg, self::$success); 27 | 28 | $record = ['audio' => ['beep' => true, 'stereo' => false]]; 29 | $this->call->record($record)->done(function($result) { 30 | $this->assertInstanceOf('SignalWire\Relay\Calling\Results\RecordResult', $result); 31 | $this->assertTrue($result->isSuccessful()); 32 | $this->assertEquals($result->getUrl(), 'record.mp3'); 33 | $this->assertEquals($result->getSize(), 4096); 34 | $this->assertObjectHasAttribute('url', $result->getEvent()->payload); 35 | }); 36 | 37 | $this->calling->notificationHandler(self::$notificationFinished); 38 | } 39 | 40 | public function testRecordSuccessWithFlattenedParams(): void { 41 | $msg = $this->_recordMsg(); 42 | $this->_mockSuccessResponse($msg, self::$success); 43 | 44 | $record = ['beep' => true, 'stereo' => false]; 45 | $this->call->record($record)->done(function($result) { 46 | $this->assertInstanceOf('SignalWire\Relay\Calling\Results\RecordResult', $result); 47 | $this->assertTrue($result->isSuccessful()); 48 | $this->assertEquals($result->getUrl(), 'record.mp3'); 49 | $this->assertEquals($result->getSize(), 4096); 50 | $this->assertObjectHasAttribute('url', $result->getEvent()->payload); 51 | }); 52 | 53 | $this->calling->notificationHandler(self::$notificationFinished); 54 | } 55 | 56 | public function testRecordFail(): void { 57 | $msg = $this->_recordMsg(); 58 | $this->_mockFailResponse($msg, self::$fail); 59 | 60 | $record = ['audio' => ['beep' => true, 'stereo' => false]]; 61 | $this->call->record($record)->done(function($result) { 62 | $this->assertInstanceOf('SignalWire\Relay\Calling\Results\RecordResult', $result); 63 | $this->assertFalse($result->isSuccessful()); 64 | }); 65 | 66 | $this->calling->notificationHandler(self::$notificationFinished); 67 | } 68 | 69 | public function testRecordAsyncSuccess(): void { 70 | $msg = $this->_recordMsg(); 71 | $this->_mockSuccessResponse($msg, self::$success); 72 | 73 | $record = ['audio' => ['beep' => true, 'stereo' => false]]; 74 | $this->call->recordAsync($record)->done(function($action) { 75 | $this->assertInstanceOf('SignalWire\Relay\Calling\Actions\RecordAction', $action); 76 | $this->assertEquals($action->getUrl(), 'record.mp3'); 77 | $this->assertInstanceOf('SignalWire\Relay\Calling\Results\RecordResult', $action->getResult()); 78 | $this->assertFalse($action->isCompleted()); 79 | 80 | $this->calling->notificationHandler(self::$notificationFinished); 81 | 82 | $this->assertTrue($action->isCompleted()); 83 | }); 84 | } 85 | 86 | public function testRecordAsyncSuccessWithFlattenedParams(): void { 87 | $msg = $this->_recordMsg(); 88 | $this->_mockSuccessResponse($msg, self::$success); 89 | 90 | $record = ['beep' => true, 'stereo' => false]; 91 | $this->call->recordAsync($record)->done(function($action) { 92 | $this->assertInstanceOf('SignalWire\Relay\Calling\Actions\RecordAction', $action); 93 | $this->assertInstanceOf('SignalWire\Relay\Calling\Results\RecordResult', $action->getResult()); 94 | $this->assertFalse($action->isCompleted()); 95 | 96 | $this->calling->notificationHandler(self::$notificationFinished); 97 | 98 | $this->assertTrue($action->isCompleted()); 99 | }); 100 | } 101 | 102 | public function testRecordAsyncFail(): void { 103 | $msg = $this->_recordMsg(); 104 | $this->_mockFailResponse($msg, self::$fail); 105 | 106 | $record = ['audio' => ['beep' => true, 'stereo' => false]]; 107 | $this->call->recordAsync($record)->done(function($action) { 108 | $this->assertInstanceOf('SignalWire\Relay\Calling\Actions\RecordAction', $action); 109 | $this->assertNull($action->getUrl()); 110 | $this->assertInstanceOf('SignalWire\Relay\Calling\Results\RecordResult', $action->getResult()); 111 | $this->assertTrue($action->isCompleted()); 112 | $this->assertEquals($action->getState(), 'failed'); 113 | }); 114 | } 115 | 116 | private function _recordMsg() { 117 | return $msg = new Execute([ 118 | 'protocol' => 'signalwire_calling_proto', 119 | 'method' => 'calling.record', 120 | 'params' => [ 121 | 'call_id' => 'call-id', 122 | 'node_id' => 'node-id', 123 | 'control_id' => self::UUID, 124 | 'record' => ['audio' => ['beep' => true, 'stereo' => false]] 125 | ] 126 | ]); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/laml/LaMLTest.php: -------------------------------------------------------------------------------- 1 | say("Hey!"); 10 | $response->play("https://ccrma.stanford.edu/~jos/mp3/gtr-nylon22.mp3", array("loop" => 5)); 11 | $this->assertEquals($response->__toString(), "\nHey!https://ccrma.stanford.edu/~jos/mp3/gtr-nylon22.mp3\n"); 12 | } 13 | 14 | public function testFaxResponseLaMLMatch(): void { 15 | $response = new SignalWire\LaML\FaxResponse(); 16 | $response->receive([ 17 | 'attr' => 'value', 18 | 'key' => 'foo' 19 | ]); 20 | $this->assertEquals($response->__toString(), "\n\n"); 21 | } 22 | 23 | public function testVoiceResponseLaMLMatch(): void { 24 | $response = new SignalWire\LaML\VoiceResponse(); 25 | $response->connect([ 26 | 'field' => 'what', 27 | ]); 28 | $this->assertEquals($response->__toString(), "\n\n"); 29 | 30 | $response = new SignalWire\LaML\VoiceResponse(); 31 | $response->dial('+12345', [ 32 | 'field' => 'what', 33 | ]); 34 | $this->assertEquals($response->__toString(), "\n+12345\n"); 35 | 36 | $response = new SignalWire\LaML\VoiceResponse(); 37 | $response->enqueue('Foo', [ 38 | 'field' => 'what', 39 | ]); 40 | $this->assertEquals($response->__toString(), "\nFoo\n"); 41 | 42 | $response = new SignalWire\LaML\VoiceResponse(); 43 | $response->gather([ 44 | 'field' => 'what', 45 | ]); 46 | $this->assertEquals($response->__toString(), "\n\n"); 47 | 48 | $response = new SignalWire\LaML\VoiceResponse(); 49 | $response->hangup(); 50 | $this->assertEquals($response->__toString(), "\n\n"); 51 | 52 | $response = new SignalWire\LaML\VoiceResponse(); 53 | $response->leave(); 54 | $this->assertEquals($response->__toString(), "\n\n"); 55 | 56 | $response = new SignalWire\LaML\VoiceResponse(); 57 | $response->pause([ 58 | 'field' => 'what', 59 | ]); 60 | $this->assertEquals($response->__toString(), "\n\n"); 61 | 62 | $response = new SignalWire\LaML\VoiceResponse(); 63 | $response->play('some-url-here', [ 64 | 'field' => 'what', 65 | ]); 66 | $this->assertEquals($response->__toString(), "\nsome-url-here\n"); 67 | 68 | $response = new SignalWire\LaML\VoiceResponse(); 69 | $response->queue('Name', [ 70 | 'field' => 'what', 71 | ]); 72 | $this->assertEquals($response->__toString(), "\nName\n"); 73 | 74 | $response = new SignalWire\LaML\VoiceResponse(); 75 | $response->record([ 76 | 'field' => 'what', 77 | ]); 78 | $this->assertEquals($response->__toString(), "\n\n"); 79 | 80 | $response = new SignalWire\LaML\VoiceResponse(); 81 | $response->redirect('redirect-to',[ 82 | 'field' => 'what', 83 | ]); 84 | $this->assertEquals($response->__toString(), "\nredirect-to\n"); 85 | 86 | $response = new SignalWire\LaML\VoiceResponse(); 87 | $response->reject([ 88 | 'field' => 'what', 89 | ]); 90 | $this->assertEquals($response->__toString(), "\n\n"); 91 | 92 | $response = new SignalWire\LaML\VoiceResponse(); 93 | $response->say('Hello!',[ 94 | 'field' => 'what', 95 | ]); 96 | $this->assertEquals($response->__toString(), "\nHello!\n"); 97 | 98 | $response = new SignalWire\LaML\VoiceResponse(); 99 | $response->sms('body-here',[ 100 | 'field' => 'what', 101 | ]); 102 | $this->assertEquals($response->__toString(), "\nbody-here\n"); 103 | 104 | $response = new SignalWire\LaML\VoiceResponse(); 105 | $response->pay([ 106 | 'field' => 'what', 107 | ]); 108 | $this->assertEquals($response->__toString(), "\n\n"); 109 | 110 | $response = new SignalWire\LaML\VoiceResponse(); 111 | $response->prompt([ 112 | 'field' => 'what', 113 | ]); 114 | $this->assertEquals($response->__toString(), "\n\n"); 115 | 116 | $response = new SignalWire\LaML\VoiceResponse(); 117 | $response->start([ 118 | 'field' => 'what', 119 | ]); 120 | $this->assertEquals($response->__toString(), "\n\n"); 121 | 122 | $response = new SignalWire\LaML\VoiceResponse(); 123 | $response->stop(); 124 | $this->assertEquals($response->__toString(), "\n\n"); 125 | 126 | 127 | $response = new SignalWire\LaML\VoiceResponse(); 128 | $response->refer([ 129 | 'field' => 'what', 130 | ]); 131 | $this->assertEquals($response->__toString(), "\n\n"); 132 | 133 | $response = new SignalWire\LaML\VoiceResponse(); 134 | $conn = $response->connect(); 135 | $ai = $conn->ai(); 136 | $ai->setEngine('gcloud'); 137 | $p1 = $ai->prompt('prompt1'); 138 | $p1->setTemperature(0.2); 139 | $ai->postPrompt('prompt2'); 140 | $swaig = $ai->swaig(); 141 | $swaig->defaults([ 'webHookURL' => "https://user:pass@server.com/commands.cgi"]); 142 | $fn = $swaig->function(); 143 | $fn->setName('fn1'); 144 | $fn->setArgument('no argument'); 145 | $fn->setPurpose('to do something'); 146 | $fn = $swaig->function(); 147 | $fn->setName('fn2'); 148 | $fn->setArgument('no argument'); 149 | $fn->setPurpose('to do something'); 150 | $fn->addMetaData("AAA", "111"); 151 | $fn->addMetaData("BBB", "222"); 152 | $this->assertEquals($response->__toString(), "\nprompt1prompt2111222\n"); 153 | } 154 | 155 | public function testMessageResponseLaMLMatch(): void { 156 | $response = new SignalWire\LaML\MessageResponse(); 157 | $response->message("Hello World", ['attr' => 'value']); 158 | $response->redirect("foo", ['method' => 'GET']); 159 | $this->assertEquals($response->__toString(), "\nHello Worldfoo\n"); 160 | } 161 | 162 | public function testMessagingResponseLaMLMatch(): void { 163 | $response = new SignalWire\LaML\MessagingResponse(); 164 | $response->message("Hello World", ['attr' => 'value']); 165 | $response->redirect("foo", ['method' => 'GET']); 166 | $this->assertEquals($response->__toString(), "\nHello Worldfoo\n"); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | $d["type"], 38 | "params" => [ 39 | "from_number" => isset($d["from"]) ? $d["from"] : $defaultFrom, 40 | "to_number" => isset($d["to"]) ? $d["to"] : "", 41 | "timeout" => isset($d["timeout"]) ? $d["timeout"] : $defaultTimeout 42 | ] 43 | ]; 44 | } 45 | 46 | $nested || isset($tmp[0]) ? array_push($final, $tmp) : array_push($final, [$tmp]); 47 | } 48 | 49 | return $final; 50 | } 51 | 52 | function checkWebSocketHost(String $host): String { 53 | $protocol = preg_match("/^(ws|wss):\/\//", $host) ? '' : 'wss://'; 54 | return $protocol . $host; 55 | } 56 | 57 | function prepareRecordParams(Array $params): Array { 58 | $type = RecordType::Audio; // Default to audio 59 | $subParams = isset($params['audio']) ? $params['audio'] : $params; 60 | unset($params['type'], $params['audio']); 61 | $subParams = $subParams + $params; 62 | $record = [ $type => $subParams ]; 63 | return $record; 64 | } 65 | 66 | function destructMedia(Array $media): Array { 67 | $type = isset($media['type']) ? $media['type'] : ''; 68 | $params = isset($media['params']) ? $media['params'] : []; 69 | unset($media['type'], $media['params']); 70 | $params = $params + $media; 71 | return ['type' => $type, 'params' => $params]; 72 | } 73 | 74 | function preparePlayParams(Array $params): Array { 75 | $volume = 0; 76 | if (count($params) === 1 && isset($params[0]['media'])) { 77 | $mediaList = $params[0]['media']; 78 | $volume = isset($params[0]['volume']) ? $params[0]['volume'] : 0; 79 | } else { 80 | $mediaList = $params; 81 | } 82 | $mediaToPlay = []; 83 | foreach($mediaList as $media) { 84 | if (is_array($media)) { 85 | array_push($mediaToPlay, destructMedia($media)); 86 | } 87 | } 88 | return [$mediaToPlay, $volume]; 89 | } 90 | 91 | function preparePlayAudioParams($params): Array { 92 | if (gettype($params) === 'string') { 93 | return [$params, 0]; 94 | } elseif (gettype($params) === 'array') { 95 | $url = isset($params['url']) ? $params['url'] : ''; 96 | $volume = isset($params['volume']) ? $params['volume'] : ''; 97 | return [$url, $volume]; 98 | } 99 | return ['', 0]; 100 | } 101 | 102 | function preparePlayRingtoneParams($params): Array { 103 | $volume = isset($params['volume']) ? $params['volume'] : 0; 104 | unset($params['volume']); 105 | if (isset($params['duration'])) { 106 | $params['duration'] = (float)$params['duration']; 107 | } 108 | return [$params, $volume]; 109 | } 110 | 111 | function preparePromptParams(Array $params, Array $mediaList = []): Array { 112 | $digits = isset($params[PromptType::Digits]) ? $params[PromptType::Digits] : []; 113 | $speech = isset($params[PromptType::Speech]) ? $params[PromptType::Speech] : []; 114 | $mediaToPlay = isset($params['media']) ? $params['media'] : $mediaList; 115 | unset($params[PromptType::Digits], $params[PromptType::Speech], $params['media']); 116 | if (!count($digits)) { 117 | if (isset($params['digits_max'])) { 118 | $digits['max'] = $params['digits_max']; 119 | } 120 | if (isset($params['digits_terminators'])) { 121 | $digits['terminators'] = $params['digits_terminators']; 122 | } 123 | if (isset($params['digits_timeout'])) { 124 | $digits['digit_timeout'] = $params['digits_timeout']; // warn: 'digits_' vs 'digit_' for consistency 125 | } 126 | } 127 | if (!count($speech)) { 128 | if (isset($params['end_silence_timeout'])) { 129 | $speech['end_silence_timeout'] = $params['end_silence_timeout']; 130 | } 131 | if (isset($params['speech_timeout'])) { 132 | $speech['speech_timeout'] = $params['speech_timeout']; 133 | } 134 | if (isset($params['speech_language'])) { 135 | $speech['language'] = $params['speech_language']; 136 | } 137 | if (isset($params['speech_hints'])) { 138 | $speech['hints'] = $params['speech_hints']; 139 | } 140 | } 141 | $collect = []; 142 | if (isset($params['initial_timeout'])) { 143 | $collect['initial_timeout'] = $params['initial_timeout']; 144 | } 145 | if (isset($params['partial_results'])) { 146 | $collect['partial_results'] = $params['partial_results']; 147 | } 148 | $type = isset($params['type']) ? $params['type'] : ''; 149 | if (count($digits)) { 150 | $collect[PromptType::Digits] = $digits; 151 | } elseif ($type == PromptType::Digits || $type == 'both') { 152 | $collect[PromptType::Digits] = new \stdClass; 153 | } 154 | if (count($speech)) { 155 | $collect[PromptType::Speech] = $speech; 156 | } elseif ($type == PromptType::Speech || $type == 'both') { 157 | $collect[PromptType::Speech] = new \stdClass; 158 | } 159 | $volume = isset($params['volume']) ? $params['volume'] : 0; 160 | list($play) = preparePlayParams($mediaToPlay); 161 | return [$collect, $play, $volume]; 162 | } 163 | 164 | function preparePromptAudioParams(Array $params, String $url = ''): Array { 165 | $url = isset($params['url']) ? $params['url'] : $url; 166 | unset($params['url']); 167 | $params['media'] = [ 168 | ['type' => PlayType::Audio, 'params' => ['url' => $url]] 169 | ]; 170 | return $params; 171 | } 172 | 173 | function preparePromptTTSParams(Array $params, Array $ttsOptions = []): Array { 174 | $keys = ['text', 'language', 'gender']; 175 | foreach ($keys as $key) { 176 | if (isset($params[$key])) { 177 | $ttsOptions[$key] = $params[$key]; 178 | unset($params[$key]); 179 | } 180 | } 181 | $params['media'] = [ 182 | ['type' => PlayType::TTS, 'params' => $ttsOptions] 183 | ]; 184 | return $params; 185 | } 186 | 187 | function preparePromptRingtoneParams(Array $params): Array { 188 | $mediaParams = []; 189 | if (isset($params['name'])) { 190 | $mediaParams['name'] = $params['name']; 191 | unset($params['name']); 192 | } 193 | if (isset($params['duration'])) { 194 | $mediaParams['duration'] = (float)$params['duration']; 195 | unset($params['duration']); 196 | } 197 | $params['media'] = [ 198 | ['type' => PlayType::Ringtone, 'params' => $mediaParams] 199 | ]; 200 | return $params; 201 | } 202 | 203 | function prepareDetectParams(Array $params) { 204 | $timeout = isset($params['timeout']) ? $params['timeout'] : null; 205 | $type = isset($params['type']) ? $params['type'] : null; 206 | $waitForBeep = isset($params['wait_for_beep']) ? $params['wait_for_beep'] : false; 207 | unset($params['type'], $params['timeout'], $params['wait_for_beep']); 208 | $detect = ['type' => $type, 'params' => $params]; 209 | 210 | return [$detect, $timeout, $waitForBeep]; 211 | } 212 | 213 | function prepareDetectFaxParamsAndEvents(array $params) { 214 | $params['type'] = DetectType::Fax; 215 | list($detect, $timeout) = prepareDetectParams($params); 216 | $faxEvents = [DetectState::CED, DetectState::CNG]; 217 | $events = []; 218 | $tone = isset($detect['params']['tone']) ? $detect['params']['tone'] : null; 219 | if ($tone && in_array($tone, $faxEvents)) { 220 | $detect['params'] = ['tone' => $tone]; 221 | array_push($events, $tone); 222 | } else { 223 | $detect['params'] = []; 224 | $events = $faxEvents; // Both CED & CNG 225 | } 226 | 227 | return [$detect, $timeout, $events]; 228 | } 229 | 230 | function prepareTapParams(array $params, array $deviceParams = []) { 231 | $tapParams = []; 232 | if (isset($params['audio_direction'])) { 233 | $tapParams['direction'] = $params['audio_direction']; 234 | } elseif (isset($params['direction'])) { 235 | $tapParams['direction'] = $params['direction']; 236 | } 237 | $tap = ['type' => TapType::Audio, 'params' => $tapParams]; 238 | 239 | $device = ['type' => '', 'params' => []]; 240 | if (isset($deviceParams['type'])) { 241 | $device['type'] = $deviceParams['type']; 242 | unset($deviceParams['type']); 243 | } elseif (isset($params['target_type'])) { 244 | $device['type'] = $params['target_type']; 245 | } 246 | 247 | if (isset($params['target_addr'])) { 248 | $deviceParams['addr'] = $params['target_addr']; 249 | } 250 | if (isset($params['target_port'])) { 251 | $deviceParams['port'] = $params['target_port']; 252 | } 253 | if (isset($params['target_ptime'])) { 254 | $deviceParams['ptime'] = $params['target_ptime']; 255 | } 256 | if (isset($params['target_uri'])) { 257 | $deviceParams['uri'] = $params['target_uri']; 258 | } 259 | if (isset($params['rate'])) { 260 | $deviceParams['rate'] = $params['rate']; 261 | } 262 | if (isset($params['codec'])) { 263 | $deviceParams['codec'] = $params['codec']; 264 | } 265 | $device['params'] = $deviceParams; 266 | 267 | return [$tap, $device]; 268 | } 269 | --------------------------------------------------------------------------------