diff --git a/.styleci.yml b/.styleci.yml index a2f2088..247a09c 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -1,6 +1 @@ -preset: laravel - -enabled: - - unalign_double_arrow - -linting: true \ No newline at end of file +preset: psr2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 66f7971..7353ce3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -Contributions are **welcome** and will be fully **credited**. We accept contributions via Pull Requests on [Github](https://github.com/botman/driver-facebook). +Contributions are **welcome** and will be fully **credited**. We accept contributions via Pull Requests on [Github](https://github.com/botman/driver-telegram). ## Pull Requests @@ -10,7 +10,7 @@ Contributions are **welcome** and will be fully **credited**. We accept contribu - **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. +- **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](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. ## Running Tests @@ -19,4 +19,4 @@ $ phpunit ``` -*Happy coding!* \ No newline at end of file +*Happy coding!* diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..8f13de0 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,20 @@ + + + + +### Required Information + +- BotMan version: +- BotMan Telegram Driver version: + +### Expected behaviour + + +### Actual behaviour + + +### Steps to reproduce + + +### Extra details + diff --git a/README.md b/README.md index 64e44f9..51e4222 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ BotMan driver to connect Telegram with [BotMan](https://github.com/botman/botman Please see [CONTRIBUTING](CONTRIBUTING.md) for details. +[![](https://sourcerer.io/fame/feralheart/botman/driver-telegram/images/0)](https://sourcerer.io/fame/feralheart/botman/driver-telegram/links/0)[![](https://sourcerer.io/fame/feralheart/botman/driver-telegram/images/1)](https://sourcerer.io/fame/feralheart/botman/driver-telegram/links/1)[![](https://sourcerer.io/fame/feralheart/botman/driver-telegram/images/2)](https://sourcerer.io/fame/feralheart/botman/driver-telegram/links/2)[![](https://sourcerer.io/fame/feralheart/botman/driver-telegram/images/3)](https://sourcerer.io/fame/feralheart/botman/driver-telegram/links/3)[![](https://sourcerer.io/fame/feralheart/botman/driver-telegram/images/4)](https://sourcerer.io/fame/feralheart/botman/driver-telegram/links/4)[![](https://sourcerer.io/fame/feralheart/botman/driver-telegram/images/5)](https://sourcerer.io/fame/feralheart/botman/driver-telegram/links/5)[![](https://sourcerer.io/fame/feralheart/botman/driver-telegram/images/6)](https://sourcerer.io/fame/feralheart/botman/driver-telegram/links/6)[![](https://sourcerer.io/fame/feralheart/botman/driver-telegram/images/7)](https://sourcerer.io/fame/feralheart/botman/driver-telegram/links/7) + ## 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. diff --git a/discovery.json b/discovery.json index f824228..e371239 100644 --- a/discovery.json +++ b/discovery.json @@ -7,6 +7,7 @@ "BotMan\\Drivers\\Telegram\\TelegramAudioDriver", "BotMan\\Drivers\\Telegram\\TelegramFileDriver", "BotMan\\Drivers\\Telegram\\TelegramLocationDriver", + "BotMan\\Drivers\\Telegram\\TelegramContactDriver", "BotMan\\Drivers\\Telegram\\TelegramPhotoDriver", "BotMan\\Drivers\\Telegram\\TelegramVideoDriver" ], diff --git a/src/Exceptions/TelegramConnectionException.php b/src/Exceptions/TelegramConnectionException.php new file mode 100644 index 0000000..2c1e3a3 --- /dev/null +++ b/src/Exceptions/TelegramConnectionException.php @@ -0,0 +1,9 @@ +event->get('from')) && ! is_null($this->event->get('contact')); + } + + /** + * @return bool + */ + public function hasMatchingEvent() + { + return false; + } + + /** + * Retrieve the chat message. + * + * @return array + */ + public function getMessages() + { + if (empty($this->messages)) { + $this->loadMessages(); + } + + return $this->messages; + } + + /** + * Load Telegram messages. + */ + public function loadMessages() + { + $message = new IncomingMessage(Contact::PATTERN, $this->event->get('from')['id'], $this->event->get('chat')['id'], $this->event); + $message->setContact(new Contact( + $this->event->get('contact')['phone_number'], + $this->event->get('contact')['first_name'], + $this->event->get('contact')['last_name'], + $this->event->get('contact')['user_id'], + $this->event->get('contact')['vcard'] + )); + + $this->messages = [$message]; + } + + /** + * @return bool + */ + public function isConfigured() + { + return false; + } +} diff --git a/src/TelegramDriver.php b/src/TelegramDriver.php index 7306ab6..cb40f0e 100644 --- a/src/TelegramDriver.php +++ b/src/TelegramDriver.php @@ -2,6 +2,7 @@ namespace BotMan\Drivers\Telegram; +use BotMan\Drivers\Telegram\Exceptions\TelegramConnectionException; use Illuminate\Support\Collection; use BotMan\BotMan\Drivers\HttpDriver; use BotMan\BotMan\Messages\Incoming\Answer; @@ -10,11 +11,13 @@ use BotMan\BotMan\Messages\Attachments\Audio; use BotMan\BotMan\Messages\Attachments\Image; use BotMan\BotMan\Messages\Attachments\Video; +use BotMan\BotMan\Messages\Attachments\Contact; use BotMan\BotMan\Messages\Outgoing\Question; use Symfony\Component\HttpFoundation\Request; use BotMan\BotMan\Drivers\Events\GenericEvent; use Symfony\Component\HttpFoundation\Response; use BotMan\BotMan\Messages\Attachments\Location; +use BotMan\BotMan\Messages\Attachments\Contact; use Symfony\Component\HttpFoundation\ParameterBag; use BotMan\BotMan\Messages\Incoming\IncomingMessage; use BotMan\BotMan\Messages\Outgoing\OutgoingMessage; @@ -52,6 +55,10 @@ public function buildPayload(Request $request) if (empty($message)) { $message = $this->payload->get('edited_message'); } + if (empty($message)) { + $message = $this->payload->get('channel_post'); + $message['from'] = ['id' => 0]; + } $this->event = Collection::make($message); $this->config = Collection::make($this->config->get('telegram')); $this->queryParameters = Collection::make($request->query); @@ -61,6 +68,7 @@ public function buildPayload(Request $request) * @param IncomingMessage $matchingMessage * @return User * @throws TelegramException + * @throws TelegramConnectionException */ public function getUser(IncomingMessage $matchingMessage) { @@ -69,7 +77,11 @@ public function getUser(IncomingMessage $matchingMessage) 'user_id' => $matchingMessage->getSender(), ]; - $response = $this->http->post($this->buildApiUrl('getChatMember'), [], $parameters); + if ($this->config->get('throw_http_exceptions')) { + $response = $this->postWithExceptionHandling($this->buildApiUrl('getChatMember'), [], $parameters); + } else { + $response = $this->http->post($this->buildApiUrl('getChatMember'), [], $parameters); + } $responseData = json_decode($response->getContent(), true); @@ -79,8 +91,13 @@ public function getUser(IncomingMessage $matchingMessage) $userData = Collection::make($responseData['result']['user']); - return new User($userData->get('id'), $userData->get('first_name'), $userData->get('last_name'), - $userData->get('username'), $responseData['result']); + return new User( + $userData->get('id'), + $userData->get('first_name'), + $userData->get('last_name'), + $userData->get('username'), + $responseData['result'] + ); } /** @@ -91,7 +108,7 @@ public function getUser(IncomingMessage $matchingMessage) public function matchesRequest() { $noAttachments = $this->event->keys()->filter(function ($key) { - return in_array($key, ['audio', 'voice', 'video', 'photo', 'location', 'document']); + return in_array($key, ['audio', 'voice', 'video', 'photo', 'location', 'contact', 'document']); })->isEmpty(); return $noAttachments && (! is_null($this->event->get('from')) || ! is_null($this->payload->get('callback_query'))) && ! is_null($this->payload->get('update_id')); @@ -160,11 +177,14 @@ protected function isValidLoginRequest() public function messagesHandled() { $callback = $this->payload->get('callback_query'); + $hideInlineKeyboard = $this->config->get('hideInlineKeyboard', true); - if ($callback !== null) { + if ($callback !== null && $hideInlineKeyboard) { $callback['message']['chat']['id']; - $this->removeInlineKeyboard($callback['message']['chat']['id'], - $callback['message']['message_id']); + $this->removeInlineKeyboard( + $callback['message']['chat']['id'], + $callback['message']['message_id'] + ); } } @@ -209,8 +229,12 @@ public function loadMessages() $callback = Collection::make($this->payload->get('callback_query')); $messages = [ - new IncomingMessage($callback->get('data'), $callback->get('from')['id'], - $callback->get('message')['chat']['id'], $callback->get('message')), + new IncomingMessage( + $callback->get('data'), + $callback->get('from')['id'], + $callback->get('message')['chat']['id'], + $callback->get('message') + ), ]; } elseif ($this->isValidLoginRequest()) { $messages = [ @@ -218,8 +242,12 @@ public function loadMessages() ]; } else { $messages = [ - new IncomingMessage($this->event->get('text'), $this->event->get('from')['id'], $this->event->get('chat')['id'], - $this->event), + new IncomingMessage( + $this->event->get('text'), + $this->event->get('from')['id'], + $this->event->get('chat')['id'], + $this->event + ), ]; } @@ -245,6 +273,9 @@ public function types(IncomingMessage $matchingMessage) 'action' => 'typing', ]; + if ($this->config->get('throw_http_exceptions')) { + return $this->postWithExceptionHandling($this->buildApiUrl('sendChatAction'), [], $parameters); + } return $this->http->post($this->buildApiUrl('sendChatAction'), [], $parameters); } @@ -283,7 +314,9 @@ private function removeInlineKeyboard($chatId, $messageId) 'message_id' => $messageId, 'inline_keyboard' => [], ]; - + if ($this->config->get('throw_http_exceptions')) { + return $this->postWithExceptionHandling($this->buildApiUrl('editMessageReplyMarkup'), [], $parameters); + } return $this->http->post($this->buildApiUrl('editMessageReplyMarkup'), [], $parameters); } @@ -298,10 +331,11 @@ public function buildServicePayload($message, $matchingMessage, $additionalParam $this->endpoint = 'sendMessage'; $recipient = $matchingMessage->getRecipient() === '' ? $matchingMessage->getSender() : $matchingMessage->getRecipient(); + $defaultAdditionalParameters = $this->config->get('default_additional_parameters', []); $parameters = array_merge_recursive([ 'chat_id' => $recipient, - ], $additionalParameters); - + ], $additionalParameters + $defaultAdditionalParameters); + /* * If we send a Question with buttons, ignore * the text and append the question. @@ -343,6 +377,15 @@ public function buildServicePayload($message, $matchingMessage, $additionalParam if (isset($parameters['title'], $parameters['address'])) { $this->endpoint = 'sendVenue'; } + } elseif ($attachment instanceof Contact) { + $this->endpoint = 'sendContact'; + $parameters['phone_number'] = $attachment->getPhoneNumber(); + $parameters['first_name'] = $attachment->getFirstName(); + $parameters['last_name'] = $attachment->getLastName(); + $parameters['user_id'] = $attachment->getUserId(); + if (null !== $attachment->getVcard()) { + $parameters['vcard'] = $attachment->getVcard(); + } } } else { $parameters['text'] = $message->getText(); @@ -360,6 +403,9 @@ public function buildServicePayload($message, $matchingMessage, $additionalParam */ public function sendPayload($payload) { + if ($this->config->get('throw_http_exceptions')) { + return $this->postWithExceptionHandling($this->buildApiUrl($this->endpoint), [], $payload); + } return $this->http->post($this->buildApiUrl($this->endpoint), [], $payload); } @@ -385,6 +431,9 @@ public function sendRequest($endpoint, array $parameters, IncomingMessage $match 'chat_id' => $matchingMessage->getRecipient(), ], $parameters); + if ($this->config->get('throw_http_exceptions')) { + return $this->postWithExceptionHandling($this->buildApiUrl($endpoint), [], $parameters); + } return $this->http->post($this->buildApiUrl($endpoint), [], $parameters); } @@ -409,4 +458,54 @@ protected function buildFileApiUrl($endpoint) { return self::FILE_API_URL.$this->config->get('token').'/'.$endpoint; } + + /** + * @param $url + * @param array $urlParameters + * @param array $postParameters + * @param array $headers + * @param bool $asJSON + * @param int $retryCount + * @return Response + * @throws TelegramConnectionException + */ + private function postWithExceptionHandling( + $url, + array $urlParameters = [], + array $postParameters = [], + array $headers = [], + $asJSON = false, + int $retryCount = 0 + ) { + $response = $this->http->post($url, $urlParameters, $postParameters, $headers, $asJSON); + $responseData = json_decode($response->getContent(), true); + if ($response->isOk() && isset($responseData['ok']) && true === $responseData['ok']) { + return $response; + } elseif ($this->config->get('retry_http_exceptions') && $retryCount <= $this->config->get('retry_http_exceptions')) { + $retryCount++; + if ($response->getStatusCode() == 429 && isset($responseData['retry_after']) && is_numeric($responseData['retry_after'])) { + usleep($responseData['retry_after'] * 1000000); + } else { + $multiplier = $this->config->get('retry_http_exceptions_multiplier')??2; + usleep($retryCount*$multiplier* 1000000); + } + return $this->postWithExceptionHandling($url, $urlParameters, $postParameters, $headers, $asJSON, $retryCount); + } + $responseData['description'] = $responseData['description'] ?? 'No description from Telegram'; + $responseData['error_code'] = $responseData['error_code'] ?? 'No error code from Telegram'; + $responseData['parameters'] = $responseData['parameters'] ?? 'No parameters from Telegram'; + + + $message = "Status Code: {$response->getStatusCode()}\n". + "Description: ".print_r($responseData['description'], true)."\n". + "Error Code: ".print_r($responseData['error_code'], true)."\n". + "Parameters: ".print_r($responseData['parameters'], true)."\n". + "URL: $url\n". + "URL Parameters: ".print_r($urlParameters, true)."\n". + "Post Parameters: ".print_r($postParameters, true)."\n". + "Headers: ". print_r($headers, true)."\n"; + + $message = str_replace($this->config->get('token'), 'TELEGRAM-TOKEN-HIDDEN', $message); + throw new TelegramConnectionException($message); + } } diff --git a/tests/TelegramDriverTest.php b/tests/TelegramDriverTest.php index 6f530df..f38739b 100644 --- a/tests/TelegramDriverTest.php +++ b/tests/TelegramDriverTest.php @@ -2,6 +2,7 @@ namespace Tests; +use BotMan\Drivers\Telegram\Exceptions\TelegramConnectionException; use Mockery as m; use BotMan\BotMan\Http\Curl; use BotMan\BotMan\Users\User; @@ -17,6 +18,7 @@ use BotMan\BotMan\Drivers\Events\GenericEvent; use Symfony\Component\HttpFoundation\Response; use BotMan\BotMan\Messages\Attachments\Location; +use BotMan\BotMan\Messages\Attachments\Contact; use BotMan\BotMan\Messages\Outgoing\Actions\Button; use BotMan\Drivers\Telegram\Exceptions\TelegramException; @@ -28,6 +30,15 @@ class TelegramDriverTest extends PHPUnit_Framework_TestCase ], ]; + protected $telegramConfigWithDefaultAdditionalParameters = [ + 'telegram' => [ + 'token' => 'TELEGRAM-BOT-TOKEN', + 'default_additional_parameters' => [ + 'foo' => 'bar' + ] + ], + ]; + public function tearDown() { m::close(); @@ -626,7 +637,7 @@ public function it_returns_answer_from_interactive_messages() } /** @test */ - public function it_hides_keyboard() + public function it_hides_keyboard_by_default() { $responseData = [ 'update_id' => '1234567890', @@ -667,6 +678,49 @@ public function it_hides_keyboard() $driver->messagesHandled(); } + /** @test */ + public function it_does_not_hide_keyboard_if_configured() + { + $responseData = [ + 'update_id' => '1234567890', + 'callback_query' => [ + 'id' => '11717237123', + 'from' => [ + 'id' => 'from_id', + ], + 'message' => [ + 'message_id' => '123', + 'from' => [ + 'id' => 'from_id', + ], + 'chat' => [ + 'id' => '1234', + ], + 'date' => '1480369277', + 'text' => 'Telegram Text', + ], + 'data' => 'FooBar', + ], + ]; + + $html = m::mock(Curl::class); + $html->shouldNotReceive('post'); + + $request = m::mock(Request::class.'[getContent]'); + $request->shouldReceive('getContent')->andReturn(json_encode($responseData)); + + $telegramConfig = [ + 'telegram' => [ + 'token' => 'TELEGRAM-BOT-TOKEN', + 'hideInlineKeyboard' => false + ], + ]; + $driver = new TelegramDriver($request, $telegramConfig, $html); + + $driver->messagesHandled(); + } + + /** @test */ public function it_can_reply_string_messages() { @@ -851,6 +905,121 @@ public function it_can_reply_with_additional_parameters() ])); } + /** @test */ + public function it_can_reply_with_default_additional_parameters() + { + $responseData = [ + 'update_id' => '1234567890', + 'message' => [ + 'message_id' => '123', + 'from' => [ + 'id' => 'from_id', + ], + 'chat' => [ + 'id' => '12345', + ], + 'date' => '1480369277', + 'text' => 'Telegram Text', + ], + ]; + + $html = m::mock(Curl::class); + $html->shouldReceive('post') + ->once() + ->with('https://api.telegram.org/botTELEGRAM-BOT-TOKEN/sendMessage', [], [ + 'chat_id' => '12345', + 'text' => 'Test', + 'foo' => 'bar', + ]); + + $request = m::mock(\Symfony\Component\HttpFoundation\Request::class.'[getContent]'); + $request->shouldReceive('getContent')->andReturn(json_encode($responseData)); + + $driver = new TelegramDriver($request, $this->telegramConfigWithDefaultAdditionalParameters, $html); + + $message = $driver->getMessages()[0]; + $driver->sendPayload($driver->buildServicePayload('Test', $message)); + } + + /** @test */ + public function it_can_reply_with_default_additional_parameters_and_runtime_additional_parameters() + { + $responseData = [ + 'update_id' => '1234567890', + 'message' => [ + 'message_id' => '123', + 'from' => [ + 'id' => 'from_id', + ], + 'chat' => [ + 'id' => '12345', + ], + 'date' => '1480369277', + 'text' => 'Telegram Text', + ], + ]; + + $html = m::mock(Curl::class); + $html->shouldReceive('post') + ->once() + ->with('https://api.telegram.org/botTELEGRAM-BOT-TOKEN/sendMessage', [], [ + 'chat_id' => '12345', + 'text' => 'Test', + 'foo' => 'bar', + 'baz' => 'foo', + ]); + + $request = m::mock(\Symfony\Component\HttpFoundation\Request::class.'[getContent]'); + $request->shouldReceive('getContent')->andReturn(json_encode($responseData)); + + $driver = new TelegramDriver($request, $this->telegramConfigWithDefaultAdditionalParameters, $html); + + $message = $driver->getMessages()[0]; + $driver->sendPayload($driver->buildServicePayload('Test', $message, [ + 'baz' => 'foo', + ])); + } + + /** @test */ + public function it_can_reply_with_default_additional_parameters_overriden_by_runtime_additional_parameters() + { + $responseData = [ + 'update_id' => '1234567890', + 'message' => [ + 'message_id' => '123', + 'from' => [ + 'id' => 'from_id', + ], + 'chat' => [ + 'id' => '12345', + ], + 'date' => '1480369277', + 'text' => 'Telegram Text', + ], + ]; + + $html = m::mock(Curl::class); + $html->shouldReceive('post') + ->once() + ->with('https://api.telegram.org/botTELEGRAM-BOT-TOKEN/sendMessage', [], [ + 'chat_id' => '12345', + 'text' => 'Test', + 'foo' => 'baz', + 'baz' => 'foo', + ]); + + $request = m::mock(\Symfony\Component\HttpFoundation\Request::class.'[getContent]'); + $request->shouldReceive('getContent')->andReturn(json_encode($responseData)); + + $driver = new TelegramDriver($request, $this->telegramConfigWithDefaultAdditionalParameters, $html); + + $message = $driver->getMessages()[0]; + $driver->sendPayload($driver->buildServicePayload('Test', $message, [ + 'foo' => 'baz', + 'baz' => 'foo', + ])); + } + /** @test */ public function it_is_configured() { @@ -910,6 +1079,168 @@ public function it_can_reply_message_objects() $driver->sendPayload($driver->buildServicePayload(\BotMan\BotMan\Messages\Outgoing\OutgoingMessage::create('Test'), $message)); } + /** @test */ + public function it_does_not_throw_an_exception_when_a_message_cant_be_sent_and_not_configured() + { + $response = new Response('{ + "ok": false, + "error_code": 400, + "description": "Bad Request: can\'t parse entities: Can\'t find end of Italic entity at byte offset 10" +}', 400); + + + $htmlInterface = m::mock(Curl::class); + $htmlInterface->shouldReceive('post')->with('https://api.telegram.org/botTELEGRAM-BOT-TOKEN/sendMessage', [], [ + 'chat_id' => null, + 'parse_mode' => 'MarkdownV2', + 'text' => 'unparsable_string' + ])->once()->andReturn($response); + + $request = m::mock(\Symfony\Component\HttpFoundation\Request::class.'[getContent]'); + $request->shouldReceive('getContent')->andReturn($response); + + $driver = new TelegramDriver($request, $this->telegramConfig, $htmlInterface); + + $message = $driver->getMessages()[0]; + $throwable = null; + try { + $driver->sendPayload($driver->buildServicePayload( + \BotMan\BotMan\Messages\Outgoing\OutgoingMessage::create('unparsable_string'), + $message, + ['parse_mode' => 'MarkdownV2'] + )); + } catch (\Throwable $t) { + $throwable = $t; + throw $t; + } + $this->assertNull($throwable); + } + + /** @test */ + public function it_throws_an_exception_when_a_message_cant_be_sent_and_configured() + { + $response = new Response('{ + "ok": false, + "error_code": 400, + "description": "Bad Request: can\'t parse entities: Can\'t find end of Italic entity at byte offset 10" +}', 400); + + + $htmlInterface = m::mock(Curl::class); + $htmlInterface->shouldReceive('post')->with('https://api.telegram.org/botTELEGRAM-BOT-TOKEN/sendMessage', [], [ + 'chat_id' => null, + 'parse_mode' => 'MarkdownV2', + 'text' => 'unparsable_string' + ], [], false)->once()->andReturn($response); + + $request = m::mock(\Symfony\Component\HttpFoundation\Request::class.'[getContent]'); + $request->shouldReceive('getContent')->andReturn($response); + + $configurationWithHttpExceptions = $this->telegramConfig; + $configurationWithHttpExceptions['telegram']['throw_http_exceptions'] = true; + + $driver = new TelegramDriver($request, $configurationWithHttpExceptions, $htmlInterface); + + $message = $driver->getMessages()[0]; + $throwable = null; + try { + $driver->sendPayload($driver->buildServicePayload( + \BotMan\BotMan\Messages\Outgoing\OutgoingMessage::create('unparsable_string'), + $message, + ['parse_mode' => 'MarkdownV2'] + )); + } catch (\Throwable $t) { + $throwable = $t; + } + $this->assertNotNull($throwable); + $this->assertSame(TelegramConnectionException::class, get_class($throwable)); + $this->assertNotContains($configurationWithHttpExceptions['telegram']['token'], $throwable->getMessage()); + $this->assertContains('TELEGRAM-TOKEN-HIDDEN', $throwable->getMessage()); + } + + /** @test */ + public function it_retries_after_throwing_an_exception_when_a_message_cant_be_sent_and_configured() + { + $responseFailed = new Response('', 500); + $responseSucceeds = new Response('{"ok": true}', 200); + + $htmlInterface = m::mock(Curl::class); + $htmlInterface->shouldReceive('post')->with('https://api.telegram.org/botTELEGRAM-BOT-TOKEN/sendMessage', [], [ + 'chat_id' => null, + 'parse_mode' => 'MarkdownV2', + 'text' => 'a message' + ], [], false)->times(3)->andReturn(clone $responseFailed, clone $responseFailed, $responseSucceeds); + + $request = m::mock(\Symfony\Component\HttpFoundation\Request::class.'[getContent]'); + $request->shouldReceive('getContent')->andReturn($responseFailed); + + $configurationWithHttpExceptions = $this->telegramConfig; + $configurationWithHttpExceptions['telegram']['throw_http_exceptions'] = true; + $configurationWithHttpExceptions['telegram']['retry_http_exceptions'] = 5; + $configurationWithHttpExceptions['telegram']['retry_http_exceptions_multiplier'] = 0.1; // to keep the tests going at a reasonable speed. + + $driver = new TelegramDriver($request, $configurationWithHttpExceptions, $htmlInterface); + + + $message = $driver->getMessages()[0]; + $throwable = null; + $duration = 0.0; + try { + $start = microtime(true); + $driver->sendPayload($driver->buildServicePayload( + \BotMan\BotMan\Messages\Outgoing\OutgoingMessage::create('a message'), + $message, + ['parse_mode' => 'MarkdownV2'] + )); + $duration = microtime(true) - $start; + } catch (\Throwable $t) { + $throwable = $t; + } + $this->assertNull($throwable); + $this->assertGreaterThanOrEqual(0.1+0.2, $duration); + } + + /** @test */ + public function it_respects_the_back_off_in_a_429_response() + { + $response429 = new Response('{"retry_after": 1.5}', 429); + $responseSucceeds = new Response('{"ok": true}', 200); + + $htmlInterface = m::mock(Curl::class); + $htmlInterface->shouldReceive('post')->with('https://api.telegram.org/botTELEGRAM-BOT-TOKEN/sendMessage', [], [ + 'chat_id' => null, + 'parse_mode' => 'MarkdownV2', + 'text' => 'a message' + ], [], false)->times(2)->andReturn($response429, $responseSucceeds); + + $request = m::mock(\Symfony\Component\HttpFoundation\Request::class.'[getContent]'); + $request->shouldReceive('getContent')->andReturn($response429); + + $configurationWithHttpExceptions = $this->telegramConfig; + $configurationWithHttpExceptions['telegram']['throw_http_exceptions'] = true; + $configurationWithHttpExceptions['telegram']['retry_http_exceptions'] = 5; + + $driver = new TelegramDriver($request, $configurationWithHttpExceptions, $htmlInterface); + + + $message = $driver->getMessages()[0]; + $throwable = null; + $duration = 0.0; + try { + $start = microtime(true); + $driver->sendPayload($driver->buildServicePayload( + \BotMan\BotMan\Messages\Outgoing\OutgoingMessage::create('a message'), + $message, + ['parse_mode' => 'MarkdownV2'] + )); + $duration = microtime(true) - $start; + } catch (\Throwable $t) { + $throwable = $t; + } + $this->assertNull($throwable); + $this->assertGreaterThanOrEqual(1.5, $duration); + } + /** @test */ public function it_can_reply_message_objects_with_image() { @@ -1051,8 +1382,10 @@ public function it_can_reply_message_objects_with_audio() $driver = new TelegramDriver($request, $this->telegramConfig, $html); $message = $driver->getMessages()[0]; - $driver->sendPayload($driver->buildServicePayload(\BotMan\BotMan\Messages\Outgoing\OutgoingMessage::create('Test', - Audio::url('http://image.url/foo.mp3')), $message)); + $driver->sendPayload($driver->buildServicePayload(\BotMan\BotMan\Messages\Outgoing\OutgoingMessage::create( + 'Test', + Audio::url('http://image.url/foo.mp3') + ), $message)); } /** @test */ @@ -1128,6 +1461,140 @@ public function it_can_reply_message_objects_with_location() $driver->sendPayload($driver->buildServicePayload(\BotMan\BotMan\Messages\Outgoing\OutgoingMessage::create('Test', new Location('123', '321')), $message)); } + /** @test */ + public function it_can_reply_message_objects_with_contact() + { + $responseData = [ + 'update_id' => '1234567890', + 'message' => [ + 'message_id' => '123', + 'from' => [ + 'id' => 'from_id', + ], + 'chat' => [ + 'id' => '12345', + ], + 'date' => '1480369277', + 'text' => 'Telegram Text', + ], + ]; + + $html = m::mock(Curl::class); + $html->shouldReceive('post') + ->once() + ->with('https://api.telegram.org/botTELEGRAM-BOT-TOKEN/sendContact', [], [ + 'chat_id' => '12345', + 'phone_number' => '0775269856', + 'first_name' => 'Daniele', + 'first_name' => 'Rapisarda', + 'user_id' => '123', + 'caption' => 'Test', + ]); + + $request = m::mock(\Symfony\Component\HttpFoundation\Request::class.'[getContent]'); + $request->shouldReceive('getContent')->andReturn(json_encode($responseData)); + + $driver = new TelegramDriver($request, $this->telegramConfig, $html); + + $message = $driver->getMessages()[0]; + $driver->sendPayload($driver->buildServicePayload(\BotMan\BotMan\Messages\Outgoing\OutgoingMessage::create('Test', new Contact('0775269856', 'Daniele', 'Rapisarda', '123')), $message)); + } + + /** @test */ + public function it_can_reply_message_objects_with_contact() + { + $responseData = [ + 'update_id' => '1234567890', + 'message' => [ + 'message_id' => '123', + 'from' => [ + 'id' => 'from_id', + ], + 'chat' => [ + 'id' => '12345', + ], + 'date' => '1480369277', + 'text' => 'Telegram Text', + ], + ]; + + $html = m::mock(Curl::class); + $html->shouldReceive('post') + ->once() + ->with('https://api.telegram.org/botTELEGRAM-BOT-TOKEN/sendContact', [], [ + 'chat_id' => '12345', + 'phone_number' => '0775269856', + 'first_name' => 'Daniele', + 'last_name' => 'Rapisarda', + 'user_id' => '123', + 'caption' => 'Test' + ]); + + $request = m::mock(\Symfony\Component\HttpFoundation\Request::class.'[getContent]'); + $request->shouldReceive('getContent')->andReturn(json_encode($responseData)); + + $driver = new TelegramDriver($request, $this->telegramConfig, $html); + + $message = $driver->getMessages()[0]; + $driver->sendPayload($driver->buildServicePayload(\BotMan\BotMan\Messages\Outgoing\OutgoingMessage::create('Test', new Contact('0775269856', 'Daniele', 'Rapisarda', '123')), $message)); + } + + /** @test */ + public function it_can_reply_message_objects_with_contact_with_vcard() + { + $responseData = [ + 'update_id' => '1234567890', + 'message' => [ + 'message_id' => '123', + 'from' => [ + 'id' => 'from_id', + ], + 'chat' => [ + 'id' => '12345', + ], + 'date' => '1480369277', + 'text' => 'Telegram Text', + ], + ]; + + $vcard = 'BEGIN:VCARD +VERSION:4.0 +N:Gump;Forrest;;Mr.; +FN:Forrest Gump +ORG:Bubba Gump Shrimp Co. +TITLE:Shrimp Man +PHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif +TEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212 +TEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212 +ADR;TYPE=WORK;PREF=1;LABEL="100 Waters Edge\nBaytown\, LA 30314\nUnited States of America":;;100 Waters Edge;Baytown;LA;30314;United States of America +ADR;TYPE=HOME;LABEL="42 Plantation St.\nBaytown\, LA 30314\nUnited States of America":;;42 Plantation St.;Baytown;LA;30314;United States of America +EMAIL:forrestgump@example.com +REV:20080424T195243Z +x-qq:21588891 +END:VCARD'; + + $html = m::mock(Curl::class); + $html->shouldReceive('post') + ->once() + ->with('https://api.telegram.org/botTELEGRAM-BOT-TOKEN/sendContact', [], [ + 'chat_id' => '12345', + 'phone_number' => '0775269856', + 'first_name' => 'Daniele', + 'last_name' => 'Rapisarda', + 'user_id' => '123', + 'caption' => 'Test', + 'vcard' => $vcard, + ]); + + $request = m::mock(\Symfony\Component\HttpFoundation\Request::class.'[getContent]'); + $request->shouldReceive('getContent')->andReturn(json_encode($responseData)); + + $driver = new TelegramDriver($request, $this->telegramConfig, $html); + + $message = $driver->getMessages()[0]; + $driver->sendPayload($driver->buildServicePayload(\BotMan\BotMan\Messages\Outgoing\OutgoingMessage::create('Test', new Contact('0775269856', 'Daniele', 'Rapisarda', '123', $vcard)), $message)); + } + /** @test */ public function it_throws_exception_in_get_user() { @@ -1154,11 +1621,14 @@ public function it_throws_exception_in_get_user() ], ], $htmlInterface); + $throwable = null; try { $driver->getUser($driver->getMessages()[0]); } catch (\Throwable $t) { - $this->assertSame(TelegramException::class, get_class($t)); + $throwable = $t; } + $this->assertNotNull($throwable); + $this->assertSame(TelegramException::class, get_class($throwable)); } /** @test */