diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4b17e1c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +* text=auto + +/.github export-ignore +/tests export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.php_cs export-ignore +.styleci.yml export-ignore +.travis.yml export-ignore +phpunit.xml export-ignore +CHANGELOG.md export-ignore +CONTRIBUTING.md export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb0edf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea/ +.DS_Store +composer.lock +.php_cs.cache +/vendor/ \ No newline at end of file diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..1327dc4 --- /dev/null +++ b/.php_cs @@ -0,0 +1,8 @@ +> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + +after_success: + - codecov \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..66f7971 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,22 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. We accept contributions via Pull Requests on [Github](https://github.com/botman/driver-facebook). + +## Pull Requests + +- **[PSR-2 Coding Standard.](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** The easiest way to apply the conventions is to install [PHP CS Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer). +- **Add tests!** Your patch won't be accepted if it doesn't have tests. +- **Document any change in behaviour.** Make sure the `README.md` and any other relevant documentation are kept up-to-date. +- **Consider our release cycle.** We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. +- **Create feature branches.** Don't ask us to pull from your master branch. +- **One pull request per feature.** If you want to do more than one thing, send multiple pull requests. +- **Send coherent history.** Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + +## Running Tests + +```bash +$ phpunit +``` + + +*Happy coding!* \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cae2fc5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Marcel Pociot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9197215 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# BotMan Cisco Spark Driver + +BotMan driver to connect Cisco Spark with [BotMan](https://github.com/botman/botman) + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +## Security Vulnerabilities + +If you discover a security vulnerability within BotMan, please send an e-mail to Marcel Pociot at m.pociot@gmail.com. All security vulnerabilities will be promptly addressed. + +## License + +BotMan is free software distributed under the terms of the MIT license. + \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..41b586c --- /dev/null +++ b/composer.json @@ -0,0 +1,43 @@ +{ + "name": "botman/driver-cisco-spark", + "license": "MIT", + "description": "Cisco Spark driver for BotMan", + "keywords": [ + "Bot", + "BotMan", + "Cisco Spark", + "Spark" + ], + "homepage": "http://github.com/botman/driver-cisco-spark", + "authors": [ + { + "name": "Marcel Pociot", + "email": "m.pociot@gmail.com" + } + ], + "require": { + "php": ">=7.0", + "mpociot/botman": "~2.0" + }, + "require-dev": { + "phpunit/phpunit": "~5.0", + "mockery/mockery": "dev-master", + "ext-curl": "*" + }, + "autoload": { + "psr-4": { + "BotMan\\Drivers\\CiscoSpark\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "scripts": { + "test": "vendor/bin/phpunit", + "cs": "php-cs-fixer fix" + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/discovery.json b/discovery.json new file mode 100644 index 0000000..724a0e5 --- /dev/null +++ b/discovery.json @@ -0,0 +1,8 @@ +{ + "botman/driver-config": [ + "stubs/cisco-spark.php" + ], + "botman/driver": [ + "BotMan\\Drivers\\CiscoSpark\\CiscoSparkDriver" + ] +} \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..2ba9eaf --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,23 @@ + + + + + tests/ + + + + + src/ + + + \ No newline at end of file diff --git a/src/CiscoSparkDriver.php b/src/CiscoSparkDriver.php new file mode 100644 index 0000000..fcc70c3 --- /dev/null +++ b/src/CiscoSparkDriver.php @@ -0,0 +1,190 @@ +payload = new ParameterBag((array) json_decode($request->getContent(), true)); + $this->event = Collection::make($this->payload->get('data')); + $this->config = Collection::make($this->config->get('cisco-spark')); + } + + /** + * @return array + */ + protected function getHeaders() + { + return [ + 'Accept:application/json', + 'Content-Type:application/json', + 'Authorization:Bearer '.$this->config->get('token') + ]; + } + + /** + * Determine if the request is for this driver. + * + * @return bool + */ + public function matchesRequest() + { + return ! is_null($this->payload->get('actorId')) && $this->payload->get('resource') === 'messages' && $this->payload->get('event') === 'created'; + } + + /** + * @param \BotMan\BotMan\Messages\Incoming\IncomingMessage $message + * @return Answer + */ + public function getConversationAnswer(IncomingMessage $message) + { + return Answer::create($message->getText())->setMessage($message); + } + + /** + * Retrieve the chat message. + * + * @return array + */ + public function getMessages() + { + $this->getBotId(); + + $messageContent = $this->getMessageContent($this->event->get('id')); + + $message = new IncomingMessage($messageContent->text, $messageContent->roomId, $messageContent->personId, $messageContent); + + if ($this->getBotId() === $messageContent->personId) { + $message->setIsFromBot(true); + } + + return [$message]; + } + + /** + * @param $messageId + * @return mixed + */ + protected function getMessageContent($messageId) + { + $response = $this->http->get(self::API_ENDPOINT.'messages/'.$messageId, [], $this->getHeaders()); + + return json_decode($response->getContent()); + } + + /** + * @param string|Question|IncomingMessage $message + * @param \BotMan\BotMan\Messages\Incoming\IncomingMessage $matchingMessage + * @param array $additionalParameters + * @return Response|null + */ + public function buildServicePayload($message, $matchingMessage, $additionalParameters = []) + { + $parameters = array_merge_recursive([ + 'roomId' => $matchingMessage->getSender(), + ], $additionalParameters); + /* + * If we send a Question with buttons, ignore + * the text and append the question. + */ + if ($message instanceof Question) { + $parameters['text'] = $message->getText(); + $parameters['markdown'] = $message->getText(); + } elseif ($message instanceof OutgoingMessage) { + $parameters['text'] = $message->getText(); + $parameters['markdown'] = $message->getText(); + + $attachment = $message->getAttachment(); + if (! is_null($attachment) && ! $attachment instanceof Location) { + $parameters['files'] = $attachment->getUrl(); + } + } else { + $parameters['text'] = $message; + $parameters['markdown'] = $message; + } + + return $parameters; + } + + /** + * @param mixed $payload + * @return Response + */ + public function sendPayload($payload) + { + return $this->http->post(self::API_ENDPOINT.'messages', [], $payload, $this->getHeaders(), true); + } + + /** + * @return bool + */ + public function isConfigured() + { + return ! is_null($this->config->get('token')); + } + + /** + * Retrieve User information. + * @param \BotMan\BotMan\Messages\Incoming\IncomingMessage $matchingMessage + * @return User + */ + public function getUser(IncomingMessage $matchingMessage) + { + $personId = $matchingMessage->getPayload()->personId; + $response = $this->http->get(self::API_ENDPOINT.'people/'.$personId, [], $this->getHeaders()); + $userInfo = Collection::make(json_decode($response->getContent(), true)); + + return new User($userInfo->get('id'), $userInfo->get('firstName'), $userInfo->get('lastName'), $userInfo->get('nickName')); + } + + /** + * Low-level method to perform driver specific API requests. + * + * @param string $endpoint + * @param array $parameters + * @param \BotMan\BotMan\Messages\Incoming\IncomingMessage $matchingMessage + * @return void + */ + public function sendRequest($endpoint, array $parameters, IncomingMessage $matchingMessage) + { + // + } + + /** + * Returns the chatbot ID + * @return string + */ + private function getBotId() + { + if (is_null($this->botId)) { + $response = $this->http->get(self::API_ENDPOINT.'people/me', [], $this->getHeaders()); + $bot = json_decode($response->getContent()); + $this->botId = $bot->id; + } + + return $this->botId; + } +} diff --git a/stubs/cisco-spark.php b/stubs/cisco-spark.php new file mode 100644 index 0000000..450edbf --- /dev/null +++ b/stubs/cisco-spark.php @@ -0,0 +1,24 @@ + env('CISCO_SPARK_KEY'), + + /* + |-------------------------------------------------------------------------- + | Webhook Secret + |-------------------------------------------------------------------------- + | + | Your webhook secret. + | + */ + 'secret' => env('CISCO_SPARK_SECRET'), +]; \ No newline at end of file diff --git a/tests/CiscoSparkDriverTest.php b/tests/CiscoSparkDriverTest.php new file mode 100644 index 0000000..5fad591 --- /dev/null +++ b/tests/CiscoSparkDriverTest.php @@ -0,0 +1,293 @@ + [ + 'token' => 'my-token' + ] + ], $htmlInterface); + } + + private function getValidTestData() + { + return [ + 'id' => 'Y2lzY29zcGFyazovL3VzL1dFQkhPT0svZjRlNjA1NjAtNjYwMi00ZmIwLWEyNWEtOTQ5ODgxNjA5NDk3', + 'name' => 'Guild Chat to http://requestb.in/1jw0w3x1', + 'resource' => 'messages', + 'event' => 'created', + 'filter' => 'roomId=Y2lzY29zcGFyazovL3VzL1JPT00vY2RlMWRkNDAtMmYwZC0xMWU1LWJhOWMtN2I2NTU2ZDIyMDdi', + 'orgId' => 'Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi8xZWI2NWZkZi05NjQzLTQxN2YtOTk3NC1hZDcyY2FlMGUxMGY', + 'createdBy' => 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS8xZjdkZTVjYi04NTYxLTQ2NzEtYmMwMy1iYzk3NDMxNDQ0MmQ', + 'appId' => 'Y2lzY29zcGFyazovL3VzL0FQUExJQ0FUSU9OL0MyNzljYjMwYzAyOTE4MGJiNGJkYWViYjA2MWI3OTY1Y2RhMzliNjAyOTdjODUwM2YyNjZhYmY2NmM5OTllYzFm', + 'ownedBy' => 'creator', + 'status' => 'active', + 'actorId' => 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS8xZjdkZTVjYi04NTYxLTQ2NzEtYmMwMy1iYzk3NDMxNDQ0MmQ', + 'data' => [ + 'id' => 'Y2lzY29zcGFyazovL3VzL01FU1NBR0UvMzIzZWUyZjAtOWFhZC0xMWU1LTg1YmYtMWRhZjhkNDJlZjlj', + 'roomId' => 'Y2lzY29zcGFyazovL3VzL1JPT00vY2RlMWRkNDAtMmYwZC0xMWU1LWJhOWMtN2I2NTU2ZDIyMDdi', + 'personId' => 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9lM2EyNjA4OC1hNmRiLTQxZjgtOTliMC1hNTEyMzkyYzAwOTg', + 'personEmail' => 'person@example.com', + 'created' => '2015-12-04T17:33:56.767Z' + ] + ]; + } + + /** @test */ + public function it_returns_the_driver_name() + { + $driver = $this->getDriver([]); + $this->assertSame('CiscoSpark', $driver->getName()); + } + + /** @test */ + public function it_matches_the_request() + { + $driver = $this->getDriver([ + 'to' => '41766013098', + 'messageId' => '0C000000075069C7', + 'text' => 'Hi Julia', + 'type' => 'text', + 'keyword' => 'HEY', + 'message_timestamp' => '2016-11-30 19:27:46', + ]); + $this->assertFalse($driver->matchesRequest()); + + $driver = $this->getDriver($this->getValidTestData()); + $this->assertTrue($driver->matchesRequest()); + } + + /** @test */ + public function it_returns_the_message_object() + { + $botResponseData = [ + 'id' => 'bot-id' + ]; + $botResponse = new Response(json_encode($botResponseData)); + + $htmlInterface = m::mock(Curl::class); + $htmlInterface->shouldReceive('get') + ->once() + ->with('https://api.ciscospark.com/v1/people/me', [], [ + 'Accept:application/json', + 'Content-Type:application/json', + 'Authorization:Bearer my-token' + ]) + ->andReturn($botResponse); + + $msgResponseData = [ + 'text' => 'Hi Julia', + 'roomId' => 'room-1234567890', + 'personId' => 'person-0987654321' + ]; + $msgResponse = new Response(json_encode($msgResponseData)); + + $htmlInterface->shouldReceive('get') + ->once() + ->with('https://api.ciscospark.com/v1/messages/Y2lzY29zcGFyazovL3VzL01FU1NBR0UvMzIzZWUyZjAtOWFhZC0xMWU1LTg1YmYtMWRhZjhkNDJlZjlj', [], [ + 'Accept:application/json', + 'Content-Type:application/json', + 'Authorization:Bearer my-token' + ]) + ->andReturn($msgResponse); + + $driver = $this->getDriver($this->getValidTestData(), $htmlInterface); + $this->assertTrue(is_array($driver->getMessages())); + } + + /** @test */ + public function it_returns_the_message_text() + { + $botResponseData = [ + 'id' => 'bot-id' + ]; + $botResponse = new Response(json_encode($botResponseData)); + + $htmlInterface = m::mock(Curl::class); + $htmlInterface->shouldReceive('get') + ->once() + ->with('https://api.ciscospark.com/v1/people/me', [], [ + 'Accept:application/json', + 'Content-Type:application/json', + 'Authorization:Bearer my-token' + ]) + ->andReturn($botResponse); + + $msgResponseData = [ + 'text' => 'Hi Julia', + 'roomId' => 'room-1234567890', + 'personId' => 'person-0987654321' + ]; + $msgResponse = new Response(json_encode($msgResponseData)); + + $htmlInterface->shouldReceive('get') + ->once() + ->with('https://api.ciscospark.com/v1/messages/Y2lzY29zcGFyazovL3VzL01FU1NBR0UvMzIzZWUyZjAtOWFhZC0xMWU1LTg1YmYtMWRhZjhkNDJlZjlj', [], [ + 'Accept:application/json', + 'Content-Type:application/json', + 'Authorization:Bearer my-token' + ]) + ->andReturn($msgResponse); + + $driver = $this->getDriver($this->getValidTestData(), $htmlInterface); + $this->assertSame('Hi Julia', $driver->getMessages()[0]->getText()); + } + + /** @test */ + public function it_detects_bots() + { + $botResponseData = [ + 'id' => 'bot-id' + ]; + $botResponse = new Response(json_encode($botResponseData)); + + $htmlInterface = m::mock(Curl::class); + $htmlInterface->shouldReceive('get') + ->once() + ->with('https://api.ciscospark.com/v1/people/me', [], [ + 'Accept:application/json', + 'Content-Type:application/json', + 'Authorization:Bearer my-token' + ]) + ->andReturn($botResponse); + + $msgResponseData = [ + 'text' => 'Hi Julia', + 'roomId' => 'room-1234567890', + 'personId' => 'bot-id' + ]; + $msgResponse = new Response(json_encode($msgResponseData)); + + $htmlInterface->shouldReceive('get') + ->once() + ->with('https://api.ciscospark.com/v1/messages/Y2lzY29zcGFyazovL3VzL01FU1NBR0UvMzIzZWUyZjAtOWFhZC0xMWU1LTg1YmYtMWRhZjhkNDJlZjlj', [], [ + 'Accept:application/json', + 'Content-Type:application/json', + 'Authorization:Bearer my-token' + ]) + ->andReturn($msgResponse); + + $driver = $this->getDriver($this->getValidTestData(), $htmlInterface); + $this->assertTrue($driver->getMessages()[0]->isFromBot()); + } + + /** @test */ + public function it_returns_the_user_id() + { + $botResponseData = [ + 'id' => 'bot-id' + ]; + $botResponse = new Response(json_encode($botResponseData)); + + $htmlInterface = m::mock(Curl::class); + $htmlInterface->shouldReceive('get') + ->once() + ->with('https://api.ciscospark.com/v1/people/me', [], [ + 'Accept:application/json', + 'Content-Type:application/json', + 'Authorization:Bearer my-token' + ]) + ->andReturn($botResponse); + + $msgResponseData = [ + 'text' => 'Hi Julia', + 'roomId' => 'room-1234567890', + 'personId' => 'person-0987654321' + ]; + $msgResponse = new Response(json_encode($msgResponseData)); + + $htmlInterface->shouldReceive('get') + ->once() + ->with('https://api.ciscospark.com/v1/messages/Y2lzY29zcGFyazovL3VzL01FU1NBR0UvMzIzZWUyZjAtOWFhZC0xMWU1LTg1YmYtMWRhZjhkNDJlZjlj', [], [ + 'Accept:application/json', + 'Content-Type:application/json', + 'Authorization:Bearer my-token' + ]) + ->andReturn($msgResponse); + + $driver = $this->getDriver($this->getValidTestData(), $htmlInterface); + $this->assertSame('room-1234567890', $driver->getMessages()[0]->getSender()); + } + + /** @test */ + public function it_returns_the_channel_id() + { + $botResponseData = [ + 'id' => 'bot-id' + ]; + $botResponse = new Response(json_encode($botResponseData)); + + $htmlInterface = m::mock(Curl::class); + $htmlInterface->shouldReceive('get') + ->once() + ->with('https://api.ciscospark.com/v1/people/me', [], [ + 'Accept:application/json', + 'Content-Type:application/json', + 'Authorization:Bearer my-token' + ]) + ->andReturn($botResponse); + + $msgResponseData = [ + 'text' => 'Hi Julia', + 'roomId' => 'room-1234567890', + 'personId' => 'person-0987654321' + ]; + $msgResponse = new Response(json_encode($msgResponseData)); + + $htmlInterface->shouldReceive('get') + ->once() + ->with('https://api.ciscospark.com/v1/messages/Y2lzY29zcGFyazovL3VzL01FU1NBR0UvMzIzZWUyZjAtOWFhZC0xMWU1LTg1YmYtMWRhZjhkNDJlZjlj', [], [ + 'Accept:application/json', + 'Content-Type:application/json', + 'Authorization:Bearer my-token' + ]) + ->andReturn($msgResponse); + + $driver = $this->getDriver($this->getValidTestData(), $htmlInterface); + $this->assertSame('person-0987654321', $driver->getMessages()[0]->getRecipient()); + } + + /** @test */ + public function it_is_configured() + { + $request = m::mock(Request::class.'[getContent]'); + $request->shouldReceive('getContent')->andReturn(''); + $htmlInterface = m::mock(Curl::class); + + $driver = new CiscoSparkDriver($request, [ + 'cisco-spark' => [ + 'token' => 'token' + ] + ], $htmlInterface); + + $this->assertTrue($driver->isConfigured()); + + $driver = new CiscoSparkDriver($request, [ + 'cisco-spark' => [ + 'token' => null + ] + ], $htmlInterface); + + $this->assertFalse($driver->isConfigured()); + + $driver = new CiscoSparkDriver($request, [], $htmlInterface); + + $this->assertFalse($driver->isConfigured()); + } +}