From e16d9ca416fcdc9c373a1b155b9fe521f99b6b1b Mon Sep 17 00:00:00 2001 From: moznion Date: Thu, 29 Sep 2016 10:58:26 +0900 Subject: [PATCH] Support the latest LINE Messaging API --- .gitignore | 8 +- .travis.yml | 12 +- CONTRIBUTING.md | 6 +- LICENSE | 4 +- Makefile | 15 +- README.md | 222 ++++--- composer.json | 7 +- devtool/check_copyright.sh | 24 + examples/EchoBot/.gitignore | 5 +- examples/EchoBot/LICENSE | 4 +- examples/EchoBot/README.md | 12 +- examples/EchoBot/composer.json | 11 +- examples/EchoBot/public/index.php | 19 +- .../src/LINEBot/EchoBot/Dependency.php | 50 ++ .../EchoBot/src/LINEBot/EchoBot/Route.php | 79 +++ .../EchoBot/src/LINEBot/EchoBot/Setting.php | 43 ++ examples/EchoBot/src/dependencies.php | 39 -- examples/EchoBot/src/routes.php | 133 ----- examples/EchoBot/src/settings.php | 32 - examples/KitchenSink/.gitignore | 4 + examples/KitchenSink/LICENSE | 201 +++++++ examples/KitchenSink/README.md | 68 +++ examples/KitchenSink/composer.json | 19 + examples/KitchenSink/public/.htaccess | 11 + examples/KitchenSink/public/index.php | 32 + .../public/static/buttons/1040.jpg | Bin 0 -> 64304 bytes examples/KitchenSink/public/static/rich/1040 | Bin 0 -> 64304 bytes examples/KitchenSink/public/static/rich/240 | Bin 0 -> 13828 bytes examples/KitchenSink/public/static/rich/300 | Bin 0 -> 16689 bytes examples/KitchenSink/public/static/rich/460 | Bin 0 -> 24832 bytes examples/KitchenSink/public/static/rich/700 | Bin 0 -> 39354 bytes examples/KitchenSink/run.sh | 17 + .../src/LINEBot/KitchenSink/Dependency.php | 50 ++ .../src/LINEBot/KitchenSink/EventHandler.php | 9 +- .../EventHandler/BeaconEventHandler.php | 54 ++ .../EventHandler/FollowEventHandler.php | 51 ++ .../EventHandler/JoinEventHandler.php | 54 ++ .../EventHandler/LeaveEventHandler.php | 54 ++ .../MessageHandler/AudioMessageHandler.php | 77 +++ .../MessageHandler/ImageMessageHandler.php | 75 +++ .../MessageHandler/LocationMessageHandler.php | 61 ++ .../MessageHandler/StickerMessageHandler.php | 55 ++ .../MessageHandler/TextMessageHandler.php | 195 +++++++ .../MessageHandler/Util/UrlBuilder.php | 40 +- .../MessageHandler/VideoMessageHandler.php | 76 +++ .../EventHandler/PostbackEventHandler.php | 54 ++ .../EventHandler/UnfollowEventHandler.php | 55 ++ .../src/LINEBot/KitchenSink/Route.php | 129 ++++ .../src/LINEBot/KitchenSink/Setting.php | 43 ++ examples/SendingSample/sample.php | 71 --- examples/SendingSample/settings.php | 8 - phpunit.xml | 1 + src/LINEBot.php | 294 +++------- .../ActionType.php} | 11 +- .../EventSourceType.php} | 14 +- .../Constant/{OpType.php => HTTPHeader.php} | 9 +- .../{EventType.php => MessageType.php} | 18 +- .../Constant/{RecipientType.php => Meta.php} | 8 +- src/LINEBot/Constant/TemplateType.php | 26 + src/LINEBot/Event/BaseEvent.php | 153 +++++ .../BeaconDetectionEvent.php} | 48 +- src/LINEBot/Event/FollowEvent.php | 37 ++ src/LINEBot/Event/JoinEvent.php | 37 ++ src/LINEBot/Event/LeaveEvent.php | 37 ++ .../MessageEvent.php} | 47 +- .../Event/MessageEvent/AudioMessage.php | 39 ++ .../Event/MessageEvent/ImageMessage.php | 39 ++ .../Event/MessageEvent/LocationMessage.php | 79 +++ .../Event/MessageEvent/StickerMessage.php | 59 ++ .../MessageEvent/TextMessage.php} | 51 +- .../Event/MessageEvent/VideoMessage.php | 39 ++ .../Event/Parser/EventRequestParser.php | 110 ++++ .../PostbackEvent.php} | 35 +- src/LINEBot/Event/UnfollowEvent.php | 37 ++ ...ception.php => CurlExecutionException.php} | 11 +- .../InvalidEventRequestException.php | 28 + ...on.php => InvalidEventSourceException.php} | 11 +- .../Exception/InvalidSignatureException.php | 8 +- ...tion.php => UnknownEventTypeException.php} | 11 +- .../Exception/UnknownMessageTypeException.php | 28 + .../UnsupportedContentTypeException.php | 21 - .../UnsupportedEventTypeException.php | 21 - .../UnsupportedOperationTypeException.php | 21 - src/LINEBot/{HTTPClient => }/HTTPClient.php | 35 +- src/LINEBot/HTTPClient/Curl.php | 97 ++++ src/LINEBot/HTTPClient/CurlHTTPClient.php | 115 ++++ src/LINEBot/HTTPClient/GuzzleHTTPClient.php | 179 ------ ...tentType.php => ImagemapActionBuilder.php} | 27 +- .../ImagemapActionBuilder/AreaBuilder.php | 67 +++ .../ImagemapMessageActionBuilder.php | 61 ++ .../ImagemapUriActionBuilder.php | 59 ++ .../Message/Builder/MessageBuilder.php | 137 ----- .../Builder/MultipleMessagesBuilder.php | 39 -- .../Message/Builder/RichMessageBuilder.php | 46 -- src/LINEBot/Message/MultipleMessages.php | 120 ---- src/LINEBot/Message/RichMessage/Markup.php | 152 ----- .../BotAPIChannel.php => MessageBuilder.php} | 22 +- .../MessageBuilder/AudioMessageBuilder.php | 62 ++ .../MessageBuilder/ImageMessageBuilder.php | 63 ++ .../Imagemap/BaseSizeBuilder.php | 57 ++ .../MessageBuilder/ImagemapMessageBuilder.php | 87 +++ .../MessageBuilder/LocationMessageBuilder.php | 73 +++ .../MessageBuilder/MultiMessageBuilder.php | 56 ++ .../MessageBuilder/StickerMessageBuilder.php | 63 ++ .../MessageBuilder/TemplateBuilder.php | 34 ++ .../TemplateBuilder/ButtonTemplateBuilder.php | 86 +++ .../CarouselColumnTemplateBuilder.php | 84 +++ .../CarouselTemplateBuilder.php | 70 +++ .../ConfirmTemplateBuilder.php | 76 +++ .../MessageBuilder/TemplateMessageBuilder.php | 62 ++ .../MessageBuilder/TextMessageBuilder.php | 67 +++ .../MessageBuilder/VideoMessageBuilder.php | 63 ++ src/LINEBot/Receive/Message.php | 88 --- src/LINEBot/Receive/Message/Audio.php | 50 -- src/LINEBot/Receive/Message/Contact.php | 63 -- src/LINEBot/Receive/Message/Location.php | 78 --- .../Receive/Message/MessageReceiveFactory.php | 55 -- src/LINEBot/Receive/Message/Sticker.php | 73 --- src/LINEBot/Receive/Message/Text.php | 55 -- src/LINEBot/Receive/Message/Video.php | 50 -- src/LINEBot/Receive/Operation/AddContact.php | 50 -- .../Receive/Operation/BlockContact.php | 50 -- .../Operation/OperationReceiveFactory.php | 44 -- src/LINEBot/Receive/Receive.php | 64 -- src/LINEBot/Receive/ReceiveFactory.php | 83 --- src/LINEBot/Response.php | 84 +++ src/LINEBot/Response/SucceededResponse.php | 57 -- src/LINEBot/SignatureValidator.php | 26 +- src/LINEBot/TemplateActionBuilder.php | 34 ++ .../MessageTemplateActionBuilder.php | 61 ++ .../PostbackTemplateActionBuilder.php | 61 ++ .../UriTemplateActionBuilder.php | 61 ++ tests/LINEBot/EventRequestParserTest.php | 311 ++++++++++ tests/LINEBot/GetProfileTest.php | 52 ++ tests/LINEBot/GettingContentsTest.php | 81 --- tests/LINEBot/MessageSendingTest.php | 549 ------------------ tests/LINEBot/MultipleMessagesSendingTest.php | 165 ------ tests/LINEBot/ReceiveFactoryTest.php | 108 ---- tests/LINEBot/RichMessageSendingTest.php | 179 ------ tests/LINEBot/SendAudioTest.php | 79 +++ tests/LINEBot/SendImageTest.php | 79 +++ tests/LINEBot/SendImagemapTest.php | 146 +++++ tests/LINEBot/SendLocationTest.php | 83 +++ tests/LINEBot/SendMultiMessageTest.php | 88 +++ tests/LINEBot/SendStickerTest.php | 73 +++ tests/LINEBot/SendTemplateTest.php | 150 +++++ tests/LINEBot/SendTextTest.php | 152 +++++ tests/LINEBot/SendVideoTest.php | 79 +++ tests/LINEBot/SignatureValidationTest.php | 110 ---- tests/LINEBot/SignatureValidatorTest.php | 190 ++++++ tests/LINEBot/Util/DummyHttpClient.php | 57 ++ tests/LINEBotTest.php | 73 --- tests/bootstrap.php | 6 +- 153 files changed, 6115 insertions(+), 3717 deletions(-) create mode 100644 devtool/check_copyright.sh create mode 100644 examples/EchoBot/src/LINEBot/EchoBot/Dependency.php create mode 100644 examples/EchoBot/src/LINEBot/EchoBot/Route.php create mode 100644 examples/EchoBot/src/LINEBot/EchoBot/Setting.php delete mode 100644 examples/EchoBot/src/dependencies.php delete mode 100644 examples/EchoBot/src/routes.php delete mode 100644 examples/EchoBot/src/settings.php create mode 100644 examples/KitchenSink/.gitignore create mode 100644 examples/KitchenSink/LICENSE create mode 100644 examples/KitchenSink/README.md create mode 100644 examples/KitchenSink/composer.json create mode 100644 examples/KitchenSink/public/.htaccess create mode 100644 examples/KitchenSink/public/index.php create mode 100644 examples/KitchenSink/public/static/buttons/1040.jpg create mode 100644 examples/KitchenSink/public/static/rich/1040 create mode 100644 examples/KitchenSink/public/static/rich/240 create mode 100644 examples/KitchenSink/public/static/rich/300 create mode 100644 examples/KitchenSink/public/static/rich/460 create mode 100644 examples/KitchenSink/public/static/rich/700 create mode 100755 examples/KitchenSink/run.sh create mode 100644 examples/KitchenSink/src/LINEBot/KitchenSink/Dependency.php rename src/LINEBot/Exception/LINEBotAPIException.php => examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler.php (81%) create mode 100644 examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/BeaconEventHandler.php create mode 100644 examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/FollowEventHandler.php create mode 100644 examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/JoinEventHandler.php create mode 100644 examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/LeaveEventHandler.php create mode 100644 examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/AudioMessageHandler.php create mode 100644 examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/ImageMessageHandler.php create mode 100644 examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/LocationMessageHandler.php create mode 100644 examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/StickerMessageHandler.php create mode 100644 examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/TextMessageHandler.php rename src/LINEBot/Receive/Operation.php => examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/Util/UrlBuilder.php (52%) create mode 100644 examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/VideoMessageHandler.php create mode 100644 examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/PostbackEventHandler.php create mode 100644 examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/UnfollowEventHandler.php create mode 100644 examples/KitchenSink/src/LINEBot/KitchenSink/Route.php create mode 100644 examples/KitchenSink/src/LINEBot/KitchenSink/Setting.php delete mode 100644 examples/SendingSample/sample.php delete mode 100644 examples/SendingSample/settings.php rename src/LINEBot/{Exception/ContentsDownloadingFailedException.php => Constant/ActionType.php} (76%) rename src/LINEBot/{Response/Response.php => Constant/EventSourceType.php} (76%) rename src/LINEBot/Constant/{OpType.php => HTTPHeader.php} (84%) rename src/LINEBot/Constant/{EventType.php => MessageType.php} (67%) rename src/LINEBot/Constant/{RecipientType.php => Meta.php} (87%) create mode 100644 src/LINEBot/Constant/TemplateType.php create mode 100644 src/LINEBot/Event/BaseEvent.php rename src/LINEBot/{Response/FailedResponse.php => Event/BeaconDetectionEvent.php} (53%) create mode 100644 src/LINEBot/Event/FollowEvent.php create mode 100644 src/LINEBot/Event/JoinEvent.php create mode 100644 src/LINEBot/Event/LeaveEvent.php rename src/LINEBot/{DownloadedContents.php => Event/MessageEvent.php} (50%) create mode 100644 src/LINEBot/Event/MessageEvent/AudioMessage.php create mode 100644 src/LINEBot/Event/MessageEvent/ImageMessage.php create mode 100644 src/LINEBot/Event/MessageEvent/LocationMessage.php create mode 100644 src/LINEBot/Event/MessageEvent/StickerMessage.php rename src/LINEBot/{Receive/Message/Image.php => Event/MessageEvent/TextMessage.php} (51%) create mode 100644 src/LINEBot/Event/MessageEvent/VideoMessage.php create mode 100644 src/LINEBot/Event/Parser/EventRequestParser.php rename src/LINEBot/{Response/ResponseFactory.php => Event/PostbackEvent.php} (54%) create mode 100644 src/LINEBot/Event/UnfollowEvent.php rename src/LINEBot/Exception/{JSONDecodingException.php => CurlExecutionException.php} (74%) create mode 100644 src/LINEBot/Exception/InvalidEventRequestException.php rename src/LINEBot/Exception/{JSONEncodingException.php => InvalidEventSourceException.php} (73%) rename src/LINEBot/Exception/{IllegalRichMessageHeightException.php => UnknownEventTypeException.php} (74%) create mode 100644 src/LINEBot/Exception/UnknownMessageTypeException.php delete mode 100644 src/LINEBot/Exception/UnsupportedContentTypeException.php delete mode 100644 src/LINEBot/Exception/UnsupportedEventTypeException.php delete mode 100644 src/LINEBot/Exception/UnsupportedOperationTypeException.php rename src/LINEBot/{HTTPClient => }/HTTPClient.php (55%) create mode 100644 src/LINEBot/HTTPClient/Curl.php create mode 100644 src/LINEBot/HTTPClient/CurlHTTPClient.php delete mode 100644 src/LINEBot/HTTPClient/GuzzleHTTPClient.php rename src/LINEBot/{Constant/ContentType.php => ImagemapActionBuilder.php} (62%) create mode 100644 src/LINEBot/ImagemapActionBuilder/AreaBuilder.php create mode 100644 src/LINEBot/ImagemapActionBuilder/ImagemapMessageActionBuilder.php create mode 100644 src/LINEBot/ImagemapActionBuilder/ImagemapUriActionBuilder.php delete mode 100644 src/LINEBot/Message/Builder/MessageBuilder.php delete mode 100644 src/LINEBot/Message/Builder/MultipleMessagesBuilder.php delete mode 100644 src/LINEBot/Message/Builder/RichMessageBuilder.php delete mode 100644 src/LINEBot/Message/MultipleMessages.php delete mode 100644 src/LINEBot/Message/RichMessage/Markup.php rename src/LINEBot/{Constant/BotAPIChannel.php => MessageBuilder.php} (64%) create mode 100644 src/LINEBot/MessageBuilder/AudioMessageBuilder.php create mode 100644 src/LINEBot/MessageBuilder/ImageMessageBuilder.php create mode 100644 src/LINEBot/MessageBuilder/Imagemap/BaseSizeBuilder.php create mode 100644 src/LINEBot/MessageBuilder/ImagemapMessageBuilder.php create mode 100644 src/LINEBot/MessageBuilder/LocationMessageBuilder.php create mode 100644 src/LINEBot/MessageBuilder/MultiMessageBuilder.php create mode 100644 src/LINEBot/MessageBuilder/StickerMessageBuilder.php create mode 100644 src/LINEBot/MessageBuilder/TemplateBuilder.php create mode 100644 src/LINEBot/MessageBuilder/TemplateBuilder/ButtonTemplateBuilder.php create mode 100644 src/LINEBot/MessageBuilder/TemplateBuilder/CarouselColumnTemplateBuilder.php create mode 100644 src/LINEBot/MessageBuilder/TemplateBuilder/CarouselTemplateBuilder.php create mode 100644 src/LINEBot/MessageBuilder/TemplateBuilder/ConfirmTemplateBuilder.php create mode 100644 src/LINEBot/MessageBuilder/TemplateMessageBuilder.php create mode 100644 src/LINEBot/MessageBuilder/TextMessageBuilder.php create mode 100644 src/LINEBot/MessageBuilder/VideoMessageBuilder.php delete mode 100644 src/LINEBot/Receive/Message.php delete mode 100644 src/LINEBot/Receive/Message/Audio.php delete mode 100644 src/LINEBot/Receive/Message/Contact.php delete mode 100644 src/LINEBot/Receive/Message/Location.php delete mode 100644 src/LINEBot/Receive/Message/MessageReceiveFactory.php delete mode 100644 src/LINEBot/Receive/Message/Sticker.php delete mode 100644 src/LINEBot/Receive/Message/Text.php delete mode 100644 src/LINEBot/Receive/Message/Video.php delete mode 100644 src/LINEBot/Receive/Operation/AddContact.php delete mode 100644 src/LINEBot/Receive/Operation/BlockContact.php delete mode 100644 src/LINEBot/Receive/Operation/OperationReceiveFactory.php delete mode 100644 src/LINEBot/Receive/Receive.php delete mode 100644 src/LINEBot/Receive/ReceiveFactory.php create mode 100644 src/LINEBot/Response.php delete mode 100644 src/LINEBot/Response/SucceededResponse.php create mode 100644 src/LINEBot/TemplateActionBuilder.php create mode 100644 src/LINEBot/TemplateActionBuilder/MessageTemplateActionBuilder.php create mode 100644 src/LINEBot/TemplateActionBuilder/PostbackTemplateActionBuilder.php create mode 100644 src/LINEBot/TemplateActionBuilder/UriTemplateActionBuilder.php create mode 100644 tests/LINEBot/EventRequestParserTest.php create mode 100644 tests/LINEBot/GetProfileTest.php delete mode 100644 tests/LINEBot/GettingContentsTest.php delete mode 100644 tests/LINEBot/MessageSendingTest.php delete mode 100644 tests/LINEBot/MultipleMessagesSendingTest.php delete mode 100644 tests/LINEBot/ReceiveFactoryTest.php delete mode 100644 tests/LINEBot/RichMessageSendingTest.php create mode 100644 tests/LINEBot/SendAudioTest.php create mode 100644 tests/LINEBot/SendImageTest.php create mode 100644 tests/LINEBot/SendImagemapTest.php create mode 100644 tests/LINEBot/SendLocationTest.php create mode 100644 tests/LINEBot/SendMultiMessageTest.php create mode 100644 tests/LINEBot/SendStickerTest.php create mode 100644 tests/LINEBot/SendTemplateTest.php create mode 100644 tests/LINEBot/SendTextTest.php create mode 100644 tests/LINEBot/SendVideoTest.php delete mode 100644 tests/LINEBot/SignatureValidationTest.php create mode 100644 tests/LINEBot/SignatureValidatorTest.php create mode 100644 tests/LINEBot/Util/DummyHttpClient.php delete mode 100644 tests/LINEBotTest.php diff --git a/.gitignore b/.gitignore index 8d7ac75b..b35a9758 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ /.idea/ -/vendor/ -/composer.lock -/composer.phar +composer.lock +composer.phar /tests/Private -/,/ /docs/ +*.iml +vendor/ diff --git a/.travis.yml b/.travis.yml index ac7865b5..235dc1e2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,7 @@ language: php - -matrix: - include: - - php: '5.4' - before_script: composer require 'indigophp/hash-compat:^1.1' - - php: '5.5' - before_script: composer require 'indigophp/hash-compat:^1.1' - - php: '5.6' - - php: '7.0' +php: + - '5.6' + - '7.0' install: composer update script: ./vendor/bin/phpunit ./tests sudo: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d019c6d4..488647b0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,10 +3,10 @@ First of all, thank you so much for taking your time to contribute! LINE Bot SDK for PHP is not very different from any other open source projects you are aware of. It will be amazing if you could help us by doing any of the following: -- File an issue in [the issue tracker](https://github.com/line/line-bot-sdk-php/issues) to report bugs and propose new features and +- File an issue in [the issue tracker](https://github.com/line/line-bot-sdk-php-v2/issues) to report bugs and propose new features and improvements. -- Ask a question using [the issue tracker](https://github.com/line/line-bot-sdk-php/issues) (__Please ask only about this SDK__). -- Contribute your work by sending [a pull request](https://github.com/line/line-bot-sdk-php/pulls). +- Ask a question using [the issue tracker](https://github.com/line/line-bot-sdk-php-v2/issues) (__Please ask only about this SDK__). +- Contribute your work by sending [a pull request](https://github.com/line/line-bot-sdk-php-v2/pulls). ### Contributor license agreement diff --git a/LICENSE b/LICENSE index 5c515ba9..378cc222 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -192,7 +192,7 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/Makefile b/Makefile index a415cbce..2ff1bfd8 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,9 @@ COMPOSER_BIN = ./vendor/bin +.PHONY: default test doc phpcs phpmd check + +default: check + test: $(COMPOSER_BIN)/phpunit ./tests @@ -7,13 +11,18 @@ doc: yes yes | $(COMPOSER_BIN)/apigen generate --source=./src --destination=./docs --title line-bot-sdk-php phpcs: - $(COMPOSER_BIN)/phpcs --standard=PSR2 src/ examples/SendingSample/ examples/EchoBot/src examples/EchoBot/public + $(COMPOSER_BIN)/phpcs --standard=PSR2 src/ tests/ examples/EchoBot/src examples/EchoBot/public examples/KitchenSink/src examples/KitchenSink/public phpmd: $(COMPOSER_BIN)/phpmd ./src text cleancode,codesize,controversial,design,unusedcode,naming | grep -v 'Avoid using static access to class' - $(COMPOSER_BIN)/phpmd ./examples/SendingSample text cleancode,codesize,controversial,design,unusedcode,naming | grep -v 'Avoid using static access to class' + $(COMPOSER_BIN)/phpmd ./tests text cleancode,codesize,controversial,design,unusedcode,naming | grep -v 'Avoid using static access to class' $(COMPOSER_BIN)/phpmd ./examples/EchoBot/src text cleancode,codesize,controversial,design,unusedcode,naming | grep -v 'Avoid using static access to class' $(COMPOSER_BIN)/phpmd ./examples/EchoBot/public text cleancode,codesize,controversial,design,unusedcode,naming | grep -v 'Avoid using static access to class' + $(COMPOSER_BIN)/phpmd ./examples/KitchenSink/src text cleancode,codesize,controversial,design,unusedcode,naming | grep -v 'Avoid using static access to class' + $(COMPOSER_BIN)/phpmd ./examples/KitchenSink/public text cleancode,codesize,controversial,design,unusedcode,naming | grep -v 'Avoid using static access to class' + +copyright: + bash ./devtool/check_copyright.sh -check: test phpcs phpmd +check: test copyright phpcs phpmd diff --git a/README.md b/README.md index 8017f92a..f7cf5676 100644 --- a/README.md +++ b/README.md @@ -2,193 +2,172 @@ line-bot-sdk-php == [![Build Status](https://travis-ci.org/line/line-bot-sdk-php.svg?branch=master)](https://travis-ci.org/line/line-bot-sdk-php) -[![Latest Stable Version](https://poser.pugx.org/linecorp/line-bot-sdk/v/stable.svg)](https://packagist.org/packages/linecorp/line-bot-sdk) -[![License](https://poser.pugx.org/linecorp/line-bot-sdk/license.svg)](https://packagist.org/packages/linecorp/line-bot-sdk) -SDK of the LINE BOT API Trial for PHP. +SDK of the LINE Messaging API for PHP. + +About LINE Messaging API +-- + +Please refer to the official API documents for details. + +en: [https://devdocs.line.me/en/](https://devdocs.line.me/en/) + +ja: [https://devdocs.line.me/ja/](https://devdocs.line.me/ja/) Installation -- -The LINE BOT API SDK can be installed with [Composer](https://getcomposer.org/). +The LINE messaging API SDK can be installed with [Composer](https://getcomposer.org/). ``` -composer require linecorp/line-bot-sdk +$ composer require linecorp/line-bot-sdk ``` -Note +Getting started -- -If you use __PHP 5.5 or lower__, please use this SDK with polyfill of [hash_equals()](http://php.net/manual/function.hash-equals.php). +### Create the bot client instance -e.g. +Instance of bot client is a handler of the Messaging API. -- [indigophp/hash-compat](https://packagist.org/packages/indigophp/hash-compat) +```php +$httpClient = new \LINE\LINEBot\HTTPClient\CurlHTTPClient(''); +$bot = new \LINE\LINEBot($httpClient, ['channelSecret' => '']); +``` -Methods --- +The constructor of bot client requires an instance of `HTTPClient`. +This library provides `CurlHTTPClient` as default. -### Constructor +### Call API -#### new LINEBot(array $args, HTTPClient $client) +You can call API through the bot client instance. -Create a `LINEBot` constructor. +Deadly simple sample is following; ```php -$config = [ - 'channelId' => '', - 'channelSecret' => '', - 'channelMid' => '', -]; -$bot = new LINEBot($config, new GuzzleHTTPClient($config)); +$response = $bot->replyText('', 'hello!'); ``` -### Sending Message - -#### LINEBot#sendText($mid, $text) +This procedure sends a message to the destination that is associated with ``. -Send a text message to mid(s). -[https://developers.line.me/bot-api/api-reference#sending_message_text](https://developers.line.me/bot-api/api-reference#sending_message_text) +More advanced sample is below; ```php -$res = $bot->sendText(['TARGET_MID'], 'Message'); +$textMessageBuilder = new \LINE\LINEBot\MessageBuilder\TextMessageBuilder('hello'); +$response = $bot->replyMessage('', $textMessageBuilder); +if ($response->isSecceeded()) { + echo 'Succeeded!'; + return; +} + +// Failed +echo $response->getHTTPStatus . ' ' . $response->getBody(); ``` -#### LINEBot#sendImage($mid, $imageURL, $previewURL) +`LINEBot#replyMessage()` takes reply token and `MessageBuilder`. +This method sends message that is built by `MessageBuilder` to the destination. -Send an image to mid(s). -[https://developers.line.me/bot-api/api-reference#sending_message_image](https://developers.line.me/bot-api/api-reference#sending_message_image) +#### MessageBuilder -```php -$bot->sendImage(['TARGET_MID'] 'http://example.com/image.jpg', 'http://example.com/preview.jpg'); -``` +Type of message depends on the type of instance of `MessageBuilder`. +That means this method sends text message if you pass `TextMessageBuilder`, +on the other hand it sends image message if you pass `ImaageMessageBuilder`. -#### LINEBot#sendVideo($mid, $videoURL, $previewImageURL) +If you want detail information of `MessageBuilder`, please refer `\LINE\LINEBot\MessageBuilder` and the namespace. -Send a video to mid(s). -[https://developers.line.me/bot-api/api-reference#sending_message_video](https://developers.line.me/bot-api/api-reference#sending_message_video) +Other methods that take `MessageBuilder` behave the same. -```php -$bot->sendVideo(['TARGET_MID'], 'http://example.com/video.mp4', 'http://example.com/video_preview.jpg'); -``` +#### Response -#### LINEBot#sendAudio($mid, $audioURL, $durationMillis) +Methods that call API returns `Response`. Response has three methods; -Send a voice message to mid(s). -[https://developers.line.me/bot-api/api-reference#sending_message_audio](https://developers.line.me/bot-api/api-reference#sending_message_audio) +- `Response#isSucceeded()` +- `Response#getHTTPStatus()` +- `Response#getBody()` -```php -$bot->sendAudio(['TARGET_MID'], 'http://example.com/audio.m4a', 5000); -``` +You can use these method to check response status and take response body. -#### LINEBot#sendLocation($mid, $text, $latitude, $longitude) +##### `Response#isSucceeded()` -Send location information to mid(s). -[https://developers.line.me/bot-api/api-reference#sending_message_location](https://developers.line.me/bot-api/api-reference#sending_message_location) +This method returns the boolean value. Return value represents "request is succeeded or not". -```php -$bot->sendLocation(['TARGET_MID'], '2 Chome-21-1 Shibuya Tokyo 150-0002, Japan', 35.658240, 139.703478); -``` +##### `Response#getHTTPStatus()` -#### LINEBot#sendSticker($mid, $stkid, $stkpkgid, $stkver) +This method returns the HTTP status code of response. -Send a sticker to mid(s). -[https://developers.line.me/bot-api/api-reference#sending_message_sticker](https://developers.line.me/bot-api/api-reference#sending_message_sticker) +##### `Response#getBody()` -```php -$bot->sendSticker(['TARGET_MID'], 1, 1, 100); -``` +This method returns the body of response as string. -#### LINEBot#sendRichMessage($mid, $imageURL, $altText, Markup $markup) +### More information -Send a rich message to mid(s). -[https://developers.line.me/bot-api/api-reference#sending_rich_content_message_request](https://developers.line.me/bot-api/api-reference#sending_rich_content_message_request) +Please check [official API documents](#about-line-messaging-api) and PHPDoc. +If you first time to use this library, we recommend to see `examples` and PHPDoc of `\LINE\LINEBot`. -```php -$markup = (new Markup(1040)) - ->setAction('SOMETHING', 'something', 'https://line.me') - ->addListener('SOMETHING', 0, 0, 1040, 1040); -$bot->sendRichMessage(['TARGET_MID'], 'https://example.com/image.jpg', "Alt text", $markup); -``` +Hints +-- -#### LINEBot#sendMultipleMessages($mid, MultipleMessages $multipleMessages) +### Examples -Send multiple messages to mids(s). -[https://developers.line.me/bot-api/api-reference#sending_multiple_messages_request](https://developers.line.me/bot-api/api-reference#sending_multiple_messages_request) +This repository contains two examples of LINE Messaging API. -```php -$multipleMessages = (new \LINE\LINEBot\Message\MultipleMessages()) - ->addText('hello!') - ->addImage('http://example.com/image.jpg', 'http://example.com/preview.jpg') - ->addAudio('http://example.com/audio.m4a', 6000) - ->addVideo('http://example.com/video.mp4', 'http://example.com/video_preview.jpg') - ->addLocation('2 Chome-21-1 Shibuya Tokyo 150-0002, Japan', 35.658240, 139.703478) - ->addSticker(1, 1, 100); -$bot->sendMultipleMessages(['TARGET_MID'], $multipleMessages); -``` +#### [EchoBot](/examples/EchoBot) -### Getting Message Contents +A simple sample implementation. This application reacts to text message that is from user. -#### LINEBot#getMessageContent($messageId, $fileHandler = null) +#### [KitchenSink](/examples/KitchenSink) -Retrieve the content of a user's message which is an image or video file. -[https://developers.line.me/bot-api/api-reference#getting_message_content_request](https://developers.line.me/bot-api/api-reference#getting_message_content_request) +A full-stack (and a bit complex) sample implementation. That will show you practical usage of LINE Messaging API. -```php -$content = $bot->getMessageContent('1234567890'); -``` +### PHPDoc -#### LINEBot#getMessageContentPreview($messageId, $fileHandler = null) +This library provides PHPDoc. That will helps you to know usages of methods. -Retrieve thumbnail preview of the message. -[https://developers.line.me/bot-api/api-reference#getting_message_content_preview_request](https://developers.line.me/bot-api/api-reference#getting_message_content_preview_request) +This library can generate pretty documents by [apigen](http://www.apigen.org/). Please try: -```php -$content = $bot->getMessageContentPreview('1234567890'); +``` +$ make doc ``` -### Getting User Profile +When HTML documents will be put on `docs/`. -#### LINEBot#getUserProfile($mid) +### Official API documents -Retrieve user profile(s) that is associated with mid(s). -[https://developers.line.me/bot-api/api-reference#getting_user_profile_information_request](https://developers.line.me/bot-api/api-reference#getting_user_profile_information_request) +[Official API documents](#about-line-messaging-api) shows the detail of Messaging API and fundamental usage of SDK. -```php -$profile = $bot->getUserProfile(['TARGET_MID']); -``` +Notes +-- -### Signature Validation +### How to switch HTTP client implementation? -#### LINEBot#validateSignature($json, $signature) +1. Implement `\LINE\LINEBot\HTTPClient` +2. Pass the implementation to the constructor of `\LINE\LINEBot` -Validate signature. +Please refer [CurlHTTPClient](/src/LINEBot/HTTPClient/CurlHTTPClient.php) that is the default HTTP client implementation. -```php -$isValid = $bot->validateSignature($requestJSON, 'expected-signature'); -``` +Requirements +-- -Run Tests +- PHP 5.6 or later + +For SDK developers -- -### Execute with `phpunit` +### How to run tests? -``` -composer install -./vendor/bin/phpunit ./tests -``` +Please use `make test`. -### Execute `make test` +### How to execute [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer)? -``` -composer install -make -``` +Please use `make phpcs`. -Hints --- +### How to execute [PHPMD](https://phpmd.org/)? + +Please use `make phpmd`. + +### How to execute them all?? -You can find some implementation examples [here](./examples). +`make` License -- @@ -200,7 +179,7 @@ LINE Corporation licenses this file to you under the Apache License, version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT @@ -209,10 +188,3 @@ License for the specific language governing permissions and limitations under the License. ``` -See Also --- - -- [https://business.line.me/](https://business.line.me/) -- [https://developers.line.me/bot-api/overview](https://developers.line.me/bot-api/overview) -- [https://developers.line.me/bot-api/getting-started-with-bot-api-trial](https://developers.line.me/bot-api/getting-started-with-bot-api-trial) - diff --git a/composer.json b/composer.json index a146c608..9d68462c 100644 --- a/composer.json +++ b/composer.json @@ -1,13 +1,13 @@ { "name": "linecorp/line-bot-sdk", - "description": "SDK of the LINE BOT API Trial for PHP", + "description": "SDK of the LINE BOT API for PHP", "keywords": [ "LINE", "bot", "sdk" ], "type": "library", - "homepage": "http://github.com/line/line-bot-sdk-php", + "homepage": "https://github.com/line/line-bot-sdk-php", "license": "Apache License Version 2.0", "authors": [ { @@ -16,8 +16,7 @@ } ], "require": { - "php": ">=5.4", - "guzzlehttp/guzzle": "^5.3" + "php": ">=5.6" }, "require-dev": { "phpunit/phpunit": "^4.8.24", diff --git a/devtool/check_copyright.sh b/devtool/check_copyright.sh new file mode 100644 index 00000000..596bd067 --- /dev/null +++ b/devtool/check_copyright.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +FILES=$(cd ./$(git rev-parse --show-cdup); find `pwd` -name '*.php' | grep -v /vendor/) + +NOT_INCLUDE_COPYRIGHT_FILES=() + +for FILE in $FILES; do + if ! grep -q 'LINE Corporation licenses this file to you under the Apache License' $FILE; then + NOT_INCLUDE_COPYRIGHT_FILES=("${NOT_INCLUDE_COPYRIGHT_FILES[@]}" $FILE) + fi +done + +EXIT_CODE=0 +if [ ${#NOT_INCLUDE_COPYRIGHT_FILES[@]} -gt 0 ]; then + echo '[ERROR] Detected file(s) that does not include copyright' + EXIT_CODE=1 +fi + +for FILE in ${NOT_INCLUDE_COPYRIGHT_FILES[@]}; do + echo $FILE +done + +exit $EXIT_CODE + diff --git a/examples/EchoBot/.gitignore b/examples/EchoBot/.gitignore index a27edd14..760f22ab 100644 --- a/examples/EchoBot/.gitignore +++ b/examples/EchoBot/.gitignore @@ -1,5 +1,2 @@ -/vendor/ -/logs/* !/logs/.gitkeep -composer.lock -composer.phar +/logs/* diff --git a/examples/EchoBot/LICENSE b/examples/EchoBot/LICENSE index 5c515ba9..378cc222 100644 --- a/examples/EchoBot/LICENSE +++ b/examples/EchoBot/LICENSE @@ -1,6 +1,6 @@ Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -192,7 +192,7 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/examples/EchoBot/README.md b/examples/EchoBot/README.md index 2c38e7ce..81f26617 100644 --- a/examples/EchoBot/README.md +++ b/examples/EchoBot/README.md @@ -1,7 +1,8 @@ line-echo-bot-sample == -A sample echo bot implementation of LINE BOT API Trial. +A sample echo bot implementation of LINE Messaging API. + This project is using [Slim framework](http://www.slimframework.com/). Getting Started @@ -10,8 +11,8 @@ Getting Started ``` $ (cd ../../ && composer install) $ composer install -$ $EDITOR ./src/settings.php # <= edit your bot information -$ php -S 0.0.0.0:8080 -t public ./public/index.php +$ $EDITOR ./src/LINEBot/EchoBot/Setting.php # <= edit your bot information +$ php -S 0.0.0.0:8080 -t public ``` Hints @@ -21,7 +22,7 @@ Hints Entry point of this application. -### [src/routes.php](./src/routes.php) +### [src/LINEBot/EchoBot/Route.php](./src/LINEBot/EchoBot/Route.php) Core logic of this application that uses LINE BOT API. @@ -35,7 +36,7 @@ LINE Corporation licenses this file to you under the Apache License, version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT @@ -43,3 +44,4 @@ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` + diff --git a/examples/EchoBot/composer.json b/examples/EchoBot/composer.json index 02868453..c3c48d60 100644 --- a/examples/EchoBot/composer.json +++ b/examples/EchoBot/composer.json @@ -7,8 +7,13 @@ } ], "require": { - "php": ">=5.5", - "slim/slim": "^3.0", - "monolog/monolog": "^1.19.0" + "php": ">=5.6", + "slim/slim": "^3.5.0", + "monolog/monolog": "^1.21.0" + }, + "autoload": { + "psr-4": { + "LINE\\": "src/" + } } } diff --git a/examples/EchoBot/public/index.php b/examples/EchoBot/public/index.php index af8f6c32..cb802165 100644 --- a/examples/EchoBot/public/index.php +++ b/examples/EchoBot/public/index.php @@ -1,4 +1,5 @@ register($app); +(new Route())->register($app); $app->run(); diff --git a/examples/EchoBot/src/LINEBot/EchoBot/Dependency.php b/examples/EchoBot/src/LINEBot/EchoBot/Dependency.php new file mode 100644 index 00000000..4994110f --- /dev/null +++ b/examples/EchoBot/src/LINEBot/EchoBot/Dependency.php @@ -0,0 +1,50 @@ +getContainer(); + + $container['logger'] = function ($c) { + $settings = $c->get('settings')['logger']; + $logger = new \Monolog\Logger($settings['name']); + $logger->pushProcessor(new \Monolog\Processor\UidProcessor()); + $logger->pushHandler(new \Monolog\Handler\StreamHandler($settings['path'], \Monolog\Logger::DEBUG)); + return $logger; + }; + + $container['bot'] = function ($c) { + $settings = $c->get('settings'); + $channelSecret = $settings['bot']['channelSecret']; + $channelToken = $settings['bot']['channelToken']; + $apiEndpointBase = $settings['apiEndpointBase']; + $bot = new LINEBot(new CurlHTTPClient($channelToken), [ + 'channelSecret' => $channelSecret, + 'endpointBase' => $apiEndpointBase, // <= Normally, you can omit this + ]); + return $bot; + }; + } +} diff --git a/examples/EchoBot/src/LINEBot/EchoBot/Route.php b/examples/EchoBot/src/LINEBot/EchoBot/Route.php new file mode 100644 index 00000000..83bc78e9 --- /dev/null +++ b/examples/EchoBot/src/LINEBot/EchoBot/Route.php @@ -0,0 +1,79 @@ +post('/callback', function (\Slim\Http\Request $req, \Slim\Http\Response $res) { + /** @var \LINE\LINEBot $bot */ + $bot = $this->bot; + /** @var \Monolog\Logger $logger */ + $logger = $this->logger; + + $signature = $req->getHeader(HTTPHeader::LINE_SIGNATURE); + if (empty($signature)) { + return $res->withStatus(400, 'Bad Request'); + } + + // Check request with signature and parse request + try { + $events = $bot->parseEventRequest($req->getBody(), $signature[0]); + } catch (InvalidSignatureException $e) { + return $res->withStatus(400, 'Invalid signature'); + } catch (UnknownEventTypeException $e) { + return $res->withStatus(400, 'Unknown event type has come'); + } catch (UnknownMessageTypeException $e) { + return $res->withStatus(400, 'Unknown message type has come'); + } catch (InvalidEventRequestException $e) { + return $res->withStatus(400, "Invalid event request"); + } + + foreach ($events as $event) { + if (!($event instanceof MessageEvent)) { + $logger->info('Non message event has come'); + continue; + } + + if (!($event instanceof TextMessage)) { + $logger->info('Non text message has come'); + continue; + } + + $replyText = $event->getText(); + $logger->info('Reply text: ' . $replyText); + $resp = $bot->replyText($event->getReplyToken(), $replyText); + $logger->info($resp->getHTTPStatus() . ': ' . $resp->getRawBody()); + } + + $res->write('OK'); + return $res; + }); + } +} diff --git a/examples/EchoBot/src/LINEBot/EchoBot/Setting.php b/examples/EchoBot/src/LINEBot/EchoBot/Setting.php new file mode 100644 index 00000000..9312e691 --- /dev/null +++ b/examples/EchoBot/src/LINEBot/EchoBot/Setting.php @@ -0,0 +1,43 @@ + [ + 'displayErrorDetails' => true, // set to false in production + + 'logger' => [ + 'name' => 'slim-app', + 'path' => __DIR__ . '/../../../logs/app.log', + ], + + 'bot' => [ + 'channelToken' => getenv('LINEBOT_CHANNEL_TOKEN') ?: '', + 'channelSecret' => getenv('LINEBOT_CHANNEL_SECRET') ?: '', + ], + + 'apiEndpointBase' => getenv('LINEBOT_API_ENDPOINT_BASE'), + ], + ]; + } +} diff --git a/examples/EchoBot/src/dependencies.php b/examples/EchoBot/src/dependencies.php deleted file mode 100644 index f1d0ea50..00000000 --- a/examples/EchoBot/src/dependencies.php +++ /dev/null @@ -1,39 +0,0 @@ -getContainer(); - -$container['logger'] = function ($c) { - $settings = $c->get('settings')['logger']; - $logger = new Monolog\Logger($settings['name']); - $logger->pushProcessor(new Monolog\Processor\UidProcessor()); - $logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], Monolog\Logger::DEBUG)); - return $logger; -}; - -$container['bot'] = function ($c) { - $settings = $c->get('settings')['bot']; - $config = [ - 'channelId' => $settings['channelId'], - 'channelSecret' => $settings['channelSecret'], - 'channelMid' => $settings['channelMid'], - ]; - $bot = new LINEBot($config, new GuzzleHTTPClient($config)); - return $bot; -}; diff --git a/examples/EchoBot/src/routes.php b/examples/EchoBot/src/routes.php deleted file mode 100644 index 18b54233..00000000 --- a/examples/EchoBot/src/routes.php +++ /dev/null @@ -1,133 +0,0 @@ -post('/callback', function (Request $req, Response $res, $arg) { - $body = $req->getBody(); - $signatureHeader = $req->getHeader('X-LINE-ChannelSignature'); - if (empty($signatureHeader) || !$this->bot->validateSignature($body, $signatureHeader[0])) { - return $res->withStatus(400, "Bad Request"); - } - - /** @var LINEBot $bot */ - $bot = $this->bot; - - /** @var Receive[] $receives */ - $receives = $bot->createReceivesFromJSON($body); - foreach ($receives as $receive) { - if ($receive->isMessage()) { - /** @var Message $receive */ - - $this->logger->info(sprintf( - 'contentId=%s, fromMid=%s, createdTime=%s', - $receive->getContentId(), - $receive->getFromMid(), - $receive->getCreatedTime() - )); - - if ($receive->isText()) { - /** @var Text $receive */ - if ($receive->getText() === 'me') { - $ret = $bot->getUserProfile($receive->getFromMid()); - $contact = $ret['contacts'][0]; - $multipleMsgs = (new MultipleMessages()) - ->addText(sprintf( - 'Hello! %s san! Your status message is %s', - $contact['displayName'], - $contact['statusMessage'] - )) - ->addImage($contact['pictureUrl'], $contact['pictureUrl']) - ->addSticker(mt_rand(0, 10), 1, 100); - $bot->sendMultipleMessages($receive->getFromMid(), $multipleMsgs); - } else { - $bot->sendText($receive->getFromMid(), $receive->getText()); - } - } elseif ($receive->isImage() || $receive->isVideo()) { - $content = $bot->getMessageContent($receive->getContentId()); - $meta = stream_get_meta_data($content->getFileHandle()); - $contentSize = filesize($meta['uri']); - $type = $receive->isImage() ? 'image' : 'video'; - - $previewContent = $bot->getMessageContentPreview($receive->getContentId()); - $previewMeta = stream_get_meta_data($previewContent->getFileHandle()); - $previewContentSize = filesize($previewMeta['uri']); - - $bot->sendText( - $receive->getFromMid(), - "Thank you for sending a $type.\nOriginal file size: " . - "$contentSize\nPreview file size: $previewContentSize" - ); - } elseif ($receive->isAudio()) { - $bot->sendText($receive->getFromMid(), "Thank you for sending a audio."); - } elseif ($receive->isLocation()) { - /** @var Location $receive */ - $bot->sendLocation( - $receive->getFromMid(), - sprintf("%s\n%s", $receive->getText(), $receive->getAddress()), - $receive->getLatitude(), - $receive->getLongitude() - ); - } elseif ($receive->isSticker()) { - /** @var Sticker $receive */ - $bot->sendSticker( - $receive->getFromMid(), - $receive->getStkId(), - $receive->getStkPkgId(), - $receive->getStkVer() - ); - } elseif ($receive->isContact()) { - /** @var Contact $receive */ - $bot->sendText( - $receive->getFromMid(), - sprintf("Thank you for sending %s information.", $receive->getDisplayName()) - ); - } else { - throw new \Exception("Received invalid message type"); - } - } elseif ($receive->isOperation()) { - /** @var Operation $receive */ - - $this->logger->info(sprintf( - 'revision=%s, fromMid=%s', - $receive->getRevision(), - $receive->getFromMid() - )); - - if ($receive->isAddContact()) { - $bot->sendText($receive->getFromMid(), "Thank you for adding me to your contact list!"); - } elseif ($receive->isBlockContact()) { - $this->logger->info("Blocked"); - } else { - throw new \Exception("Received invalid operation type"); - } - } else { - throw new \Exception("Received invalid receive type"); - } - } - - return $res->getBody()->write("OK"); -}); diff --git a/examples/EchoBot/src/settings.php b/examples/EchoBot/src/settings.php deleted file mode 100644 index edce9d8f..00000000 --- a/examples/EchoBot/src/settings.php +++ /dev/null @@ -1,32 +0,0 @@ - [ - 'displayErrorDetails' => true, // set to false in production - - 'logger' => [ - 'name' => 'slim-app', - 'path' => __DIR__ . '/../logs/app.log', - ], - - 'bot' => [ - 'channelId' => '', - 'channelSecret' => '', - 'channelMid' => '', - ], - ], -]; diff --git a/examples/KitchenSink/.gitignore b/examples/KitchenSink/.gitignore new file mode 100644 index 00000000..20778ea7 --- /dev/null +++ b/examples/KitchenSink/.gitignore @@ -0,0 +1,4 @@ +!/logs/.gitkeep +/logs/* +!/public/static/tmpdir/.gitkeep +/public/static/tmpdir/* diff --git a/examples/KitchenSink/LICENSE b/examples/KitchenSink/LICENSE new file mode 100644 index 00000000..378cc222 --- /dev/null +++ b/examples/KitchenSink/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (C) 2016 LINE Corp. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/KitchenSink/README.md b/examples/KitchenSink/README.md new file mode 100644 index 00000000..46fd3ccb --- /dev/null +++ b/examples/KitchenSink/README.md @@ -0,0 +1,68 @@ +line-bot-sample +== + +A full-stack LINE Messaging API sample implementation. This sample will show you practical usage of LINE Messaging API. + +This project is using [Slim framework](http://www.slimframework.com/). + +Getting Started +-- + +``` +$ (cd ../../ && composer install) +$ composer install +$ $EDITOR ./src/LINEBot/KitchenSink/Setting.php # <= edit your bot information +$ ./run.sh 8080 +``` + +Hints +-- + +### [public/index.php](./public/index.php) + +Entry point of this application. + +### [src/LINEBot/KitchenSink/Route.php](./src/LINEBot/KitchenSink/Route.php) + +Core logic of this application that uses LINE BOT API. + +### [Event handlers](./src/LINEBot/KitchenSink/EventHandler) + +Handlers for LINE Messaging API events. + +Notes +-- + +### Temporary directory + +This application downloads multimedia files on `./public/static/tmpdir/`. +`./run.sh` wrapper removes such contents on shutting down the PHP server. + +### Base URL + +This application serves downloaded multimedia files. + +Default, this app constructs URL of such content with `\Slim\Http\Request->getUri()->getBaseUrl()` as base URL. +Unfortunately this processing doesn't work correctly if this app runs on reverse-proxied environment. + +If you get such symptom, please configure base URL as you like => [UrlBuilder](./src/LINEBot/KitchenSink/EventHandler/MessageHandler/Util/UrlBuilder.php) + +License +-- + +``` +Copyright 2016 LINE Corporation + +LINE Corporation licenses this file to you under the Apache License, +version 2.0 (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at: + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. +``` + diff --git a/examples/KitchenSink/composer.json b/examples/KitchenSink/composer.json new file mode 100644 index 00000000..c3c48d60 --- /dev/null +++ b/examples/KitchenSink/composer.json @@ -0,0 +1,19 @@ +{ + "license": "Apache License, Version 2.0", + "authors": [ + { + "name": "moznion", + "email": "moznion@gmail.com" + } + ], + "require": { + "php": ">=5.6", + "slim/slim": "^3.5.0", + "monolog/monolog": "^1.21.0" + }, + "autoload": { + "psr-4": { + "LINE\\": "src/" + } + } +} diff --git a/examples/KitchenSink/public/.htaccess b/examples/KitchenSink/public/.htaccess new file mode 100644 index 00000000..c1245d13 --- /dev/null +++ b/examples/KitchenSink/public/.htaccess @@ -0,0 +1,11 @@ +RewriteEngine On + +# Some hosts may require you to use the `RewriteBase` directive. +# If you need to use the `RewriteBase` directive, it should be the +# absolute physical path to the directory that contains this htaccess file. +# +# RewriteBase / + +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^ index.php [QSA,L] + diff --git a/examples/KitchenSink/public/index.php b/examples/KitchenSink/public/index.php new file mode 100644 index 00000000..d582037a --- /dev/null +++ b/examples/KitchenSink/public/index.php @@ -0,0 +1,32 @@ +register($app); +(new Dependency())->register($app); + +$app->run(); diff --git a/examples/KitchenSink/public/static/buttons/1040.jpg b/examples/KitchenSink/public/static/buttons/1040.jpg new file mode 100644 index 0000000000000000000000000000000000000000..de65e1c9a3a3635b99c7da4cc1f16307cff349a0 GIT binary patch literal 64304 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 literal 0 HcmV?d00001 diff --git a/examples/KitchenSink/public/static/rich/1040 b/examples/KitchenSink/public/static/rich/1040 new file mode 100644 index 0000000000000000000000000000000000000000..de65e1c9a3a3635b99c7da4cc1f16307cff349a0 GIT binary patch literal 64304 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 literal 0 HcmV?d00001 diff --git a/examples/KitchenSink/public/static/rich/240 b/examples/KitchenSink/public/static/rich/240 new file mode 100644 index 0000000000000000000000000000000000000000..eeedf27de99114acfd98d0520454879b7da69273 GIT binary patch literal 13828 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 literal 0 HcmV?d00001 diff --git a/examples/KitchenSink/public/static/rich/300 b/examples/KitchenSink/public/static/rich/300 new file mode 100644 index 0000000000000000000000000000000000000000..357c444b5c2cfc434e9ae1e413f2c0f9ac2796a3 GIT binary patch literal 16689 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%0A8Mt~r?ND;!vxOqe7oUM82?zR_J-; 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 literal 0 HcmV?d00001 diff --git a/examples/KitchenSink/run.sh b/examples/KitchenSink/run.sh new file mode 100755 index 00000000..b49dd35c --- /dev/null +++ b/examples/KitchenSink/run.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +PORT=$1 +if [ -z $PORT ]; then + cat <getContainer(); + + $container['logger'] = function ($c) { + $settings = $c->get('settings')['logger']; + $logger = new \Monolog\Logger($settings['name']); + $logger->pushProcessor(new \Monolog\Processor\UidProcessor()); + $logger->pushHandler(new \Monolog\Handler\StreamHandler($settings['path'], \Monolog\Logger::DEBUG)); + return $logger; + }; + + $container['bot'] = function ($c) { + $settings = $c->get('settings'); + $channelSecret = $settings['bot']['channelSecret']; + $channelToken = $settings['bot']['channelToken']; + $apiEndpointBase = $settings['apiEndpointBase']; + $bot = new LINEBot(new CurlHTTPClient($channelToken), [ + 'channelSecret' => $channelSecret, + 'endpointBase' => $apiEndpointBase, // <= Normally, you can omit this + ]); + return $bot; + }; + } +} diff --git a/src/LINEBot/Exception/LINEBotAPIException.php b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler.php similarity index 81% rename from src/LINEBot/Exception/LINEBotAPIException.php rename to examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler.php index 5c8fe92f..33c6280c 100644 --- a/src/LINEBot/Exception/LINEBotAPIException.php +++ b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler.php @@ -1,4 +1,5 @@ bot = $bot; + $this->logger = $logger; + $this->beaconEvent = $beaconEvent; + } + + public function handle() + { + $this->bot->replyText( + $this->beaconEvent->getReplyToken(), + 'Got beacon message ' . $this->beaconEvent->getHwid() + ); + } +} diff --git a/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/FollowEventHandler.php b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/FollowEventHandler.php new file mode 100644 index 00000000..678fe3df --- /dev/null +++ b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/FollowEventHandler.php @@ -0,0 +1,51 @@ +bot = $bot; + $this->logger = $logger; + $this->followEvent = $followEvent; + } + + public function handle() + { + $this->bot->replyText($this->followEvent->getReplyToken(), 'Got followed event'); + } +} diff --git a/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/JoinEventHandler.php b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/JoinEventHandler.php new file mode 100644 index 00000000..568b51fa --- /dev/null +++ b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/JoinEventHandler.php @@ -0,0 +1,54 @@ +bot = $bot; + $this->logger = $logger; + $this->joinEvent = $joinEvent; + } + + public function handle() + { + $this->bot->replyText( + $this->joinEvent->getReplyToken(), + sprintf('Joined %s %s', $this->joinEvent->getType(), $this->joinEvent->getUserId()) + ); + } +} diff --git a/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/LeaveEventHandler.php b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/LeaveEventHandler.php new file mode 100644 index 00000000..90fcc96c --- /dev/null +++ b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/LeaveEventHandler.php @@ -0,0 +1,54 @@ +bot = $bot; + $this->logger = $logger; + $this->leaveEvent = $leaveEvent; + } + + public function handle() + { + $this->bot->replyText( + $this->leaveEvent->getReplyToken(), + sprintf('Leaved %s %s', $this->leaveEvent->getType(), $this->leaveEvent->getUserId()) + ); + } +} diff --git a/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/AudioMessageHandler.php b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/AudioMessageHandler.php new file mode 100644 index 00000000..ddbf1902 --- /dev/null +++ b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/AudioMessageHandler.php @@ -0,0 +1,77 @@ +bot = $bot; + $this->logger = $logger; + $this->req = $req; + $this->audioMessage = $audioMessage; + } + + public function handle() + { + $contentId = $this->audioMessage->getMessageId(); + $audio = $this->bot->getMessageContent($contentId)->getRawBody(); + + $tmpfilePath = tempnam($_SERVER['DOCUMENT_ROOT'] . '/static/tmpdir', 'audio-'); + unlink($tmpfilePath); + $filePath = $tmpfilePath . '.mp4'; + $filename = basename($filePath); + + $fh = fopen($filePath, 'x'); + fwrite($fh, $audio); + fclose($fh); + + $replyToken = $this->audioMessage->getReplyToken(); + + $url = UrlBuilder::buildUrl($this->req, ['static', 'tmpdir', $filename]); + + $resp = $this->bot->replyMessage( + $replyToken, + new AudioMessageBuilder($url, 100) + ); + $this->logger->info($resp->getRawBody()); + } +} diff --git a/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/ImageMessageHandler.php b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/ImageMessageHandler.php new file mode 100644 index 00000000..5ab7ae23 --- /dev/null +++ b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/ImageMessageHandler.php @@ -0,0 +1,75 @@ +bot = $bot; + $this->logger = $logger; + $this->req = $req; + $this->imageMessage = $imageMessage; + } + + public function handle() + { + $contentId = $this->imageMessage->getMessageId(); + $image = $this->bot->getMessageContent($contentId)->getRawBody(); + + $tmpfilePath = tempnam($_SERVER['DOCUMENT_ROOT'] . '/static/tmpdir', 'image-'); + unlink($tmpfilePath); + $filePath = $tmpfilePath . '.jpg'; + $filename = basename($filePath); + + $fh = fopen($filePath, 'x'); + fwrite($fh, $image); + fclose($fh); + + $replyToken = $this->imageMessage->getReplyToken(); + + $url = UrlBuilder::buildUrl($this->req, ['static', 'tmpdir', $filename]); + + // NOTE: You should pass the url of small image to `previewImageUrl`. + // This sample doesn't treat that. + $this->bot->replyMessage($replyToken, new ImageMessageBuilder($url, $url)); + } +} diff --git a/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/LocationMessageHandler.php b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/LocationMessageHandler.php new file mode 100644 index 00000000..59e3678e --- /dev/null +++ b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/LocationMessageHandler.php @@ -0,0 +1,61 @@ +bot = $bot; + $this->logger = $logger; + $this->locationMessage = $locationMessage; + } + + public function handle() + { + $replyToken = $this->locationMessage->getReplyToken(); + $title = $this->locationMessage->getTitle(); + $address = $this->locationMessage->getAddress(); + $latitude = $this->locationMessage->getLatitude(); + $longitude = $this->locationMessage->getLongitude(); + + $this->bot->replyMessage( + $replyToken, + new LocationMessageBuilder($title, $address, $latitude, $longitude) + ); + } +} diff --git a/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/StickerMessageHandler.php b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/StickerMessageHandler.php new file mode 100644 index 00000000..05f6f137 --- /dev/null +++ b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/StickerMessageHandler.php @@ -0,0 +1,55 @@ +bot = $bot; + $this->logger = $logger; + $this->stickerMessage = $stickerMessage; + } + + public function handle() + { + $replyToken = $this->stickerMessage->getReplyToken(); + $packageId = $this->stickerMessage->getPackageId(); + $stickerId = $this->stickerMessage->getStickerId(); + $this->bot->replyMessage($replyToken, new StickerMessageBuilder($packageId, $stickerId)); + } +} diff --git a/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/TextMessageHandler.php b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/TextMessageHandler.php new file mode 100644 index 00000000..e2732f2c --- /dev/null +++ b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/TextMessageHandler.php @@ -0,0 +1,195 @@ +bot = $bot; + $this->logger = $logger; + $this->req = $req; + $this->textMessage = $textMessage; + } + + public function handle() + { + $text = $this->textMessage->getText(); + $replyToken = $this->textMessage->getReplyToken(); + $this->logger->info("Got text message from $replyToken: $text"); + + switch ($text) { + case 'profile': + $userId = $this->textMessage->getUserId(); + $this->sendProfile($replyToken, $userId); + break; + case 'bye': + if ($this->textMessage->isRoomEvent()) { + $this->bot->replyText($replyToken, 'Leaving room'); + $this->bot->leaveRoom($this->textMessage->getRoomId()); + break; + } + if ($this->textMessage->isGroupEvent()) { + $this->bot->replyText($replyToken, 'Leaving group'); + $this->bot->leaveGroup($this->textMessage->getGroupId()); + break; + } + $this->bot->replyText($replyToken, 'Bot cannot leave from 1:1 chat'); + break; + case 'confirm': + $this->bot->replyMessage( + $replyToken, + new TemplateMessageBuilder( + 'Confirm alt text', + new ConfirmTemplateBuilder('Do it?', [ + new MessageTemplateActionBuilder('Yes', 'Yes!'), + new MessageTemplateActionBuilder('No', 'No!'), + ]) + ) + ); + break; + case 'buttons': + $imageUrl = UrlBuilder::buildUrl($this->req, ['static', 'buttons', '1040.jpg']); + $buttonTemplateBuilder = new ButtonTemplateBuilder( + 'My button sample', + 'Hello my button', + $imageUrl, + [ + new UriTemplateActionBuilder('Go to line.me', 'https://line.me'), + new PostbackTemplateActionBuilder('Buy', 'action=buy&itemid=123'), + new PostbackTemplateActionBuilder('Add to cart', 'action=add&itemid=123'), + new MessageTemplateActionBuilder('Say message', 'hello hello'), + ] + ); + $templateMessage = new TemplateMessageBuilder('Button alt text', $buttonTemplateBuilder); + $this->bot->replyMessage($replyToken, $templateMessage); + break; + case 'carousel': + $imageUrl = UrlBuilder::buildUrl($this->req, ['static', 'buttons', '1040.jpg']); + $carouselTemplateBuilder = new CarouselTemplateBuilder([ + new CarouselColumnTemplateBuilder('foo', 'bar', $imageUrl, [ + new UriTemplateActionBuilder('Go to line.me', 'https://line.me'), + new PostbackTemplateActionBuilder('Buy', 'action=buy&itemid=123'), + ]), + new CarouselColumnTemplateBuilder('buz', 'qux', $imageUrl, [ + new PostbackTemplateActionBuilder('Add to cart', 'action=add&itemid=123'), + new MessageTemplateActionBuilder('Say message', 'hello hello'), + ]), + ]); + $templateMessage = new TemplateMessageBuilder('Button alt text', $carouselTemplateBuilder); + $this->bot->replyMessage($replyToken, $templateMessage); + break; + case 'imagemap': + $richMessageUrl = UrlBuilder::buildUrl($this->req, ['static', 'rich']); + $imagemapMessageBuilder = new ImagemapMessageBuilder( + $richMessageUrl, + 'This is alt text', + new BaseSizeBuilder(1040, 1040), + [ + new ImagemapUriActionBuilder( + 'https://store.line.me/family/manga/en', + new AreaBuilder(0, 0, 520, 520) + ), + new ImagemapUriActionBuilder( + 'https://store.line.me/family/music/en', + new AreaBuilder(520, 0, 520, 520) + ), + new ImagemapUriActionBuilder( + 'https://store.line.me/family/play/en', + new AreaBuilder(0, 520, 520, 520) + ), + new ImagemapMessageActionBuilder( + 'URANAI!', + new AreaBuilder(520, 520, 520, 520) + ) + ] + ); + $this->bot->replyMessage($replyToken, $imagemapMessageBuilder); + break; + default: + $this->echoBack($replyToken, $text); + break; + } + } + + /** + * @param string $replyToken + * @param string $text + */ + private function echoBack($replyToken, $text) + { + $this->logger->info("Returns echo message $replyToken: $text"); + $this->bot->replyText($replyToken, $text); + } + + private function sendProfile($replyToken, $userId) + { + if (!isset($userId)) { + $this->bot->replyText($replyToken, "Bot can't use profile API without user ID"); + return; + } + + $response = $this->bot->getProfile($userId); + if (!$response->isSucceeded()) { + $this->bot->replyText($replyToken, $response->getRawBody()); + return; + } + + $profile = $response->getJSONDecodedBody(); + $this->bot->replyText( + $replyToken, + 'Display name: ' . $profile['displayName'], + 'Status message: ' . $profile['statusMessage'] + ); + } +} diff --git a/src/LINEBot/Receive/Operation.php b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/Util/UrlBuilder.php similarity index 52% rename from src/LINEBot/Receive/Operation.php rename to examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/Util/UrlBuilder.php index 3213aaa8..d88574e9 100644 --- a/src/LINEBot/Receive/Operation.php +++ b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/Util/UrlBuilder.php @@ -1,4 +1,5 @@ getResult()['content']['revision']; - } +namespace LINE\LINEBot\KitchenSink\EventHandler\MessageHandler\Util; - public function getFromMid() - { - return $this->getResult()['content']['params'][0]; - } - - public function isAddContact() - { - return false; - } - - public function isBlockContact() +class UrlBuilder +{ + public static function buildUrl(\Slim\Http\Request $req, array $paths) { - return false; + // NOTE: You should configure $baseUri according to your environment + // Perhaps, it is prefer to use $_SERVER['HTTP_HOST'], $_SERVER['HTTP_X_FORWARDED_HOST'] or etc + $baseUri = $req->getUri()->getBaseUrl(); + foreach ($paths as $path) { + $baseUri .= '/' . urlencode($path); + } + return $baseUri; } } diff --git a/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/VideoMessageHandler.php b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/VideoMessageHandler.php new file mode 100644 index 00000000..fc5c5b6a --- /dev/null +++ b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/MessageHandler/VideoMessageHandler.php @@ -0,0 +1,76 @@ +bot = $bot; + $this->logger = $logger; + $this->req = $req; + $this->videoMessage = $videoMessage; + } + + public function handle() + { + $contentId = $this->videoMessage->getMessageId(); + $video = $this->bot->getMessageContent($contentId)->getRawBody(); + + $tmpfilePath = tempnam($_SERVER['DOCUMENT_ROOT'] . '/static/tmpdir', 'video-'); + unlink($tmpfilePath); + $filePath = $tmpfilePath . '.mp4'; + $filename = basename($filePath); + + $fh = fopen($filePath, 'x'); + fwrite($fh, $video); + fclose($fh); + + $replyToken = $this->videoMessage->getReplyToken(); + + $url = UrlBuilder::buildUrl($this->req, ['static', 'tmpdir', $filename]); + + // NOTE: You should pass the url of thumbnail image to `previewImageUrl`. + // This sample doesn't treat that so this sample cannot show the thumbnail. + $this->bot->replyMessage($replyToken, new VideoMessageBuilder($url, $url)); + } +} diff --git a/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/PostbackEventHandler.php b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/PostbackEventHandler.php new file mode 100644 index 00000000..ebe74ff5 --- /dev/null +++ b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/PostbackEventHandler.php @@ -0,0 +1,54 @@ +bot = $bot; + $this->logger = $logger; + $this->postbackEvent = $postbackEvent; + } + + public function handle() + { + $this->bot->replyText( + $this->postbackEvent->getReplyToken(), + 'Got postback ' . $this->postbackEvent->getPostbackData() + ); + } +} diff --git a/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/UnfollowEventHandler.php b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/UnfollowEventHandler.php new file mode 100644 index 00000000..b5d71c33 --- /dev/null +++ b/examples/KitchenSink/src/LINEBot/KitchenSink/EventHandler/UnfollowEventHandler.php @@ -0,0 +1,55 @@ +bot = $bot; + $this->logger = $logger; + $this->unfollowEvent = $unfollowEvent; + } + + public function handle() + { + $this->logger->info(sprintf( + 'Unfollowed this bot %s %s', + $this->unfollowEvent->getType(), + $this->unfollowEvent->getUserId() + )); + } +} diff --git a/examples/KitchenSink/src/LINEBot/KitchenSink/Route.php b/examples/KitchenSink/src/LINEBot/KitchenSink/Route.php new file mode 100644 index 00000000..a5392c25 --- /dev/null +++ b/examples/KitchenSink/src/LINEBot/KitchenSink/Route.php @@ -0,0 +1,129 @@ +post('/callback', function (\Slim\Http\Request $req, \Slim\Http\Response $res) { + /** @var LINEBot $bot */ + $bot = $this->bot; + /** @var \Monolog\Logger $logger */ + $logger = $this->logger; + + $signature = $req->getHeader(HTTPHeader::LINE_SIGNATURE); + if (empty($signature)) { + $logger->info('Signature is missing'); + return $res->withStatus(400, 'Bad Request'); + } + + try { + $events = $bot->parseEventRequest($req->getBody(), $signature[0]); + } catch (InvalidSignatureException $e) { + $logger->info('Invalid signature'); + return $res->withStatus(400, 'Invalid signature'); + } catch (UnknownEventTypeException $e) { + return $res->withStatus(400, 'Unknown event type has come'); + } catch (UnknownMessageTypeException $e) { + return $res->withStatus(400, 'Unknown message type has come'); + } catch (InvalidEventRequestException $e) { + return $res->withStatus(400, "Invalid event request"); + } + + foreach ($events as $event) { + /** @var EventHandler $handler */ + $handler = null; + + if ($event instanceof MessageEvent) { + if ($event instanceof TextMessage) { + $handler = new TextMessageHandler($bot, $logger, $req, $event); + } elseif ($event instanceof StickerMessage) { + $handler = new StickerMessageHandler($bot, $logger, $event); + } elseif ($event instanceof LocationMessage) { + $handler = new LocationMessageHandler($bot, $logger, $event); + } elseif ($event instanceof ImageMessage) { + $handler = new ImageMessageHandler($bot, $logger, $req, $event); + } elseif ($event instanceof AudioMessage) { + $handler = new AudioMessageHandler($bot, $logger, $req, $event); + } elseif ($event instanceof VideoMessage) { + $handler = new VideoMessageHandler($bot, $logger, $req, $event); + } else { + // Just in case... + $logger->info('Unknown message type has come'); + continue; + } + } elseif ($event instanceof UnfollowEvent) { + $handler = new UnfollowEventHandler($bot, $logger, $event); + } elseif ($event instanceof FollowEvent) { + $handler = new FollowEventHandler($bot, $logger, $event); + } elseif ($event instanceof JoinEvent) { + $handler = new JoinEventHandler($bot, $logger, $event); + } elseif ($event instanceof LeaveEvent) { + $handler = new LeaveEventHandler($bot, $logger, $event); + } elseif ($event instanceof PostbackEvent) { + $handler = new PostbackEventHandler($bot, $logger, $event); + } elseif ($event instanceof BeaconDetectionEvent) { + $handler = new BeaconEventHandler($bot, $logger, $event); + } else { + // Just in case... + $logger->info('Unknown event type has come'); + continue; + } + + $handler->handle(); + } + + $res->write('OK'); + return $res; + }); + } +} diff --git a/examples/KitchenSink/src/LINEBot/KitchenSink/Setting.php b/examples/KitchenSink/src/LINEBot/KitchenSink/Setting.php new file mode 100644 index 00000000..a0dcd398 --- /dev/null +++ b/examples/KitchenSink/src/LINEBot/KitchenSink/Setting.php @@ -0,0 +1,43 @@ + [ + 'displayErrorDetails' => true, // set to false in production + + 'logger' => [ + 'name' => 'slim-app', + 'path' => __DIR__ . '/../../../logs/app.log', + ], + + 'bot' => [ + 'channelToken' => getenv('LINEBOT_CHANNEL_TOKEN') ?: '', + 'channelSecret' => getenv('LINEBOT_CHANNEL_SECRET') ?: '', + ], + + 'apiEndpointBase' => getenv('LINEBOT_API_ENDPOINT_BASE'), + ], + ]; + } +} diff --git a/examples/SendingSample/sample.php b/examples/SendingSample/sample.php deleted file mode 100644 index 595c1c18..00000000 --- a/examples/SendingSample/sample.php +++ /dev/null @@ -1,71 +0,0 @@ - $channelId, - 'channelSecret' => $channelSecret, - 'channelMid' => $channelMid, -]; -$sdk = new LINEBot($config, new GuzzleHTTPClient($config)); - -// Send a text message -$sdk->sendText([$targetMid], 'hello!'); - -// Send an image -$sdk->sendImage([$targetMid], 'http://example.com/image.jpg', 'http://example.com/preview.jpg'); - -// Send an voice message -$sdk->sendAudio([$targetMid], 'http://example.com/audio.m4a', 5000); - -// Send a video -$sdk->sendVideo([$targetMid], 'http://example.com/video.mp4', 'http://example.com/video_preview.jpg'); - -// Send a location -$sdk->sendLocation([$targetMid], '2 Chome-21-1 Shibuya Tokyo 150-0002, Japan', 35.658240, 139.703478); - -// Send a sticker -$sdk->sendSticker([$targetMid], 1, 1, 100); - -// Send a rich message -$markup = (new Markup(1040)) - ->setAction('SOMETHING', 'something', 'https://line.me') - ->addListener('SOMETHING', 0, 0, 1040, 1040); -$sdk->sendRichMessage([$targetMid], 'https://example.com/image.jpg', "Alt text", $markup); - -// Send multiple messages -$multipleMessages = (new MultipleMessages()) - ->addText('hello!') - ->addImage('http://example.com/image.jpg', 'http://example.com/preview.jpg') - ->addAudio('http://example.com/audio.m4a', 6000) - ->addVideo('http://example.com/video.mp4', 'http://example.com/video_preview.jpg') - ->addLocation('2 Chome-21-1 Shibuya Tokyo 150-0002, Japan', 35.658240, 139.703478) - ->addSticker(1, 1, 100); -$sdk->sendMultipleMessages([$targetMid], $multipleMessages); diff --git a/examples/SendingSample/settings.php b/examples/SendingSample/settings.php deleted file mode 100644 index 95f5bf04..00000000 --- a/examples/SendingSample/settings.php +++ /dev/null @@ -1,8 +0,0 @@ - '', - 'channelSecret' => '', - 'channelMid' => '', - 'targetMid' => '', -]; diff --git a/phpunit.xml b/phpunit.xml index f278eeb1..fefe4038 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,3 +1,4 @@ + diff --git a/src/LINEBot.php b/src/LINEBot.php index edf0bed6..064e7a27 100644 --- a/src/LINEBot.php +++ b/src/LINEBot.php @@ -1,4 +1,5 @@ client = $client; - $this->channelId = $args['channelId']; + $this->httpClient = $httpClient; $this->channelSecret = $args['channelSecret']; - $this->channelMid = $args['channelMid']; - $this->eventAPIEndpoint = isset($args['eventAPIEndpoint']) ? - $args['eventAPIEndpoint'] : 'https://trialbot-api.line.me/v1/events'; - $this->botAPIEndpoint = isset($args['botAPIEndpoint']) ? - $args['botAPIEndpoint'] : 'https://trialbot-api.line.me/v1'; + $this->endpointBase = + array_key_exists('endpointBase', $args) && $args['endpointBase'] ?: LINEBot::DEFAULT_ENDPOINT_BASE; } /** - * Send a text message to mid(s). + * Gets specified user's profile through API calling. * - * @link https://developers.line.me/bot-api/api-reference#sending_message_text - * @param string|array $mid Target user's MID string or MID array. - * @param string $text String you want to send. Message can contain up to 1024 characters - * @param int $toType Type of user who will receive the message (Default: 1 = user). - * @return FailedResponse|SucceededResponse + * @param string $userId The user ID to retrieve profile. + * @return Response */ - public function sendText($mid, $text, $toType = RecipientType::USER) + public function getProfile($userId) { - return $this->sendMessage($mid, MessageBuilder::buildText($text), $toType); + return $this->httpClient->get($this->endpointBase . '/v2/bot/profile/' . urlencode($userId)); } /** - * Send a image to mid(s). + * Gets message content which is associated with specified message ID. * - * @link https://developers.line.me/bot-api/api-reference#sending_message_image - * @param string|array $mid Target user's MID string or MID array. - * @param string $imageURL URL of image. Only JPEG format supported. Image size cannot be larger than 1024×1024. - * @param string $previewURL URL of thumbnail image. For preview. Only JPEG format supported. - * Image size cannot be larger than 240×240. - * @param int $toType Type of user who will receive the message (Default: 1 = user). - * @return FailedResponse|SucceededResponse + * @param string $messageId The message ID to retrieve content. + * @return Response */ - public function sendImage($mid, $imageURL, $previewURL, $toType = RecipientType::USER) + public function getMessageContent($messageId) { - return $this->sendMessage($mid, MessageBuilder::buildImage($imageURL, $previewURL), $toType); + return $this->httpClient->get($this->endpointBase . '/v2/bot/message/' . urlencode($messageId) . '/content'); } /** - * Send a video to mid(s). + * Replies arbitrary message to destination which is associated with reply token. * - * @link https://developers.line.me/bot-api/api-reference#sending_message_video - * @param string|array $mid Target user's MID string or MID array. - * @param string $videoURL URL of the movie. The “mp4” format is recommended. - * @param string $previewImageURL URL of thumbnail image used as a preview. - * @param int $toType Type of user who will receive the message (Default: 1 = user). - * @return FailedResponse|SucceededResponse + * @param string $replyToken Identifier of destination. + * @param MessageBuilder $messageBuilder Message builder to send. + * @return Response */ - public function sendVideo($mid, $videoURL, $previewImageURL, $toType = RecipientType::USER) + public function replyMessage($replyToken, MessageBuilder $messageBuilder) { - return $this->sendMessage($mid, MessageBuilder::buildVideo($videoURL, $previewImageURL), $toType); + return $this->httpClient->post($this->endpointBase . '/v2/bot/message/reply', [ + 'replyToken' => $replyToken, + 'messages' => $messageBuilder->buildMessage(), + ]); } /** - * Send a voice message to mid(s). + * Replies text message(s) to destination which is associated with reply token. * - * @link https://developers.line.me/bot-api/api-reference#sending_message_audio - * @param string|array $mid Target user's MID string or MID array. - * @param string $audioURL URL of audio file. The “m4a” format is recommended. - * @param int $durationMillis Length of voice message. The unit is given in milliseconds. - * @param int $toType Type of user who will receive the message (Default: 1 = user). - * @return FailedResponse|SucceededResponse - */ - public function sendAudio($mid, $audioURL, $durationMillis, $toType = RecipientType::USER) - { - return $this->sendMessage($mid, MessageBuilder::buildAudio($audioURL, $durationMillis), $toType); - } - - /** - * Send location information to mid(s). + * This method receives variable texts. It can send text(s) message as bulk. * - * @link https://developers.line.me/bot-api/api-reference#sending_message_location - * @param string|array $mid Target user's MID string or MID array. - * @param string $text String used to explain the location information (example: name of restaurant, address). - * @param float $latitude Latitude. - * @param float $longitude Longitude. - * @param int $toType Type of user who will receive the message (Default: 1 = user). - * @return FailedResponse|SucceededResponse + * @param string $replyToken Identifier of destination. + * @param string $text Text of message. + * @param string[] $extraTexts Extra text of message. + * @return Response */ - public function sendLocation($mid, $text, $latitude, $longitude, $toType = RecipientType::USER) + public function replyText($replyToken, $text, ...$extraTexts) { - return $this->sendMessage($mid, MessageBuilder::buildLocation($text, $latitude, $longitude), $toType); + $textMessageBuilder = new TextMessageBuilder($text, ...$extraTexts); + return $this->replyMessage($replyToken, $textMessageBuilder); } /** - * Send a sticker to mid(s). + * Sends arbitrary message to destination. * - * @link https://developers.line.me/bot-api/api-reference#sending_message_sticker - * @param string|array $mid Target user's MID string or MID array. - * @param int $stkid ID of the sticker. - * @param int $stkpkgid Package ID of the sticker. - * @param int $stkver Version number of the sticker. If omitted, the latest version number is applied. - * @param int $toType Type of user who will receive the message (Default: 1 = user). - * @return FailedResponse|SucceededResponse + * @param string $to Identifier of destination. + * @param MessageBuilder $messageBuilder Message builder to send. + * @return Response */ - public function sendSticker($mid, $stkid, $stkpkgid, $stkver = null, $toType = RecipientType::USER) + public function pushMessage($to, MessageBuilder $messageBuilder) { - return $this->sendMessage($mid, MessageBuilder::buildSticker($stkid, $stkpkgid, $stkver), $toType); + return $this->httpClient->post($this->endpointBase . '/v2/bot/message/push', [ + 'to' => $to, + 'messages' => $messageBuilder->buildMessage(), + ]); } /** - * Send a rich message to mid(s). + * Leaves from group. * - * @link https://developers.line.me/bot-api/api-reference#sending_rich_content_message_request - * @param string|array $mid Target user's MID string or MID array. - * @param string $imageURL URL of image which is on your server. - * @param string $altText Alternative string displayed on low-level devices. - * @param Markup $markup Markup json of rich message object. - * @param int $toType Type of user who will receive the message (Default: 1 = user). - * @return FailedResponse|SucceededResponse + * @param string $groupId Identifier of group to leave. + * @return Response */ - public function sendRichMessage($mid, $imageURL, $altText, Markup $markup, $toType = RecipientType::USER) + public function leaveGroup($groupId) { - return $this->sendMessage($mid, RichMessageBuilder::buildRichMessage($imageURL, $altText, $markup), $toType); + return $this->httpClient->post($this->endpointBase . '/v2/bot/group/' . urlencode($groupId) . '/leave', []); } /** - * Send multiple messages to mid(s). + * Leaves from room. * - * @link https://developers.line.me/bot-api/api-reference#sending_multiple_messages_request - * @param string|array $mid Target user's MID string or MID array. - * @param MultipleMessages $multipleMessages Multiple messages to send. - * @param int $messageNotified Zero-based index of the message to be notified. Default value is 0. - * @return FailedResponse|SucceededResponse + * @param string $roomId Identifier of room to leave. + * @return Response */ - public function sendMultipleMessages($mid, MultipleMessages $multipleMessages, $messageNotified = 0) + public function leaveRoom($roomId) { - $multipleMessages = MultipleMessagesBuilder::buildMultipleMessages($multipleMessages, $messageNotified); - return $this->sendMessage($mid, $multipleMessages, null, EventType::SENDING_MULTIPLE_MESSAGES); + return $this->httpClient->post($this->endpointBase . '/v2/bot/room/' . urlencode($roomId) . '/leave', []); } /** - * Retrieve the content of a user's message which is an image or video file. + * Parse event request to Event objects. * - * @link https://developers.line.me/bot-api/api-reference#getting_message_content_request - * @param string $messageId ID of the message. - * @param resource $fileHandler File handler to store contents temporally. - * @return DownloadedContents + * @param string $body Request body. + * @param string $signature Signature of request. + * @return LINEBot\Event\BaseEvent[] */ - public function getMessageContent($messageId, $fileHandler = null) + public function parseEventRequest($body, $signature) { - return $this->client->downloadContents($this->botAPIEndpoint . "/bot/message/$messageId/content", $fileHandler); + return EventRequestParser::parseEventRequest($body, $this->channelSecret, $signature); } /** - * Retrieve thumbnail preview of the message. + * Validate request with signature. * - * @link https://developers.line.me/bot-api/api-reference#getting_message_content_preview_request - * @param string $messageId ID of the message. - * @param resource $fileHandler File handler to store contents temporally. - * @return DownloadedContents + * @param string $body Request body. + * @param string $signature Signature of request. + * @return bool Request is valid or not. */ - public function getMessageContentPreview($messageId, $fileHandler = null) + public function validateSignature($body, $signature) { - return $this->client - ->downloadContents($this->botAPIEndpoint . "/bot/message/$messageId/content/preview", $fileHandler); - } - - /** - * Retrieve user profiles. - * - * @link https://developers.line.me/bot-api/api-reference#getting_user_profile_information_request - * @param string|array $mid Array of MIDs to retrieve user profile. - * @return array User profiles. - * @throws LINEBotAPIException When request is failed or received invalid response. - */ - public function getUserProfile($mid) - { - $query = http_build_query( - ['mids' => (is_array($mid) ? implode(',', $mid) : $mid)] - ); - return $this->client->get($this->botAPIEndpoint . '/profiles?' . $query); - } - - /** - * Validate signature. - * - * @param string $json JSON body. - * @param string $signature The signature to validate. - * @return bool - */ - public function validateSignature($json, $signature) - { - return SignatureValidator::validateSignature($json, $this->channelSecret, $signature); - } - - /** - * Create receives from JSON request string. - * - * @param string $json JSON body. - * @return Receive[] - */ - public function createReceivesFromJSON($json) - { - return ReceiveFactory::createFromJSON([ - 'channelId' => $this->channelId, - 'channelSecret' => $this->channelSecret, - 'channelMid' => $this->channelMid, - ], $json); - } - - /** - * Send a message. - * - * @param string|array $mid - * @param array $data - * @param int $toType - * @param string $eventType - * @return FailedResponse|SucceededResponse - */ - private function sendMessage( - $mid, - array $data, - $toType = RecipientType::USER, - $eventType = EventType::SENDING_MESSAGE - ) { - return $this->postMessage([ - 'to' => is_array($mid) ? $mid : [$mid], - 'content' => array_merge(['toType' => $toType], $data), - ], $eventType); - } - - /** - * POST a message. - * - * @param array $data - * @param string $eventType - * @return FailedResponse|SucceededResponse - */ - private function postMessage(array $data, $eventType = EventType::SENDING_MESSAGE) - { - $data['toChannel'] = BotAPIChannel::SENDING_CHANNEL_ID; - $data['eventType'] = $eventType; - - $res = $this->client->post($this->eventAPIEndpoint, $data); - return ResponseFactory::createResponse($res); + return SignatureValidator::validateSignature($body, $this->channelSecret, $signature); } } diff --git a/src/LINEBot/Exception/ContentsDownloadingFailedException.php b/src/LINEBot/Constant/ActionType.php similarity index 76% rename from src/LINEBot/Exception/ContentsDownloadingFailedException.php rename to src/LINEBot/Constant/ActionType.php index e53a26a7..1649e9ec 100644 --- a/src/LINEBot/Exception/ContentsDownloadingFailedException.php +++ b/src/LINEBot/Constant/ActionType.php @@ -1,4 +1,5 @@ event = $event; + } + + /** + * Returns event type. + * + * @return string + */ + public function getType() + { + return $this->event['type']; + } + + /** + * Returns timestamp of the event. + * + * @return int + */ + public function getTimestamp() + { + return $this->event['timestamp']; + } + + /** + * Returns reply token of the event. + * + * @return string|null + */ + public function getReplyToken() + { + return array_key_exists('replyToken', $this->event) ? $this->event['replyToken'] : null; + } + + /** + * Returns the event is user's one or not. + * + * @return bool + */ + public function isUserEvent() + { + return $this->event['source']['type'] === EventSourceType::USER; + } + + /** + * Returns the event is group's one or not. + * + * @return bool + */ + public function isGroupEvent() + { + return $this->event['source']['type'] === EventSourceType::GROUP; + } + + /** + * Returns the event is room's one or not. + * + * @return bool + */ + public function isRoomEvent() + { + return $this->event['source']['type'] === EventSourceType::ROOM; + } + + /** + * Returns user ID of the event. + * + * @return string|null + * @throws InvalidEventSourceException Raise when called with non user type event. + */ + public function getUserId() + { + if (!$this->isUserEvent()) { + throw new InvalidEventSourceException('This event source is not a user type'); + } + return array_key_exists('userId', $this->event['source']) + ? $this->event['source']['userId'] + : null; + } + + /** + * Returns group ID of the event. + * + * @return string|null + * @throws InvalidEventSourceException Raise when called with non group type event. + */ + public function getGroupId() + { + if (!$this->isGroupEvent()) { + throw new InvalidEventSourceException('This event source is not a group type'); + } + return array_key_exists('groupId', $this->event['source']) + ? $this->event['source']['groupId'] + : null; + } + + /** + * Returns room ID of the event. + * + * @return string|null + * @throws InvalidEventSourceException Raise when called with non room type event. + */ + public function getRoomId() + { + if (!$this->isRoomEvent()) { + throw new InvalidEventSourceException('This event source is not a room type'); + } + return array_key_exists('roomId', $this->event['source']) + ? $this->event['source']['roomId'] + : null; + } +} diff --git a/src/LINEBot/Response/FailedResponse.php b/src/LINEBot/Event/BeaconDetectionEvent.php similarity index 53% rename from src/LINEBot/Response/FailedResponse.php rename to src/LINEBot/Event/BeaconDetectionEvent.php index b13f87ee..e23ff68c 100644 --- a/src/LINEBot/Response/FailedResponse.php +++ b/src/LINEBot/Event/BeaconDetectionEvent.php @@ -1,4 +1,5 @@ data = $data; - } - public function getHTTPStatus() - { - return $this->data['httpStatus']; - } +namespace LINE\LINEBot\Event; - public function isSucceeded() - { - return false; - } - - public function getStatusCode() +/** + * A class that represents the event of beacon detection. + * + * @package LINE\LINEBot\Event + */ +class BeaconDetectionEvent extends BaseEvent +{ + /** + * BeaconDetectionEvent constructor. + * + * @param array $event + */ + public function __construct($event) { - return $this->data['statusCode']; + parent::__construct($event); } - public function getStatusMessage() + /** + * Get hardware ID of the beacon. + * + * @return string + */ + public function getHwid() { - return $this->data['statusMessage']; + return $this->event['beacon']['hwid']; } } diff --git a/src/LINEBot/Event/FollowEvent.php b/src/LINEBot/Event/FollowEvent.php new file mode 100644 index 00000000..e8152fc8 --- /dev/null +++ b/src/LINEBot/Event/FollowEvent.php @@ -0,0 +1,37 @@ +fileHandler = $fileHandler; - $this->headers = $headers; - } + parent::__construct($event); - /** - * Get file handler. - * - * @return resource - */ - public function getFileHandle() - { - return $this->fileHandler; + $this->message = $event['message']; } /** - * Get headers. + * Returns the identifier of the message. * - * @return array + * @return string */ - public function getHeaders() + public function getMessageId() { - return $this->headers; + return $this->message['id']; } } diff --git a/src/LINEBot/Event/MessageEvent/AudioMessage.php b/src/LINEBot/Event/MessageEvent/AudioMessage.php new file mode 100644 index 00000000..3be1b195 --- /dev/null +++ b/src/LINEBot/Event/MessageEvent/AudioMessage.php @@ -0,0 +1,39 @@ +message['title']; + } + + /** + * Returns address of the location message. + * + * @return string + */ + public function getAddress() + { + return $this->message['address']; + } + + /** + * Returns latitude of the location message. + * + * @return double + */ + public function getLatitude() + { + return $this->message['latitude']; + } + + /** + * Returns longitude of the location message. + * + * @return double + */ + public function getLongitude() + { + return $this->message['longitude']; + } +} diff --git a/src/LINEBot/Event/MessageEvent/StickerMessage.php b/src/LINEBot/Event/MessageEvent/StickerMessage.php new file mode 100644 index 00000000..bfe20fa1 --- /dev/null +++ b/src/LINEBot/Event/MessageEvent/StickerMessage.php @@ -0,0 +1,59 @@ +message['packageId']; + } + + /** + * Returns the identifier of the sticker. + * + * @return string + */ + public function getStickerId() + { + return $this->message['stickerId']; + } +} diff --git a/src/LINEBot/Receive/Message/Image.php b/src/LINEBot/Event/MessageEvent/TextMessage.php similarity index 51% rename from src/LINEBot/Receive/Message/Image.php rename to src/LINEBot/Event/MessageEvent/TextMessage.php index 26805bc7..0df46113 100644 --- a/src/LINEBot/Receive/Message/Image.php +++ b/src/LINEBot/Event/MessageEvent/TextMessage.php @@ -1,4 +1,5 @@ config = $config; - $this->result = $result; - } - - public function getResult() - { - return $this->result; - } - - public function getConfig() +/** + * A class that represents the message event of text. + * + * @package LINE\LINEBot\Event\MessageEvent + */ +class TextMessage extends MessageEvent +{ + /** + * TextMessage constructor. + * + * @param array $event + */ + public function __construct($event) { - return $this->config; + parent::__construct($event); } - public function isImage() + /** + * Returns text of the message. + * + * @return string + */ + public function getText() { - return true; + return $this->message['text']; } } diff --git a/src/LINEBot/Event/MessageEvent/VideoMessage.php b/src/LINEBot/Event/MessageEvent/VideoMessage.php new file mode 100644 index 00000000..fa080f07 --- /dev/null +++ b/src/LINEBot/Event/MessageEvent/VideoMessage.php @@ -0,0 +1,39 @@ + 'LINE\LINEBot\Event\MessageEvent', + 'follow' => 'LINE\LINEBot\Event\FollowEvent', + 'unfollow' => 'LINE\LINEBot\Event\UnfollowEvent', + 'join' => 'LINE\LINEBot\Event\JoinEvent', + 'leave' => 'LINE\LINEBot\Event\LeaveEvent', + 'postback' => 'LINE\LINEBot\Event\PostbackEvent', + 'beacon' => 'LINE\LINEBot\Event\BeaconDetectionEvent', + ]; + + private static $messageType2class = [ + 'text' => 'LINE\LINEBot\Event\MessageEvent\TextMessage', + 'image' => 'LINE\LINEBot\Event\MessageEvent\ImageMessage', + 'video' => 'LINE\LINEBot\Event\MessageEvent\VideoMessage', + 'audio' => 'LINE\LINEBot\Event\MessageEvent\AudioMessage', + 'location' => 'LINE\LINEBot\Event\MessageEvent\LocationMessage', + 'sticker' => 'LINE\LINEBot\Event\MessageEvent\StickerMessage', + ]; + + /** + * @param string $body + * @param string $channelSecret + * @param string $signature + * @return \LINE\LINEBot\Event\BaseEvent[] array + * @throws InvalidEventRequestException + * @throws InvalidSignatureException + * @throws UnknownEventTypeException + */ + public static function parseEventRequest($body, $channelSecret, $signature) + { + if (!isset($signature)) { + throw new InvalidSignatureException('Request does not contain signature'); + } + + if (!SignatureValidator::validateSignature($body, $channelSecret, $signature)) { + throw new InvalidSignatureException('Invalid signature has given'); + } + + $events = []; + + $parsedReq = json_decode($body, true); + if (!array_key_exists('events', $parsedReq)) { + throw new InvalidEventRequestException(); + } + + foreach ($parsedReq['events'] as $eventData) { + $eventType = $eventData['type']; + $eventClass = self::$eventType2class[$eventType]; + if (!isset($eventClass)) { + throw new UnknownEventTypeException('Unknown event type has come: ' . $eventType); + } + + if ($eventType === 'message') { + $events[] = self::parseMessageEvent($eventData); + continue; + } + + $refClass = new ReflectionClass($eventClass); + $events[] = $refClass->newInstance($eventData); + } + + return $events; + } + + /** + * @param array $eventData + * @return MessageEvent|object + * @throws UnknownMessageTypeException + */ + private static function parseMessageEvent($eventData) + { + $messageType = $eventData['message']['type']; + $messageClass = self::$messageType2class[$messageType]; + if (!isset($messageClass)) { + throw new UnknownMessageTypeException('Unknown message type has come: ' . $messageType); + } + $refClass = new ReflectionClass($messageClass); + return $refClass->newInstance($eventData); + } +} diff --git a/src/LINEBot/Response/ResponseFactory.php b/src/LINEBot/Event/PostbackEvent.php similarity index 54% rename from src/LINEBot/Response/ResponseFactory.php rename to src/LINEBot/Event/PostbackEvent.php index 8ee918fb..3c70fee3 100644 --- a/src/LINEBot/Response/ResponseFactory.php +++ b/src/LINEBot/Event/PostbackEvent.php @@ -1,4 +1,5 @@ event['postback']['data']; } } diff --git a/src/LINEBot/Event/UnfollowEvent.php b/src/LINEBot/Event/UnfollowEvent.php new file mode 100644 index 00000000..667042cf --- /dev/null +++ b/src/LINEBot/Event/UnfollowEvent.php @@ -0,0 +1,37 @@ +ch = curl_init($url); + } + + /** + * Set multiple options for a cURL transfer + * + * @param array $options Returns TRUE if all options were successfully set. If an option could not be + * successfully set, FALSE is immediately returned, ignoring any future options in the options array. + * @return bool + */ + public function setoptArray(array $options) + { + return curl_setopt_array($this->ch, $options); + } + + /** + * Perform a cURL session + * + * @return bool Returns TRUE on success or FALSE on failure. However, if the CURLOPT_RETURNTRANSFER + * option is set, it will return the result on success, FALSE on failure. + */ + public function exec() + { + return curl_exec($this->ch); + } + + /** + * Gets information about the last transfer. + * + * @return array + */ + public function getinfo() + { + return curl_getinfo($this->ch); + } + + /** + * @return int Returns the error number or 0 (zero) if no error occurred. + */ + public function errno() + { + return curl_errno($this->ch); + } + + /** + * @return string Returns the error message or '' (the empty string) if no error occurred. + */ + public function error() + { + return curl_error($this->ch); + } + + /** + * Closes a cURL session and frees all resources. The cURL handle, ch, is also deleted. + */ + public function __destruct() + { + curl_close($this->ch); + } +} diff --git a/src/LINEBot/HTTPClient/CurlHTTPClient.php b/src/LINEBot/HTTPClient/CurlHTTPClient.php new file mode 100644 index 00000000..b1644cbb --- /dev/null +++ b/src/LINEBot/HTTPClient/CurlHTTPClient.php @@ -0,0 +1,115 @@ +authHeaders = [ + "Authorization: Bearer $channelToken", + ]; + } + + /** + * Sends GET request to LINE Messaging API. + * + * @param string $url Request URL. + * @return Response Response of API request. + */ + public function get($url) + { + return $this->sendRequest('GET', $url, [], []); + } + + /** + * Sends POST request to LINE Messaging API. + * + * @param string $url Request URL. + * @param array $data Request body. + * @return Response Response of API request. + */ + public function post($url, array $data) + { + return $this->sendRequest('POST', $url, ['Content-Type: application/json; charset=utf-8'], $data); + } + + /** + * @param string $method + * @param string $url + * @param array $additionalHeader + * @param array $reqBody + * @return Response + * @throws CurlExecutionException + */ + private function sendRequest($method, $url, array $additionalHeader, array $reqBody) + { + $curl = new Curl($url); + + $headers = array_merge($this->authHeaders, $this->userAgentHeader, $additionalHeader); + + $options = [ + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_HEADER => false, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_BINARYTRANSFER => true, + ]; + + if ($method === 'POST' && !empty($reqBody)) { + $options[CURLOPT_POSTFIELDS] = json_encode($reqBody); + } + + $curl->setoptArray($options); + + $body = $curl->exec(); + + $info = $curl->getinfo(); + $httpStatus = $info['http_code']; + + if ($curl->errno()) { + throw new CurlExecutionException($curl->error()); + } + + return new Response($httpStatus, $body); + } +} diff --git a/src/LINEBot/HTTPClient/GuzzleHTTPClient.php b/src/LINEBot/HTTPClient/GuzzleHTTPClient.php deleted file mode 100644 index 90c9d274..00000000 --- a/src/LINEBot/HTTPClient/GuzzleHTTPClient.php +++ /dev/null @@ -1,179 +0,0 @@ -channelId = $args['channelId']; - $this->channelSecret = $args['channelSecret']; - $this->channelMid = $args['channelMid']; - - $this->guzzle = new Client($args); - } - - /** - * Send get request with credential headers. - * - * @param string $url Destination URL to send. - * @return array - * @throws LINEBotAPIException When request is failed or received invalid response. - */ - public function get($url) - { - try { - $res = $this->guzzle->get($url, ['headers' => $this->credentials()]); - } catch (BadResponseException $e) { - $res = $e->getResponse(); - } - - $resContent = $res->getBody(); - $resStatus = $res->getStatusCode(); - - if (!$resContent || !preg_match('/\A{.+}\z/u', $resContent)) { - throw new LINEBotAPIException("LINE BOT API error: $resStatus"); - } - - $ret = json_decode($resContent, true); - if ($ret === null) { - throw new LINEBotAPIException("LINE BOT API error: $resStatus"); - } - return $ret; - } - - /** - * Send post request with credential headers. - * - * @param string $url Destination URL to send. - * @param array $data Request body - * @return array - * @throws JSONEncodingException When invalid request has come. - * @throws LINEBotAPIException When request is failed or received invalid response. - */ - public function post($url, array $data) - { - $json = json_encode($data); - if ($json === false) { - throw new JSONEncodingException("Failed to encode request JSON"); - } - - $headers = array_merge($this->credentials(), [ - 'Content-Type' => 'application/json; charset=UTF-8', - 'Content-Length' => strlen($json), - ]); - - try { - $res = $this->guzzle->post($url, [ - 'headers' => $headers, - 'body' => $json, - ]); - } catch (BadResponseException $e) { - $res = $e->getResponse(); - } - - $resContent = $res->getBody(); - $resStatus = $res->getStatusCode(); - - if (!$resContent || !preg_match('/\A{.+}\z/u', $resContent)) { - throw new LINEBotAPIException("LINE BOT API error: $resStatus"); - } - - $ret = json_decode($resContent, true); - if ($ret === null) { - throw new LINEBotAPIException("LINE BOT API error: $resStatus"); - } - - $ret['httpStatus'] = $resStatus; - return $ret; - } - - /** - * Download contents. - * - * @param string $url Contents URL. - * @param resource $fileHandler File handler to store contents temporally. - * @return DownloadedContents - * @throws ContentsDownloadingFailedException When failed to download contents. - */ - public function downloadContents($url, $fileHandler = null) - { - if ($fileHandler === null) { - $fileHandler = tmpfile(); - } - $stream = Stream::factory($fileHandler); - - try { - $res = $this->guzzle->get($url, [ - 'save_to' => $stream, - 'headers' => $this->credentials(), - ]); - } catch (BadResponseException $e) { - $res = $e->getResponse(); - } - - $resStatus = $res->getStatusCode(); - if ($resStatus !== 200) { - $resContent = $res->getBody(); - throw new ContentsDownloadingFailedException( - "LINE BOT API contents_download error: $resStatus $url\ncontent=$resContent" - ); - } - - return new DownloadedContents($stream->detach(), $res->getHeaders()); - } - - private function credentials() - { - return [ - 'X-Line-ChannelID' => $this->channelId, - 'X-Line-ChannelSecret' => $this->channelSecret, - 'X-Line-Trusted-User-With-ACL' => $this->channelMid, - ]; - } -} diff --git a/src/LINEBot/Constant/ContentType.php b/src/LINEBot/ImagemapActionBuilder.php similarity index 62% rename from src/LINEBot/Constant/ContentType.php rename to src/LINEBot/ImagemapActionBuilder.php index fb23550d..c7065ad5 100644 --- a/src/LINEBot/Constant/ContentType.php +++ b/src/LINEBot/ImagemapActionBuilder.php @@ -1,4 +1,5 @@ x = $x; + $this->y = $y; + $this->width = $width; + $this->height = $height; + } + + /** + * Builds imagemap area structure. + * + * @return array Built area structure. + */ + public function build() + { + return [ + 'x' => $this->x, + 'y' => $this->y, + 'width' => $this->width, + 'height' => $this->height, + ]; + } +} diff --git a/src/LINEBot/ImagemapActionBuilder/ImagemapMessageActionBuilder.php b/src/LINEBot/ImagemapActionBuilder/ImagemapMessageActionBuilder.php new file mode 100644 index 00000000..255fea8f --- /dev/null +++ b/src/LINEBot/ImagemapActionBuilder/ImagemapMessageActionBuilder.php @@ -0,0 +1,61 @@ +text = $text; + $this->areaBuilder = $areaBuilder; + } + + /** + * Builds imagemap message action structure. + * + * @return array Built imagemap structure. + */ + public function buildImagemapAction() + { + return [ + 'type' => ActionType::MESSAGE, + 'text' => $this->text, + 'area' => $this->areaBuilder->build(), + ]; + } +} diff --git a/src/LINEBot/ImagemapActionBuilder/ImagemapUriActionBuilder.php b/src/LINEBot/ImagemapActionBuilder/ImagemapUriActionBuilder.php new file mode 100644 index 00000000..3256168a --- /dev/null +++ b/src/LINEBot/ImagemapActionBuilder/ImagemapUriActionBuilder.php @@ -0,0 +1,59 @@ +linkUri = $linkUri; + $this->areaBuilder = $areaBuilder; + } + + /** + * Builds imagemap URI action structure. + * + * @return array Built URI action structure. + */ + public function buildImagemapAction() + { + return [ + 'type' => ActionType::URI, + 'linkUri' => $this->linkUri, + 'area' => $this->areaBuilder->build(), + ]; + } +} diff --git a/src/LINEBot/Message/Builder/MessageBuilder.php b/src/LINEBot/Message/Builder/MessageBuilder.php deleted file mode 100644 index 9b67cefc..00000000 --- a/src/LINEBot/Message/Builder/MessageBuilder.php +++ /dev/null @@ -1,137 +0,0 @@ - ContentType::TEXT, - 'text' => $text, - ]; - } - - /** - * Build image message payload. - * - * @link https://developers.line.me/bot-api/api-reference#sending_message_image - * @param string $imageURL - * @param string $previewURL - * @return array - */ - public static function buildImage($imageURL, $previewURL) - { - return [ - 'contentType' => ContentType::IMAGE, - 'originalContentUrl' => $imageURL, - 'previewImageUrl' => $previewURL, - ]; - } - - /** - * Build video message payload. - * - * @link https://developers.line.me/bot-api/api-reference#sending_message_video - * @param string $videoURL - * @param string $previewImageURL - * @return array - */ - public static function buildVideo($videoURL, $previewImageURL) - { - return [ - 'contentType' => ContentType::VIDEO, - 'originalContentUrl' => $videoURL, - 'previewImageUrl' => $previewImageURL, - ]; - } - - /** - * Build voice message payload. - * - * @link https://developers.line.me/bot-api/api-reference#sending_message_audio - * @param string $audioURL - * @param int $durationMillis - * @return array - */ - public static function buildAudio($audioURL, $durationMillis) - { - return [ - 'contentType' => ContentType::AUDIO, - 'originalContentUrl' => $audioURL, - 'contentMetadata' => [ - 'AUDLEN' => (string)$durationMillis, - ], - ]; - } - - /** - * Build location message payload. - * - * @link https://developers.line.me/bot-api/api-reference#sending_message_location - * @param string $text - * @param float $latitude - * @param float $longitude - * @return array - */ - public static function buildLocation($text, $latitude, $longitude) - { - return [ - 'contentType' => ContentType::LOCATION, - 'text' => $text, - 'location' => [ - 'title' => $text, - 'latitude' => $latitude, - 'longitude' => $longitude, - ], - ]; - } - - /** - * Build sticker message payload. - * - * @link https://developers.line.me/bot-api/api-reference#sending_message_sticker - * @param int $stkid - * @param int $stkpkgid - * @param int $stkver - * @return array - */ - public static function buildSticker($stkid, $stkpkgid, $stkver = null) - { - $meta = [ - 'STKID' => (string)$stkid, - 'STKPKGID' => (string)$stkpkgid, - ]; - if ($stkver !== null) { - $meta = array_merge($meta, ['STKVER' => (string)$stkver]); - } - - return [ - 'contentType' => ContentType::STICKER, - 'contentMetadata' => $meta, - ]; - } -} diff --git a/src/LINEBot/Message/Builder/MultipleMessagesBuilder.php b/src/LINEBot/Message/Builder/MultipleMessagesBuilder.php deleted file mode 100644 index 22350f7f..00000000 --- a/src/LINEBot/Message/Builder/MultipleMessagesBuilder.php +++ /dev/null @@ -1,39 +0,0 @@ - $messageNotified, - 'messages' => $multipleMessages->getMessages(), - ]; - } -} diff --git a/src/LINEBot/Message/Builder/RichMessageBuilder.php b/src/LINEBot/Message/Builder/RichMessageBuilder.php deleted file mode 100644 index 756fe4a5..00000000 --- a/src/LINEBot/Message/Builder/RichMessageBuilder.php +++ /dev/null @@ -1,46 +0,0 @@ - ContentType::RICH_MESSAGE, - 'contentMetadata' => [ - 'SPEC_REV' => '1', - 'DOWNLOAD_URL' => $imageURL, - 'ALT_TEXT' => $altText, - 'MARKUP_JSON' => $markup->build(), - ], - ]; - } -} diff --git a/src/LINEBot/Message/MultipleMessages.php b/src/LINEBot/Message/MultipleMessages.php deleted file mode 100644 index e28cea1e..00000000 --- a/src/LINEBot/Message/MultipleMessages.php +++ /dev/null @@ -1,120 +0,0 @@ -messages[] = MessageBuilder::buildText($text); - return $this; - } - - /** - * Add image message. - * - * @link https://developers.line.me/bot-api/api-reference#sending_message_image - * @param string $imageURL - * @param string $previewURL - * @return MultipleMessages $this - */ - public function addImage($imageURL, $previewURL) - { - $this->messages[] = MessageBuilder::buildImage($imageURL, $previewURL); - return $this; - } - - /** - * Add video message. - * - * @link https://developers.line.me/bot-api/api-reference#sending_message_video - * @param string $videoURL - * @param string $previewImageURL - * @return MultipleMessages $this - */ - public function addVideo($videoURL, $previewImageURL) - { - $this->messages[] = MessageBuilder::buildVideo($videoURL, $previewImageURL); - return $this; - } - - /** - * Add voice message. - * - * @link https://developers.line.me/bot-api/api-reference#sending_message_audio - * @param string $audioURL - * @param int $duration - * @return MultipleMessages $this - */ - public function addAudio($audioURL, $duration) - { - $this->messages[] = MessageBuilder::buildAudio($audioURL, $duration); - return $this; - } - - /** - * Add location message. - * - * @link https://developers.line.me/bot-api/api-reference#sending_message_location - * @param string $text - * @param float $latitude - * @param float $longitude - * @return MultipleMessages $this - */ - public function addLocation($text, $latitude, $longitude) - { - $this->messages[] = MessageBuilder::buildLocation($text, $latitude, $longitude); - return $this; - } - - /** - * Add sticker message. - * - * @link https://developers.line.me/bot-api/api-reference#sending_message_sticker - * @param int $stkid - * @param int $stkpkgid - * @param int $stkver - * @return MultipleMessages $this - */ - public function addSticker($stkid, $stkpkgid, $stkver = null) - { - $this->messages[] = MessageBuilder::buildSticker($stkid, $stkpkgid, $stkver); - return $this; - } - - /** - * Get registered messages. - * - * @return array - */ - public function getMessages() - { - return $this->messages; - } -} diff --git a/src/LINEBot/Message/RichMessage/Markup.php b/src/LINEBot/Message/RichMessage/Markup.php deleted file mode 100644 index 11c069fa..00000000 --- a/src/LINEBot/Message/RichMessage/Markup.php +++ /dev/null @@ -1,152 +0,0 @@ - 2080) { - throw new IllegalRichMessageHeightException('Rich Message canvas\'s height ' . - 'should be less than or equals 2080px'); - } - - $this->canvas = [ - 'height' => $height, // Integer value. Max value is 2080px. - 'width' => 1040, // Integer fixed value: 1040. - 'initialScene' => 'scene1' // Fixed string: 'scene1' - ]; - - $this->images = [ - 'image1' => [ - 'x' => 0, // Fixed 0. - 'y' => 0, // Fixed 0. - 'w' => 1040, // Integer fixed value: 1040. - 'h' => $height, // Integer value. Max value is 2080px. - ], - ]; - - $this->actions = []; - - $this->scenes = [ - 'scene1' => [ - 'draws' => [ - [ - 'image' => 'image1', // Use the image ID "image1". - 'x' => 0, // Fixed 0. - 'y' => 0, // Fixed 0. - 'w' => 1040, // Integer fixed value: 1040. - 'h' => $height, // Integer value. Max value is 2080px. - ] - ], - 'listeners' => [], - ], - ]; - } - - /** - * Set an action. - * - * @param string $actionName Name of an action. - * @param string $text Alternative string displayed on low-level devices. - * @param string $linkURI URL to opened in the web browser. - * @param string $type Action type. - * @return Markup $this - */ - public function setAction($actionName, $text, $linkURI, $type = 'web') - { - $obj = [ - 'type' => $type, - ]; - - if ($type === 'web') { - $obj['text'] = $text; - $obj['params'] = [ - 'linkUri' => $linkURI, - ]; - } elseif ($type === 'sendMessage') { - $obj['params'] = [ - 'text' => $text, - ]; - } - - $this->actions[$actionName] = $obj; - - return $this; - } - - /** - * Add a listener which is associated with an action. - * - * @param string $actionName Action name to associate with listener. - * @param int $x x-coordinate value. - * @param int $y y-coordinate value. - * @param int $width Width of the image. - * @param int $height Height of the image. - * @return Markup $this - */ - public function addListener($actionName, $x, $y, $width, $height) - { - $this->scenes['scene1']['listeners'][] = [ - 'type' => 'touch', # Fixed string: 'touch' - 'params' => [$x, $y, $width, $height], - 'action' => $actionName, - ]; - - return $this; - } - - /** - * Generate markup JSON from this instance. - * - * @return string Markup JSON. - * @throws JSONEncodingException - */ - public function build() - { - $json = json_encode([ - 'canvas' => $this->canvas, - 'images' => $this->images, - 'actions' => $this->actions, - 'scenes' => $this->scenes, - ]); - - if ($json === false) { - throw new JSONEncodingException("Failed to encode markup JSON"); - } - return $json; - } -} diff --git a/src/LINEBot/Constant/BotAPIChannel.php b/src/LINEBot/MessageBuilder.php similarity index 64% rename from src/LINEBot/Constant/BotAPIChannel.php rename to src/LINEBot/MessageBuilder.php index 57460d60..3505da3e 100644 --- a/src/LINEBot/Constant/BotAPIChannel.php +++ b/src/LINEBot/MessageBuilder.php @@ -1,4 +1,5 @@ originalContentUrl = $originalContentUrl; + $this->duration = $duration; + } + + /** + * Builds + * @return array + */ + public function buildMessage() + { + return [ + [ + 'type' => MessageType::AUDIO, + 'originalContentUrl' => $this->originalContentUrl, + 'duration' => $this->duration, + ] + ]; + } +} diff --git a/src/LINEBot/MessageBuilder/ImageMessageBuilder.php b/src/LINEBot/MessageBuilder/ImageMessageBuilder.php new file mode 100644 index 00000000..1d64183e --- /dev/null +++ b/src/LINEBot/MessageBuilder/ImageMessageBuilder.php @@ -0,0 +1,63 @@ +originalContentUrl = $originalContentUrl; + $this->previewImageUrl = $previewImageUrl; + } + + /** + * Builds image message structure. + * + * @return array + */ + public function buildMessage() + { + return [ + [ + 'type' => MessageType::IMAGE, + 'originalContentUrl' => $this->originalContentUrl, + 'previewImageUrl' => $this->previewImageUrl, + ] + ]; + } +} diff --git a/src/LINEBot/MessageBuilder/Imagemap/BaseSizeBuilder.php b/src/LINEBot/MessageBuilder/Imagemap/BaseSizeBuilder.php new file mode 100644 index 00000000..603abded --- /dev/null +++ b/src/LINEBot/MessageBuilder/Imagemap/BaseSizeBuilder.php @@ -0,0 +1,57 @@ +height = $height; + $this->width = $width; + } + + /** + * Builds base size of imagemap. + * + * @return array + */ + public function build() + { + return [ + 'height' => $this->height, + 'width' => $this->width, + ]; + } +} diff --git a/src/LINEBot/MessageBuilder/ImagemapMessageBuilder.php b/src/LINEBot/MessageBuilder/ImagemapMessageBuilder.php new file mode 100644 index 00000000..7a4cb16e --- /dev/null +++ b/src/LINEBot/MessageBuilder/ImagemapMessageBuilder.php @@ -0,0 +1,87 @@ +baseUrl = $baseUrl; + $this->altText = $altText; + $this->baseSizeBuilder = $baseSizeBuilder; + $this->imagemapActionBuilders = $imagemapActionBuilders; + } + + /** + * Builds imagemap message strucutre. + * + * @return array + */ + public function buildMessage() + { + if (!empty($this->message)) { + return $this->message; + } + + $actions = []; + foreach ($this->imagemapActionBuilders as $builder) { + $actions[] = $builder->buildImagemapAction(); + } + + $this->message[] = [ + 'type' => MessageType::IMAGEMAP, + 'baseUrl' => $this->baseUrl, + 'altText' => $this->altText, + 'baseSize' => $this->baseSizeBuilder->build(), + 'actions' => $actions, + ]; + + return $this->message; + } +} diff --git a/src/LINEBot/MessageBuilder/LocationMessageBuilder.php b/src/LINEBot/MessageBuilder/LocationMessageBuilder.php new file mode 100644 index 00000000..0650f3ba --- /dev/null +++ b/src/LINEBot/MessageBuilder/LocationMessageBuilder.php @@ -0,0 +1,73 @@ +title = $title; + $this->address = $address; + $this->latitude = $latitude; + $this->longitude = $longitude; + } + + /** + * Builds location message structure. + * + * @return array + */ + public function buildMessage() + { + return [ + [ + 'type' => MessageType::LOCATION, + 'title' => $this->title, + 'address' => $this->address, + 'latitude' => $this->latitude, + 'longitude' => $this->longitude, + ] + ]; + } +} diff --git a/src/LINEBot/MessageBuilder/MultiMessageBuilder.php b/src/LINEBot/MessageBuilder/MultiMessageBuilder.php new file mode 100644 index 00000000..4a204695 --- /dev/null +++ b/src/LINEBot/MessageBuilder/MultiMessageBuilder.php @@ -0,0 +1,56 @@ +messageBuilders[] = $messageBuilder; + return $this; + } + + /** + * Builds message structure. + * + * @return array Built message structure. + */ + public function buildMessage() + { + $messages = []; + foreach ($this->messageBuilders as $messageBuilder) { + foreach ($messageBuilder->buildMessage() as $message) { + $messages[] = $message; + } + } + + return $messages; + } +} diff --git a/src/LINEBot/MessageBuilder/StickerMessageBuilder.php b/src/LINEBot/MessageBuilder/StickerMessageBuilder.php new file mode 100644 index 00000000..dfc336e6 --- /dev/null +++ b/src/LINEBot/MessageBuilder/StickerMessageBuilder.php @@ -0,0 +1,63 @@ +packageId = $packageId; + $this->stickerId = $stickerId; + } + + /** + * Builds sticker message structure. + * + * @return array + */ + public function buildMessage() + { + return [ + [ + 'type' => MessageType::STICKER, + 'packageId' => $this->packageId, + 'stickerId' => $this->stickerId, + ] + ]; + } +} diff --git a/src/LINEBot/MessageBuilder/TemplateBuilder.php b/src/LINEBot/MessageBuilder/TemplateBuilder.php new file mode 100644 index 00000000..37bcc2be --- /dev/null +++ b/src/LINEBot/MessageBuilder/TemplateBuilder.php @@ -0,0 +1,34 @@ +title = $title; + $this->text = $text; + $this->thumbnailImageUrl = $thumbnailImageUrl; + $this->actionBuilders = $actionBuilders; + } + + /** + * Builds button template message structure. + * + * @return array + */ + public function buildTemplate() + { + if (!empty($this->template)) { + return $this->template; + } + + $actions = []; + foreach ($this->actionBuilders as $actionBuilder) { + $actions[] = $actionBuilder->buildTemplateAction(); + } + + $this->template = [ + 'type' => TemplateType::BUTTONS, + 'thumbnailImageUrl' => $this->thumbnailImageUrl, + 'title' => $this->title, + 'text' => $this->text, + 'actions' => $actions, + ]; + + return $this->template; + } +} diff --git a/src/LINEBot/MessageBuilder/TemplateBuilder/CarouselColumnTemplateBuilder.php b/src/LINEBot/MessageBuilder/TemplateBuilder/CarouselColumnTemplateBuilder.php new file mode 100644 index 00000000..db35dc59 --- /dev/null +++ b/src/LINEBot/MessageBuilder/TemplateBuilder/CarouselColumnTemplateBuilder.php @@ -0,0 +1,84 @@ +title = $title; + $this->text = $text; + $this->thumbnailImageUrl = $thumbnailImageUrl; + $this->actionBuilders = $actionBuilders; + } + + /** + * Builds column of carousel template structure. + * + * @return array + */ + public function buildTemplate() + { + if (!empty($this->template)) { + return $this->template; + } + + $actions = []; + foreach ($this->actionBuilders as $actionBuilder) { + $actions[] = $actionBuilder->buildTemplateAction(); + } + + $this->template = [ + 'thumbnailImageUrl' => $this->thumbnailImageUrl, + 'title' => $this->title, + 'text' => $this->text, + 'actions' => $actions, + ]; + + return $this->template; + } +} diff --git a/src/LINEBot/MessageBuilder/TemplateBuilder/CarouselTemplateBuilder.php b/src/LINEBot/MessageBuilder/TemplateBuilder/CarouselTemplateBuilder.php new file mode 100644 index 00000000..0e8b6de8 --- /dev/null +++ b/src/LINEBot/MessageBuilder/TemplateBuilder/CarouselTemplateBuilder.php @@ -0,0 +1,70 @@ +columnTemplateBuilders = $columnTemplateBuilders; + } + + /** + * Builds carousel template structure. + * + * @return array + */ + public function buildTemplate() + { + if (!empty($this->template)) { + return $this->template; + } + + $columns = []; + foreach ($this->columnTemplateBuilders as $columnTemplateBuilder) { + $columns[] = $columnTemplateBuilder->buildTemplate(); + } + + $this->template = [ + 'type' => TemplateType::CAROUSEL, + 'columns' => $columns, + ]; + + return $this->template; + } +} diff --git a/src/LINEBot/MessageBuilder/TemplateBuilder/ConfirmTemplateBuilder.php b/src/LINEBot/MessageBuilder/TemplateBuilder/ConfirmTemplateBuilder.php new file mode 100644 index 00000000..d5ac80fc --- /dev/null +++ b/src/LINEBot/MessageBuilder/TemplateBuilder/ConfirmTemplateBuilder.php @@ -0,0 +1,76 @@ +text = $text; + $this->actionBuilders = $actionBuilders; + } + + /** + * Builds confirm template structure. + * + * @return array + */ + public function buildTemplate() + { + if (!empty($this->template)) { + return $this->template; + } + + $actions = []; + foreach ($this->actionBuilders as $actionBuilder) { + $actions[] = $actionBuilder->buildTemplateAction(); + } + + $this->template = [ + 'type' => TemplateType::CONFIRM, + 'text' => $this->text, + 'actions' => $actions, + ]; + + return $this->template; + } +} diff --git a/src/LINEBot/MessageBuilder/TemplateMessageBuilder.php b/src/LINEBot/MessageBuilder/TemplateMessageBuilder.php new file mode 100644 index 00000000..4c9d7845 --- /dev/null +++ b/src/LINEBot/MessageBuilder/TemplateMessageBuilder.php @@ -0,0 +1,62 @@ +altText = $altText; + $this->templateBuilder = $templateBuilder; + } + + /** + * Builds template message structure. + * + * @return array + */ + public function buildMessage() + { + return [ + [ + 'type' => MessageType::TEMPLATE, + 'altText' => $this->altText, + 'template' => $this->templateBuilder->buildTemplate(), + ] + ]; + } +} diff --git a/src/LINEBot/MessageBuilder/TextMessageBuilder.php b/src/LINEBot/MessageBuilder/TextMessageBuilder.php new file mode 100644 index 00000000..655ae35b --- /dev/null +++ b/src/LINEBot/MessageBuilder/TextMessageBuilder.php @@ -0,0 +1,67 @@ +texts = array_merge([$text], $extraTexts); + } + + /** + * Builds text message structure. + * + * @return array + */ + public function buildMessage() + { + if (!empty($this->message)) { + return $this->message; + } + + foreach ($this->texts as $text) { + $this->message[] = [ + 'type' => MessageType::TEXT, + 'text' => $text, + ]; + } + + return $this->message; + } +} diff --git a/src/LINEBot/MessageBuilder/VideoMessageBuilder.php b/src/LINEBot/MessageBuilder/VideoMessageBuilder.php new file mode 100644 index 00000000..55c23500 --- /dev/null +++ b/src/LINEBot/MessageBuilder/VideoMessageBuilder.php @@ -0,0 +1,63 @@ +originalContentUrl = $originalContentUrl; + $this->previewImageUrl = $previewImageUrl; + } + + /** + * Builds video message structure. + * + * @return array + */ + public function buildMessage() + { + return [ + [ + 'type' => MessageType::VIDEO, + 'originalContentUrl' => $this->originalContentUrl, + 'previewImageUrl' => $this->previewImageUrl, + ] + ]; + } +} diff --git a/src/LINEBot/Receive/Message.php b/src/LINEBot/Receive/Message.php deleted file mode 100644 index 8fe84e8c..00000000 --- a/src/LINEBot/Receive/Message.php +++ /dev/null @@ -1,88 +0,0 @@ -getConfig()['channelMid']; - foreach ($this->getResult()['content']['to'] as $mid) { - if ($myMid === $mid) { - return true; - } - } - return false; - } - - public function getContentId() - { - return $this->getResult()['content']['id']; - } - - public function getCreatedTime() - { - return $this->getResult()['content']['createdTime']; - } - - public function getFromMid() - { - return $this->getResult()['content']['from']; - } - - public function isText() - { - return false; - } - - public function isImage() - { - return false; - } - - public function isVideo() - { - return false; - } - - public function isAudio() - { - return false; - } - - public function isLocation() - { - return false; - } - - public function isSticker() - { - return false; - } - - public function isContact() - { - return false; - } -} diff --git a/src/LINEBot/Receive/Message/Audio.php b/src/LINEBot/Receive/Message/Audio.php deleted file mode 100644 index b2ce2902..00000000 --- a/src/LINEBot/Receive/Message/Audio.php +++ /dev/null @@ -1,50 +0,0 @@ -config = $config; - $this->result = $result; - } - - public function getResult() - { - return $this->result; - } - - public function getConfig() - { - return $this->config; - } - - public function isAudio() - { - return true; - } -} diff --git a/src/LINEBot/Receive/Message/Contact.php b/src/LINEBot/Receive/Message/Contact.php deleted file mode 100644 index c53a0f7e..00000000 --- a/src/LINEBot/Receive/Message/Contact.php +++ /dev/null @@ -1,63 +0,0 @@ -config = $config; - $this->result = $result; - $this->meta = $result['content']['contentMetadata']; - } - - public function getResult() - { - return $this->result; - } - - public function getConfig() - { - return $this->config; - } - - public function isContact() - { - return true; - } - - public function getMid() - { - return $this->meta['mid']; - } - - public function getDisplayName() - { - return $this->meta['displayName']; - } -} diff --git a/src/LINEBot/Receive/Message/Location.php b/src/LINEBot/Receive/Message/Location.php deleted file mode 100644 index bc9332c6..00000000 --- a/src/LINEBot/Receive/Message/Location.php +++ /dev/null @@ -1,78 +0,0 @@ -config = $config; - $this->result = $result; - $this->location = $result['content']['location']; - } - - public function getResult() - { - return $this->result; - } - - public function getConfig() - { - return $this->config; - } - - public function isLocation() - { - return true; - } - - public function getText() - { - return $this->location['title']; // `$this->location['text'] }` is always null - } - - public function getTitle() - { - return $this->location['title']; - } - - public function getAddress() - { - return $this->location['address']; - } - - public function getLatitude() - { - return $this->location['latitude']; - } - - public function getLongitude() - { - return $this->location['longitude']; - } -} diff --git a/src/LINEBot/Receive/Message/MessageReceiveFactory.php b/src/LINEBot/Receive/Message/MessageReceiveFactory.php deleted file mode 100644 index f5e381c0..00000000 --- a/src/LINEBot/Receive/Message/MessageReceiveFactory.php +++ /dev/null @@ -1,55 +0,0 @@ -config = $config; - $this->result = $result; - $this->meta = $result['content']['contentMetadata']; - } - - public function getResult() - { - return $this->result; - } - - public function getConfig() - { - return $this->config; - } - - public function isSticker() - { - return true; - } - - public function getStkPkgId() - { - return $this->meta['STKPKGID']; - } - - public function getStkId() - { - return $this->meta['STKID']; - } - - public function getStkVer() - { - return $this->meta['STKVER']; - } - - public function getStkTxt() - { - return $this->meta['STKTXT']; - } -} diff --git a/src/LINEBot/Receive/Message/Text.php b/src/LINEBot/Receive/Message/Text.php deleted file mode 100644 index 2a143d70..00000000 --- a/src/LINEBot/Receive/Message/Text.php +++ /dev/null @@ -1,55 +0,0 @@ -config = $config; - $this->result = $result; - } - - public function getResult() - { - return $this->result; - } - - public function getConfig() - { - return $this->config; - } - - public function isText() - { - return true; - } - - public function getText() - { - return $this->result['content']['text']; - } -} diff --git a/src/LINEBot/Receive/Message/Video.php b/src/LINEBot/Receive/Message/Video.php deleted file mode 100644 index b464e550..00000000 --- a/src/LINEBot/Receive/Message/Video.php +++ /dev/null @@ -1,50 +0,0 @@ -config = $config; - $this->result = $result; - } - - public function getResult() - { - return $this->result; - } - - public function getConfig() - { - return $this->config; - } - - public function isVideo() - { - return true; - } -} diff --git a/src/LINEBot/Receive/Operation/AddContact.php b/src/LINEBot/Receive/Operation/AddContact.php deleted file mode 100644 index 871fdff2..00000000 --- a/src/LINEBot/Receive/Operation/AddContact.php +++ /dev/null @@ -1,50 +0,0 @@ -config = $config; - $this->result = $result; - } - - public function getResult() - { - return $this->result; - } - - public function getConfig() - { - return $this->config; - } - - public function isAddContact() - { - return true; - } -} diff --git a/src/LINEBot/Receive/Operation/BlockContact.php b/src/LINEBot/Receive/Operation/BlockContact.php deleted file mode 100644 index 332412ec..00000000 --- a/src/LINEBot/Receive/Operation/BlockContact.php +++ /dev/null @@ -1,50 +0,0 @@ -config = $config; - $this->result = $result; - } - - public function getResult() - { - return $this->result; - } - - public function getConfig() - { - return $this->config; - } - - public function isBlockContact() - { - return true; - } -} diff --git a/src/LINEBot/Receive/Operation/OperationReceiveFactory.php b/src/LINEBot/Receive/Operation/OperationReceiveFactory.php deleted file mode 100644 index 9fbc8367..00000000 --- a/src/LINEBot/Receive/Operation/OperationReceiveFactory.php +++ /dev/null @@ -1,44 +0,0 @@ -getResult(); - $config = $this->getConfig(); - - return $result['toChannel'] == $config['channelId'] && - $result['fromChannel'] == BotAPIChannel::RECEIVING_CHANNEL_ID && - $result['from'] == BotAPIChannel::RECEIVING_CHANNEL_MID; - } - - public function getId() - { - return $this->getResult()['id']; - } - - /** - * Validate request with signature. - * - * @param string $json JSON request. - * @param string $signature - * @return bool - */ - public function validateSignature($json, $signature) - { - return SignatureValidator::validateSignature($json, $this->getConfig()['channelSecret'], $signature); - } - - abstract function getResult(); - - abstract function getConfig(); -} diff --git a/src/LINEBot/Receive/ReceiveFactory.php b/src/LINEBot/Receive/ReceiveFactory.php deleted file mode 100644 index e9c3cec1..00000000 --- a/src/LINEBot/Receive/ReceiveFactory.php +++ /dev/null @@ -1,83 +0,0 @@ -httpStatus = $httpStatus; + $this->body = $body; + } + + /** + * Returns HTTP status code of response. + * + * @return int HTTP status code of response. + */ + public function getHTTPStatus() + { + return $this->httpStatus; + } + + /** + * Returns request is succeeded or not. + * + * @return bool Request is succeeded or not. + */ + public function isSucceeded() + { + return $this->httpStatus === 200; + } + + /** + * Returns raw request body. + * + * @return string Raw request body. + */ + public function getRawBody() + { + return $this->body; + } + + /** + * Returns request body as array (it means, returns JSON decoded body). + * + * @return array Request body that is JSON decoded. + */ + public function getJSONDecodedBody() + { + return json_decode($this->body, true); + } +} diff --git a/src/LINEBot/Response/SucceededResponse.php b/src/LINEBot/Response/SucceededResponse.php deleted file mode 100644 index a888f4f1..00000000 --- a/src/LINEBot/Response/SucceededResponse.php +++ /dev/null @@ -1,57 +0,0 @@ -data = $data; - } - - public function getHTTPStatus() - { - return $this->data['httpStatus']; - } - - public function isSucceeded() - { - return true; - } - - public function getVersion() - { - return $this->data['version']; - } - - public function getMessageId() - { - return $this->data['messageId']; - } - - public function getTimestamp() - { - return $this->data['timestamp']; - } - - public function getFailed() - { - return $this->data['failed']; - } -} diff --git a/src/LINEBot/SignatureValidator.php b/src/LINEBot/SignatureValidator.php index 6bff0d1d..8210b8d5 100644 --- a/src/LINEBot/SignatureValidator.php +++ b/src/LINEBot/SignatureValidator.php @@ -1,4 +1,5 @@ label = $label; + $this->text = $text; + } + + /** + * Builds message action structure. + * + * @return array Built message action structure. + */ + public function buildTemplateAction() + { + return [ + 'type' => ActionType::MESSAGE, + 'label' => $this->label, + 'text' => $this->text, + ]; + } +} diff --git a/src/LINEBot/TemplateActionBuilder/PostbackTemplateActionBuilder.php b/src/LINEBot/TemplateActionBuilder/PostbackTemplateActionBuilder.php new file mode 100644 index 00000000..507afad1 --- /dev/null +++ b/src/LINEBot/TemplateActionBuilder/PostbackTemplateActionBuilder.php @@ -0,0 +1,61 @@ +label = $label; + $this->data = $data; + } + + /** + * Builds postback action structure. + * + * @return array Built postback action structure. + */ + public function buildTemplateAction() + { + return [ + 'type' => ActionType::POSTBACK, + 'label' => $this->label, + 'data' => $this->data, + ]; + } +} diff --git a/src/LINEBot/TemplateActionBuilder/UriTemplateActionBuilder.php b/src/LINEBot/TemplateActionBuilder/UriTemplateActionBuilder.php new file mode 100644 index 00000000..f8eb7755 --- /dev/null +++ b/src/LINEBot/TemplateActionBuilder/UriTemplateActionBuilder.php @@ -0,0 +1,61 @@ +label = $label; + $this->uri = $uri; + } + + /** + * Builds URI action structure. + * + * @return array Built URI action structure. + */ + public function buildTemplateAction() + { + return [ + 'type' => ActionType::URI, + 'label' => $this->label, + 'uri' => $this->uri, + ]; + } +} diff --git a/tests/LINEBot/EventRequestParserTest.php b/tests/LINEBot/EventRequestParserTest.php new file mode 100644 index 00000000..af65f9b5 --- /dev/null +++ b/tests/LINEBot/EventRequestParserTest.php @@ -0,0 +1,311 @@ + 'testsecret']); + $events = $bot->parseEventRequest($this::$json, 'Nq7AExtg27CQRfM3ngKtQxtVeIM/757ZTyDOrxQtWNg='); + + $this->assertEquals(count($events), 12); + + { + // text + $event = $events[0]; + $this->assertEquals(12345678901234, $event->getTimestamp()); + $this->assertTrue($event->isUserEvent()); + $this->assertEquals('userid', $event->getUserId()); + $this->assertInstanceOf('LINE\LINEBot\Event\MessageEvent', $event); + $this->assertInstanceOf('LINE\LINEBot\Event\MessageEvent\TextMessage', $event); + /** @var TextMessage $event */ + $this->assertEquals('replytoken', $event->getReplyToken()); + $this->assertEquals('contentid', $event->getMessageId()); + $this->assertEquals('message', $event->getText()); + } + + { + // image + $event = $events[1]; + $this->assertTrue($event->isGroupEvent()); + $this->assertEquals('groupid', $event->getGroupId()); + $this->assertInstanceOf('LINE\LINEBot\Event\MessageEvent\ImageMessage', $event); + /** @var ImageMessage $event */ + $this->assertEquals('replytoken', $event->getReplyToken()); + } + + { + // video + $event = $events[2]; + $this->assertTrue($event->isRoomEvent()); + $this->assertEquals('roomid', $event->getRoomId()); + $this->assertInstanceOf('LINE\LINEBot\Event\MessageEvent\VideoMessage', $event); + /** @var VideoMessage $event */ + $this->assertEquals('replytoken', $event->getReplyToken()); + } + + { + // audio + $event = $events[3]; + $this->assertInstanceOf('LINE\LINEBot\Event\MessageEvent\AudioMessage', $event); + /** @var AudioMessage $event */ + $this->assertEquals('replytoken', $event->getReplyToken()); + } + + { + // location + $event = $events[4]; + $this->assertInstanceOf('LINE\LINEBot\Event\MessageEvent\LocationMessage', $event); + /** @var LocationMessage $event */ + $this->assertEquals('replytoken', $event->getReplyToken()); + $this->assertEquals('label', $event->getTitle()); + $this->assertEquals('tokyo', $event->getAddress()); + $this->assertEquals('-34.12', $event->getLatitude()); + $this->assertEquals('134.23', $event->getLongitude()); + } + + { + // sticker + $event = $events[5]; + $this->assertInstanceOf('LINE\LINEBot\Event\MessageEvent\StickerMessage', $event); + /** @var StickerMessage $event */ + $this->assertEquals('replytoken', $event->getReplyToken()); + $this->assertEquals(1, $event->getPackageId()); + $this->assertEquals(2, $event->getStickerId()); + } + + { + // follow + $event = $events[6]; + $this->assertInstanceOf('LINE\LINEBot\Event\FollowEvent', $event); + /** @var FollowEvent $event */ + $this->assertEquals('replytoken', $event->getReplyToken()); + } + + { + // unfollow + $event = $events[7]; + $this->assertInstanceOf('LINE\LINEBot\Event\UnfollowEvent', $event); + /** @var UnfollowEvent $event */ + $this->assertTrue($event->getReplyToken() === null); + } + + { + // join + $event = $events[8]; + $this->assertInstanceOf('LINE\LINEBot\Event\JoinEvent', $event); + /** @var JoinEvent $event */ + $this->assertEquals('replytoken', $event->getReplyToken()); + } + + { + // leave + $event = $events[9]; + $this->assertInstanceOf('LINE\LINEBot\Event\LeaveEvent', $event); + /** @var LeaveEvent $event */ + $this->assertTrue($event->getReplyToken() === null); + } + + { + // postback + $event = $events[10]; + $this->assertInstanceOf('LINE\LINEBot\Event\PostbackEvent', $event); + /** @var PostbackEvent $event */ + $this->assertEquals('replytoken', $event->getReplyToken()); + $this->assertEquals('postback', $event->getPostbackData()); + } + + { + // beacon + $event = $events[11]; + $this->assertInstanceOf('LINE\LINEBot\Event\BeaconDetectionEvent', $event); + /** @var BeaconDetectionEvent $event */ + $this->assertEquals('replytoken', $event->getReplyToken()); + $this->assertEquals('bid', $event->getHwid()); + } + } +} diff --git a/tests/LINEBot/GetProfileTest.php b/tests/LINEBot/GetProfileTest.php new file mode 100644 index 00000000..a11e75f0 --- /dev/null +++ b/tests/LINEBot/GetProfileTest.php @@ -0,0 +1,52 @@ +assertEquals('GET', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/profile/USER_ID', $url); + + return [ + 'displayName' => 'BOT API', + 'userId' => 'userId', + 'pictureUrl' => 'https://example.com/abcdefghijklmn', + 'statusMessage' => 'Hello, LINE!', + ]; + }; + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->getProfile('USER_ID'); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + + $data = $res->getJSONDecodedBody(); + $this->assertEquals('BOT API', $data['displayName']); + $this->assertEquals('userId', $data['userId']); + $this->assertEquals('https://example.com/abcdefghijklmn', $data['pictureUrl']); + $this->assertEquals('Hello, LINE!', $data['statusMessage']); + } +} diff --git a/tests/LINEBot/GettingContentsTest.php b/tests/LINEBot/GettingContentsTest.php deleted file mode 100644 index 0f82c4fa..00000000 --- a/tests/LINEBot/GettingContentsTest.php +++ /dev/null @@ -1,81 +0,0 @@ - '1000000000', - 'channelSecret' => 'testsecret', - 'channelMid' => 'TEST_MID', - ]; - - $histories = new History(); - $emitter = new Emitter(); - $emitter->attach($mock); - $emitter->attach($histories); - - $sdk = new LINEBot( - $config, - new LINEBot\HTTPClient\GuzzleHTTPClient(array_merge($config, ['emitter' => $emitter])) - ); - - $res = $sdk->getUserProfile('DUMMY_MID_GET_DISPLAY_NAME'); - $this->assertEquals($res['count'], 1); - $this->assertEquals($res['contacts'][0]['displayName'], 'BOT API'); - - $history = $histories->getIterator()[0]; - - /** @var Request $req */ - $req = $history['request']; - $this->assertEquals($req->getMethod(), 'GET'); - $this->assertEquals( - $req->getUrl(), - 'https://trialbot-api.line.me/v1/profiles?mids=DUMMY_MID_GET_DISPLAY_NAME' - ); - - $channelIdHeader = $req->getHeaderAsArray('X-Line-ChannelID'); - $this->assertEquals(sizeof($channelIdHeader), 1); - $this->assertEquals($channelIdHeader[0], '1000000000'); - - $channelSecretHeader = $req->getHeaderAsArray('X-Line-ChannelSecret'); - $this->assertEquals(sizeof($channelSecretHeader), 1); - $this->assertEquals($channelSecretHeader[0], 'testsecret'); - - $channelMidHeader = $req->getHeaderAsArray('X-Line-Trusted-User-With-ACL'); - $this->assertEquals(sizeof($channelMidHeader), 1); - $this->assertEquals($channelMidHeader[0], 'TEST_MID'); - } -} diff --git a/tests/LINEBot/MessageSendingTest.php b/tests/LINEBot/MessageSendingTest.php deleted file mode 100644 index de2e9a8e..00000000 --- a/tests/LINEBot/MessageSendingTest.php +++ /dev/null @@ -1,549 +0,0 @@ - '1000000000', - 'channelSecret' => 'testsecret', - 'channelMid' => 'TEST_MID', - ]; - - public function testSendText() - { - $mock = new Mock([ - new Response( - 200, - [], - Stream::factory('{"failed":[],"messageId":"1460826285060","timestamp":1460826285060,"version":1}') - ), - new Response( - 400, - [], - Stream::factory('{"statusCode":"422","statusMessage":"invalid users"}') - ), - new Response( - 500, - [], - Stream::factory( - '{"statusCode":"500","statusMessage":"unexpected error found at call bot api sendMessage"}' - ) - ), - ]); - - $histories = new History(); - $emitter = new Emitter(); - $emitter->attach($mock); - $emitter->attach($histories); - - $sdk = new LINEBot( - $this::$config, - new GuzzleHTTPClient(array_merge($this::$config, ['emitter' => $emitter])) - ); - - $res = $sdk->sendText(['DUMMY_MID'], 'hello!'); - $this->assertInstanceOf('\LINE\LINEBot\Response\SucceededResponse', $res); - /** @var \LINE\LINEBot\Response\SucceededResponse $res */ - $this->assertTrue($res->isSucceeded()); - $this->assertEquals(200, $res->getHTTPStatus()); - $this->assertEmpty($res->getFailed()); - $this->assertEquals('1460826285060', $res->getMessageId()); - $this->assertEquals(1460826285060, $res->getTimestamp()); - $this->assertEquals(1, $res->getVersion()); - - $res = $sdk->sendText(['INVALID_MID'], 'hello!'); - $this->assertInstanceOf('\LINE\LINEBot\Response\FailedResponse', $res); - /** @var \LINE\LINEBot\Response\FailedResponse $res */ - $this->assertFalse($res->isSucceeded()); - $this->assertEquals(400, $res->getHTTPStatus()); - $this->assertEquals('422', $res->getStatusCode()); - $this->assertEquals('invalid users', $res->getStatusMessage()); - - $res = $sdk->sendText(['DUMMY_MID'], 'SOMETHING WRONG PAYLOAD'); - $this->assertInstanceOf('\LINE\LINEBot\Response\FailedResponse', $res); - /** @var \LINE\LINEBot\Response\FailedResponse $res */ - $this->assertFalse($res->isSucceeded()); - $this->assertEquals(500, $res->getHTTPStatus()); - $this->assertEquals('500', $res->getStatusCode()); - $this->assertEquals('unexpected error found at call bot api sendMessage', $res->getStatusMessage()); - - $history = $histories->getIterator()[0]; - /** @var Request $req */ - $req = $history['request']; - $this->assertEquals($req->getMethod(), 'POST'); - $this->assertEquals($req->getUrl(), 'https://trialbot-api.line.me/v1/events'); - - $data = json_decode($req->getBody(), true); - $this->assertEquals($data['eventType'], 138311608800106203); - $this->assertEquals($data['to'], ['DUMMY_MID']); - $this->assertEquals($data['content']['text'], 'hello!'); - $this->assertEquals($data['content']['contentType'], ContentType::TEXT); - $this->assertEquals($data['content']['toType'], RecipientType::USER); - - $channelIdHeader = $req->getHeaderAsArray('X-Line-ChannelID'); - $this->assertEquals(sizeof($channelIdHeader), 1); - $this->assertEquals($channelIdHeader[0], '1000000000'); - - $channelSecretHeader = $req->getHeaderAsArray('X-Line-ChannelSecret'); - $this->assertEquals(sizeof($channelSecretHeader), 1); - $this->assertEquals($channelSecretHeader[0], 'testsecret'); - - $channelMidHeader = $req->getHeaderAsArray('X-Line-Trusted-User-With-ACL'); - $this->assertEquals(sizeof($channelMidHeader), 1); - $this->assertEquals($channelMidHeader[0], 'TEST_MID'); - - } - - public function testSendImage() - { - $mock = new Mock([ - new Response( - 200, - [], - Stream::factory('{"failed":[],"messageId":"1460826285060","timestamp":1460826285060,"version":1}') - ), - new Response( - 400, - [], - Stream::factory('{"statusCode":"422","statusMessage":"invalid users"}')), - new Response( - 500, - [], - Stream::factory( - '{"statusCode":"500","statusMessage":"unexpected error found at call bot api sendMessage"}' - ) - ), - ]); - - $histories = new History(); - $emitter = new Emitter(); - $emitter->attach($mock); - $emitter->attach($histories); - - $sdk = new LINEBot( - $this::$config, - new GuzzleHTTPClient(array_merge($this::$config, ['emitter' => $emitter])) - ); - - $res = $sdk->sendImage(['DUMMY_MID'], 'http://example.com/image.jpg', 'http://example.com/preview.jpg'); - $this->assertInstanceOf('\LINE\LINEBot\Response\SucceededResponse', $res); - /** @var \LINE\LINEBot\Response\SucceededResponse $res */ - $this->assertTrue($res->isSucceeded()); - $this->assertEquals(200, $res->getHTTPStatus()); - $this->assertEmpty($res->getFailed()); - $this->assertEquals('1460826285060', $res->getMessageId()); - $this->assertEquals(1460826285060, $res->getTimestamp()); - $this->assertEquals(1, $res->getVersion()); - - $res = $sdk->sendImage(['INVALID_MID'], 'http://example.com/image.jpg', 'http://example.com/preview.jpg'); - $this->assertInstanceOf('\LINE\LINEBot\Response\FailedResponse', $res); - /** @var \LINE\LINEBot\Response\FailedResponse $res */ - $this->assertFalse($res->isSucceeded()); - $this->assertEquals(400, $res->getHTTPStatus()); - $this->assertEquals('422', $res->getStatusCode()); - $this->assertEquals('invalid users', $res->getStatusMessage()); - - $res = $sdk->sendImage(['DUMMY_MID'], 'http://example.com/image.jpg', 'http://example.com/preview.jpg'); - $this->assertInstanceOf('\LINE\LINEBot\Response\FailedResponse', $res); - /** @var \LINE\LINEBot\Response\FailedResponse $res */ - $this->assertFalse($res->isSucceeded()); - $this->assertEquals(500, $res->getHTTPStatus()); - $this->assertEquals('500', $res->getStatusCode()); - $this->assertEquals('unexpected error found at call bot api sendMessage', $res->getStatusMessage()); - - $history = $histories->getIterator()[0]; - /** @var Request $req */ - $req = $history['request']; - $this->assertEquals($req->getMethod(), 'POST'); - $this->assertEquals($req->getUrl(), 'https://trialbot-api.line.me/v1/events'); - - $data = json_decode($req->getBody(), true); - $this->assertEquals($data['eventType'], 138311608800106203); - $this->assertEquals($data['to'], ['DUMMY_MID']); - $this->assertEquals($data['content']['originalContentUrl'], 'http://example.com/image.jpg'); - $this->assertEquals($data['content']['previewImageUrl'], 'http://example.com/preview.jpg'); - $this->assertEquals($data['content']['contentType'], ContentType::IMAGE); - $this->assertEquals($data['content']['toType'], RecipientType::USER); - - $channelIdHeader = $req->getHeaderAsArray('X-Line-ChannelID'); - $this->assertEquals(sizeof($channelIdHeader), 1); - $this->assertEquals($channelIdHeader[0], '1000000000'); - - $channelSecretHeader = $req->getHeaderAsArray('X-Line-ChannelSecret'); - $this->assertEquals(sizeof($channelSecretHeader), 1); - $this->assertEquals($channelSecretHeader[0], 'testsecret'); - - $channelMidHeader = $req->getHeaderAsArray('X-Line-Trusted-User-With-ACL'); - $this->assertEquals(sizeof($channelMidHeader), 1); - $this->assertEquals($channelMidHeader[0], 'TEST_MID'); - } - - public function testSendVideo() - { - $mock = new Mock([ - new Response( - 200, - [], - Stream::factory('{"failed":[],"messageId":"1460867315795","timestamp":1460867315795,"version":1}') - ), - new Response( - 400, - [], - Stream::factory('{"statusCode":"422","statusMessage":"invalid users"}') - ), - new Response( - 500, - [], - Stream::factory( - '{"statusCode":"500","statusMessage":"unexpected error found at call bot api sendMessage"}' - ) - ), - ]); - - $histories = new History(); - $emitter = new Emitter(); - $emitter->attach($mock); - $emitter->attach($histories); - - $sdk = new LINEBot( - $this::$config, - new GuzzleHTTPClient(array_merge($this::$config, ['emitter' => $emitter])) - ); - - $res = $sdk->sendVideo(['DUMMY_MID'], 'http://example.com/video.mp4', 'http://example.com/preview.jpg'); - $this->assertInstanceOf('\LINE\LINEBot\Response\SucceededResponse', $res); - /** @var \LINE\LINEBot\Response\SucceededResponse $res */ - $this->assertTrue($res->isSucceeded()); - $this->assertEquals(200, $res->getHTTPStatus()); - $this->assertEmpty($res->getFailed()); - $this->assertEquals('1460867315795', $res->getMessageId()); - $this->assertEquals(1460867315795, $res->getTimestamp()); - $this->assertEquals(1, $res->getVersion()); - - $res = $sdk->sendVideo(['INVALID_MID'], 'http://example.com/video.mp4', 'http://example.com/preview.jpg'); - $this->assertInstanceOf('\LINE\LINEBot\Response\FailedResponse', $res); - /** @var \LINE\LINEBot\Response\FailedResponse $res */ - $this->assertFalse($res->isSucceeded()); - $this->assertEquals(400, $res->getHTTPStatus()); - $this->assertEquals('422', $res->getStatusCode()); - $this->assertEquals('invalid users', $res->getStatusMessage()); - - $res = $sdk->sendVideo(['DUMMY_MID'], 'http://example.com/video.mp4', 'http://example.com/preview.jpg'); - $this->assertInstanceOf('\LINE\LINEBot\Response\FailedResponse', $res); - /** @var \LINE\LINEBot\Response\FailedResponse $res */ - $this->assertFalse($res->isSucceeded()); - $this->assertEquals(500, $res->getHTTPStatus()); - $this->assertEquals('500', $res->getStatusCode()); - $this->assertEquals('unexpected error found at call bot api sendMessage', $res->getStatusMessage()); - - $history = $histories->getIterator()[0]; - /** @var Request $req */ - $req = $history['request']; - $this->assertEquals($req->getMethod(), 'POST'); - $this->assertEquals($req->getUrl(), 'https://trialbot-api.line.me/v1/events'); - - $data = json_decode($req->getBody(), true); - $this->assertEquals($data['eventType'], 138311608800106203); - $this->assertEquals($data['to'], ['DUMMY_MID']); - $this->assertEquals($data['content']['originalContentUrl'], 'http://example.com/video.mp4'); - $this->assertEquals($data['content']['previewImageUrl'], 'http://example.com/preview.jpg'); - $this->assertEquals($data['content']['contentType'], ContentType::VIDEO); - $this->assertEquals($data['content']['toType'], RecipientType::USER); - - $channelIdHeader = $req->getHeaderAsArray('X-Line-ChannelID'); - $this->assertEquals(sizeof($channelIdHeader), 1); - $this->assertEquals($channelIdHeader[0], '1000000000'); - - $channelSecretHeader = $req->getHeaderAsArray('X-Line-ChannelSecret'); - $this->assertEquals(sizeof($channelSecretHeader), 1); - $this->assertEquals($channelSecretHeader[0], 'testsecret'); - - $channelMidHeader = $req->getHeaderAsArray('X-Line-Trusted-User-With-ACL'); - $this->assertEquals(sizeof($channelMidHeader), 1); - $this->assertEquals($channelMidHeader[0], 'TEST_MID'); - } - - public function testSendAudio() - { - $mock = new Mock([ - new Response( - 200, - [], - Stream::factory('{"failed":[],"messageId":"1460867315795","timestamp":1460867315795,"version":1}') - ), - new Response( - 400, - [], - Stream::factory('{"statusCode":"422","statusMessage":"invalid users"}') - ), - new Response( - 500, - [], - Stream::factory( - '{"statusCode":"500","statusMessage":"unexpected error found at call bot api sendMessage"}' - ) - ), - ]); - - $histories = new History(); - $emitter = new Emitter(); - $emitter->attach($mock); - $emitter->attach($histories); - - $sdk = new LINEBot( - $this::$config, - new GuzzleHTTPClient(array_merge($this::$config, ['emitter' => $emitter])) - ); - - $res = $sdk->sendAudio(['DUMMY_MID'], 'http://example.com/sound.m4a', 5000); - $this->assertInstanceOf('\LINE\LINEBot\Response\SucceededResponse', $res); - /** @var \LINE\LINEBot\Response\SucceededResponse $res */ - $this->assertTrue($res->isSucceeded()); - $this->assertEquals(200, $res->getHTTPStatus()); - $this->assertEmpty($res->getFailed()); - $this->assertEquals('1460867315795', $res->getMessageId()); - $this->assertEquals(1460867315795, $res->getTimestamp()); - $this->assertEquals(1, $res->getVersion()); - - $res = $sdk->sendAudio(['INVALID_MID'], 'http://example.com/sound.m4a', 5000); - $this->assertInstanceOf('\LINE\LINEBot\Response\FailedResponse', $res); - /** @var \LINE\LINEBot\Response\FailedResponse $res */ - $this->assertFalse($res->isSucceeded()); - $this->assertEquals(400, $res->getHTTPStatus()); - $this->assertEquals('422', $res->getStatusCode()); - $this->assertEquals('invalid users', $res->getStatusMessage()); - - $res = $sdk->sendAudio(['DUMMY_MID'], 'http://example.com/sound.m4a', 5000); - $this->assertInstanceOf('\LINE\LINEBot\Response\FailedResponse', $res); - /** @var \LINE\LINEBot\Response\FailedResponse $res */ - $this->assertFalse($res->isSucceeded()); - $this->assertEquals(500, $res->getHTTPStatus()); - $this->assertEquals('500', $res->getStatusCode()); - $this->assertEquals('unexpected error found at call bot api sendMessage', $res->getStatusMessage()); - - $history = $histories->getIterator()[0]; - /** @var Request $req */ - $req = $history['request']; - $this->assertEquals($req->getMethod(), 'POST'); - $this->assertEquals($req->getUrl(), 'https://trialbot-api.line.me/v1/events'); - - $data = json_decode($req->getBody(), true); - $this->assertEquals($data['eventType'], 138311608800106203); - $this->assertEquals($data['to'], ['DUMMY_MID']); - $this->assertEquals($data['content']['originalContentUrl'], 'http://example.com/sound.m4a'); - $this->assertEquals($data['content']['contentMetadata']['AUDLEN'], '5000'); - $this->assertEquals($data['content']['contentType'], ContentType::AUDIO); - $this->assertEquals($data['content']['toType'], RecipientType::USER); - - $channelIdHeader = $req->getHeaderAsArray('X-Line-ChannelID'); - $this->assertEquals(sizeof($channelIdHeader), 1); - $this->assertEquals($channelIdHeader[0], '1000000000'); - - $channelSecretHeader = $req->getHeaderAsArray('X-Line-ChannelSecret'); - $this->assertEquals(sizeof($channelSecretHeader), 1); - $this->assertEquals($channelSecretHeader[0], 'testsecret'); - - $channelMidHeader = $req->getHeaderAsArray('X-Line-Trusted-User-With-ACL'); - $this->assertEquals(sizeof($channelMidHeader), 1); - $this->assertEquals($channelMidHeader[0], 'TEST_MID'); - } - - public function testSendLocation() - { - $mock = new Mock([ - new Response( - 200, - [], - Stream::factory('{"failed":[],"messageId":"1460867315795","timestamp":1460867315795,"version":1}') - ), - new Response( - 400, - [], - Stream::factory('{"statusCode":"422","statusMessage":"invalid users"}')), - new Response( - 500, - [], - Stream::factory('{"statusCode":"500","statusMessage":"unexpected error found at call bot api sendMessage"}') - ), - ]); - - $histories = new History(); - $emitter = new Emitter(); - $emitter->attach($mock); - $emitter->attach($histories); - - $sdk = new LINEBot( - $this::$config, - new GuzzleHTTPClient(array_merge($this::$config, ['emitter' => $emitter])) - ); - - $res = $sdk->sendLocation(['DUMMY_MID'], '2 Chome-21-1 Shibuya Tokyo 150-0002, Japan', 35.658240, 139.703478); - $this->assertInstanceOf('\LINE\LINEBot\Response\SucceededResponse', $res); - /** @var \LINE\LINEBot\Response\SucceededResponse $res */ - $this->assertTrue($res->isSucceeded()); - $this->assertEquals(200, $res->getHTTPStatus()); - $this->assertEmpty($res->getFailed()); - $this->assertEquals('1460867315795', $res->getMessageId()); - $this->assertEquals(1460867315795, $res->getTimestamp()); - $this->assertEquals(1, $res->getVersion()); - - $res = $sdk->sendLocation(['INVALID_MID'], '2 Chome-21-1 Shibuya Tokyo 150-0002, Japan', 35.658240, 139.703478); - $this->assertInstanceOf('\LINE\LINEBot\Response\FailedResponse', $res); - /** @var \LINE\LINEBot\Response\FailedResponse $res */ - $this->assertFalse($res->isSucceeded()); - $this->assertEquals(400, $res->getHTTPStatus()); - $this->assertEquals('422', $res->getStatusCode()); - $this->assertEquals('invalid users', $res->getStatusMessage()); - - $res = $sdk->sendLocation(['DUMMY_MID'], '2 Chome-21-1 Shibuya Tokyo 150-0002, Japan', 35.658240, 139.703478); - $this->assertInstanceOf('\LINE\LINEBot\Response\FailedResponse', $res); - /** @var \LINE\LINEBot\Response\FailedResponse $res */ - $this->assertFalse($res->isSucceeded()); - $this->assertEquals(500, $res->getHTTPStatus()); - $this->assertEquals('500', $res->getStatusCode()); - $this->assertEquals('unexpected error found at call bot api sendMessage', $res->getStatusMessage()); - - $history = $histories->getIterator()[0]; - /** @var Request $req */ - $req = $history['request']; - $this->assertEquals($req->getMethod(), 'POST'); - $this->assertEquals($req->getUrl(), 'https://trialbot-api.line.me/v1/events'); - - $data = json_decode($req->getBody(), true); - $this->assertEquals($data['eventType'], 138311608800106203); - $this->assertEquals($data['to'], ['DUMMY_MID']); - - $content = $data['content']; - $location = $content['location']; - - $this->assertEquals($content['contentType'], ContentType::LOCATION); - $this->assertEquals($content['text'], '2 Chome-21-1 Shibuya Tokyo 150-0002, Japan'); - $this->assertEquals($location['title'], $content['text']); - $this->assertEquals($location['latitude'], 35.658240); - $this->assertEquals($location['longitude'], 139.703478); - - $channelIdHeader = $req->getHeaderAsArray('X-Line-ChannelID'); - $this->assertEquals(sizeof($channelIdHeader), 1); - $this->assertEquals($channelIdHeader[0], '1000000000'); - - $channelSecretHeader = $req->getHeaderAsArray('X-Line-ChannelSecret'); - $this->assertEquals(sizeof($channelSecretHeader), 1); - $this->assertEquals($channelSecretHeader[0], 'testsecret'); - - $channelMidHeader = $req->getHeaderAsArray('X-Line-Trusted-User-With-ACL'); - $this->assertEquals(sizeof($channelMidHeader), 1); - $this->assertEquals($channelMidHeader[0], 'TEST_MID'); - } - - public function testSendSticker() - { - $mock = new Mock([ - new Response( - 200, - [], - Stream::factory('{"failed":[],"messageId":"1460867315795","timestamp":1460867315795,"version":1}') - ), - new Response( - 400, - [], - Stream::factory('{"statusCode":"422","statusMessage":"invalid users"}') - ), - new Response( - 500, - [], - Stream::factory( - '{"statusCode":"500","statusMessage":"unexpected error found at call bot api sendMessage"}' - ) - ), - ]); - - $histories = new History(); - $emitter = new Emitter(); - $emitter->attach($mock); - $emitter->attach($histories); - - $sdk = new LINEBot( - $this::$config, - new GuzzleHTTPClient(array_merge($this::$config, ['emitter' => $emitter])) - ); - - $res = $sdk->sendSticker(['DUMMY_MID'], 1, 2, 100); - $this->assertInstanceOf('\LINE\LINEBot\Response\SucceededResponse', $res); - /** @var \LINE\LINEBot\Response\SucceededResponse $res */ - $this->assertTrue($res->isSucceeded()); - $this->assertEquals(200, $res->getHTTPStatus()); - $this->assertEmpty($res->getFailed()); - $this->assertEquals('1460867315795', $res->getMessageId()); - $this->assertEquals(1460867315795, $res->getTimestamp()); - $this->assertEquals(1, $res->getVersion()); - - $res = $sdk->sendSticker(['INVALID_MID'], 1, 1, 100); - $this->assertInstanceOf('\LINE\LINEBot\Response\FailedResponse', $res); - /** @var \LINE\LINEBot\Response\FailedResponse $res */ - $this->assertFalse($res->isSucceeded()); - $this->assertEquals(400, $res->getHTTPStatus()); - $this->assertEquals('422', $res->getStatusCode()); - $this->assertEquals('invalid users', $res->getStatusMessage()); - - $res = $sdk->sendSticker(['DUMMY_MID'], 1, 1, 100); - $this->assertInstanceOf('\LINE\LINEBot\Response\FailedResponse', $res); - /** @var \LINE\LINEBot\Response\FailedResponse $res */ - $this->assertFalse($res->isSucceeded()); - $this->assertEquals(500, $res->getHTTPStatus()); - $this->assertEquals('500', $res->getStatusCode()); - $this->assertEquals('unexpected error found at call bot api sendMessage', $res->getStatusMessage()); - - $history = $histories->getIterator()[0]; - /** @var Request $req */ - $req = $history['request']; - $this->assertEquals($req->getMethod(), 'POST'); - $this->assertEquals($req->getUrl(), 'https://trialbot-api.line.me/v1/events'); - - $data = json_decode($req->getBody(), true); - $this->assertEquals($data['eventType'], 138311608800106203); - $this->assertEquals($data['to'], ['DUMMY_MID']); - - $this->assertEquals($data['content']['contentType'], ContentType::STICKER); - $this->assertEquals($data['content']['contentMetadata']['STKID'], '1'); - $this->assertEquals($data['content']['contentMetadata']['STKPKGID'], '2'); - $this->assertEquals($data['content']['contentMetadata']['STKVER'], '100'); - - $channelIdHeader = $req->getHeaderAsArray('X-Line-ChannelID'); - $this->assertEquals(sizeof($channelIdHeader), 1); - $this->assertEquals($channelIdHeader[0], '1000000000'); - - $channelSecretHeader = $req->getHeaderAsArray('X-Line-ChannelSecret'); - $this->assertEquals(sizeof($channelSecretHeader), 1); - $this->assertEquals($channelSecretHeader[0], 'testsecret'); - - $channelMidHeader = $req->getHeaderAsArray('X-Line-Trusted-User-With-ACL'); - $this->assertEquals(sizeof($channelMidHeader), 1); - $this->assertEquals($channelMidHeader[0], 'TEST_MID'); - } -} \ No newline at end of file diff --git a/tests/LINEBot/MultipleMessagesSendingTest.php b/tests/LINEBot/MultipleMessagesSendingTest.php deleted file mode 100644 index 12387244..00000000 --- a/tests/LINEBot/MultipleMessagesSendingTest.php +++ /dev/null @@ -1,165 +0,0 @@ - '1000000000', - 'channelSecret' => 'testsecret', - 'channelMid' => 'TEST_MID', - ]; - - $histories = new History(); - $emitter = new Emitter(); - $emitter->attach($mock); - $emitter->attach($histories); - - $sdk = new LINEBot($config, new GuzzleHTTPClient(array_merge($config, ['emitter' => $emitter]))); - - $multipleMessages = (new MultipleMessages()) - ->addText('hello!') - ->addImage('http://example.com/image.jpg', 'http://example.com/preview.jpg') - ->addAudio('http://example.com/audio.m4a', 6000) - ->addVideo('http://example.com/video.mp4', 'http://example.com/video_preview.jpg') - ->addLocation('2 Chome-21-1 Shibuya Tokyo 150-0002, Japan', 35.658240, 139.703478) - ->addSticker(1, 2, 100); - - $res = $sdk->sendMultipleMessages(['DUMMY_MID'], $multipleMessages); - $this->assertInstanceOf('\LINE\LINEBot\Response\SucceededResponse', $res); - /** @var \LINE\LINEBot\Response\SucceededResponse $res */ - $this->assertTrue($res->isSucceeded()); - $this->assertEquals(200, $res->getHTTPStatus()); - $this->assertEmpty($res->getFailed()); - $this->assertEquals('1460867315795', $res->getMessageId()); - $this->assertEquals(1460867315795, $res->getTimestamp()); - $this->assertEquals(1, $res->getVersion()); - - $res = $sdk->sendMultipleMessages(['INVALID_MID'], $multipleMessages); - $this->assertInstanceOf('\LINE\LINEBot\Response\FailedResponse', $res); - /** @var \LINE\LINEBot\Response\FailedResponse $res */ - $this->assertFalse($res->isSucceeded()); - $this->assertEquals(400, $res->getHTTPStatus()); - $this->assertEquals('422', $res->getStatusCode()); - $this->assertEquals('invalid users', $res->getStatusMessage()); - - $res = $sdk->sendMultipleMessages(['DUMMY_MID'], $multipleMessages); - $this->assertInstanceOf('\LINE\LINEBot\Response\FailedResponse', $res); - /** @var \LINE\LINEBot\Response\FailedResponse $res */ - $this->assertFalse($res->isSucceeded()); - $this->assertEquals(500, $res->getHTTPStatus()); - $this->assertEquals('500', $res->getStatusCode()); - $this->assertEquals('unexpected error found at call bot api sendMessage', $res->getStatusMessage()); - - $history = $histories->getIterator()[0]; - /** @var Request $req */ - $req = $history['request']; - $this->assertEquals($req->getMethod(), 'POST'); - $this->assertEquals($req->getUrl(), 'https://trialbot-api.line.me/v1/events'); - - $data = json_decode($req->getBody(), true); - $this->assertEquals($data['eventType'], 140177271400161403); - $this->assertEquals($data['to'], ['DUMMY_MID']); - - $this->assertEquals(sizeof($data['content']['messages']), 6); - - { - $content = $data['content']['messages'][0]; - $this->assertEquals($content['text'], 'hello!'); - $this->assertEquals($content['contentType'], ContentType::TEXT); - } - { - $content = $data['content']['messages'][1]; - $this->assertEquals($content['originalContentUrl'], 'http://example.com/image.jpg'); - $this->assertEquals($content['previewImageUrl'], 'http://example.com/preview.jpg'); - $this->assertEquals($content['contentType'], ContentType::IMAGE); - } - { - $content = $data['content']['messages'][2]; - $this->assertEquals($content['originalContentUrl'], 'http://example.com/audio.m4a'); - $this->assertEquals($content['contentMetadata']['AUDLEN'], '6000'); - $this->assertEquals($content['contentType'], ContentType::AUDIO); - } - { - $content = $data['content']['messages'][3]; - $this->assertEquals($content['originalContentUrl'], 'http://example.com/video.mp4'); - $this->assertEquals($content['previewImageUrl'], 'http://example.com/video_preview.jpg'); - $this->assertEquals($content['contentType'], ContentType::VIDEO); - } - { - $content = $data['content']['messages'][4]; - $location = $content['location']; - $this->assertEquals($content['text'], '2 Chome-21-1 Shibuya Tokyo 150-0002, Japan'); - $this->assertEquals($location['title'], $content['text']); - $this->assertEquals($location['latitude'], 35.658240); - $this->assertEquals($location['longitude'], 139.703478); - $this->assertEquals($content['contentType'], ContentType::LOCATION); - } - { - $content = $data['content']['messages'][5]; - $this->assertEquals($content['contentType'], ContentType::STICKER); - $this->assertEquals($content['contentMetadata']['STKID'], '1'); - $this->assertEquals($content['contentMetadata']['STKPKGID'], '2'); - $this->assertEquals($content['contentMetadata']['STKVER'], '100'); - } - - $channelIdHeader = $req->getHeaderAsArray('X-Line-ChannelID'); - $this->assertEquals(sizeof($channelIdHeader), 1); - $this->assertEquals($channelIdHeader[0], '1000000000'); - - $channelSecretHeader = $req->getHeaderAsArray('X-Line-ChannelSecret'); - $this->assertEquals(sizeof($channelSecretHeader), 1); - $this->assertEquals($channelSecretHeader[0], 'testsecret'); - - $channelMidHeader = $req->getHeaderAsArray('X-Line-Trusted-User-With-ACL'); - $this->assertEquals(sizeof($channelMidHeader), 1); - $this->assertEquals($channelMidHeader[0], 'TEST_MID'); - } -} \ No newline at end of file diff --git a/tests/LINEBot/ReceiveFactoryTest.php b/tests/LINEBot/ReceiveFactoryTest.php deleted file mode 100644 index 7108b7a0..00000000 --- a/tests/LINEBot/ReceiveFactoryTest.php +++ /dev/null @@ -1,108 +0,0 @@ - '1441301333', - 'channelSecret' => 'testsecret', - 'channelMid' => 'u0a556cffd4da0dd89c94fb36e36e1cdc', - ]; - - private static $json = <<assertEquals(sizeof($reqs), 2); - - { - $req = $reqs[0]; - $this->assertInstanceOf('\LINE\LINEBot\Receive\Message\Text', $req); - /** @var Text $req */ - $this->assertTrue($req->isMessage()); - $this->assertFalse($req->isOperation()); - $this->assertTrue($req->isValidEvent()); - $this->assertTrue($req->isSentMe()); - - $this->assertEquals($req->getId(), 'ABCDEF-12345678901'); - $this->assertEquals($req->getContentId(), '325708'); - $this->assertEquals($req->getCreatedTime(), '1332394961610'); - $this->assertEquals($req->getFromMid(), 'uff2aec188e58752ee1fb0f9507c6529a'); - - $this->assertTrue($req->isText()); - $this->assertEquals($req->getText(), 'hello'); - } - - { - $req = $reqs[1]; - $this->assertInstanceOf('\LINE\LINEBot\Receive\Operation\AddContact', $req); - /** @var AddContact $req */ - $this->assertFalse($req->isMessage()); - $this->assertTrue($req->isOperation()); - $this->assertTrue($req->isValidEvent()); - - $this->assertTrue($req->isAddContact()); - $this->assertEquals($req->getRevision(), '2469'); - $this->assertEquals($req->getFromMid(), 'u0f3bfc598b061eba02183bfc5280886a'); - } - } -} \ No newline at end of file diff --git a/tests/LINEBot/RichMessageSendingTest.php b/tests/LINEBot/RichMessageSendingTest.php deleted file mode 100644 index 90cb62fb..00000000 --- a/tests/LINEBot/RichMessageSendingTest.php +++ /dev/null @@ -1,179 +0,0 @@ - '1000000000', - 'channelSecret' => 'testsecret', - 'channelMid' => 'TEST_MID', - ]; - - $histories = new History(); - $emitter = new Emitter(); - $emitter->attach($mock); - $emitter->attach($histories); - - $sdk = new LINEBot($config, new GuzzleHTTPClient(array_merge($config, ['emitter' => $emitter]))); - - $markup = (new Markup(1040)) - ->setAction('SOMETHING', 'something', 'https://line.me') - ->addListener('SOMETHING', 0, 0, 520, 520); - - $res = $sdk->sendRichMessage(["DUMMY_MID"], 'http://example.com/image.jpg', "Alt text", $markup); - $this->assertInstanceOf('\LINE\LINEBot\Response\SucceededResponse', $res); - /** @var \LINE\LINEBot\Response\SucceededResponse $res */ - $this->assertTrue($res->isSucceeded()); - $this->assertEquals(200, $res->getHTTPStatus()); - $this->assertEmpty($res->getFailed()); - $this->assertEquals('1460867315795', $res->getMessageId()); - $this->assertEquals(1460867315795, $res->getTimestamp()); - $this->assertEquals(1, $res->getVersion()); - - $res = $sdk->sendRichMessage(["INVALID_MID"], 'http://example.com/image.jpg', "Alt text", $markup); - $this->assertInstanceOf('\LINE\LINEBot\Response\FailedResponse', $res); - /** @var \LINE\LINEBot\Response\FailedResponse $res */ - $this->assertFalse($res->isSucceeded()); - $this->assertEquals(400, $res->getHTTPStatus()); - $this->assertEquals('422', $res->getStatusCode()); - $this->assertEquals('invalid users', $res->getStatusMessage()); - - $res = $sdk->sendRichMessage(["DUMMY_MID"], 'http://example.com/image.jpg', "Alt text", $markup); - $this->assertInstanceOf('\LINE\LINEBot\Response\FailedResponse', $res); - /** @var \LINE\LINEBot\Response\FailedResponse $res */ - $this->assertFalse($res->isSucceeded()); - $this->assertEquals(500, $res->getHTTPStatus()); - $this->assertEquals('500', $res->getStatusCode()); - $this->assertEquals('unexpected error found at call bot api sendMessage', $res->getStatusMessage()); - - $history = $histories->getIterator()[0]; - /** @var Request $req */ - $req = $history['request']; - - $this->assertEquals($req->getMethod(), 'POST'); - $this->assertEquals($req->getUrl(), 'https://trialbot-api.line.me/v1/events'); - - $data = json_decode($req->getBody(), true); - $this->assertEquals($data['eventType'], 138311608800106203); - $this->assertEquals($data['to'], ['DUMMY_MID']); - $this->assertEquals($data['content']['contentMetadata']['ALT_TEXT'], 'Alt text'); - $this->assertEquals( - $data['content']['contentMetadata']['DOWNLOAD_URL'], - 'http://example.com/image.jpg' - ); - - $json = $data['content']['contentMetadata']['MARKUP_JSON']; - $this->assertEquals( - json_decode($json, true), - [ - 'scenes' => [ - 'scene1' => [ - 'listeners' => [ - [ - 'params' => [ - 0, - 0, - 520, - 520, - ], - 'type' => 'touch', - 'action' => 'SOMETHING', - ], - ], - 'draws' => [ - [ - 'image' => 'image1', - 'x' => 0, - 'y' => 0, - 'w' => 1040, - 'h' => 1040, - ], - ], - ], - ], - 'images' => [ - 'image1' => [ - 'x' => 0, - 'y' => 0, - 'w' => 1040, - 'h' => 1040, - ], - ], - 'actions' => [ - 'SOMETHING' => [ - 'text' => 'something', - 'params' => [ - 'linkUri' => 'https://line.me', - ], - 'type' => 'web', - ], - ], - 'canvas' => [ - 'initialScene' => 'scene1', - 'width' => 1040, - 'height' => 1040, - ], - ] - ); - - $channelIdHeader = $req->getHeaderAsArray('X-Line-ChannelID'); - $this->assertEquals(sizeof($channelIdHeader), 1); - $this->assertEquals($channelIdHeader[0], '1000000000'); - - $channelSecretHeader = $req->getHeaderAsArray('X-Line-ChannelSecret'); - $this->assertEquals(sizeof($channelSecretHeader), 1); - $this->assertEquals($channelSecretHeader[0], 'testsecret'); - - $channelMidHeader = $req->getHeaderAsArray('X-Line-Trusted-User-With-ACL'); - $this->assertEquals(sizeof($channelMidHeader), 1); - $this->assertEquals($channelMidHeader[0], 'TEST_MID'); - } -} \ No newline at end of file diff --git a/tests/LINEBot/SendAudioTest.php b/tests/LINEBot/SendAudioTest.php new file mode 100644 index 00000000..ca8ae533 --- /dev/null +++ b/tests/LINEBot/SendAudioTest.php @@ -0,0 +1,79 @@ +assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/reply', $url); + + $testRunner->assertEquals('REPLY-TOKEN', $data['replyToken']); + $testRunner->assertEquals(1, count($data['messages'])); + $testRunner->assertEquals(MessageType::AUDIO, $data['messages'][0]['type']); + $testRunner->assertEquals('https://example.com/audio.mp4', $data['messages'][0]['originalContentUrl']); + $testRunner->assertEquals(12345, $data['messages'][0]['duration']); + + return ['status' => 200]; + }; + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->replyMessage( + 'REPLY-TOKEN', + new AudioMessageBuilder('https://example.com/audio.mp4', 12345) + ); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } + + public function testPushAudio() + { + $mock = function ($testRunner, $httpMethod, $url, $data) { + /** @var \PHPUnit_Framework_TestCase $testRunner */ + $testRunner->assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/push', $url); + + $testRunner->assertEquals('DESTINATION', $data['to']); + $testRunner->assertEquals(1, count($data['messages'])); + $testRunner->assertEquals(MessageType::AUDIO, $data['messages'][0]['type']); + $testRunner->assertEquals('https://example.com/audio.mp4', $data['messages'][0]['originalContentUrl']); + $testRunner->assertEquals(12345, $data['messages'][0]['duration']); + + return ['status' => 200]; + }; + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->pushMessage( + 'DESTINATION', + new AudioMessageBuilder('https://example.com/audio.mp4', 12345) + ); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } +} diff --git a/tests/LINEBot/SendImageTest.php b/tests/LINEBot/SendImageTest.php new file mode 100644 index 00000000..6525ea7e --- /dev/null +++ b/tests/LINEBot/SendImageTest.php @@ -0,0 +1,79 @@ +assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/reply', $url); + + $testRunner->assertEquals('REPLY-TOKEN', $data['replyToken']); + $testRunner->assertEquals(1, count($data['messages'])); + $testRunner->assertEquals(MessageType::IMAGE, $data['messages'][0]['type']); + $testRunner->assertEquals('https://example.com/image.jpg', $data['messages'][0]['originalContentUrl']); + $testRunner->assertEquals('https://example.com/image_preview.jpg', $data['messages'][0]['previewImageUrl']); + + return ['status' => 200]; + }; + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->replyMessage( + 'REPLY-TOKEN', + new ImageMessageBuilder('https://example.com/image.jpg', 'https://example.com/image_preview.jpg') + ); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } + + public function testPushImage() + { + $mock = function ($testRunner, $httpMethod, $url, $data) { + /** @var \PHPUnit_Framework_TestCase $testRunner */ + $testRunner->assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/push', $url); + + $testRunner->assertEquals('DESTINATION', $data['to']); + $testRunner->assertEquals(1, count($data['messages'])); + $testRunner->assertEquals(MessageType::IMAGE, $data['messages'][0]['type']); + $testRunner->assertEquals('https://example.com/image.jpg', $data['messages'][0]['originalContentUrl']); + $testRunner->assertEquals('https://example.com/image_preview.jpg', $data['messages'][0]['previewImageUrl']); + + return ['status' => 200]; + }; + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->pushMessage( + 'DESTINATION', + new ImageMessageBuilder('https://example.com/image.jpg', 'https://example.com/image_preview.jpg') + ); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } +} diff --git a/tests/LINEBot/SendImagemapTest.php b/tests/LINEBot/SendImagemapTest.php new file mode 100644 index 00000000..d9fd924a --- /dev/null +++ b/tests/LINEBot/SendImagemapTest.php @@ -0,0 +1,146 @@ +assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/reply', $url); + + $testRunner->assertEquals('REPLY-TOKEN', $data['replyToken']); + $testRunner->assertEquals(1, count($data['messages'])); + + $message = $data['messages'][0]; + $testRunner->assertEquals(MessageType::IMAGEMAP, $message['type']); + $testRunner->assertEquals('https://example.com/imagemap_base', $message['baseUrl']); + $testRunner->assertEquals('alt test', $message['altText']); + $testRunner->assertEquals(1040, $message['baseSize']['width']); + $testRunner->assertEquals(1040, $message['baseSize']['height']); + + $testRunner->assertEquals(2, count($message['actions'])); + $testRunner->assertEquals(ActionType::URI, $message['actions'][0]['type']); + $testRunner->assertEquals(0, $message['actions'][0]['area']['x']); + $testRunner->assertEquals(0, $message['actions'][0]['area']['y']); + $testRunner->assertEquals(1040, $message['actions'][0]['area']['width']); + $testRunner->assertEquals(520, $message['actions'][0]['area']['height']); + + $testRunner->assertEquals(ActionType::MESSAGE, $message['actions'][1]['type']); + $testRunner->assertEquals(0, $message['actions'][1]['area']['x']); + $testRunner->assertEquals(520, $message['actions'][1]['area']['y']); + $testRunner->assertEquals(1040, $message['actions'][1]['area']['width']); + $testRunner->assertEquals(520, $message['actions'][1]['area']['height']); + + return ['status' => 200]; + }; + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->replyMessage( + 'REPLY-TOKEN', + new ImagemapMessageBuilder( + 'https://example.com/imagemap_base', + 'alt test', + new BaseSizeBuilder(1040, 1040), + [ + new ImagemapUriActionBuilder( + 'https://example.com/foo/bar', + new AreaBuilder(0, 0, 1040, 520) + ), + new ImagemapMessageActionBuilder( + 'Fortune', + new AreaBuilder(0, 520, 1040, 520) + ), + ] + ) + ); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } + + public function testPushImagemap() + { + $mock = function ($testRunner, $httpMethod, $url, $data) { + /** @var \PHPUnit_Framework_TestCase $testRunner */ + $testRunner->assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/push', $url); + + $testRunner->assertEquals('DESTINATION', $data['to']); + $testRunner->assertEquals(1, count($data['messages'])); + + $message = $data['messages'][0]; + $testRunner->assertEquals(MessageType::IMAGEMAP, $message['type']); + $testRunner->assertEquals('https://example.com/imagemap_base', $message['baseUrl']); + $testRunner->assertEquals('alt test', $message['altText']); + $testRunner->assertEquals(1040, $message['baseSize']['width']); + $testRunner->assertEquals(1040, $message['baseSize']['height']); + + $testRunner->assertEquals(2, count($message['actions'])); + $testRunner->assertEquals(ActionType::URI, $message['actions'][0]['type']); + $testRunner->assertEquals(0, $message['actions'][0]['area']['x']); + $testRunner->assertEquals(0, $message['actions'][0]['area']['y']); + $testRunner->assertEquals(1040, $message['actions'][0]['area']['width']); + $testRunner->assertEquals(520, $message['actions'][0]['area']['height']); + + $testRunner->assertEquals(ActionType::MESSAGE, $message['actions'][1]['type']); + $testRunner->assertEquals(0, $message['actions'][1]['area']['x']); + $testRunner->assertEquals(520, $message['actions'][1]['area']['y']); + $testRunner->assertEquals(1040, $message['actions'][1]['area']['width']); + $testRunner->assertEquals(520, $message['actions'][1]['area']['height']); + + return ['status' => 200]; + }; + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->pushMessage( + 'DESTINATION', + new ImagemapMessageBuilder( + 'https://example.com/imagemap_base', + 'alt test', + new BaseSizeBuilder(1040, 1040), + [ + new ImagemapUriActionBuilder( + 'https://example.com/foo/bar', + new AreaBuilder(0, 0, 1040, 520) + ), + new ImagemapMessageActionBuilder( + 'Fortune', + new AreaBuilder(0, 520, 1040, 520) + ), + ] + ) + ); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } +} diff --git a/tests/LINEBot/SendLocationTest.php b/tests/LINEBot/SendLocationTest.php new file mode 100644 index 00000000..d5f994e9 --- /dev/null +++ b/tests/LINEBot/SendLocationTest.php @@ -0,0 +1,83 @@ +assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/reply', $url); + + $testRunner->assertEquals('REPLY-TOKEN', $data['replyToken']); + $testRunner->assertEquals(1, count($data['messages'])); + $testRunner->assertEquals(MessageType::LOCATION, $data['messages'][0]['type']); + $testRunner->assertEquals('Location test', $data['messages'][0]['title']); + $testRunner->assertEquals('Tokyo Shibuya', $data['messages'][0]['address']); + $testRunner->assertEquals(35.6566285, $data['messages'][0]['latitude']); + $testRunner->assertEquals(139.6999638, $data['messages'][0]['longitude']); + + return ['status' => 200]; + }; + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->replyMessage( + 'REPLY-TOKEN', + new LocationMessageBuilder('Location test', 'Tokyo Shibuya', 35.6566285, 139.6999638) + ); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } + + public function testPushLocation() + { + $mock = function ($testRunner, $httpMethod, $url, $data) { + /** @var \PHPUnit_Framework_TestCase $testRunner */ + $testRunner->assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/push', $url); + + $testRunner->assertEquals('DESTINATION', $data['to']); + $testRunner->assertEquals(1, count($data['messages'])); + $testRunner->assertEquals(MessageType::LOCATION, $data['messages'][0]['type']); + $testRunner->assertEquals('Location test', $data['messages'][0]['title']); + $testRunner->assertEquals('Tokyo Shibuya', $data['messages'][0]['address']); + $testRunner->assertEquals(35.6566285, $data['messages'][0]['latitude']); + $testRunner->assertEquals(139.6999638, $data['messages'][0]['longitude']); + + return ['status' => 200]; + }; + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->pushMessage( + 'DESTINATION', + new LocationMessageBuilder('Location test', 'Tokyo Shibuya', 35.6566285, 139.6999638) + ); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } +} diff --git a/tests/LINEBot/SendMultiMessageTest.php b/tests/LINEBot/SendMultiMessageTest.php new file mode 100644 index 00000000..e0ee6c7a --- /dev/null +++ b/tests/LINEBot/SendMultiMessageTest.php @@ -0,0 +1,88 @@ +assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/reply', $url); + + $testRunner->assertEquals('REPLY-TOKEN', $data['replyToken']); + $testRunner->assertEquals(3, count($data['messages'])); + $testRunner->assertEquals(MessageType::TEXT, $data['messages'][0]['type']); + $testRunner->assertEquals('text1', $data['messages'][0]['text']); + $testRunner->assertEquals(MessageType::TEXT, $data['messages'][1]['type']); + $testRunner->assertEquals('text2', $data['messages'][1]['text']); + $testRunner->assertEquals(MessageType::AUDIO, $data['messages'][2]['type']); + + return ['status' => 200]; + }; + + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->replyMessage( + 'REPLY-TOKEN', + (new LINEBot\MessageBuilder\MultiMessageBuilder())->add(new TextMessageBuilder('text1', 'text2')) + ->add(new AudioMessageBuilder('https://example.com/audio.mp4', 1000)) + ); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } + + public function testPushMultiMessage() + { + $mock = function ($testRunner, $httpMethod, $url, $data) { + /** @var \PHPUnit_Framework_TestCase $testRunner */ + $testRunner->assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/push', $url); + + $testRunner->assertEquals('DESTINATION', $data['to']); + $testRunner->assertEquals(3, count($data['messages'])); + $testRunner->assertEquals(MessageType::TEXT, $data['messages'][0]['type']); + $testRunner->assertEquals('text1', $data['messages'][0]['text']); + $testRunner->assertEquals(MessageType::TEXT, $data['messages'][1]['type']); + $testRunner->assertEquals('text2', $data['messages'][1]['text']); + $testRunner->assertEquals(MessageType::AUDIO, $data['messages'][2]['type']); + + return ['status' => 200]; + }; + + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->pushMessage( + 'DESTINATION', + (new LINEBot\MessageBuilder\MultiMessageBuilder())->add(new TextMessageBuilder('text1', 'text2')) + ->add(new AudioMessageBuilder('https://example.com/audio.mp4', 1000)) + ); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } +} diff --git a/tests/LINEBot/SendStickerTest.php b/tests/LINEBot/SendStickerTest.php new file mode 100644 index 00000000..a22bd06c --- /dev/null +++ b/tests/LINEBot/SendStickerTest.php @@ -0,0 +1,73 @@ +assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/reply', $url); + + $testRunner->assertEquals('REPLY-TOKEN', $data['replyToken']); + $testRunner->assertEquals(1, count($data['messages'])); + $testRunner->assertEquals(MessageType::STICKER, $data['messages'][0]['type']); + $testRunner->assertEquals('1', $data['messages'][0]['packageId']); + $testRunner->assertEquals('2', $data['messages'][0]['stickerId']); + + return ['status' => 200]; + }; + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->replyMessage('REPLY-TOKEN', new StickerMessageBuilder('1', '2')); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } + + public function testPushSticker() + { + $mock = function ($testRunner, $httpMethod, $url, $data) { + /** @var \PHPUnit_Framework_TestCase $testRunner */ + $testRunner->assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/push', $url); + + $testRunner->assertEquals('DESTINATION', $data['to']); + $testRunner->assertEquals(1, count($data['messages'])); + $testRunner->assertEquals(MessageType::STICKER, $data['messages'][0]['type']); + $testRunner->assertEquals('1', $data['messages'][0]['packageId']); + $testRunner->assertEquals('2', $data['messages'][0]['stickerId']); + + return ['status' => 200]; + }; + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->pushMessage('DESTINATION', new StickerMessageBuilder('1', '2')); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } +} diff --git a/tests/LINEBot/SendTemplateTest.php b/tests/LINEBot/SendTemplateTest.php new file mode 100644 index 00000000..6a27226e --- /dev/null +++ b/tests/LINEBot/SendTemplateTest.php @@ -0,0 +1,150 @@ +assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/reply', $url); + + $testRunner->assertEquals('REPLY-TOKEN', $data['replyToken']); + $testRunner->assertEquals(1, count($data['messages'])); + + $message = $data['messages'][0]; + $testRunner->assertEquals(MessageType::TEMPLATE, $message['type']); + $testRunner->assertEquals('alt test', $message['altText']); + + $template = $message['template']; + $testRunner->assertEquals(TemplateType::BUTTONS, $template['type']); + $testRunner->assertEquals('button title', $template['title']); + $testRunner->assertEquals('button button', $template['text']); + $testRunner->assertEquals('https://example.com/thumbnail.jpg', $template['thumbnailImageUrl']); + + $actions = $template['actions']; + $testRunner->assertEquals(3, count($actions)); + $testRunner->assertEquals(ActionType::POSTBACK, $actions[0]['type']); + $testRunner->assertEquals('postback label', $actions[0]['label']); + $testRunner->assertEquals('post=back', $actions[0]['data']); + + $testRunner->assertEquals(ActionType::MESSAGE, $actions[1]['type']); + $testRunner->assertEquals('message label', $actions[1]['label']); + $testRunner->assertEquals('test message', $actions[1]['text']); + + $testRunner->assertEquals(ActionType::URI, $actions[2]['type']); + $testRunner->assertEquals('uri label', $actions[2]['label']); + $testRunner->assertEquals('https://example.com', $actions[2]['uri']); + + return ['status' => 200]; + }; + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->replyMessage( + 'REPLY-TOKEN', + new LINEBot\MessageBuilder\TemplateMessageBuilder( + 'alt test', + new ButtonTemplateBuilder( + 'button title', + 'button button', + 'https://example.com/thumbnail.jpg', + [ + new PostbackTemplateActionBuilder('postback label', 'post=back'), + new MessageTemplateActionBuilder('message label', 'test message'), + new UriTemplateActionBuilder('uri label', 'https://example.com'), + ] + ) + ) + ); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } + + public function testPushTemplate() + { + $mock = function ($testRunner, $httpMethod, $url, $data) { + /** @var \PHPUnit_Framework_TestCase $testRunner */ + $testRunner->assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/push', $url); + + $testRunner->assertEquals('DESTINATION', $data['to']); + $testRunner->assertEquals(1, count($data['messages'])); + + $message = $data['messages'][0]; + $testRunner->assertEquals(MessageType::TEMPLATE, $message['type']); + $testRunner->assertEquals('alt test', $message['altText']); + + $template = $message['template']; + $testRunner->assertEquals(TemplateType::BUTTONS, $template['type']); + $testRunner->assertEquals('button title', $template['title']); + $testRunner->assertEquals('button button', $template['text']); + $testRunner->assertEquals('https://example.com/thumbnail.jpg', $template['thumbnailImageUrl']); + + $actions = $template['actions']; + $testRunner->assertEquals(3, count($actions)); + $testRunner->assertEquals(ActionType::POSTBACK, $actions[0]['type']); + $testRunner->assertEquals('postback label', $actions[0]['label']); + $testRunner->assertEquals('post=back', $actions[0]['data']); + + $testRunner->assertEquals(ActionType::MESSAGE, $actions[1]['type']); + $testRunner->assertEquals('message label', $actions[1]['label']); + $testRunner->assertEquals('test message', $actions[1]['text']); + + $testRunner->assertEquals(ActionType::URI, $actions[2]['type']); + $testRunner->assertEquals('uri label', $actions[2]['label']); + $testRunner->assertEquals('https://example.com', $actions[2]['uri']); + + return ['status' => 200]; + }; + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->pushMessage( + 'DESTINATION', + new LINEBot\MessageBuilder\TemplateMessageBuilder( + 'alt test', + new ButtonTemplateBuilder( + 'button title', + 'button button', + 'https://example.com/thumbnail.jpg', + [ + new PostbackTemplateActionBuilder('postback label', 'post=back'), + new MessageTemplateActionBuilder('message label', 'test message'), + new UriTemplateActionBuilder('uri label', 'https://example.com'), + ] + ) + ) + ); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } +} diff --git a/tests/LINEBot/SendTextTest.php b/tests/LINEBot/SendTextTest.php new file mode 100644 index 00000000..4b96a928 --- /dev/null +++ b/tests/LINEBot/SendTextTest.php @@ -0,0 +1,152 @@ +assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/reply', $url); + + $testRunner->assertEquals('REPLY-TOKEN', $data['replyToken']); + $testRunner->assertEquals(1, count($data['messages'])); + $testRunner->assertEquals(MessageType::TEXT, $data['messages'][0]['type']); + $testRunner->assertEquals('test text', $data['messages'][0]['text']); + + return ['status' => 200]; + }; + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->replyText('REPLY-TOKEN', 'test text'); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } + + public function testReplyMultiTexts() + { + $mock = function ($testRunner, $httpMethod, $url, $data) { + /** @var \PHPUnit_Framework_TestCase $testRunner */ + $testRunner->assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/reply', $url); + + $testRunner->assertEquals('REPLY-TOKEN', $data['replyToken']); + $testRunner->assertEquals(3, count($data['messages'])); + $testRunner->assertEquals(MessageType::TEXT, $data['messages'][0]['type']); + $testRunner->assertEquals('test text1', $data['messages'][0]['text']); + $testRunner->assertEquals(MessageType::TEXT, $data['messages'][1]['type']); + $testRunner->assertEquals('test text2', $data['messages'][1]['text']); + $testRunner->assertEquals(MessageType::TEXT, $data['messages'][2]['type']); + $testRunner->assertEquals('test text3', $data['messages'][2]['text']); + + return ['status' => 200]; + }; + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->replyText('REPLY-TOKEN', 'test text1', 'test text2', 'test text3'); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } + + public function testReplyMessageWithSingleText() + { + $mock = function ($testRunner, $httpMethod, $url, $data) { + /** @var \PHPUnit_Framework_TestCase $testRunner */ + $testRunner->assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/reply', $url); + + $testRunner->assertEquals('REPLY-TOKEN', $data['replyToken']); + $testRunner->assertEquals(1, count($data['messages'])); + $testRunner->assertEquals(MessageType::TEXT, $data['messages'][0]['type']); + $testRunner->assertEquals('test text', $data['messages'][0]['text']); + + return ['status' => 200]; + }; + + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->replyMessage('REPLY-TOKEN', new TextMessageBuilder('test text')); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } + + public function testReplyMessageWithMultiTexts() + { + $mock = function ($testRunner, $httpMethod, $url, $data) { + /** @var \PHPUnit_Framework_TestCase $testRunner */ + $testRunner->assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/reply', $url); + + $testRunner->assertEquals('REPLY-TOKEN', $data['replyToken']); + $testRunner->assertEquals(3, count($data['messages'])); + $testRunner->assertEquals(MessageType::TEXT, $data['messages'][0]['type']); + $testRunner->assertEquals('test text1', $data['messages'][0]['text']); + $testRunner->assertEquals(MessageType::TEXT, $data['messages'][1]['type']); + $testRunner->assertEquals('test text2', $data['messages'][1]['text']); + $testRunner->assertEquals(MessageType::TEXT, $data['messages'][2]['type']); + $testRunner->assertEquals('test text3', $data['messages'][2]['text']); + + return ['status' => 200]; + }; + + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->replyMessage('REPLY-TOKEN', new TextMessageBuilder('test text1', 'test text2', 'test text3')); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } + + public function testPushTextMessage() + { + $mock = function ($testRunner, $httpMethod, $url, $data) { + /** @var \PHPUnit_Framework_TestCase $testRunner */ + $testRunner->assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/push', $url); + + $testRunner->assertEquals('DESTINATION', $data['to']); + $testRunner->assertEquals(3, count($data['messages'])); + $testRunner->assertEquals(MessageType::TEXT, $data['messages'][0]['type']); + $testRunner->assertEquals('test text1', $data['messages'][0]['text']); + $testRunner->assertEquals(MessageType::TEXT, $data['messages'][1]['type']); + $testRunner->assertEquals('test text2', $data['messages'][1]['text']); + $testRunner->assertEquals(MessageType::TEXT, $data['messages'][2]['type']); + $testRunner->assertEquals('test text3', $data['messages'][2]['text']); + + return ['status' => 200]; + }; + + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->pushMessage('DESTINATION', new TextMessageBuilder('test text1', 'test text2', 'test text3')); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } +} diff --git a/tests/LINEBot/SendVideoTest.php b/tests/LINEBot/SendVideoTest.php new file mode 100644 index 00000000..0f43fdea --- /dev/null +++ b/tests/LINEBot/SendVideoTest.php @@ -0,0 +1,79 @@ +assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/reply', $url); + + $testRunner->assertEquals('REPLY-TOKEN', $data['replyToken']); + $testRunner->assertEquals(1, count($data['messages'])); + $testRunner->assertEquals(MessageType::VIDEO, $data['messages'][0]['type']); + $testRunner->assertEquals('https://example.com/video.mp4', $data['messages'][0]['originalContentUrl']); + $testRunner->assertEquals('https://example.com/video_preview.jpg', $data['messages'][0]['previewImageUrl']); + + return ['status' => 200]; + }; + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->replyMessage( + 'REPLY-TOKEN', + new VideoMessageBuilder('https://example.com/video.mp4', 'https://example.com/video_preview.jpg') + ); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } + + public function testPushVideo() + { + $mock = function ($testRunner, $httpMethod, $url, $data) { + /** @var \PHPUnit_Framework_TestCase $testRunner */ + $testRunner->assertEquals('POST', $httpMethod); + $testRunner->assertEquals('https://api.line.me/v2/bot/message/push', $url); + + $testRunner->assertEquals('DESTINATION', $data['to']); + $testRunner->assertEquals(1, count($data['messages'])); + $testRunner->assertEquals(MessageType::VIDEO, $data['messages'][0]['type']); + $testRunner->assertEquals('https://example.com/video.mp4', $data['messages'][0]['originalContentUrl']); + $testRunner->assertEquals('https://example.com/video_preview.jpg', $data['messages'][0]['previewImageUrl']); + + return ['status' => 200]; + }; + $bot = new LINEBot(new DummyHttpClient($this, $mock), ['channelSecret' => 'CHANNEL-SECRET']); + $res = $bot->pushMessage( + 'DESTINATION', + new VideoMessageBuilder('https://example.com/video.mp4', 'https://example.com/video_preview.jpg') + ); + + $this->assertEquals(200, $res->getHTTPStatus()); + $this->assertTrue($res->isSucceeded()); + $this->assertEquals(200, $res->getJSONDecodedBody()['status']); + } +} diff --git a/tests/LINEBot/SignatureValidationTest.php b/tests/LINEBot/SignatureValidationTest.php deleted file mode 100644 index d67737a3..00000000 --- a/tests/LINEBot/SignatureValidationTest.php +++ /dev/null @@ -1,110 +0,0 @@ - '1441301333', - 'channelSecret' => 'testsecret', - 'channelMid' => 'u0a556cffd4da0dd89c94fb36e36e1cdc', - ]; - private static $json = <<assertTrue(SignatureValidator::validateSignature( - $this::$json, - $this::$config['channelSecret'], - 'kPXp0nPWSzfWAapWHiesbcztpKnXJoX8krCa1CcTghk=' - )); - $this->assertFalse(SignatureValidator::validateSignature( - $this::$json, - $this::$config['channelSecret'], - 'XXX' - )); - } - - public function testValidateSignatureByReceive() - { - $reqs = ReceiveFactory::createFromJSON($this::$config, $this::$json); - /** @var Text $req */ - $req = $reqs[0]; - $this->assertTrue($req->validateSignature($this::$json, 'kPXp0nPWSzfWAapWHiesbcztpKnXJoX8krCa1CcTghk=')); - $this->assertFalse($req->validateSignature($this::$json, 'XXX')); - } - - public function testValidateSignatureByBot() - { - $bot = new LINEBot($this::$config, new GuzzleHTTPClient($this::$config)); - $this->assertTrue($bot->validateSignature($this::$json, 'kPXp0nPWSzfWAapWHiesbcztpKnXJoX8krCa1CcTghk=')); - $this->assertFalse($bot->validateSignature($this::$json, 'XXX')); - } - - /** - * @expectedException \LINE\LINEBot\Exception\InvalidSignatureException - */ - public function testValidateSignatureWithEmptySignature() - { - SignatureValidator::validateSignature($this::$json, $this::$config['channelSecret'], ''); - } -} \ No newline at end of file diff --git a/tests/LINEBot/SignatureValidatorTest.php b/tests/LINEBot/SignatureValidatorTest.php new file mode 100644 index 00000000..2193dae9 --- /dev/null +++ b/tests/LINEBot/SignatureValidatorTest.php @@ -0,0 +1,190 @@ +assertTrue(SignatureValidator::validateSignature( + $this::$json, + $this::$channelSecret, + 'Nq7AExtg27CQRfM3ngKtQxtVeIM/757ZTyDOrxQtWNg=' + )); + $this->assertFalse(SignatureValidator::validateSignature( + $this::$json, + $this::$channelSecret, + 'deadbeef' + )); + } +} diff --git a/tests/LINEBot/Util/DummyHttpClient.php b/tests/LINEBot/Util/DummyHttpClient.php new file mode 100644 index 00000000..172c2135 --- /dev/null +++ b/tests/LINEBot/Util/DummyHttpClient.php @@ -0,0 +1,57 @@ +testRunner = $testRunner; + $this->mock = $mock; + } + + /** + * @param string $url + * @return Response + */ + public function get($url) + { + $ret = call_user_func($this->mock, $this->testRunner, 'GET', $url, []); + return new Response(200, json_encode($ret)); + } + + /** + * @param string $url + * @param array $data + * @return Response + */ + public function post($url, array $data) + { + $ret = call_user_func($this->mock, $this->testRunner, 'POST', $url, $data); + return new Response(200, json_encode($ret)); + } +} diff --git a/tests/LINEBotTest.php b/tests/LINEBotTest.php deleted file mode 100644 index 73ecca47..00000000 --- a/tests/LINEBotTest.php +++ /dev/null @@ -1,73 +0,0 @@ - '1000000000', - 'channelSecret' => 'testsecret', - 'channelMid' => 'TEST_MID', - ]; - - /** - * @expectedException \LINE\LINEBot\Exception\LINEBotAPIException - */ - public function testLINEBotAPIExceptionCausedByEmptyResponseBody() - { - $mock = new Mock([ - new Response(200, []), // missing body - ]); - - $emitter = new Emitter(); - $emitter->attach($mock); - - $sdk = new LINEBot( - $this::$config, - new GuzzleHTTPClient(array_merge($this::$config, ['emitter' => $emitter])) - ); - - $sdk->sendText(['DUMMY_MID'], 'hello!'); - } - - /** - * @expectedException \LINE\LINEBot\Exception\LINEBotAPIException - */ - public function textLINEBotAPIExceptionCausedByInvalidResponseBody() - { - $mock = new Mock([ - new Response(200, [], Stream::factory('I AM NOT A JSON')), // not JSON - ]); - - $emitter = new Emitter(); - $emitter->attach($mock); - - $sdk = new LINEBot( - $this::$config, - new GuzzleHTTPClient(array_merge($this::$config, ['emitter' => $emitter])) - ); - - $sdk->sendText(['DUMMY_MID'], 'hello!'); - } -} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index bb1bb515..e4543911 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,4 +1,5 @@