├── 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 | 
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 |
--------------------------------------------------------------------------------