├── .travis.yml ├── LICENSE ├── README.md ├── autoload.php ├── composer.json ├── phpunit.xml ├── src ├── Api │ ├── Api.php │ ├── ApiClient.php │ └── HttpClient.php ├── Command │ ├── AnonymousCommand.php │ ├── ApiCommand.php │ ├── Authenticate.php │ ├── Command.php │ ├── DeleteActivity.php │ ├── GetListSigningKeys.php │ ├── GetOpenIdConfiguration.php │ ├── SendActivity.php │ ├── SendAttachment.php │ └── SendMessage.php ├── Config.php ├── DataProvider │ ├── DataProvider.php │ ├── OpenIdConfigProvider.php │ ├── OpenIdKeysProvider.php │ └── TokenProvider.php ├── Entity │ ├── AccessToken.php │ ├── Activity.php │ ├── Address.php │ ├── Attachment.php │ ├── AttachmentFactory.php │ ├── Card │ │ ├── AnimationCard.php │ │ ├── AudioCard.php │ │ ├── Base.php │ │ ├── CardAction.php │ │ ├── CardImage.php │ │ ├── Fact.php │ │ ├── HeroCard.php │ │ ├── MediaCard.php │ │ ├── MediaUrl.php │ │ ├── ReceiptCard.php │ │ ├── ReceiptItem.php │ │ ├── SignInCard.php │ │ ├── ThumbnailCard.php │ │ ├── Traits │ │ │ ├── HasImage.php │ │ │ ├── HasSubtitle.php │ │ │ ├── HasText.php │ │ │ ├── HasTitle.php │ │ │ ├── ImageList.php │ │ │ └── Tapable.php │ │ └── VideoCard.php │ ├── ContactUpdatePayload.php │ ├── Conversation.php │ ├── ConversationUpdatePayload.php │ ├── Entity.php │ ├── Error.php │ ├── Jwk │ │ ├── JsonWebKey.php │ │ ├── JwkInfo.php │ │ ├── JwkPayload.php │ │ └── OpenIdConfig.php │ ├── MessagePayload.php │ ├── Payload.php │ └── Result.php ├── Exception │ ├── CurlException.php │ ├── PayloadException.php │ ├── RequestException.php │ ├── ResponseException.php │ └── SecurityException.php ├── Interfaces │ ├── ApiLogger.php │ ├── ApiResultProcessor.php │ ├── ContactUpdateHandler.php │ ├── ConversationUpdateHandler.php │ ├── DataStorage.php │ └── MessageHandler.php ├── Listener │ ├── Dispatcher.php │ ├── PayloadFactory.php │ ├── Request.php │ └── Security.php ├── SkypeBot.php └── Storage │ ├── FileStorage.php │ ├── MemoryStorage.php │ └── SimpleApiLogger.php └── tests ├── SkypeBot ├── CardsTest.php ├── CommandTest.php ├── ConfigTest.php ├── DataProviderTest.php ├── DispatcherTest.php ├── EntityTest.php ├── ExceptionTest.php ├── HttpClientTest.php ├── RequestTest.php ├── SecurityTest.php ├── SkypeBotTest.php └── StorageTest.php └── bootstrap.php /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | sudo: false 3 | 4 | php: 5 | - 5.5 6 | 7 | addons: 8 | code_climate: 9 | repo_token: 4e46bba90af9f2a2e37ce22be162c47b329a646e258038ef937028e02f2a464b 10 | 11 | before_script: 12 | - composer install --dev 13 | 14 | script: phpunit --coverage-clover build/logs/clover.xml 15 | 16 | after_script: 17 | - vendor/bin/test-reporter 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 github.com/penicylline 3 | 4 | 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: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # skype-bot-sdk 2 | Skype bot framework PHP sdk 3 | 4 | [![Build Status](https://travis-ci.org/penicylline/skype-bot-sdk.svg?branch=master)](https://travis-ci.org/penicylline/skype-bot-sdk) 5 | [![Code Climate](https://codeclimate.com/github/penicylline/skype-bot-sdk/badges/gpa.svg)](https://codeclimate.com/github/penicylline/skype-bot-sdk) 6 | [![Test Coverage](https://codeclimate.com/github/penicylline/skype-bot-sdk/badges/coverage.svg)](https://codeclimate.com/github/penicylline/skype-bot-sdk/coverage) 7 | [![Issue Count](https://codeclimate.com/github/penicylline/skype-bot-sdk/badges/issue_count.svg)](https://codeclimate.com/github/penicylline/skype-bot-sdk) 8 | 9 | How to use 10 | ------- 11 | 12 | Create new application, get app id and app password 13 | 14 | https://apps.dev.microsoft.com/#/quickstart/skypebot 15 | 16 | Create your bot 17 | 18 | https://dev.botframework.com/bots/new 19 | 20 | Set your bot's "Messaging endpoint" to https://yourdomain/listener.php 21 | 22 | Installation 23 | 24 | composer require penicylline/skype-bot-sdk 25 | or require_once /SkypeBot/autoload.php 26 | 27 | Initialize bot 28 | 29 | ```php 30 | $dataStorate = new \SkypeBot\Storage\FileStorage(sys_get_temp_dir()); 31 | $config = new \SkypeBot\Config( 32 | 'YOUR SKYPE BOT ID', 33 | 'YOUR SKYPE BOT SECRET' 34 | ); 35 | 36 | $bot = \SkypeBot\SkypeBot::init($config, $dataStorate); 37 | ``` 38 | 39 | In your notification listener (listener.php) 40 | 41 | ```php 42 | $bot->getNotificationListener()->setMessageHandler( 43 | function($payload) { 44 | file_put_contents( 45 | sys_get_temp_dir() . '/conversation_id.txt', 46 | $payload->getConversation()->getId(); 47 | ); 48 | } 49 | ); 50 | ``` 51 | 52 | Send message to conversation 53 | 54 | ```php 55 | $bot->getApiClient()->call( 56 | new \SkypeBot\Command\SendMessage( 57 | 'Hello World.', 58 | 'Your conversation id' 59 | ) 60 | ); 61 | ``` 62 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/src' 6 | ]; 7 | foreach ($mapping as $prefix => $dir) { 8 | $len = strlen($prefix); 9 | if (strncmp($prefix, $class, $len) !== 0) { 10 | continue; 11 | } 12 | $relative_class = substr($class, $len); 13 | $file = $dir . '/' . str_replace('\\', '/', $relative_class) . '.php'; 14 | 15 | if (file_exists($file)) { 16 | require $file; 17 | return true; 18 | } 19 | } 20 | return false; 21 | }); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "penicylline/skype-bot-sdk", 3 | "description": "PHP SDK for Microsoft skype bot framework", 4 | "require": { 5 | "php": ">=5.5", 6 | "ext-curl": "*" 7 | }, 8 | "minimum-stability": "dev", 9 | "authors": [ 10 | { 11 | "name": "Penicylline", 12 | "email": "penicylline@yahoo.com" 13 | } 14 | ], 15 | "autoload": { 16 | "psr-4": { 17 | "SkypeBot\\": "src" 18 | } 19 | }, 20 | "require-dev": { 21 | "codeclimate/php-test-reporter": "dev-master" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 18 | tests/SkypeBot 19 | 20 | 21 | 22 | 23 | ./src 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Api/Api.php: -------------------------------------------------------------------------------- 1 | requestUrl = $url; 20 | if (isset($options[static::PARAM_PARAMS])) { 21 | $this->requestParams = $options[static::PARAM_PARAMS]; 22 | } 23 | if (isset($options[static::PARAM_JSON_REQUEST])) { 24 | $this->jsonRequest = $options[static::PARAM_JSON_REQUEST]; 25 | } 26 | $this->options = $options; 27 | } 28 | 29 | public function setHeader($key, $value) 30 | { 31 | $this->options[static::PARAM_HEADERS][$key] = $value; 32 | return $this; 33 | } 34 | 35 | public function setRequestMethod($method) 36 | { 37 | $supportedMethods = [ 38 | HttpClient::METHOD_GET, 39 | HttpClient::METHOD_POST, 40 | HttpClient::METHOD_PUT, 41 | HttpClient::METHOD_DELETE 42 | ]; 43 | if (in_array($method, $supportedMethods)) { 44 | $this->options[static::PARAM_METHOD] = $method; 45 | } else { 46 | $this->options[static::PARAM_METHOD] = HttpClient::METHOD_POST; 47 | } 48 | return $this; 49 | } 50 | 51 | public function getRequestMethod() { 52 | if (isset($this->options[static::PARAM_METHOD])) 53 | { 54 | return $this->options[static::PARAM_METHOD]; 55 | } 56 | return HttpClient::METHOD_POST; 57 | } 58 | 59 | public function getRequestParams() 60 | { 61 | return $this->requestParams; 62 | } 63 | 64 | public function getRequestUrl() 65 | { 66 | return $this->requestUrl; 67 | } 68 | 69 | public function getRequestHeaders() { 70 | if (isset($this->options[static::PARAM_HEADERS])) 71 | { 72 | return $this->options[static::PARAM_HEADERS]; 73 | } 74 | } 75 | 76 | public function isJsonRequest() { 77 | if ($this->requestParams instanceof \stdClass) { 78 | return true; 79 | } 80 | return $this->jsonRequest ? true : false; 81 | } 82 | 83 | public function getJsonResult() { 84 | if (isset($this->options[static::PARAM_JSON_RESPONSE])) 85 | { 86 | return $this->options[static::PARAM_JSON_RESPONSE]; 87 | } 88 | return true; 89 | } 90 | 91 | public function setParam($key, $value) 92 | { 93 | $this->requestParams[$key] = $value; 94 | return $this; 95 | } 96 | 97 | public function setParams($values) 98 | { 99 | $this->requestParams = array_merge($this->requestParams, $values); 100 | return $this; 101 | } 102 | } -------------------------------------------------------------------------------- /src/Api/ApiClient.php: -------------------------------------------------------------------------------- 1 | getHttpClient()->setLogger($logger); 48 | return $this; 49 | } 50 | 51 | /** 52 | * @return HttpClient 53 | */ 54 | protected function getHttpClient() 55 | { 56 | if ($this->httpClient == null) { 57 | $this->httpClient = SkypeBot::getInstance()->getHttpClient(); 58 | } 59 | return $this->httpClient; 60 | } 61 | 62 | public function call(Command $command) 63 | { 64 | $result = $this->callCommand($command); 65 | if ($result === false) { 66 | if ($command instanceof Authenticate) { 67 | throw new SecurityException('Failed to authenticate!'); 68 | } 69 | $this->getTokenProvider()->getNewToken(); 70 | return $this->callCommand($command); 71 | } 72 | return $command->processResult($result); 73 | } 74 | 75 | private function callCommand(Command $command) 76 | { 77 | $api = $command->getApi(); 78 | $fullUrl = $api->getRequestUrl(); 79 | if (!($command instanceof AnonymousCommand)) { 80 | $this->getHttpClient()->setHeader( 81 | 'Authorization', 82 | 'Bearer ' . $this->getTokenProvider()->getAccessToken()->getToken() 83 | ); 84 | } 85 | $requestMethod = $api->getRequestMethod(); 86 | if ($requestMethod == HttpClient::METHOD_GET) { 87 | $result = $this->getHttpClient()->get($fullUrl, $api->getRequestParams()); 88 | } else { 89 | $params = $api->getRequestParams(); 90 | if ($api->isJsonRequest()) { 91 | $params = json_encode($params); 92 | } 93 | switch ($requestMethod) { 94 | case HttpClient::METHOD_POST: 95 | $result = $this->getHttpClient()->post($fullUrl, $params); 96 | break; 97 | case HttpClient::METHOD_PUT: 98 | $result = $this->getHttpClient()->put($fullUrl, $params); 99 | break; 100 | case HttpClient::METHOD_DELETE: 101 | $result = $this->getHttpClient()->delete($fullUrl, $params); 102 | break; 103 | } 104 | } 105 | if (!$result) { 106 | if ($this->getHttpClient()->getReturnCode() == 401) { 107 | return false; 108 | } 109 | } 110 | if ($api->getJsonResult()) { 111 | $json = json_decode($result); 112 | if ($json === null) { 113 | throw new ResponseException('Invalid Json format: ' . $result); 114 | } 115 | return $json; 116 | } 117 | 118 | return $result; 119 | } 120 | 121 | /** 122 | * @return TokenProvider 123 | */ 124 | private function getTokenProvider() 125 | { 126 | if ($this->tokenProvider === null) { 127 | $this->tokenProvider = SkypeBot::getInstance()->getTokenProvider(); 128 | } 129 | return $this->tokenProvider; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Api/HttpClient.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 46 | return $this; 47 | } 48 | 49 | /** 50 | * @param $header 51 | * @param null $value 52 | * @return $this 53 | */ 54 | public function setHeader($header, $value = null) 55 | { 56 | if ($value === null) { 57 | $this->headers[] = $header; 58 | } else { 59 | $this->headers[$header] = $value; 60 | } 61 | return $this; 62 | } 63 | 64 | /** 65 | * @param $url 66 | * @param array $params 67 | * @return bool 68 | */ 69 | public function get($url, $params = array()) 70 | { 71 | if (empty($params)) { 72 | $params = array(); 73 | } 74 | $strParams = http_build_query($params); 75 | $channel = $this->initCurl(); 76 | if (strpos($url, '?')) { 77 | if (substr($url, -1) == '&') { 78 | $url .= $strParams; 79 | } else { 80 | $url .= '&' . $strParams; 81 | } 82 | } else { 83 | if (count($params)) { 84 | $url .= '?' . $strParams; 85 | } 86 | } 87 | return $this->doRequest(static::METHOD_GET, $url); 88 | } 89 | 90 | public function post($url, $params = array()) 91 | { 92 | return $this->doRequest(static::METHOD_POST, $url, $params); 93 | } 94 | 95 | public function put($url, $params = array()) 96 | { 97 | return $this->doRequest(static::METHOD_PUT, $url, $params); 98 | } 99 | 100 | public function delete($url, $params = array()) 101 | { 102 | return $this->doRequest(static::METHOD_DELETE, $url, $params); 103 | } 104 | 105 | private function doRequest($type, $url, $params = array()) 106 | { 107 | $method = strtoupper($type); 108 | $this->log('>>>>>> ' . $method . ' >>>>>>'); 109 | $this->log($url); 110 | 111 | $channel = $this->initCurl(); 112 | curl_setopt($channel, CURLOPT_URL, $url); 113 | curl_setopt($channel, CURLOPT_CUSTOMREQUEST, $method); 114 | if (!empty($params)) { 115 | $this->log(print_r($params, true)); 116 | if (is_string($params)) { 117 | $strParams = $params; 118 | } else { 119 | $strParams = http_build_query($params); 120 | } 121 | if (!is_string($params)) { 122 | curl_setopt($channel, CURLOPT_POST, count($params)); 123 | } 124 | curl_setopt($channel, CURLOPT_POSTFIELDS, $strParams); 125 | } 126 | 127 | $result = $this->fetchResult($channel); 128 | if (!$result) { 129 | $this->error = curl_error($channel); 130 | $this->log($this->error); 131 | } 132 | $this->closeCurl($channel); 133 | return $result; 134 | } 135 | 136 | public function getError() 137 | { 138 | return $this->error; 139 | } 140 | 141 | protected function fetchResult($channel) 142 | { 143 | $this->log('==============================='); 144 | $response = curl_exec($channel); 145 | $this->log($response); 146 | $headerSize = curl_getinfo($channel, CURLINFO_HEADER_SIZE); 147 | $body = substr($response, $headerSize); 148 | return $body; 149 | } 150 | 151 | public function getReturnCode() 152 | { 153 | if (is_array($this->info) && isset($this->info['http_code'])) { 154 | return $this->info['http_code']; 155 | } 156 | return null; 157 | } 158 | 159 | private function initCurl() 160 | { 161 | $channel = curl_init(); 162 | curl_setopt($channel, CURLOPT_RETURNTRANSFER, 1); 163 | curl_setopt($channel, CURLOPT_FOLLOWLOCATION, 1); 164 | curl_setopt($channel, CURLOPT_HEADER, 1); 165 | curl_setopt($channel, CURLOPT_HTTPHEADER, $this->buildHeaders()); 166 | return $channel; 167 | } 168 | 169 | private function closeCurl($channel) 170 | { 171 | $this->info = curl_getinfo($channel); 172 | curl_close($channel); 173 | } 174 | 175 | private function buildHeaders() 176 | { 177 | $headers = array(); 178 | foreach ($this->headers as $key => $value) { 179 | if (is_numeric($key)) { 180 | $headers[] = $value; 181 | } else { 182 | $headers[] = $key . ': ' . $value; 183 | } 184 | } 185 | return $headers; 186 | } 187 | 188 | private function log($message) 189 | { 190 | if ($this->logger === null) { 191 | return; 192 | } 193 | $this->logger->log($message); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Command/AnonymousCommand.php: -------------------------------------------------------------------------------- 1 | resultProcessor = $processor; 15 | } 16 | 17 | /** 18 | * @param $result 19 | * @return Result 20 | */ 21 | public function processResult($result) 22 | { 23 | $result = new Result($result); 24 | if ($this->resultProcessor instanceof ApiResultProcessor) { 25 | return $this->resultProcessor->processResult($result); 26 | } 27 | if (is_callable($this->resultProcessor)) { 28 | return call_user_func_array($this->resultProcessor, [$result]); 29 | } 30 | return $result; 31 | } 32 | } -------------------------------------------------------------------------------- /src/Command/Authenticate.php: -------------------------------------------------------------------------------- 1 | getConfig(); 19 | $this->appId = $config->getAppId(); 20 | $this->appSecret = $config->getAppSecret(); 21 | $this->authenticateEndpoint = $config->getAuthEndpoint(); 22 | } 23 | 24 | /** 25 | * @return Api 26 | */ 27 | public function getApi() 28 | { 29 | return new Api( 30 | $this->authenticateEndpoint, 31 | array ( 32 | Api::PARAM_PARAMS => array( 33 | 'grant_type' => 'client_credentials', 34 | 'scope' => 'https://graph.microsoft.com/.default', 35 | 'client_id' => $this->appId, 36 | 'client_secret' => $this->appSecret 37 | ), 38 | Api::PARAM_HEADERS => array( 39 | 'Content-Type: application/x-www-form-urlencoded' 40 | ) 41 | ) 42 | ); 43 | } 44 | 45 | public function processResult($result) 46 | { 47 | SkypeBot::getInstance()->getTokenProvider()->saveToken(new AccessToken($result)); 48 | } 49 | } -------------------------------------------------------------------------------- /src/Command/Command.php: -------------------------------------------------------------------------------- 1 | activityId = $activityId; 24 | $this->conversation = $conversation; 25 | } 26 | 27 | /** 28 | * @return Api 29 | */ 30 | public function getApi() 31 | { 32 | $config = SkypeBot::getInstance()->getConfig(); 33 | $api = new Api( 34 | $config->getApiEndpoint() . '/v3/conversations/' . $this->conversation . '/activities/' . $this->activityId 35 | ); 36 | $api->setRequestMethod(HttpClient::METHOD_DELETE); 37 | return $api; 38 | } 39 | } -------------------------------------------------------------------------------- /src/Command/GetListSigningKeys.php: -------------------------------------------------------------------------------- 1 | endpoint = $endpoint; 19 | } 20 | 21 | /** 22 | * @return Api 23 | */ 24 | public function getApi() 25 | { 26 | return new Api( 27 | $this->endpoint, 28 | [ 29 | Api::PARAM_JSON_REQUEST => true, 30 | Api::PARAM_METHOD => HttpClient::METHOD_GET 31 | ] 32 | ); 33 | } 34 | 35 | public function processResult($result) 36 | { 37 | if (!property_exists($result, 'keys') || !is_array($result->keys)) { 38 | throw new PayloadException('Signing keys should be an array'); 39 | } 40 | $keys = array(); 41 | foreach ($result->keys as $obj) { 42 | $key = new JsonWebKey($obj); 43 | $keys[$key->getKeyId()] = $key; 44 | } 45 | SkypeBot::getInstance()->getOpenIdKeysProvider()->saveKeys($keys); 46 | } 47 | } -------------------------------------------------------------------------------- /src/Command/GetOpenIdConfiguration.php: -------------------------------------------------------------------------------- 1 | getConfig()->getOpenIdEndpoint() . '/v1/.well-known/openidconfiguration', 19 | array( 20 | Api::PARAM_JSON_RESPONSE => true, 21 | APi::PARAM_METHOD => HttpClient::METHOD_GET 22 | ) 23 | ); 24 | } 25 | 26 | public function processResult($result) 27 | { 28 | if (!$result) { 29 | return; 30 | } 31 | $configEntity = new OpenIdConfig($result, true); 32 | SkypeBot::getInstance()->getOpenIdConfigProvider()->saveConfig($configEntity); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Command/SendActivity.php: -------------------------------------------------------------------------------- 1 | activity = $activity; 24 | $this->conversation = $conversation; 25 | } 26 | 27 | /** 28 | * @return Api 29 | */ 30 | public function getApi() 31 | { 32 | $config = SkypeBot::getInstance()->getConfig(); 33 | return new Api( 34 | $config->getApiEndpoint() . '/v3/conversations/' . $this->conversation . '/activities', 35 | array( 36 | APi::PARAM_PARAMS => $this->activity->getRaw() 37 | ) 38 | ); 39 | } 40 | 41 | /** 42 | * @return Activity 43 | */ 44 | public function getActivity() 45 | { 46 | return $this->activity; 47 | } 48 | 49 | /** 50 | * @return mixed 51 | */ 52 | public function getConversation() 53 | { 54 | return $this->conversation; 55 | } 56 | 57 | 58 | } -------------------------------------------------------------------------------- /src/Command/SendAttachment.php: -------------------------------------------------------------------------------- 1 | activity = new Activity(); 16 | $this->activity->addAttachment($attachment); 17 | if ($message) { 18 | $this->activity->setText($message); 19 | } 20 | $this->conversation = $conversation; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Command/SendMessage.php: -------------------------------------------------------------------------------- 1 | activity = new Activity(); 15 | $this->activity->setText($message); 16 | $this->conversation = $conversation; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | appId = $appId; 24 | $this->appSecret = $appSecret; 25 | if ($authEndpoint) { 26 | $this->authEndpoint = $authEndpoint; 27 | } 28 | if ($apiEndpoint) { 29 | $this->apiEndpoint = $apiEndpoint; 30 | } 31 | if ($openIdEndpoint) { 32 | $this->openIdEndpoint = $openIdEndpoint; 33 | } 34 | } 35 | 36 | public function getAppId() 37 | { 38 | return $this->appId; 39 | } 40 | 41 | public function getAppSecret() 42 | { 43 | return $this->appSecret; 44 | } 45 | 46 | public function getAuthEndpoint() 47 | { 48 | return $this->authEndpoint; 49 | } 50 | 51 | public function getApiEndpoint() 52 | { 53 | return $this->apiEndpoint; 54 | } 55 | 56 | public function getOpenIdEndpoint() 57 | { 58 | return $this->openIdEndpoint; 59 | } 60 | } -------------------------------------------------------------------------------- /src/DataProvider/DataProvider.php: -------------------------------------------------------------------------------- 1 | apiClient = $client; 18 | } 19 | 20 | /** 21 | * @return ApiClient 22 | */ 23 | protected function getApiClient() 24 | { 25 | if ($this->apiClient === null) { 26 | $this->apiClient = SkypeBot::getInstance()->getApiClient(); 27 | } 28 | return $this->apiClient; 29 | } 30 | } -------------------------------------------------------------------------------- /src/DataProvider/OpenIdConfigProvider.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 24 | } 25 | 26 | /** 27 | * @param $config 28 | */ 29 | public function saveConfig($config) { 30 | $this->config = $config; 31 | $this->storage->set(static::KEY_STORAGE, $config); 32 | return $this; 33 | } 34 | 35 | /** 36 | * @return OpenIdConfig 37 | */ 38 | public function getConfig() { 39 | if ($this->config === null) { 40 | $this->config = $this->storage->get(static::KEY_STORAGE); 41 | } 42 | if ($this->config === null) { 43 | $api = new \SkypeBot\Command\GetOpenIdConfiguration($this); 44 | $this->getApiClient()->call($api); 45 | $this->config = $this->storage->get(static::KEY_STORAGE); 46 | } 47 | return $this->config; 48 | } 49 | } -------------------------------------------------------------------------------- /src/DataProvider/OpenIdKeysProvider.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 26 | } 27 | 28 | /** 29 | * @param $keys 30 | */ 31 | public function saveKeys($keys) { 32 | $this->keys = $keys; 33 | $this->storage->set(static::KEY_STORAGE, $keys); 34 | } 35 | 36 | /** 37 | * @return JsonWebKey[] 38 | */ 39 | public function getKeys() { 40 | if ($this->keys === null) { 41 | $this->keys = $this->storage->get(static::KEY_STORAGE); 42 | } 43 | if ($this->keys === null) { 44 | $this->resyncKeys(); 45 | } 46 | return $this->keys; 47 | } 48 | 49 | public function resyncKeys() 50 | { 51 | $jwkUri = SkypeBot::getInstance()->getOpenIdConfigProvider()->getConfig()->getJwksUri(); 52 | $api = new GetListSigningKeys($jwkUri); 53 | $this->getApiClient()->call($api); 54 | $this->keys = $this->storage->get(static::KEY_STORAGE); 55 | } 56 | 57 | /** 58 | * @param $kId 59 | * @return JsonWebKey 60 | */ 61 | public function getKeyById($kId) 62 | { 63 | $keys = $this->getKeys(); 64 | if (isset($keys[$kId])) { 65 | return $keys[$kId]; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/DataProvider/TokenProvider.php: -------------------------------------------------------------------------------- 1 | config = SkypeBot::getInstance()->getConfig(); 37 | $this->storage = $dataStorage; 38 | } 39 | 40 | /** 41 | * @return AccessToken 42 | */ 43 | public function getAccessToken() 44 | { 45 | if (!$this->token) { 46 | $this->token = $this->storage->get(static::KEY_STORAGE); 47 | } 48 | 49 | if (!$this->token || $this->token->getExpiredTime() < time() - 60) { 50 | return $this->getNewToken(); 51 | } 52 | 53 | return $this->token; 54 | } 55 | 56 | public function getNewToken() 57 | { 58 | $command = new Authenticate( 59 | $this->config->getAppId(), 60 | $this->config->getAppSecret(), 61 | $this 62 | ); 63 | 64 | $this->getApiClient()->call($command); 65 | return $this->token; 66 | } 67 | 68 | public function saveToken(AccessToken $token) 69 | { 70 | $this->token = $token; 71 | $this->storage->set( 72 | static::KEY_STORAGE, 73 | $token 74 | ); 75 | } 76 | 77 | public function clearToken() 78 | { 79 | $this->storage->remove(static::KEY_STORAGE); 80 | } 81 | } -------------------------------------------------------------------------------- /src/Entity/AccessToken.php: -------------------------------------------------------------------------------- 1 | expried_time = time() + $obj->expires_in; 10 | parent::__construct($obj); 11 | } 12 | 13 | public function getToken() { 14 | return $this->get('access_token'); 15 | } 16 | 17 | public function getExpiredTime() { 18 | return $this->get('expried_time'); 19 | } 20 | 21 | /** 22 | * @return array 23 | */ 24 | protected function getRequiredFields() 25 | { 26 | return array('access_token', 'expires_in'); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Entity/Activity.php: -------------------------------------------------------------------------------- 1 | rawObj->type = static::TYPE_TEXT; 20 | } 21 | 22 | function setConversation(Conversation $conversation) 23 | { 24 | return $this->set('conversation', $conversation); 25 | } 26 | 27 | function getConversation() 28 | { 29 | return $this->get('conversation', Conversation::class); 30 | } 31 | 32 | public function setType($type) 33 | { 34 | return $this->set('type', $type); 35 | } 36 | 37 | public function getType() 38 | { 39 | return $this->get('type'); 40 | } 41 | 42 | public function getText() { 43 | return $this->get('text'); 44 | } 45 | 46 | public function setText($text) 47 | { 48 | $this->set('text', $text); 49 | } 50 | 51 | public function setAttachmentLayout($layout) 52 | { 53 | return $this->set('attachmentLayout', $layout); 54 | } 55 | 56 | public function getAttachmentLayout() 57 | { 58 | return $this->get('attachmentLayout'); 59 | } 60 | 61 | public function addAttachment(Attachment $attachment) 62 | { 63 | $this->add('attachments', $attachment); 64 | if (count($this->rawObj->attachments) > 1 && property_exists($this->rawObj, 'attachmentLayout')) { 65 | $this->set('attachmentLayout', static::LAYOUT_CAROUSEL); 66 | } 67 | return $this; 68 | } 69 | } -------------------------------------------------------------------------------- /src/Entity/Address.php: -------------------------------------------------------------------------------- 1 | get('id'); 9 | } 10 | 11 | public function getName() { 12 | return $this->get('name'); 13 | } 14 | 15 | public function setId($id) { 16 | return $this->set('id', $id); 17 | } 18 | 19 | public function setName($name) { 20 | return $this->set('name', $name); 21 | } 22 | 23 | /** 24 | * @return array 25 | */ 26 | protected function getRequiredFields() 27 | { 28 | return array('id'); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Entity/Attachment.php: -------------------------------------------------------------------------------- 1 | set('contentType', $type); 12 | } 13 | 14 | public function getContentType() 15 | { 16 | return $this->get('contentType'); 17 | } 18 | 19 | public function setContentUrl($url) 20 | { 21 | return $this->set('contentUrl', $url); 22 | } 23 | 24 | public function getContentUrl() 25 | { 26 | return $this->get('contentUrl'); 27 | } 28 | 29 | public function setContent(Base $content) 30 | { 31 | $this->setContentType($content->getContentType()); 32 | return $this->set('content', $content); 33 | } 34 | 35 | public function getContent() 36 | { 37 | return $this->get('content'); 38 | } 39 | 40 | public function setName($name) 41 | { 42 | return $this->set('name', $name); 43 | } 44 | 45 | public function getName() 46 | { 47 | return $this->get('name'); 48 | } 49 | 50 | public function setThumbnailUrl($url) 51 | { 52 | return $this->set('thumbnailUrl', $url); 53 | } 54 | 55 | public function getThumbnailUrl() 56 | { 57 | return $this->get('thumbnailUrl'); 58 | } 59 | } -------------------------------------------------------------------------------- /src/Entity/AttachmentFactory.php: -------------------------------------------------------------------------------- 1 | setContentType($type); 29 | $attachment->setContentUrl($url); 30 | if ($name) { 31 | $attachment->setName($name); 32 | } 33 | return $attachment; 34 | } 35 | } -------------------------------------------------------------------------------- /src/Entity/Card/AnimationCard.php: -------------------------------------------------------------------------------- 1 | set('aspect', $aspect); 16 | } 17 | 18 | function getAspect() 19 | { 20 | return $this->get('aspect'); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Entity/Card/Base.php: -------------------------------------------------------------------------------- 1 | rawObj = new \stdClass(); 17 | } 18 | 19 | function addButton(CardAction $button) 20 | { 21 | return $this->add('buttons', $button); 22 | } 23 | 24 | function getButtons() 25 | { 26 | return $this->get('buttons'); 27 | } 28 | 29 | public abstract function getContentType(); 30 | } -------------------------------------------------------------------------------- /src/Entity/Card/CardAction.php: -------------------------------------------------------------------------------- 1 | set('image', $image); 23 | } 24 | 25 | function getImage() 26 | { 27 | return $this->get('image'); 28 | } 29 | 30 | 31 | function setType($type) 32 | { 33 | return $this->set('type', $type); 34 | } 35 | 36 | function getType() 37 | { 38 | return $this->get('type'); 39 | } 40 | 41 | function setValue($value) 42 | { 43 | return $this->set('value', $value); 44 | } 45 | 46 | function getValue() 47 | { 48 | return $this->get('value'); 49 | } 50 | } -------------------------------------------------------------------------------- /src/Entity/Card/CardImage.php: -------------------------------------------------------------------------------- 1 | set('alt', $alt); 21 | } 22 | 23 | function getAlt() 24 | { 25 | return $this->get('alt'); 26 | } 27 | 28 | function setUrl($url) 29 | { 30 | return $this->set('url', $url); 31 | } 32 | 33 | function getUrl() 34 | { 35 | return $this->get('url'); 36 | } 37 | } -------------------------------------------------------------------------------- /src/Entity/Card/Fact.php: -------------------------------------------------------------------------------- 1 | set('key', $key); 12 | } 13 | 14 | function getKey() 15 | { 16 | return $this->get('key'); 17 | } 18 | 19 | function setValue($value) 20 | { 21 | return $this->set('value', $value); 22 | } 23 | 24 | function getValue() 25 | { 26 | return $this->get('value'); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/Entity/Card/HeroCard.php: -------------------------------------------------------------------------------- 1 | set('autoloop', $autoloop); 16 | } 17 | 18 | function getAutoloop() 19 | { 20 | return $this->get('autoloop'); 21 | } 22 | 23 | function setAutostart($autostart) 24 | { 25 | return $this->set('autostart', $autostart); 26 | } 27 | 28 | function getAutostart() 29 | { 30 | return $this->get('autostart'); 31 | } 32 | 33 | /** 34 | * @param MediaUrl $media 35 | * @return $this 36 | */ 37 | function addMedia(MediaUrl $media) 38 | { 39 | return $this->add('media', $media); 40 | } 41 | 42 | /** 43 | * @return null|MediaUrl 44 | */ 45 | function getMedia() 46 | { 47 | return $this->get('media'); 48 | } 49 | 50 | function setShareable($shareable) 51 | { 52 | return $this->set('shareable', $shareable); 53 | } 54 | 55 | function getShareable() 56 | { 57 | return $this->get('shareable'); 58 | } 59 | } -------------------------------------------------------------------------------- /src/Entity/Card/MediaUrl.php: -------------------------------------------------------------------------------- 1 | set('profile', $profile); 12 | } 13 | 14 | function getProfile() 15 | { 16 | return $this->get('profile'); 17 | } 18 | 19 | function setUrl($url) 20 | { 21 | return $this->set('url', $url); 22 | } 23 | 24 | function getUrl() 25 | { 26 | return $this->get('url'); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Entity/Card/ReceiptCard.php: -------------------------------------------------------------------------------- 1 | add('facts', $fact); 19 | } 20 | 21 | function getFacts() 22 | { 23 | return $this->get('facts'); 24 | } 25 | 26 | function addItem(ReceiptItem $item) 27 | { 28 | return $this->add('items', $item); 29 | } 30 | 31 | function getItems() 32 | { 33 | return $this->get('items'); 34 | } 35 | 36 | function setTax($tax) 37 | { 38 | return $this->set('tax', $tax); 39 | } 40 | 41 | function getTax() 42 | { 43 | return $this->get('tax'); 44 | } 45 | 46 | function setTotal($total) 47 | { 48 | return $this->set('total', $total); 49 | } 50 | 51 | function getTotal() 52 | { 53 | return $this->get('total'); 54 | } 55 | 56 | function setVat($vat) 57 | { 58 | return $this->set('vat', $vat); 59 | } 60 | 61 | function getVat() 62 | { 63 | return $this->get('vat'); 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /src/Entity/Card/ReceiptItem.php: -------------------------------------------------------------------------------- 1 | set('price', $price); 23 | } 24 | 25 | function getPrice() 26 | { 27 | return $this->get('price'); 28 | } 29 | 30 | function setQuantity($quantity) 31 | { 32 | return $this->set('quantity', $quantity); 33 | } 34 | 35 | function getQuantity() 36 | { 37 | return $this->get('quantity'); 38 | } 39 | } -------------------------------------------------------------------------------- /src/Entity/Card/SignInCard.php: -------------------------------------------------------------------------------- 1 | set('image', $image); 18 | } 19 | 20 | /** 21 | * @return null|CardImage 22 | */ 23 | function getImage() 24 | { 25 | return $this->get('image', CardImage::class); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Entity/Card/Traits/HasSubtitle.php: -------------------------------------------------------------------------------- 1 | set('subtitle', $subtitle); 17 | } 18 | 19 | /** 20 | * @return mixed 21 | */ 22 | function getSubtitle() 23 | { 24 | return $this->get('subtitle'); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Entity/Card/Traits/HasText.php: -------------------------------------------------------------------------------- 1 | set('text', $text); 17 | } 18 | 19 | /** 20 | * @return null|string 21 | */ 22 | function getText() 23 | { 24 | return $this->get('text'); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Entity/Card/Traits/HasTitle.php: -------------------------------------------------------------------------------- 1 | set('title', $title); 14 | } 15 | 16 | /** 17 | * @return null|string 18 | */ 19 | function getTitle() 20 | { 21 | return $this->get('title'); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Entity/Card/Traits/ImageList.php: -------------------------------------------------------------------------------- 1 | add('images', $image); 16 | } 17 | 18 | /** 19 | * @return null 20 | */ 21 | function getImages() 22 | { 23 | return $this->get('images'); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Entity/Card/Traits/Tapable.php: -------------------------------------------------------------------------------- 1 | set('tap', $tap); 17 | } 18 | 19 | /** 20 | * @return null|CardAction 21 | */ 22 | function getTap() 23 | { 24 | return $this->get('tap', CardAction::class); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Entity/Card/VideoCard.php: -------------------------------------------------------------------------------- 1 | get('action'); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Entity/Conversation.php: -------------------------------------------------------------------------------- 1 | get('isGroup'); 8 | } 9 | 10 | public function setIsGroup($isGroup) 11 | { 12 | return $this->set('isGroup', $isGroup); 13 | } 14 | } -------------------------------------------------------------------------------- /src/Entity/ConversationUpdatePayload.php: -------------------------------------------------------------------------------- 1 | get('membersAdded'); 10 | } 11 | 12 | public function getMembersRemoved() 13 | { 14 | return $this->get('membersRemoved'); 15 | } 16 | 17 | public function getTopic() 18 | { 19 | return $this->get('topicName'); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Entity/Entity.php: -------------------------------------------------------------------------------- 1 | validateObject($obj); 15 | $this->rawObj = $obj; 16 | } 17 | 18 | public function getRaw() 19 | { 20 | return $this->rawObj; 21 | } 22 | 23 | public function get($property, $type = null) { 24 | if (property_exists($this->rawObj, $property)) { 25 | if ($type && !($this->rawObj->{$property} instanceof $type)) { 26 | $this->rawObj->{$property} = new $type($this->rawObj->{$property}); 27 | } 28 | return $this->rawObj->{$property}; 29 | } 30 | return null; 31 | } 32 | 33 | /** 34 | * @param $key 35 | * @param $value 36 | * @return $this 37 | * @throws PayloadException 38 | */ 39 | public function set($key, $value, $xmlSafe = true) 40 | { 41 | $this->validateInput($key, $value); 42 | if ($value instanceof Entity) { 43 | $this->rawObj->{$key} = $value->getRaw(); 44 | } else { 45 | if (is_string($value) && $xmlSafe) { 46 | $this->rawObj->{$key} = str_replace( 47 | ['<', '>', '&'], 48 | ['<', '>', '&'], 49 | $value 50 | ); 51 | } else { 52 | $this->rawObj->{$key} = $value; 53 | } 54 | } 55 | return $this; 56 | } 57 | 58 | /** 59 | * @param $key 60 | * @param $value 61 | * @return $this 62 | * @throws PayloadException 63 | */ 64 | public function add($key, $value) 65 | { 66 | $this->validateInput($key, $value); 67 | if (!property_exists($this->rawObj, $key)) { 68 | $this->rawObj->{$key} = []; 69 | } 70 | $this->rawObj->{$key}[] = $value->getRaw(); 71 | return $this; 72 | } 73 | 74 | /** 75 | * @param $obj 76 | * @throws PayloadException 77 | */ 78 | protected function validateObject($obj) { 79 | $fields = $this->getRequiredFields(); 80 | foreach ($fields as &$field) { 81 | if (!property_exists($obj, $field)) { 82 | throw new PayloadException('Missing field "' . $field . '" in ' . json_encode($obj)); 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * @param $key 89 | * @param $value 90 | * @return bool 91 | * @throws PayloadException 92 | */ 93 | protected function validateInput($key, $value) 94 | { 95 | if (!is_string($key)) { 96 | throw new PayloadException('Key should be string'); 97 | } 98 | 99 | if (!(is_scalar($value) || is_array($value) || $value instanceof \stdClass || $value instanceof Entity)) { 100 | throw new PayloadException('Object should be scalar value or instance of stdClass or an Entity'); 101 | } 102 | return true; 103 | } 104 | 105 | /** 106 | * @return array 107 | */ 108 | protected function getRequiredFields() 109 | { 110 | return []; 111 | } 112 | } -------------------------------------------------------------------------------- /src/Entity/Error.php: -------------------------------------------------------------------------------- 1 | get('message'); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Entity/Jwk/JsonWebKey.php: -------------------------------------------------------------------------------- 1 | get('kty'); 32 | } 33 | 34 | public function getPublicKeyUse() 35 | { 36 | return $this->get('use'); 37 | } 38 | 39 | public function getKeyId() 40 | { 41 | return $this->get('kid'); 42 | } 43 | 44 | public function getModulus() 45 | { 46 | return $this->get('n'); 47 | } 48 | 49 | public function getCertificateChain() 50 | { 51 | return $this->get('x5c'); 52 | } 53 | } -------------------------------------------------------------------------------- /src/Entity/Jwk/JwkInfo.php: -------------------------------------------------------------------------------- 1 | get('kid'); 11 | } 12 | 13 | public function getAlgorithm() { 14 | return $this->get('alg'); 15 | } 16 | 17 | public function getType() { 18 | return $this->get('typ'); 19 | } 20 | 21 | /** 22 | * @return array 23 | */ 24 | protected function getRequiredFields() 25 | { 26 | return array('typ', 'alg', 'kid', 'x5t'); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Entity/Jwk/JwkPayload.php: -------------------------------------------------------------------------------- 1 | get('exp'); 11 | } 12 | 13 | public function getNotBefore() { 14 | return $this->get('nbf'); 15 | } 16 | 17 | /** 18 | * @return array 19 | */ 20 | protected function getRequiredFields() 21 | { 22 | return array('iss', 'aud', 'exp', 'nbf'); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Entity/Jwk/OpenIdConfig.php: -------------------------------------------------------------------------------- 1 | get('issuer'); 27 | } 28 | 29 | public function getJwksUri() 30 | { 31 | return $this->get('jwks_uri'); 32 | } 33 | 34 | public function getSigningAlg() { 35 | return $this->get('id_token_signing_alg_values_supported'); 36 | } 37 | } -------------------------------------------------------------------------------- /src/Entity/MessagePayload.php: -------------------------------------------------------------------------------- 1 | ]+>([^\<]+)<[^\>]+>/'; 10 | $text = preg_replace($pattern, '$1', trim($this->get('text'))); 11 | $text = str_replace("\xc2\xa0", ' ', $text); 12 | return html_entity_decode($text); 13 | } 14 | 15 | public function setText($text) 16 | { 17 | $this->set('text', $text); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Entity/Payload.php: -------------------------------------------------------------------------------- 1 | get('channelId'); 17 | } 18 | 19 | public function getServiceUrl() 20 | { 21 | return $this->get('serviceUrl'); 22 | } 23 | 24 | /** 25 | * @return Conversation 26 | */ 27 | public function getConversation() 28 | { 29 | return $this->get('conversation', Conversation::class); 30 | } 31 | 32 | function setConversation(Conversation $conversation) 33 | { 34 | return $this->set('conversation', $conversation); 35 | } 36 | 37 | public function setType($type) 38 | { 39 | return $this->set('type', $type); 40 | } 41 | 42 | /** 43 | * @return Address 44 | */ 45 | public function getFrom() 46 | { 47 | return $this->get('from', Address::class); 48 | } 49 | 50 | /** 51 | * @return Address 52 | */ 53 | public function getRecipient() 54 | { 55 | return $this->get('recipient', Address::class); 56 | } 57 | 58 | public function getType() 59 | { 60 | if ($this->type == null) { 61 | switch ($this->get('type')) { 62 | case 'message/text': 63 | case 'message': 64 | $this->type = static::TYPE_MESSAGE_TEXT; 65 | break; 66 | case 'activity/contactRelationUpdate': 67 | case 'contactRelationUpdate': 68 | $this->type = static::TYPE_CONTACT_UPDATE; 69 | break; 70 | case 'activity/conversationUpdate': 71 | case 'conversationUpdate': 72 | $this->type = static::TYPE_CONVERSATION_UPDATE; 73 | break; 74 | default: 75 | $this->type = static::TYPE_UNKNOWN; 76 | } 77 | } 78 | return $this->type; 79 | } 80 | } -------------------------------------------------------------------------------- /src/Entity/Result.php: -------------------------------------------------------------------------------- 1 | get('error', Error::class); 13 | } 14 | 15 | public function getId() 16 | { 17 | return $this->get('id'); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Exception/CurlException.php: -------------------------------------------------------------------------------- 1 | curlInfo = $info; 10 | return $this; 11 | } 12 | 13 | public function getInfo() { 14 | return $this->curlInfo; 15 | } 16 | } -------------------------------------------------------------------------------- /src/Exception/PayloadException.php: -------------------------------------------------------------------------------- 1 | security = $security; 48 | } 49 | 50 | public function dispatch() 51 | { 52 | $payload = $this->fetchRequest(); 53 | if ($payload instanceof MessagePayload) { 54 | $this->handleMessage($payload); 55 | } 56 | if ($payload instanceof ConversationUpdatePayload) { 57 | $this->handleConversationUpdate($payload); 58 | } 59 | if ($payload instanceof ContactUpdatePayload) { 60 | $this->handleContactUpdate($payload); 61 | } 62 | 63 | $this->sendResponse(); 64 | } 65 | 66 | public function setContactHandler($handler) 67 | { 68 | $this->contactUpdateHandler = $handler; 69 | return $this; 70 | } 71 | 72 | public function setConversationHandler($handler) 73 | { 74 | $this->conversationUpdateHandler = $handler; 75 | return $this; 76 | } 77 | 78 | public function setMessageHandler($handler) 79 | { 80 | $this->messageHandler = $handler; 81 | return $this; 82 | } 83 | 84 | public function setApiLogger(ApiLogger $logger) 85 | { 86 | $this->apiLogger = $logger; 87 | return $this; 88 | } 89 | 90 | protected function fetchRequest() 91 | { 92 | $request = SkypeBot::getInstance()->getRequest(); 93 | $headers = $request->getHeaders(); 94 | if (!isset($headers['Authorization'])) { 95 | throw new SecurityException('Request missing authorization header'); 96 | } 97 | $this->validateAuthenticateHeader($headers['Authorization']); 98 | 99 | $requestBody = $request->getRawBody(); 100 | $requestObj = json_decode($requestBody); 101 | if (empty($requestObj)) { 102 | throw new PayloadException('Empty or invalid json format: "' . $requestBody . '"'); 103 | } 104 | $this->logRequest($request->getHeaders(), $requestBody); 105 | return PayloadFactory::createPayload($requestObj); 106 | } 107 | 108 | protected function sendResponse() 109 | { 110 | http_response_code(201); 111 | } 112 | 113 | protected function handleMessage(MessagePayload $payload) { 114 | if ($this->messageHandler === null) { 115 | return; 116 | } 117 | if ($this->messageHandler instanceof MessageHandler) { 118 | return $this->messageHandler->handlerMessage($payload); 119 | } 120 | if (is_callable($this->messageHandler)){ 121 | call_user_func_array($this->messageHandler, [$payload]); 122 | } 123 | } 124 | 125 | protected function handleContactUpdate(ContactUpdatePayload $payload) { 126 | if ($this->contactUpdateHandler === null) { 127 | return; 128 | } 129 | if ($this->contactUpdateHandler instanceof ContactUpdateHandler) { 130 | return $this->contactUpdateHandler->handlerPayload($payload); 131 | } 132 | if (is_callable($this->contactUpdateHandler)){ 133 | call_user_func_array($this->contactUpdateHandler, [$payload]); 134 | } 135 | } 136 | 137 | protected function handleConversationUpdate(ConversationUpdatePayload $payload) 138 | { 139 | if ($this->conversationUpdateHandler === null) { 140 | return; 141 | } 142 | if ($this->conversationUpdateHandler instanceof ConversationUpdateHandler) { 143 | return $this->conversationUpdateHandler->handlerPayload($payload); 144 | } 145 | if (is_callable($this->conversationUpdateHandler)){ 146 | call_user_func_array($this->conversationUpdateHandler, [$payload]); 147 | } 148 | } 149 | 150 | protected function validateAuthenticateHeader($authHeader) 151 | { 152 | $result = $this->security->validateBearerHeader($authHeader); 153 | if ($result != 1) { 154 | throw new SecurityException('Invalid authenticate data'); 155 | } 156 | } 157 | 158 | protected function logRequest($requestHeader, $requestBody) 159 | { 160 | if ($this->apiLogger === null) { 161 | return; 162 | } 163 | $message = print_r($requestHeader, true) . PHP_EOL . $requestBody; 164 | $this->apiLogger->log($message); 165 | } 166 | } -------------------------------------------------------------------------------- /src/Listener/PayloadFactory.php: -------------------------------------------------------------------------------- 1 | getType()) { 17 | case Payload::TYPE_MESSAGE_TEXT: 18 | return new MessagePayload($requestObj); 19 | case Payload::TYPE_CONTACT_UPDATE: 20 | return new ContactUpdatePayload($requestObj); 21 | case Payload::TYPE_CONVERSATION_UPDATE: 22 | return new ConversationUpdatePayload($requestObj); 23 | } 24 | 25 | return $payload; 26 | } 27 | } -------------------------------------------------------------------------------- /src/Listener/Request.php: -------------------------------------------------------------------------------- 1 | $value) { 19 | if (substr($name, 0, 5) == 'HTTP_') { 20 | $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; 21 | } 22 | } 23 | return $headers; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Listener/Security.php: -------------------------------------------------------------------------------- 1 | keyProvider === null) { 26 | $this->keyProvider = SkypeBot::getInstance()->getOpenIdKeysProvider(); 27 | } 28 | return $this->keyProvider; 29 | } 30 | 31 | public function validateBearerHeader($header) 32 | { 33 | $bearerData = $this->extractBearerData($header); 34 | $tokenParts = explode('.', $bearerData); 35 | if (count($tokenParts) !== 3) { 36 | throw new SecurityException('Authenticate header is not valid format'); 37 | } 38 | list($keyInfo, $payload, $signature) = $tokenParts; 39 | $keyInfoObj = json_decode(base64_decode($keyInfo)); 40 | $payloadObj = json_decode(base64_decode($payload)); 41 | 42 | if (!isset($keyInfoObj, $payloadObj)) { 43 | throw new SecurityException('Authenticate header key info part is not valid format'); 44 | } 45 | $jwkInfo = new JwkInfo($keyInfoObj); 46 | $jwkPayload = new JwkPayload($payloadObj); 47 | 48 | //checking key valid time 49 | $now = time(); 50 | if ($jwkPayload->getExpired() < $now - static::TIME_MARGIN) { 51 | throw new SecurityException('Token expired at: ' . date('Y-m-d H:i:s', $jwkPayload->getExpired())); 52 | } 53 | if ($jwkPayload->getNotBefore() > $now + static::TIME_MARGIN) { 54 | throw new SecurityException('Token cannot use before: ' . date('Y-m-d H:i:s', $jwkPayload->getNotBefore())); 55 | } 56 | 57 | //checking supported encrypt algorithm 58 | if ($jwkInfo->getAlgorithm() != static::SUPPORTED_ALGORITHM) { 59 | throw new SecurityException('Unsupported key type: ' . $jwkInfo->getAlgorithm()); 60 | } 61 | 62 | //get correct public key from the list 63 | $signingKey = $this->getKeyProvider()->getKeyById($jwkInfo->getKeyId()); 64 | if ($signingKey === null) { 65 | $this->getKeyProvider()->resyncKeys(); 66 | $signingKey = $this->getKeyProvider()->getKeyById($jwkInfo->getKeyId()); 67 | if ($signingKey === null) { 68 | throw new SecurityException('Cannot find signing key of the header'); 69 | } 70 | } 71 | 72 | // checking public key usage - only support signature 73 | if ($signingKey->getPublicKeyUse() != JsonWebKey::PUBLIC_KEY_SIGNATURE) { 74 | throw new SecurityException('Unsupported public key usage: ' . $signingKey->getPublicKeyUse()); 75 | } 76 | $pKey = $this->getPublicKey($signingKey->getCertificateChain()[0]); 77 | return $this->verify( 78 | $keyInfo, 79 | $payload, 80 | $signature, 81 | $pKey, 82 | 'SHA256' 83 | ); 84 | } 85 | 86 | private function extractBearerData($header) 87 | { 88 | if (substr($header, 0, 7) != 'Bearer ') { 89 | throw new SecurityException('Authorization header should start with Bearer'); 90 | } 91 | return substr($header, 7); 92 | } 93 | 94 | private function base64UrlDecode($input) 95 | { 96 | return base64_decode(strtr($input, '-_', '+/')); 97 | } 98 | 99 | private function verify($keyInfo, $payload, $signature, $key, $algorithm) 100 | { 101 | $msg = $keyInfo . '.' . $payload; 102 | $signature = $this->base64UrlDecode($signature); 103 | return openssl_verify($msg, $signature, $key, $algorithm); 104 | } 105 | 106 | private function getPublicKey($cert) 107 | { 108 | $certObj = openssl_x509_read( 109 | sprintf( 110 | "-----BEGIN CERTIFICATE-----\n%s-----END CERTIFICATE-----\n", 111 | chunk_split($cert, 64) 112 | ) 113 | ); 114 | $pkeyObj = openssl_pkey_get_public($certObj); 115 | $pkeyArr = openssl_pkey_get_details($pkeyObj); 116 | return $pkeyArr['key']; 117 | } 118 | 119 | } -------------------------------------------------------------------------------- /src/SkypeBot.php: -------------------------------------------------------------------------------- 1 | resolvedObjs = array(); 69 | $this->rawObjs = array(); 70 | $this->set('config', $config); 71 | $this->set('data_storage', $dataStorage); 72 | $this->initComponents($config, $dataStorage); 73 | } 74 | 75 | protected function initComponents(Config $config, DataStorage $dataStorage) 76 | { 77 | $this->set('api_client', function (){ 78 | return ApiClient::getInstance(); 79 | }); 80 | $this->set('openid_config', function () use($dataStorage) { 81 | return new OpenIdConfigProvider($dataStorage); 82 | }); 83 | $this->set('openid_keys', function () use($dataStorage) { 84 | return new OpenIdKeysProvider($dataStorage); 85 | }); 86 | $this->set('token_provider', function() use($dataStorage) { 87 | return new TokenProvider($dataStorage); 88 | }); 89 | $this->set('http_client', function() { 90 | return HttpClient::getInstance(); 91 | }); 92 | $this->set('security', function() { 93 | return new Security(SkypeBot::getInstance()->getOpenIdKeysProvider()); 94 | }); 95 | $this->set('dispatcher', function() { 96 | return new Dispatcher(SkypeBot::getInstance()->getSecurity()); 97 | }); 98 | $this->set('request', function() { 99 | return new Request(); 100 | }); 101 | } 102 | 103 | /** 104 | * @param Config $config 105 | * @param DataStorage $dataStorage 106 | * @return SkypeBot 107 | */ 108 | public static function init(Config $config, DataStorage $dataStorage) 109 | { 110 | static::$instance = new SkypeBot($config, $dataStorage); 111 | return static::$instance; 112 | } 113 | 114 | /** 115 | * @return SkypeBot 116 | * @throws \Exception 117 | */ 118 | public static function getInstance() 119 | { 120 | if (static::$instance === null) { 121 | throw new \Exception('SkypeBot not initialized.'); 122 | } 123 | return static::$instance; 124 | } 125 | 126 | /** 127 | * @return Config 128 | */ 129 | public function getConfig() 130 | { 131 | return $this->get('config'); 132 | } 133 | 134 | /** 135 | * @return DataStorage 136 | */ 137 | public function getDataStorage() 138 | { 139 | return $this->get('data_storage'); 140 | } 141 | 142 | /** 143 | * @return ApiClient 144 | */ 145 | public function getApiClient() 146 | { 147 | return $this->get('api_client'); 148 | } 149 | 150 | /** 151 | * @return OpenIdConfigProvider 152 | */ 153 | public function getOpenIdConfigProvider() 154 | { 155 | return $this->get('openid_config'); 156 | } 157 | 158 | /** 159 | * @return OpenIdKeysProvider 160 | */ 161 | public function getOpenIdKeysProvider() 162 | { 163 | return $this->get('openid_keys'); 164 | } 165 | 166 | /** 167 | * @return TokenProvider 168 | */ 169 | public function getTokenProvider() 170 | { 171 | return $this->get('token_provider'); 172 | } 173 | 174 | /** 175 | * @return HttpClient 176 | */ 177 | public function getHttpClient() 178 | { 179 | return $this->get('http_client'); 180 | } 181 | 182 | /** 183 | * @return Security 184 | */ 185 | public function getSecurity() 186 | { 187 | return $this->get('security'); 188 | } 189 | 190 | /** 191 | * @return Dispatcher 192 | */ 193 | public function getNotificationListener() 194 | { 195 | return $this->get('dispatcher'); 196 | } 197 | 198 | /** 199 | * @return Request 200 | */ 201 | public function getRequest() 202 | { 203 | return $this->get('request'); 204 | } 205 | 206 | /** 207 | * @param $key 208 | * @param $value 209 | * @return $this 210 | */ 211 | public function set($key, $value) 212 | { 213 | $this->rawObjs[$key] = $value; 214 | unset($this->resolvedObjs[$key]); 215 | return $this; 216 | } 217 | 218 | /** 219 | * @param $key 220 | * @return mixed 221 | */ 222 | public function get($key) 223 | { 224 | return $this->fetch($key); 225 | } 226 | 227 | /** 228 | * @param $key 229 | * @return mixed|void 230 | */ 231 | protected function resolve($key) 232 | { 233 | if (!isset($this->rawObjs[$key])) { 234 | return; 235 | } 236 | if (is_callable($this->rawObjs[$key])) { 237 | return call_user_func($this->rawObjs[$key]); 238 | } 239 | return $this->rawObjs[$key]; 240 | } 241 | 242 | /** 243 | * @param $key 244 | * @return mixed 245 | */ 246 | protected function fetch($key) 247 | { 248 | if (!isset($this->resolvedObj[$key])) { 249 | $this->resolvedObj[$key] = $this->resolve($key); 250 | } 251 | return $this->resolvedObj[$key]; 252 | } 253 | 254 | 255 | } -------------------------------------------------------------------------------- /src/Storage/FileStorage.php: -------------------------------------------------------------------------------- 1 | path = $path; 20 | } 21 | 22 | public function set($key, $value) 23 | { 24 | $file = $this->getPathByKey($key); 25 | file_put_contents($file, serialize($value)); 26 | } 27 | 28 | public function get($key) 29 | { 30 | $file = $this->getPathByKey($key); 31 | if (!file_exists($file)) { 32 | return null; 33 | } 34 | $data = file_get_contents($file); 35 | return unserialize($data); 36 | } 37 | 38 | public function remove($key) 39 | { 40 | unlink($this->getPathByKey($key)); 41 | } 42 | 43 | private function getPathByKey($key) 44 | { 45 | return sprintf('%s/%s.data', $this->path, $key); 46 | } 47 | } -------------------------------------------------------------------------------- /src/Storage/MemoryStorage.php: -------------------------------------------------------------------------------- 1 | data = array(); 16 | } 17 | 18 | public function set($key, $value) 19 | { 20 | $this->data[$key] = $value; 21 | } 22 | 23 | public function get($key) 24 | { 25 | if (isset($this->data[$key])) { 26 | return $this->data[$key]; 27 | } 28 | } 29 | 30 | public function remove($key) 31 | { 32 | unset($this->data[$key]); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Storage/SimpleApiLogger.php: -------------------------------------------------------------------------------- 1 | handler = fopen('php://output', 'w'); 16 | return; 17 | } 18 | $this->handler = fopen('php://memory', 'w'); 19 | } 20 | 21 | public function __destruct() 22 | { 23 | if ($this->handler) { 24 | fclose($this->handler); 25 | } 26 | } 27 | 28 | public function log($message) 29 | { 30 | fwrite($this->handler, $message); 31 | fwrite($this->handler, PHP_EOL); 32 | } 33 | } -------------------------------------------------------------------------------- /tests/SkypeBot/CardsTest.php: -------------------------------------------------------------------------------- 1 | setTitle('a'); 12 | $this->assertEquals($card->getTitle(), 'a'); 13 | $card->setText('b'); 14 | $this->assertEquals($card->getText(), 'b'); 15 | $button = new \SkypeBot\Entity\Card\CardAction(); 16 | $button->setTitle('t'); 17 | $card->addButton($button); 18 | $this->assertEquals($card->getButtons()[0]->title, 't'); 19 | } 20 | 21 | function testSubElements() 22 | { 23 | $button = new \SkypeBot\Entity\Card\CardAction(); 24 | $button->setImage('i'); 25 | $this->assertEquals($button->getImage(), 'i'); 26 | $button->setType('t'); 27 | $this->assertEquals($button->getType(), 't'); 28 | $button->setValue('v'); 29 | $this->assertEquals($button->getValue(), 'v'); 30 | 31 | $image = new \SkypeBot\Entity\Card\CardImage(); 32 | $image->setAlt('a'); 33 | $this->assertEquals($image->getAlt(), 'a'); 34 | $image->setUrl('u'); 35 | $this->assertEquals($image->getUrl(), 'u'); 36 | 37 | $fact = new \SkypeBot\Entity\Card\Fact(); 38 | $fact->setKey('k'); 39 | $this->assertEquals($fact->getKey(), 'k'); 40 | $fact->setValue('v'); 41 | $this->assertEquals($fact->getValue(), 'v'); 42 | 43 | $media = new \SkypeBot\Entity\Card\MediaUrl(); 44 | $media->setUrl('u'); 45 | $this->assertEquals($media->getUrl(), 'u'); 46 | $media->setProfile('p'); 47 | $this->assertEquals($media->getProfile(), 'p'); 48 | 49 | $item = new \SkypeBot\Entity\Card\ReceiptItem(); 50 | $item->setImage($image); 51 | $this->assertEquals($item->getImage()->getAlt(), 'a'); 52 | $item->setPrice('p'); 53 | $this->assertEquals($item->getPrice(), 'p'); 54 | $item->setQuantity(5); 55 | $this->assertEquals($item->getQuantity(), 5); 56 | } 57 | 58 | function testAudioCard() 59 | { 60 | $image = new \SkypeBot\Entity\Card\CardImage(); 61 | $image->setAlt('a'); 62 | $media = new \SkypeBot\Entity\Card\MediaUrl(); 63 | $media->setUrl('u'); 64 | $card = new \SkypeBot\Entity\Card\AudioCard(); 65 | $card->setImage($image); 66 | $this->assertEquals($card->getImage()->getAlt(), 'a'); 67 | $card->addMedia($media); 68 | $this->assertEquals($card->getMedia()[0]->url, 'u'); 69 | $card->setAutoloop(true); 70 | $this->assertTrue($card->getAutoloop()); 71 | $card->setAutostart(true); 72 | $this->assertTrue($card->getAutostart()); 73 | $card->setShareable(true); 74 | $this->assertTrue($card->getShareable()); 75 | $card->setAspect('a'); 76 | $this->assertEquals($card->getAspect(), 'a'); 77 | $card->setSubtitle('s'); 78 | $this->assertEquals($card->getSubtitle(), 's'); 79 | } 80 | 81 | function testHeroCard() 82 | { 83 | $card = new \SkypeBot\Entity\Card\HeroCard(); 84 | $button = new \SkypeBot\Entity\Card\CardAction(); 85 | $button->setValue('v'); 86 | $card->addButton($button); 87 | $this->assertEquals($card->getButtons()[0]->value, 'v'); 88 | $image = new \SkypeBot\Entity\Card\CardImage(); 89 | $image->setUrl('u'); 90 | $card->addImage($image); 91 | $this->assertEquals($card->getImages()[0]->url, 'u'); 92 | $this->assertEquals($card->getContentType(), 'application/vnd.microsoft.card.hero'); 93 | } 94 | 95 | function testAnicationCard() 96 | { 97 | $card = new \SkypeBot\Entity\Card\AnimationCard(); 98 | $this->assertEquals($card->getContentType(), 'application/vnd.microsoft.card.animation'); 99 | } 100 | 101 | function testReceiptCard() 102 | { 103 | $button = new \SkypeBot\Entity\Card\CardAction(); 104 | $button->setImage('i'); 105 | $card = new \SkypeBot\Entity\Card\ReceiptCard(); 106 | $card->setTap($button); 107 | $this->assertEquals($card->getTap()->getImage(), 'i'); 108 | $this->assertEquals($card->getContentType(), 'application/vnd.microsoft.card.receipt'); 109 | $card->setTax(1); 110 | $this->assertEquals($card->getTax(), 1); 111 | $card->setVat(2); 112 | $this->assertEquals($card->getVat(), 2); 113 | $card->setTotal(3); 114 | $this->assertEquals($card->getTotal(), 3); 115 | 116 | $fact = new \SkypeBot\Entity\Card\Fact(); 117 | $fact->setKey('k'); 118 | $card->addFact($fact); 119 | $this->assertEquals($card->getFacts()[0]->key, 'k'); 120 | 121 | $item = new \SkypeBot\Entity\Card\ReceiptItem(); 122 | $item->setPrice('p'); 123 | $card->addItem($item); 124 | $this->assertEquals($card->getItems()[0]->price, 'p'); 125 | 126 | } 127 | 128 | } -------------------------------------------------------------------------------- /tests/SkypeBot/CommandTest.php: -------------------------------------------------------------------------------- 1 | setAttachmentLayout('list'); 18 | $cmd = new \SkypeBot\Command\SendActivity($activity, 'cid'); 19 | $this->assertEquals($cmd->getApi()->getRequestParams()->attachmentLayout, 'list'); 20 | $resObj = new stdClass(); 21 | $resObj->id = 12; 22 | $this->assertEquals($cmd->processResult($resObj)->getId(), 12); 23 | } 24 | 25 | function testSendMessage() 26 | { 27 | $cmd = new \SkypeBot\Command\SendMessage('aaa', 'bbb'); 28 | $cmd->setResultProcessor(function ($result){ 29 | return $result->getId(); 30 | }); 31 | $pos = strpos($cmd->getApi()->getRequestUrl(), 'bbb'); 32 | $this->assertGreaterThan(0, $pos); 33 | $this->assertEquals($cmd->getApi()->getRequestParams()->text, 'aaa'); 34 | $resObj = new stdClass(); 35 | $resObj->id = 11; 36 | $this->assertEquals($cmd->processResult($resObj), 11); 37 | } 38 | 39 | function testSendAttachment() 40 | { 41 | $att = new \SkypeBot\Entity\Attachment(); 42 | $card = new \SkypeBot\Entity\Card\AudioCard(); 43 | $att->setContent($card); 44 | $cmd = new \SkypeBot\Command\SendAttachment($att, 'bbb', 'msg'); 45 | $this->assertEquals($cmd->getApi()->getRequestParams()->attachments[0]->contentType, $att->getContentType()); 46 | $this->assertTrue($cmd->getApi()->isJsonRequest()); 47 | 48 | $mock = $this->getMock('SkypeBot\\Interfaces\\ApiResultProcessor'); 49 | $mock->expects($this->any()) 50 | ->method('processResult') 51 | ->will($this->returnValue(13)); 52 | $cmd->setResultProcessor($mock); 53 | $this->assertEquals($cmd->processResult(null), 13); 54 | } 55 | 56 | function testDeleteActivity() 57 | { 58 | $cmd = new \SkypeBot\Command\DeleteActivity('a', 'b'); 59 | $this->assertEquals($cmd->getApi()->getRequestMethod(), \SkypeBot\Api\HttpClient::METHOD_DELETE); 60 | } 61 | } -------------------------------------------------------------------------------- /tests/SkypeBot/ConfigTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($config->getAppId(), 1); 12 | $this->assertEquals($config->getAppSecret(), 2); 13 | $this->assertEquals($config->getApiEndpoint(), 3); 14 | $this->assertEquals($config->getAuthEndpoint(), 4); 15 | $this->assertEquals($config->getOpenIdEndpoint(), 5); 16 | } 17 | } -------------------------------------------------------------------------------- /tests/SkypeBot/DataProviderTest.php: -------------------------------------------------------------------------------- 1 | bot = SkypeBot\SkypeBot::init($config, $storage); 17 | } 18 | 19 | function testProviders() 20 | { 21 | $now = time(); 22 | $this->bot->set('http_client', $this->createHttpClientMock()); 23 | 24 | // 25 | $token = $this->bot->getTokenProvider()->getAccessToken(); 26 | $this->assertGreaterThan($now, $token->getExpiredTime()); 27 | $this->assertEquals($token->getToken(), 1); 28 | $this->assertEquals($this->bot->getTokenProvider()->getAccessToken()->getToken(), 1); 29 | $this->bot->getTokenProvider()->clearToken(); 30 | $this->assertNull($this->bot->getDataStorage()->get(\SkypeBot\DataProvider\TokenProvider::KEY_STORAGE)); 31 | 32 | // 33 | $config = $this->bot->getOpenIdConfigProvider()->getConfig(); 34 | $this->assertEquals($config->getIssuer(), 1); 35 | $this->assertEquals($config->getJwksUri(), 2); 36 | $this->assertEquals($config->getSigningAlg(), 4); 37 | 38 | // 39 | $keys = $this->bot->getOpenIdKeysProvider()->getKeys(); 40 | $this->assertEquals(count($keys), 1); 41 | $key = array_shift($keys); 42 | $this->assertEquals($key->getKeyType(), 1); 43 | $this->assertEquals($key->getPublicKeyUse(), 2); 44 | $this->assertEquals($key->getKeyId(), 3); 45 | $this->assertEquals($key->getModulus(), 5); 46 | $this->assertEquals($key->getCertificateChain(), 7); 47 | } 48 | 49 | protected function createHttpClientMock() 50 | { 51 | $httpClient = $this->getMockBuilder('\\SkypeBot\\Api\\HttpClient') 52 | ->disableOriginalConstructor() 53 | ->setMethods(['post', 'get']) 54 | ->getMock(); 55 | 56 | $tokenObj = new stdClass(); 57 | $tokenObj->access_token = 1; 58 | $tokenObj->expires_in = 2; 59 | $httpClient->expects($this->any()) 60 | ->method('post') 61 | ->will($this->returnValue(json_encode($tokenObj))); 62 | 63 | $configObj = new stdClass(); 64 | $configObj->issuer = 1; 65 | $configObj->jwks_uri = 2; 66 | $configObj->authorization_endpoint = 3; 67 | $configObj->id_token_signing_alg_values_supported = 4; 68 | $configObj->token_endpoint_auth_methods_supported = 5; 69 | $httpClient->expects($this->at(1)) 70 | ->method('get') 71 | ->will($this->returnValue(json_encode($configObj))); 72 | 73 | 74 | $jwk = new stdClass(); 75 | $jwk->kty = 1; 76 | $jwk->use = 2; 77 | $jwk->kid = 3; 78 | $jwk->x5t = 4; 79 | $jwk->n = 5; 80 | $jwk->e = 6; 81 | $jwk->x5c = 7; 82 | $httpClient->expects($this->at(2)) 83 | ->method('get') 84 | ->will($this->returnValue(json_encode(['keys' => [$jwk]]))); 85 | 86 | return $httpClient; 87 | } 88 | } -------------------------------------------------------------------------------- /tests/SkypeBot/DispatcherTest.php: -------------------------------------------------------------------------------- 1 | bot = \SkypeBot\SkypeBot::init($config, $storage); 17 | $securityMock = $this->getMock('\\SkypeBot\\Listener\\Security'); 18 | $securityMock->expects($this->any()) 19 | ->method('validateBearerHeader') 20 | ->will($this->returnValue(1)); 21 | 22 | $this->bot->set('security', $securityMock); 23 | 24 | $message = ' 25 | { 26 | "channelId":"skype", 27 | "serviceUrl":"https://apis.skype.com", 28 | "conversation": { 29 | "id":"111", 30 | "name":"Alice", 31 | "isGroup":true 32 | }, 33 | "id":"1234567890", 34 | "type":"message/text", 35 | "text":"hello" 36 | } 37 | '; 38 | 39 | $contact = ' 40 | { 41 | "channelId":"skype", 42 | "serviceUrl":"https://apis.skype.com", 43 | "from": { 44 | "id":"29:f2ca6a4a-93bd-434a-9ca5-5f81f8f9b455", 45 | "name":"Display Name" 46 | }, 47 | "recipient": { 48 | "id":"28:ad35d471-ae65-4626-af00-c01ffbfc581f", 49 | "name":"Trivia Master" 50 | }, 51 | "type":"activity/contactRelationUpdate", 52 | "action":"add" 53 | } 54 | '; 55 | 56 | $conversation = ' 57 | { 58 | "channelId":"skype", 59 | "serviceUrl":"https://apis.skype.com", 60 | "conversation": { 61 | "id":"cid", 62 | "isGroup":true 63 | }, 64 | "type":"activity/conversationUpdate", 65 | "membersAdded":["a"], 66 | "membersRemoved":["b"], 67 | "topicName": "new_name" 68 | } 69 | '; 70 | 71 | $headers = ['Authorization' => 'a']; 72 | $mock = $this->getMock('\\SkypeBot\\Listener\\Request'); 73 | $mock->expects($this->any()) 74 | ->method('getHeaders') 75 | ->will($this->returnValue($headers)); 76 | $mock->expects($this->at(1)) 77 | ->method('getRawBody') 78 | ->will($this->returnValue($message)); 79 | $mock->expects($this->at(4)) 80 | ->method('getRawBody') 81 | ->will($this->returnValue($contact)); 82 | $mock->expects($this->at(7)) 83 | ->method('getRawBody') 84 | ->will($this->returnValue($conversation)); 85 | 86 | $this->bot->set('request', $mock); 87 | $this->bot->getNotificationListener()->setApiLogger($this->getMock('\\SkypeBot\\Storage\\SimpleApiLogger', ['log'])); 88 | } 89 | 90 | function testHandlers() 91 | { 92 | $this->bot->getNotificationListener()->setMessageHandler(function($payload){ 93 | echo $payload->getText(); 94 | }); 95 | $this->bot->getNotificationListener()->setContactHandler(function($payload){ 96 | echo $payload->getAction(); 97 | }); 98 | $this->bot->getNotificationListener()->setConversationHandler(function($payload){ 99 | echo $payload->getMembersAdded()[0]; 100 | }); 101 | 102 | $this->bot->getNotificationListener()->dispatch(); 103 | $this->expectOutputString("hello"); 104 | $this->bot->getNotificationListener()->dispatch(); 105 | $this->expectOutputString("helloadd"); 106 | $this->bot->getNotificationListener()->dispatch(); 107 | $this->expectOutputString("helloadda"); 108 | } 109 | } -------------------------------------------------------------------------------- /tests/SkypeBot/EntityTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($message->getType(), \SkypeBot\Entity\Payload::TYPE_MESSAGE_TEXT); 33 | $this->assertEquals($message->getChannelId(), 'skype'); 34 | $this->assertEquals($message->getConversation()->getId(), 111); 35 | $this->assertEquals($message->getServiceUrl(), 'https://apis.skype.com'); 36 | $this->assertEquals($message->getFrom()->getId(), 'id_from'); 37 | $this->assertEquals($message->getRecipient()->getName(), 'recipient_name'); 38 | $this->assertEquals($message->getText(), 'hello'); 39 | $this->assertEquals($message->getConversation()->isGroup(), true); 40 | 41 | $this->assertEquals($message->getRaw(), $obj); 42 | } 43 | 44 | function testConversationUpdatePayload() 45 | { 46 | $obj = json_decode(' 47 | { 48 | "channelId":"skype", 49 | "serviceUrl":"https://apis.skype.com", 50 | "conversation": { 51 | "id":"cid", 52 | "isGroup":true 53 | }, 54 | "from": { 55 | "id":"fid", 56 | "name":"Display Name" 57 | }, 58 | "recipient": { 59 | "id":"rid", 60 | "name":"Trivia Master" 61 | }, 62 | "type":"activity/conversationUpdate", 63 | "membersAdded":["a"], 64 | "membersRemoved":["b"], 65 | "topicName": "new_name" 66 | } 67 | '); 68 | $update = \SkypeBot\Listener\PayloadFactory::createPayload($obj); 69 | $this->assertEquals($update->getMembersAdded(), ["a"]); 70 | $this->assertEquals($update->getMembersRemoved(), ["b"]); 71 | $this->assertEquals($update->getTopic(), 'new_name'); 72 | } 73 | 74 | function testContactUpdatePayload() 75 | { 76 | $obj = json_decode(' 77 | { 78 | "channelId":"skype", 79 | "serviceUrl":"https://apis.skype.com", 80 | "conversation": { 81 | "id":"29:f2ca6a4a-93bd-434a-9ca5-5f81f8f9b455", 82 | "name":"Display Name" 83 | }, 84 | "from": { 85 | "id":"29:f2ca6a4a-93bd-434a-9ca5-5f81f8f9b455", 86 | "name":"Display Name" 87 | }, 88 | "recipient": { 89 | "id":"28:ad35d471-ae65-4626-af00-c01ffbfc581f", 90 | "name":"Trivia Master" 91 | }, 92 | "type":"activity/contactRelationUpdate", 93 | "action":"add" 94 | } 95 | '); 96 | $update = \SkypeBot\Listener\PayloadFactory::createPayload($obj); 97 | $this->assertEquals($update->getAction(), \SkypeBot\Entity\ContactUpdatePayload::ACTION_ADD); 98 | } 99 | 100 | function testJWKPayload() 101 | { 102 | $obj = json_decode(' 103 | { 104 | "iss":"1", 105 | "aud":"2", 106 | "exp":"3", 107 | "nbf":"4" 108 | } 109 | '); 110 | $payload = new \SkypeBot\Entity\Jwk\JwkPayload($obj); 111 | $this->assertEquals($payload->getExpired(), 3); 112 | $this->assertEquals($payload->getNotBefore(), 4); 113 | } 114 | 115 | function testJWKInfo() 116 | { 117 | $obj = json_decode(' 118 | { 119 | "typ":"1", 120 | "alg":"2", 121 | "kid":"3", 122 | "x5t":"4" 123 | } 124 | '); 125 | $info = new \SkypeBot\Entity\Jwk\JwkInfo($obj); 126 | $this->assertEquals($info->getAlgorithm(), 2); 127 | $this->assertEquals($info->getKeyId(), 3); 128 | $this->assertEquals($info->getType(), 1); 129 | } 130 | 131 | function testResult() 132 | { 133 | $obj = json_decode(' 134 | { 135 | "error": {"message": "e"}, 136 | "id": "1" 137 | } 138 | '); 139 | $result = new \SkypeBot\Entity\Result($obj); 140 | $this->assertEquals($result->getError()->getMessage(), 'e'); 141 | $this->assertEquals($result->getId(), 1); 142 | } 143 | 144 | function testAttachmentFactory() 145 | { 146 | $attachment = \SkypeBot\Entity\AttachmentFactory::createMedia( 147 | \SkypeBot\Entity\AttachmentFactory::MEDIA_AUDIO, 148 | 'u', 149 | 'n' 150 | ); 151 | $this->assertEquals($attachment->getContentUrl(), 'u'); 152 | $this->assertEquals($attachment->getName(), 'n'); 153 | try { 154 | \SkypeBot\Entity\AttachmentFactory::createMedia( 155 | 'exception', 156 | 'u' 157 | ); 158 | } catch (\SkypeBot\Exception\PayloadException $ex) { 159 | $msg = $ex->getMessage(); 160 | } 161 | 162 | $this->assertNotNull($msg); 163 | } 164 | 165 | } -------------------------------------------------------------------------------- /tests/SkypeBot/ExceptionTest.php: -------------------------------------------------------------------------------- 1 | getMessage(); 15 | } 16 | $this->assertNotNull($msg); 17 | } 18 | } -------------------------------------------------------------------------------- /tests/SkypeBot/HttpClientTest.php: -------------------------------------------------------------------------------- 1 | setHeader('a', 'b'); 12 | $client->setHeader('c: d'); 13 | $res = $client->get('http://mockbin.com/request', ['c' => 'd']); 14 | $obj = json_decode($res); 15 | $this->assertEquals($obj->headers->a, 'b'); 16 | $this->assertEquals($obj->headers->c, 'd'); 17 | $this->assertEquals($obj->queryString->c, 'd'); 18 | 19 | $res = $client->get('http://mockbin.com/request?a=b'); 20 | $obj = json_decode($res); 21 | $this->assertEquals($obj->queryString->a, 'b'); 22 | $this->assertEquals($client->getReturnCode(), 200); 23 | 24 | $res = $client->get('http://mockbin.com/request?a=b&', ['c' => 'd']); 25 | $obj = json_decode($res); 26 | $this->assertEquals($obj->method, 'GET'); 27 | $this->assertEquals($obj->queryString->c, 'd'); 28 | $this->assertEquals($client->getReturnCode(), 200); 29 | } 30 | 31 | function testPost() 32 | { 33 | $client = \SkypeBot\Api\HttpClient::getInstance(); 34 | $res = $client->post('http://mockbin.com/request', ['c' => 'd']); 35 | $obj = json_decode($res); 36 | $this->assertEquals($obj->postData->params->c, 'd'); 37 | $this->assertEquals($obj->method, 'POST'); 38 | } 39 | 40 | function testDelete() 41 | { 42 | $client = \SkypeBot\Api\HttpClient::getInstance(); 43 | $client->setLogger(new \SkypeBot\Storage\SimpleApiLogger()); 44 | $res = $client->delete('http://mockbin.com/request'); 45 | $obj = json_decode($res); 46 | $this->assertEquals($obj->method, 'DELETE'); 47 | } 48 | 49 | function testPut() 50 | { 51 | $client = \SkypeBot\Api\HttpClient::getInstance(); 52 | $res = $client->put('http://mockbin.com/request', 'abc'); 53 | $obj = json_decode($res); 54 | 55 | $this->assertEquals($obj->method, 'PUT'); 56 | $this->assertEquals($obj->postData->text, 'abc'); 57 | } 58 | 59 | function testApi() 60 | { 61 | $api = new \SkypeBot\Api\Api( 62 | 'a' 63 | ); 64 | $api->setRequestMethod(\SkypeBot\Api\HttpClient::METHOD_GET); 65 | $this->assertEquals($api->getRequestMethod(), \SkypeBot\Api\HttpClient::METHOD_GET); 66 | $api->setRequestMethod('other'); 67 | $this->assertEquals($api->getRequestMethod(), \SkypeBot\Api\HttpClient::METHOD_POST); 68 | $api->setParam('a', 'b'); 69 | $api->setParams(['c' => 'd']); 70 | $params = $api->getRequestParams(); 71 | $this->assertEquals($params['a'], 'b'); 72 | $this->assertEquals($params['c'], 'd'); 73 | $api->setHeader('a', 'b'); 74 | $headers = $api->getRequestHeaders(); 75 | $this->assertEquals($headers['a'], 'b'); 76 | } 77 | } -------------------------------------------------------------------------------- /tests/SkypeBot/RequestTest.php: -------------------------------------------------------------------------------- 1 | assertEmpty($request->getRawBody()); 12 | $this->assertEmpty($request->getHeaders()); 13 | } 14 | } -------------------------------------------------------------------------------- /tests/SkypeBot/SecurityTest.php: -------------------------------------------------------------------------------- 1 | bot = \SkypeBot\SkypeBot::init($config, $storage); 17 | 18 | $k = new stdClass(); 19 | $k->kty = 'RSA'; 20 | $k->use = 'sig'; 21 | $k->kid = 'aaa'; 22 | $k->x5t = 'aaa'; 23 | $k->n = 'bbb'; 24 | $k->e = 'AQAB'; 25 | $k->x5c = ['MIIB0zCCAX2gAwIBAgIJAI3IleFgCVOrMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMTI0MTAwMTMyWhcNMTcxMTI0MTAwMTMyWjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALcrQ09cMPoQbi4pWhNgo88G7HnaYJaDp38w/cvHUC+qdHPSPmKK0EeJjj1PtbJvJucYRlruJvTwjmB+YXM+uTsCAwEAAaNQME4wHQYDVR0OBBYEFAJk3IpUH8iwul2rpV6ciWTNc1cwMB8GA1UdIwQYMBaAFAJk3IpUH8iwul2rpV6ciWTNc1cwMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADQQAMKN/HpL0NVQqPfGAfnO27cOK4qouP6/o62PhuqNZHsAt9dFqTaW9baj5d3NuNwLz8Cye/afAmYLLcdiQz6AFL']; 26 | 27 | $key = new \SkypeBot\Entity\Jwk\JsonWebKey($k); 28 | 29 | $keys = $this->getMockBuilder('\\SkypeBot\\DataProvider\\OpenIdKeysProvider') 30 | ->disableOriginalConstructor() 31 | ->getMock(); 32 | $keys->expects($this->any(0)) 33 | ->method('getKeyById') 34 | ->will($this->returnValue($key)); 35 | $keys->expects($this->any(1)) 36 | ->method('getKeyById') 37 | ->will($this->returnValue(null)); 38 | // $keys->expects($this->at(1)) 39 | // ->method('getKeyById') 40 | // ->will($this->returnValue(null)); 41 | $this->bot->set('openid_keys', $keys); 42 | } 43 | 44 | function testValidToken() 45 | { 46 | $security = new \SkypeBot\Listener\Security(); 47 | $res = $security->validateBearerHeader('Bearer ' . $this->buildToken()); 48 | $this->assertEquals(1, $res); 49 | } 50 | 51 | /** 52 | * @dataProvider exceptionList 53 | */ 54 | function testInvalidToken($header, $message) 55 | { 56 | $security = new \SkypeBot\Listener\Security(); 57 | try { 58 | $security->validateBearerHeader($header); 59 | } catch (\SkypeBot\Exception\SecurityException $ex) { 60 | $msg = $ex->getMessage(); 61 | } 62 | $this->assertEquals($msg, $message); 63 | } 64 | 65 | public function exceptionList() 66 | { 67 | return [ 68 | [ 69 | 'header' => '', 70 | 'message' => 'Authorization header should start with Bearer' 71 | ], 72 | [ 73 | 'header' => 'Bearer ', 74 | 'message' => 'Authenticate header is not valid format' 75 | ], 76 | [ 77 | 'header' => 'Bearer a', 78 | 'message' => 'Authenticate header is not valid format' 79 | ], 80 | [ 81 | 'header' => 'Bearer a.b', 82 | 'message' => 'Authenticate header is not valid format' 83 | ], 84 | [ 85 | 'header' => 'Bearer a.b.c', 86 | 'message' => 'Authenticate header key info part is not valid format' 87 | ], 88 | [ 89 | 'header' => 'Bearer '. 90 | base64_encode(' 91 | { 92 | "typ":"1", 93 | "alg":"RS2566", 94 | "kid":"3", 95 | "x5t":"4" 96 | } 97 | '). '.' . 98 | base64_encode(' 99 | { 100 | "iss":"1", 101 | "aud":"2", 102 | "exp": ' . (time() + 9999) . ', 103 | "nbf":"0" 104 | } 105 | ') . '.a', 106 | 'message' => 'Unsupported key type: RS2566' 107 | ] 108 | ]; 109 | } 110 | 111 | /** 112 | * @dataProvider invalidTimeToken 113 | */ 114 | function testInvalidTimeToken($header, $message) 115 | { 116 | $security = new \SkypeBot\Listener\Security(); 117 | try { 118 | $security->validateBearerHeader($header); 119 | } catch (\SkypeBot\Exception\SecurityException $ex) { 120 | $msg = $ex->getMessage(); 121 | } 122 | $this->assertEquals(0, strpos($msg, $message)); 123 | } 124 | 125 | public function invalidTimeToken() 126 | { 127 | return [ 128 | [ 129 | 'header' => 'Bearer '. 130 | base64_encode(' 131 | { 132 | "typ":"1", 133 | "alg":"RS256", 134 | "kid":"3", 135 | "x5t":"4" 136 | } 137 | '). '.' . 138 | base64_encode(' 139 | { 140 | "iss":"1", 141 | "aud":"2", 142 | "exp": "0", 143 | "nbf":"1" 144 | } 145 | ') . '.a', 146 | 'message' => 'Token expired at' 147 | ], 148 | [ 149 | 'header' => 'Bearer '. 150 | base64_encode(' 151 | { 152 | "typ":"1", 153 | "alg":"RS256", 154 | "kid":"3", 155 | "x5t":"4" 156 | } 157 | '). '.' . 158 | base64_encode(' 159 | { 160 | "iss":"1", 161 | "aud":"2", 162 | "exp": ' . (time() + 9999) . ', 163 | "nbf":"2222222222" 164 | } 165 | ') . '.a', 166 | 'message' => 'Token cannot use before' 167 | ] 168 | ]; 169 | } 170 | 171 | private function buildToken() 172 | { 173 | $payload = base64_encode(' 174 | { 175 | "iss":"1", 176 | "aud":"2", 177 | "exp": '. (time() + 99999999) .', 178 | "nbf":"1" 179 | } 180 | '); 181 | $info = base64_encode(' 182 | { 183 | "typ":"1", 184 | "alg":"RS256", 185 | "kid":"3", 186 | "x5t":"4" 187 | } 188 | '); 189 | $data = $info . '.' . $payload; 190 | $pkey = openssl_pkey_get_private('-----BEGIN ENCRYPTED PRIVATE KEY----- 191 | MIIBpjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIgYPeieA1PasCAggA 192 | MBQGCCqGSIb3DQMHBAhOIn83Un7MVgSCAWDCvEo4LoeyXLsg3L01qlJTnC9R9DYF 193 | yv2paxh8ejtnP+hvcycG1DjRj4oj2SpbRDomBoM06iAUrkg0BOKo8LYKV4dAUE8Q 194 | V2mH3tZm+qRURfDpI9ieZFl3iRb7cPNK/tXjQSt8BlETtCR59gassmMH5M443h6D 195 | ar/DCdoTw9nHc1U6ATN4mTmrnZXQ+vwEvPyvy5X/gk8A3ZZg5dVtRSpgSIw5ZS7L 196 | RaU+37Ag9RB5zKZpMxS6OTqDCginQ7Ii585DsQ7aX0IxuP72VDywa8Edn130JPUU 197 | IgDFi1w/p5nQ49cBlRZei5UP2OrDsSr4k6CGrqs6j/+COBWvgoz4Br/XU7NVzUHs 198 | wmMuZ7B6UoumJY9cMW1obfaXmD+GOXny4TsZni8L8+wo0AdM3S8hP7i48wgGL9jE 199 | M8Sb+LtG4UvbOTi+nzpdD0Z7TbKUDeX5MATIb+CmASawGra2Hc7zNStP 200 | -----END ENCRYPTED PRIVATE KEY-----', '1234'); 201 | 202 | openssl_sign($data, $signature, $pkey, 'SHA256'); 203 | return $data . '.' . base64_encode($signature); 204 | } 205 | } -------------------------------------------------------------------------------- /tests/SkypeBot/SkypeBotTest.php: -------------------------------------------------------------------------------- 1 | set('aa', 1); 14 | $this->assertEquals($bot->get('aa'), 1); 15 | $bot->set('bb', function (){ 16 | return 1; 17 | }); 18 | $this->assertEquals($bot->get('bb'), 1); 19 | } 20 | } -------------------------------------------------------------------------------- /tests/SkypeBot/StorageTest.php: -------------------------------------------------------------------------------- 1 | set('a', 'b'); 12 | $this->assertEquals($storage->get('a'), 'b'); 13 | $storage->remove('a'); 14 | $this->assertNull($storage->get('a')); 15 | } 16 | 17 | function testSimpleLogger() 18 | { 19 | $logger = new \SkypeBot\Storage\SimpleApiLogger(); 20 | $logger->log('a'); 21 | $this->expectOutputString("a\n"); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |