├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── config └── line.php ├── docs ├── bot.md ├── notification.md └── socialite.md ├── src ├── Contracts │ ├── BotFactory.php │ └── WebhookHandler.php ├── Facades │ └── Bot.php ├── Messaging │ ├── Bot.php │ ├── BotClient.php │ ├── Concerns │ │ ├── EventParser.php │ │ └── Replyable.php │ ├── Http │ │ ├── Actions │ │ │ ├── WebhookEventDispatcher.php │ │ │ ├── WebhookLogHandler.php │ │ │ └── WebhookNullHandler.php │ │ ├── Controllers │ │ │ └── WebhookController.php │ │ └── Middleware │ │ │ └── ValidateSignature.php │ └── ReplyMessage.php ├── Notifications │ ├── LineChannel.php │ └── LineMessage.php ├── Providers │ ├── LineServiceProvider.php │ ├── LineSocialiteServiceProvider.php │ ├── MacroServiceProvider.php │ └── RouteServiceProvider.php └── Socialite │ └── LineLoginProvider.php └── stubs └── listeners └── Line ├── FollowListener.php ├── JoinListener.php ├── LeaveListener.php ├── MessageListener.php └── UnfollowListener.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | I'm not going to write all the changes. 4 | 5 | ## v4.0.0 (2025-02-23) 6 | - Laravel 12 support 7 | - Drop Laravel10/PHP8.1 support 8 | - Remove LINE Notify 9 | 10 | ## v3.3.0 (2024-10-07) 11 | - Add Massaging API version of Notification channel. 12 | - **Deprecated LINE Notify** (Removed in v4.0) 13 | - Add some listeners stubs. 14 | 15 | ## v3.0.0 (2023-05-30) 16 | - Update line-bot-sdk v8.0 17 | - Remove listeners except MessageListener. 18 | 19 | ## v2.2.0 (2023-02-27) 20 | - Code maintenance 21 | - Change some argument name and return type. 22 | 23 | ## v2.1.0 (2023-01-28) 24 | - Laravel 10 support 25 | 26 | ## v2.0.0 (2022-01-22) 27 | - Add Laravel9 support 28 | - Require PHP8.0+ 29 | 30 | ## v1.3.0 (2021-01-01) 31 | - Drop PHP7.2 support 32 | 33 | ## v1.2.0 (2020-10-21) 34 | - Add ValidateSignature middleware 35 | - Add EventParser trait 36 | 37 | ## v1.1.0 (2020-10-19) 38 | - Improve Laravel-based support. 39 | 40 | ## v1.0.6 (2020-10-19) 41 | - Split ServiceProvider. 42 | 43 | ## v1.0.5 (2020-10-18) 44 | - Support Guzzle 6 45 | - Support Laravel-based framework(Lumen, Laravel Zero) 46 | 47 | ## v1.0.0 (2020-10-18) 48 | - First 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 kawax 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LINE SDK for Laravel 2 | 3 | [![packagist](https://badgen.net/packagist/v/revolution/laravel-line-sdk)](https://packagist.org/packages/revolution/laravel-line-sdk) 4 | [![tests](https://github.com/invokable/laravel-line-sdk/actions/workflows/tests.yml/badge.svg)](https://github.com/invokable/laravel-line-sdk/actions/workflows/tests.yml) 5 | [![Maintainability](https://qlty.sh/badges/937e8320-9fb3-4cda-bc1b-bd6325325f25/maintainability.svg)](https://qlty.sh/gh/invokable/projects/laravel-line-sdk) 6 | [![Code Coverage](https://qlty.sh/badges/937e8320-9fb3-4cda-bc1b-bd6325325f25/test_coverage.svg)](https://qlty.sh/gh/invokable/projects/laravel-line-sdk) 7 | 8 | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/invokable/laravel-line-sdk) 9 | 10 | ## Features 11 | - Working with Laravel Event System. Including Webhook routing and controller. 12 | - Extensible Bot Client. 13 | - Working with Laravel Notification System(LINE Messaging API) 14 | - Including Socialite drivers(LINE Login) 15 | 16 | ## Requirements 17 | - PHP >= 8.2 18 | - Laravel >= 11.0 19 | 20 | ## Installation 21 | 22 | ``` 23 | composer require revolution/laravel-line-sdk 24 | ``` 25 | 26 | ### Uninstall 27 | ```shell 28 | composer remove revolution/laravel-line-sdk 29 | ``` 30 | 31 | - Delete related files. See below. 32 | 33 | ## Configuration 34 | 35 | ### .env 36 | Set up in LINE Developers console. 37 | https://developers.line.biz/ 38 | 39 | Create two channels `Messaging API` and `LINE Login`. 40 | 41 | - Messaging API : Get `Channel access token (long-lived)` and `Channel secret`. Set `Webhook URL` 42 | - LINE Login : Get `Channel ID` and `Channel secret`. Set `Callback URL` 43 | 44 | ``` 45 | LINE_BOT_CHANNEL_TOKEN= 46 | LINE_BOT_CHANNEL_SECRET= 47 | 48 | LINE_LOGIN_CLIENT_ID= 49 | LINE_LOGIN_CLIENT_SECRET= 50 | LINE_LOGIN_REDIRECT= 51 | ``` 52 | 53 | ### Publishing(Optional) 54 | 55 | ``` 56 | php artisan vendor:publish --tag=line-config 57 | ``` 58 | 59 | ## Quick Start 60 | 61 | ### Prepare 62 | - Create `Messaging API` channel in LINE Developers console. 63 | - Get `Channel access token (long-lived)`, `Channel secret` and QR code. 64 | - A web server that can receive webhooks from LINE. Not possible on a normal local server. 65 | 66 | ### Create new Laravel project 67 | ``` 68 | # Docker 69 | curl -s "https://laravel.build/line-bot" | bash 70 | 71 | cd ./line-bot 72 | composer require revolution/laravel-line-sdk 73 | ``` 74 | 75 | Edit `.env` 76 | 77 | ``` 78 | LINE_BOT_CHANNEL_TOKEN= 79 | LINE_BOT_CHANNEL_SECRET= 80 | ``` 81 | 82 | Publishing Listeners 83 | ``` 84 | php artisan vendor:publish --tag=line-listeners 85 | ``` 86 | 87 | ### Deploy to web server 88 | - Set `Webhook URL` in LINE Developers console. `https://example.com/line/webhook` 89 | - Verify Webhook URL. 90 | 91 | ### Add bot as a friend. 92 | - Using QR code. 93 | 94 | ### Send test message 95 | Bot returns same message. 96 | 97 | ## Documents 98 | - [Messaging API / Bot](./docs/bot.md) 99 | - [Socialite](./docs/socialite.md) 100 | - [Notifications](./docs/notification.md) 101 | 102 | ## Demo 103 | https://github.com/kawax/laravel-line-project 104 | 105 | ## LICENSE 106 | MIT 107 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "revolution/laravel-line-sdk", 3 | "description": "LINE SDK for Laravel", 4 | "keywords": [ 5 | "laravel", 6 | "line", 7 | "bot", 8 | "sdk" 9 | ], 10 | "license": "MIT", 11 | "require": { 12 | "php": "^8.2", 13 | "ext-json": "*", 14 | "laravel/socialite": "^5.0", 15 | "illuminate/support": "^11.0||^12.0", 16 | "linecorp/line-bot-sdk": "^10.0||^11.0" 17 | }, 18 | "require-dev": { 19 | "orchestra/testbench": "^10.0", 20 | "laravel/pint": "^1.22" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Revolution\\Line\\": "src/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Tests\\": "tests/" 30 | } 31 | }, 32 | "authors": [ 33 | { 34 | "name": "kawax", 35 | "email": "kawaxbiz@gmail.com" 36 | } 37 | ], 38 | "extra": { 39 | "laravel": { 40 | "providers": [ 41 | "Revolution\\Line\\Providers\\LineServiceProvider", 42 | "Revolution\\Line\\Providers\\LineSocialiteServiceProvider", 43 | "Revolution\\Line\\Providers\\RouteServiceProvider", 44 | "Revolution\\Line\\Providers\\MacroServiceProvider" 45 | ] 46 | } 47 | }, 48 | "minimum-stability": "stable" 49 | } 50 | -------------------------------------------------------------------------------- /config/line.php: -------------------------------------------------------------------------------- 1 | [ 8 | 'channel_token' => env('LINE_BOT_CHANNEL_TOKEN'), 9 | 'channel_secret' => env('LINE_BOT_CHANNEL_SECRET'), 10 | 'path' => env('LINE_BOT_WEBHOOK_PATH', 'line/webhook'), 11 | 'route' => env('LINE_BOT_WEBHOOK_ROUTE', 'line.webhook'), 12 | 'domain' => env('LINE_BOT_WEBHOOK_DOMAIN'), 13 | 'middleware' => env('LINE_BOT_WEBHOOK_MIDDLEWARE', 'throttle'), 14 | ], 15 | 16 | /** 17 | * LINE Login. 18 | */ 19 | 'login' => [ 20 | 'client_id' => env('LINE_LOGIN_CLIENT_ID'), 21 | 'client_secret' => env('LINE_LOGIN_CLIENT_SECRET'), 22 | 'redirect' => env('LINE_LOGIN_REDIRECT'), 23 | ], 24 | ]; 25 | -------------------------------------------------------------------------------- /docs/bot.md: -------------------------------------------------------------------------------- 1 | # Messaging API / Bot 2 | 3 | https://developers.line.biz/en/docs/messaging-api/ 4 | 5 | ## Bot 6 | `Revolution\Line\Facades\Bot` can use all methods of the `LINE\Clients\MessagingApi\Api\MessagingApiApi` class. 7 | 8 | Delegate to LINEBot. 9 | ```php 10 | use Revolution\Line\Facades\Bot; 11 | 12 | Bot::replyMessage(); 13 | Bot::pushMessage(); 14 | ``` 15 | 16 | It also has the original `reply` function. 17 | 18 | ```php 19 | use Revolution\Line\Facades\Bot; 20 | 21 | Bot::reply($token)->text('text'); 22 | Bot::reply($token)->withSender('alt-name')->text('text1', 'text2'); 23 | Bot::reply($token)->sticker(package: 1, sticker: 1); 24 | ``` 25 | 26 | ## Webhook 27 | 28 | The SDK includes Webhook routing and controller. 29 | 30 | ### Webhook URL 31 | `https://localhost/line/webhook` 32 | 33 | You can change `line/webhook` in .env 34 | 35 | ``` 36 | LINE_BOT_WEBHOOK_PATH=webhook 37 | ``` 38 | 39 | ### Working with Laravel Event System 40 | When a Webhook event is received, Laravel event is dispatching. 41 | 42 | #### Laravel10 43 | For Event discovery, add `shouldDiscoverEvents()` to your `EventServiceProvider` 44 | ```php 45 | public function shouldDiscoverEvents(): bool 46 | { 47 | return true; 48 | } 49 | ``` 50 | No need to change $listen. 51 | 52 | Note: In production, you should run `php artisan event:cache` command. 53 | 54 | ### Publishing default Listeners 55 | Publish to `app/Listeners/Line/`. 56 | ``` 57 | php artisan vendor:publish --tag=line-listeners 58 | ``` 59 | 60 | `LINE\Webhook\Model\MessageEvent` event is handled by `MessageListener`. 61 | 62 | ```php 63 | namespace App\Listeners\Line; 64 | 65 | use Illuminate\Contracts\Queue\ShouldQueue; 66 | use Illuminate\Queue\InteractsWithQueue; 67 | use LINE\Clients\MessagingApi\ApiException; 68 | use LINE\Webhook\Model\MessageEvent; 69 | use LINE\Webhook\Model\StickerMessageContent; 70 | use LINE\Webhook\Model\TextMessageContent; 71 | use Revolution\Line\Facades\Bot; 72 | 73 | class MessageListener 74 | { 75 | protected string $token; 76 | 77 | /** 78 | * Create the event listener. 79 | * 80 | * @return void 81 | */ 82 | public function __construct() 83 | { 84 | // 85 | } 86 | 87 | /** 88 | * Handle the event. 89 | * 90 | * @param MessageEvent $event 91 | * @return void 92 | * @throws ApiException 93 | */ 94 | public function handle(MessageEvent $event): void 95 | { 96 | $message = $event->getMessage(); 97 | $this->token = $event->getReplyToken(); 98 | 99 | match ($message::class) { 100 | TextMessageContent::class => $this->text($message), 101 | StickerMessageContent::class => $this->sticker($message), 102 | }; 103 | } 104 | 105 | /** 106 | * @throws ApiException 107 | */ 108 | protected function text(TextMessageContent $message): void 109 | { 110 | Bot::reply($this->token)->text($message->getText()); 111 | } 112 | 113 | /** 114 | * @throws ApiException 115 | */ 116 | protected function sticker(StickerMessageContent $message): void 117 | { 118 | Bot::reply($this->token)->sticker( 119 | $message->getPackageId(), 120 | $message->getStickerId() 121 | ); 122 | } 123 | } 124 | ``` 125 | 126 | ## Customizing 127 | 128 | ### Bot macro 129 | `Bot` is Macroable, it means "You can add any method" 130 | 131 | Register at `AppServiceProvider@boot` 132 | ```php 133 | use Revolution\Line\Facades\Bot; 134 | 135 | public function boot() 136 | { 137 | Bot::macro('foo', function () { 138 | return $this->bot()->... 139 | }); 140 | } 141 | ``` 142 | Use it anywhere. 143 | ```php 144 | $foo = Bot::foo(); 145 | ``` 146 | 147 | ### Replacing `MessagingApiApi` instance 148 | `Bot::bot()` returns MessagingApiApi instance. You can swap instances with `Bot::botUsing()` 149 | 150 | ```php 151 | $bot = new MyBot(); 152 | 153 | Bot::botUsing($bot); 154 | ``` 155 | Accepts a callable. 156 | ```php 157 | Bot::botUsing(function () { 158 | return new MyBot(); 159 | }); 160 | ``` 161 | 162 | ### Webhook default route middleware 163 | The `throttle` middleware is enabled. To disable it, configure in `.env`. 164 | 165 | ``` 166 | LINE_BOT_WEBHOOK_MIDDLEWARE=null 167 | ``` 168 | 169 | Or change the `throttle` settings. 170 | 171 | ``` 172 | LINE_BOT_WEBHOOK_MIDDLEWARE=throttle:120,1 173 | ``` 174 | 175 | Laravel>=8 176 | ```php 177 | use Illuminate\Cache\RateLimiting\Limit; 178 | use Illuminate\Support\Facades\RateLimiter; 179 | 180 | RateLimiter::for('line', function (Request $request) { 181 | return Limit::perMinute(120); 182 | }); 183 | ``` 184 | ``` 185 | LINE_BOT_WEBHOOK_MIDDLEWARE=throttle:line 186 | ``` 187 | 188 | ### Another way not to use the Laravel Event system 189 | 190 | Make your `app/Actions/LineWebhook.php` 191 | 192 | ```php 193 | each(function ($event) { 210 | //event($event); 211 | if($event instanceof MessageEvent){ 212 | // 213 | } 214 | }); 215 | 216 | return response('OK'); 217 | } 218 | } 219 | ``` 220 | 221 | Register at `AppServiceProvider@register` 222 | ```php 223 | use App\Actions\LineWebhook; 224 | use Revolution\Line\Contracts\WebhookHandler; 225 | 226 | public function register() 227 | { 228 | $this->app->singleton(WebhookHandler::class, LineWebhook::class); 229 | } 230 | ``` 231 | 232 | Anything is possible by replacing the WebhookHandler. 233 | 234 | ### Http::line() (Required Laravel>=7) 235 | We've already extended the `Http` class, so you can make API requests without using the LINEBot class. 236 | 237 | ```php 238 | use Illuminate\Support\Facades\Http; 239 | 240 | $response = Http::line()->post('/v2/bot/channel/webhook/test', [ 241 | 'endpoint' => '', 242 | ]); 243 | ``` 244 | 245 | Combine with Bot macro. 246 | 247 | ```php 248 | use Illuminate\Support\Facades\Http; 249 | use Revolution\Line\Facades\Bot; 250 | 251 | Bot::macro('verifyWebhook', function ($endpoint = '') { 252 | return Http::line()->post('/v2/bot/channel/webhook/test', [ 253 | 'endpoint' => $endpoint, 254 | ])->json(); 255 | }); 256 | ``` 257 | 258 | ```php 259 | $response = Bot::verifyWebhook(); 260 | ``` 261 | -------------------------------------------------------------------------------- /docs/notification.md: -------------------------------------------------------------------------------- 1 | # Laravel Notifications 2 | 3 | Messaging API version. 4 | 5 | ## Notification class 6 | 7 | ```php 8 | use Illuminate\Notifications\Notification; 9 | use Revolution\Line\Notifications\LineChannel; 10 | use Revolution\Line\Notifications\LineMessage; 11 | 12 | class TestNotification extends Notification 13 | { 14 | public function via(object $notifiable): array 15 | { 16 | return [ 17 | LineChannel::class 18 | ]; 19 | } 20 | 21 | public function toLine(object $notifiable): LineMessage 22 | { 23 | return LineMessage::create(text: 'test'); 24 | } 25 | } 26 | ``` 27 | 28 | ## On-Demand Notifications 29 | 30 | ```php 31 | use Illuminate\Support\Facades\Notification; 32 | 33 | Notification::route('line', 'to') 34 | ->notify(new TestNotification()); 35 | ``` 36 | 37 | ## User Notifications 38 | 39 | ```php 40 | use Illuminate\Notifications\Notifiable; 41 | 42 | class User 43 | { 44 | use Notifiable; 45 | 46 | public function routeNotificationForLine($notification): string 47 | { 48 | return $this->line_id; 49 | } 50 | } 51 | ``` 52 | 53 | ```php 54 | $user->notify(new TestNotification()); 55 | ``` 56 | 57 | ## to 58 | 59 | userId or groupId. ID can be obtained from FollowEvent or JoinEvent. 60 | 61 | ## TextMessage 62 | 63 | You can send up to 5 messages. 64 | 65 | ```php 66 | use Revolution\Line\Notifications\LineMessage; 67 | 68 | public function toLine(object $notifiable): LineMessage 69 | { 70 | return LineMessage::create() 71 | ->text('text 1') 72 | ->text('text 2'); 73 | } 74 | ``` 75 | 76 | ## Customize icon and display name 77 | 78 | Make sure you use `withSender()` before any other message adding methods. 79 | 80 | ```php 81 | use Revolution\Line\Notifications\LineMessage; 82 | 83 | public function toLine(object $notifiable): LineMessage 84 | { 85 | return LineMessage::create() 86 | ->withSender(name: 'alt-name', icon: 'https://...png') 87 | ->text('text 1') 88 | ->text('text 2'); 89 | } 90 | ``` 91 | 92 | You can also specify a name and icon with `create()` method, which is easier if you're just creating a single text message. 93 | 94 | ```php 95 | use Revolution\Line\Notifications\LineMessage; 96 | 97 | public function toLine(object $notifiable): LineMessage 98 | { 99 | return LineMessage::create(text: 'test', name: 'alt-name', icon: 'https://...png'); 100 | } 101 | ``` 102 | 103 | ## StickerMessage 104 | 105 | Only the stickers on this page can be used. 106 | https://developers.line.biz/en/docs/messaging-api/sticker-list/ 107 | 108 | ```php 109 | use Revolution\Line\Notifications\LineMessage; 110 | 111 | public function toLine(object $notifiable): LineMessage 112 | { 113 | return LineMessage::create() 114 | ->text('test') 115 | ->sticker(package: 446, sticker: 1988); 116 | } 117 | ``` 118 | 119 | ## ImageMessage 120 | 121 | Specify a public URL. 122 | 123 | ```php 124 | use Revolution\Line\Notifications\LineMessage; 125 | 126 | public function toLine(object $notifiable): LineMessage 127 | { 128 | return LineMessage::create() 129 | ->image(original: 'https://.../test.png', preview: 'https://.../preview.png'); 130 | } 131 | ``` 132 | 133 | ## VideoMessage 134 | 135 | Specify a public URL. 136 | 137 | ```php 138 | use Revolution\Line\Notifications\LineMessage; 139 | 140 | public function toLine(object $notifiable): LineMessage 141 | { 142 | return LineMessage::create() 143 | ->video(original: 'https://.../test.mp4', preview: 'https://.../preview.png'); 144 | } 145 | ``` 146 | 147 | ## Other Message Types 148 | 149 | You can add any message type with `message()`. Don't forget to specify the type with `setType()`. 150 | 151 | ```php 152 | use LINE\Clients\MessagingApi\Model\LocationMessage; 153 | use LINE\Constants\MessageType; 154 | use Revolution\Line\Notifications\LineMessage; 155 | 156 | public function toLine(object $notifiable): LineMessage 157 | { 158 | $location = (new LocationMessage()) 159 | ->setType(MessageType::LOCATION) 160 | ->setTitle('title') 161 | ->setAddress('address') 162 | ->setLatitude(0.0) 163 | ->setLongitude(0.0); 164 | 165 | return LineMessage::create() 166 | ->message($location); 167 | } 168 | ``` 169 | -------------------------------------------------------------------------------- /docs/socialite.md: -------------------------------------------------------------------------------- 1 | # Socialite drivers for LINE 2 | 3 | ## Available drivers 4 | - LINE Login `line-login` 5 | - https://developers.line.biz/en/docs/line-login/ 6 | ### Unavailable 7 | - LINE WORKS 8 | - https://developers.worksmobile.com/ 9 | 10 | ## Configuration 11 | 12 | ### .env 13 | LINE Login 14 | ``` 15 | LINE_LOGIN_CLIENT_ID= 16 | LINE_LOGIN_CLIENT_SECRET= 17 | LINE_LOGIN_REDIRECT= 18 | ``` 19 | 20 | ## Usage(LINE Login) 21 | 22 | ### routes/web.php 23 | ```php 24 | use App\Http\Controllers\SocialiteController; 25 | 26 | Route::get('login', [SocialiteController::class, 'login']); 27 | Route::get('callback', [SocialiteController::class, 'callback']); 28 | ``` 29 | 30 | ### Controller 31 | 32 | ```php 33 | redirect(); 45 | } 46 | 47 | public function callback(Request $request) 48 | { 49 | if ($request->missing('code')) { 50 | dd($request); 51 | } 52 | 53 | /** 54 | * @var \Laravel\Socialite\Two\User 55 | */ 56 | $user = Socialite::driver('line-login')->user(); 57 | 58 | $loginUser = User::updateOrCreate([ 59 | 'line_id' => $user->id, 60 | ], [ 61 | 'name' => $user->nickname, 62 | 'avatar' => $user->avatar, 63 | 'access_token' => $user->token, 64 | 'refresh_token' => $user->refreshToken, 65 | ]); 66 | 67 | auth()->login($loginUser, true); 68 | 69 | return redirect()->route('home'); 70 | } 71 | } 72 | ``` 73 | 74 | with optional parameters. 75 | 76 | ```php 77 | public function login() 78 | { 79 | return Socialite::driver('line-login')->with([ 80 | 'prompt' => 'consent', 81 | 'bot_prompt' => 'normal', 82 | ])->redirect(); 83 | } 84 | ``` 85 | 86 | ### Scopes 87 | 88 | https://developers.line.biz/en/docs/line-login/integrate-line-login/#scopes 89 | 90 | ```php 91 | public function login() 92 | { 93 | return Socialite::driver('line-login') 94 | ->setScopes(['profile', 'openid']) 95 | ->redirect(); 96 | } 97 | ``` 98 | -------------------------------------------------------------------------------- /src/Contracts/BotFactory.php: -------------------------------------------------------------------------------- 1 | bot; 29 | } 30 | 31 | public function botUsing(callable|MessagingApiApi $bot): static 32 | { 33 | $this->bot = is_callable($bot) ? $bot() : $bot; 34 | 35 | return $this; 36 | } 37 | 38 | /** 39 | * Magic call method. 40 | * 41 | * @param string $method 42 | * @param array $parameters 43 | * @return mixed 44 | * 45 | * @throws BadMethodCallException 46 | */ 47 | public function __call($method, $parameters) 48 | { 49 | if (method_exists($this->bot(), $method)) { 50 | return $this->bot()->{$method}(...array_values($parameters)); 51 | } 52 | 53 | return $this->macroCall($method, $parameters); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Messaging/Concerns/EventParser.php: -------------------------------------------------------------------------------- 1 | header(HTTPHeader::LINE_SIGNATURE); 21 | 22 | $events = EventRequestParser::parseEventRequest( 23 | body: $request->getContent(), 24 | channelSecret: config('line.bot.channel_secret'), 25 | signature: $signature 26 | )->getEvents(); 27 | 28 | return collect($events); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Messaging/Concerns/Replyable.php: -------------------------------------------------------------------------------- 1 | withBot($this->bot()) 14 | ->withToken($token); 15 | } 16 | 17 | abstract public function bot(): MessagingApiApi; 18 | } 19 | -------------------------------------------------------------------------------- /src/Messaging/Http/Actions/WebhookEventDispatcher.php: -------------------------------------------------------------------------------- 1 | each(fn ($event) => event($event)); 15 | 16 | return response(class_basename(static::class)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Messaging/Http/Actions/WebhookLogHandler.php: -------------------------------------------------------------------------------- 1 | each(function ($event) { 16 | /** 17 | * @var MessageEvent $event 18 | */ 19 | $context = [ 20 | 'type' => $event->getType(), 21 | 'mode' => $event->getMode(), 22 | 'timestamp' => $event->getTimestamp(), 23 | 'replyToken' => $event->getReplyToken(), 24 | ]; 25 | 26 | info(class_basename(get_class($event)), $context); 27 | }); 28 | 29 | return response(class_basename(static::class)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Messaging/Http/Actions/WebhookNullHandler.php: -------------------------------------------------------------------------------- 1 | hasHeader(HTTPHeader::LINE_SIGNATURE), 400, 'Request does not contain signature'); 21 | 22 | abort_unless($this->validateSignature($request), 400, 'Invalid signature has given'); 23 | 24 | abort_if($request->missing('events'), 400, 'Invalid event request'); 25 | 26 | return $next($request); // @codeCoverageIgnore 27 | } 28 | 29 | /** 30 | * @throws InvalidSignatureException 31 | */ 32 | protected function validateSignature(Request $request): bool 33 | { 34 | return SignatureValidator::validateSignature( 35 | $request->getContent(), 36 | config('line.bot.channel_secret'), 37 | $request->header(HTTPHeader::LINE_SIGNATURE) 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Messaging/ReplyMessage.php: -------------------------------------------------------------------------------- 1 | bot = $bot; 33 | 34 | return $this; 35 | } 36 | 37 | public function withToken(string $token): self 38 | { 39 | $this->token = $token; 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * @throws ApiException 46 | */ 47 | public function message(Message ...$messages): ReplyMessageResponse|ErrorResponse 48 | { 49 | $reply = new ReplyMessageRequest([ 50 | 'replyToken' => $this->token, 51 | 'messages' => $messages, 52 | ]); 53 | 54 | return $this->bot->replyMessage($reply); 55 | } 56 | 57 | /** 58 | * @throws ApiException 59 | */ 60 | public function text(mixed ...$text): ErrorResponse|ReplyMessageResponse 61 | { 62 | $messages = collect($text) 63 | ->reject(fn ($item) => blank($item)) 64 | ->map(function ($item) { 65 | $text = (new TextMessage(['text' => $item]))->setType(MessageType::TEXT); 66 | if (filled($this->quick)) { 67 | $text->setQuickReply($this->quick); 68 | } 69 | if (filled($this->sender)) { 70 | $text->setSender($this->sender); 71 | } 72 | 73 | return $text; 74 | }) 75 | ->toArray(); 76 | 77 | return $this->message(...$messages); 78 | } 79 | 80 | /** 81 | * @throws ApiException 82 | */ 83 | public function sticker(int|string $package, int|string $sticker): ErrorResponse|ReplyMessageResponse 84 | { 85 | $message = new StickerMessage([ 86 | 'type' => MessageType::STICKER, 87 | 'packageId' => $package, 88 | 'stickerId' => $sticker, 89 | ]); 90 | 91 | return $this->message($message); 92 | } 93 | 94 | public function withQuickReply(QuickReply $quickReply): self 95 | { 96 | $this->quick = $quickReply; 97 | 98 | return $this; 99 | } 100 | 101 | public function withSender(?string $name = null, ?string $icon = null): self 102 | { 103 | $this->sender = new Sender(['name' => $name, 'iconUrl' => $icon]); 104 | 105 | return $this; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Notifications/LineChannel.php: -------------------------------------------------------------------------------- 1 | toLine($notifiable); 21 | 22 | // @phpstan-ignore-next-line 23 | if (! $message instanceof Arrayable) { 24 | return null; // @codeCoverageIgnore 25 | } 26 | 27 | if (! $to = $notifiable->routeNotificationFor('line', $notification)) { 28 | return null; 29 | } 30 | 31 | $data = Arr::add($message->toArray(), 'to', $to); 32 | 33 | return Bot::pushMessage(new PushMessageRequest($data)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Notifications/LineMessage.php: -------------------------------------------------------------------------------- 1 | unless(empty($name) && empty($icon), fn (self $message) => $message->withSender($name, $icon)) 34 | ->unless(empty($text), fn (self $message) => $message->text($text)); 35 | } 36 | 37 | public function withSender(?string $name = null, ?string $icon = null): self 38 | { 39 | $this->sender = new Sender(['name' => $name, 'iconUrl' => $icon]); 40 | 41 | return $this; 42 | } 43 | 44 | public function withQuickReply(QuickReply $quickReply): self 45 | { 46 | $this->quick = $quickReply; 47 | 48 | return $this; 49 | } 50 | 51 | /** 52 | * Add TextMessage. 53 | */ 54 | public function text(string $text): self 55 | { 56 | return $this->message( 57 | (new TextMessage) 58 | ->setType(MessageType::TEXT) 59 | ->setText($text) 60 | ); 61 | } 62 | 63 | /** 64 | * Add StickerMessage. 65 | */ 66 | public function sticker(int $package, int $sticker): self 67 | { 68 | return $this->message( 69 | (new StickerMessage) 70 | ->setType(MessageType::STICKER) 71 | ->setPackageId($package) 72 | ->setStickerId($sticker) 73 | ); 74 | } 75 | 76 | /** 77 | * Add ImageMessage. 78 | */ 79 | public function image(string $original, string $preview): self 80 | { 81 | return $this->message( 82 | (new ImageMessage) 83 | ->setType(MessageType::IMAGE) 84 | ->setOriginalContentUrl($original) 85 | ->setPreviewImageUrl($preview) 86 | ); 87 | } 88 | 89 | /** 90 | * Add VideoMessage. 91 | */ 92 | public function video(string $original, string $preview): self 93 | { 94 | return $this->message( 95 | (new VideoMessage) 96 | ->setType(MessageType::VIDEO) 97 | ->setOriginalContentUrl($original) 98 | ->setPreviewImageUrl($preview) 99 | ); 100 | } 101 | 102 | /** 103 | * Add any Message object. 104 | */ 105 | public function message(Message $message): self 106 | { 107 | if (! empty($this->sender)) { 108 | $message->setSender($this->sender); 109 | } 110 | 111 | if (! empty($this->quick)) { 112 | $message->setQuickReply($this->quick); 113 | } 114 | 115 | $this->messages[] = $message; 116 | 117 | return $this; 118 | } 119 | 120 | /** 121 | * Set other options. 122 | */ 123 | public function with(array $options): self 124 | { 125 | $this->options = $options; 126 | 127 | return $this; 128 | } 129 | 130 | public function toArray(): array 131 | { 132 | return array_merge( 133 | ['messages' => collect($this->messages)->take(5)->all()], 134 | $this->options 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Providers/LineServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom( 21 | __DIR__.'/../../config/line.php', 22 | 'line' 23 | ); 24 | 25 | $this->registerBot(); 26 | 27 | $this->registerWebhookHandler(); 28 | } 29 | 30 | /** 31 | * Bot. 32 | */ 33 | protected function registerBot(): void 34 | { 35 | $this->app->scoped(MessagingApiApi::class, function ($app) { 36 | $config = (new Configuration)->setAccessToken(config('line.bot.channel_token')); 37 | 38 | return new MessagingApiApi(config: $config); 39 | }); 40 | 41 | $this->app->scoped(BotFactory::class, BotClient::class); 42 | } 43 | 44 | /** 45 | * Default WebhookHandler. 46 | */ 47 | protected function registerWebhookHandler(): void 48 | { 49 | $this->app->scoped(WebhookHandler::class, WebhookEventDispatcher::class); 50 | } 51 | 52 | /** 53 | * Bootstrap any application services. 54 | */ 55 | public function boot(): void 56 | { 57 | $this->configurePublishing(); 58 | } 59 | 60 | /** 61 | * Configure publishing for the package. 62 | */ 63 | protected function configurePublishing(): void 64 | { 65 | if (! $this->app->runningInConsole()) { 66 | return; // @codeCoverageIgnore 67 | } 68 | 69 | $this->publishes([ 70 | __DIR__.'/../../config/line.php' => $this->app->configPath('line.php'), 71 | ], 'line-config'); 72 | 73 | $this->publishes([ 74 | __DIR__.'/../../stubs/listeners' => $this->app->path('Listeners'), 75 | ], 'line-listeners'); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Providers/LineSocialiteServiceProvider.php: -------------------------------------------------------------------------------- 1 | baseUrl($endpoint ?? 'https://api.line.me'); 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | domain(config('line.bot.domain')) 16 | ->group(function () { 17 | Route::post(config('line.bot.path', 'line/webhook')) 18 | ->name(config('line.bot.route', 'line.webhook')) 19 | ->middleware(ValidateSignature::class) 20 | ->uses(WebhookController::class); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Socialite/LineLoginProvider.php: -------------------------------------------------------------------------------- 1 | buildAuthUrlFromBase('https://access.line.me/oauth2/v2.1/authorize', $state); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | protected function getTokenUrl(): string 60 | { 61 | return 'https://api.line.me/oauth2/v2.1/token'; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function getAccessTokenResponse($code) 68 | { 69 | $headers = [ 70 | 'Content-Type' => 'application/x-www-form-urlencoded', 71 | ]; 72 | 73 | $form_params = $this->getTokenFields($code); 74 | 75 | $response = $this->getHttpClient()->post( 76 | $this->getTokenUrl(), 77 | compact('headers', 'form_params') 78 | ); 79 | 80 | $response = json_decode($response->getBody(), true); 81 | 82 | if (Arr::exists($response, 'id_token')) { 83 | $this->getEmail(Arr::string($response, 'id_token')); 84 | } 85 | 86 | return $response; 87 | } 88 | 89 | /** 90 | * email is included in the id_token but cannot be obtained without permission. 91 | */ 92 | protected function getEmail(string $id_token): void 93 | { 94 | $headers = [ 95 | 'Content-Type' => 'application/x-www-form-urlencoded', 96 | ]; 97 | 98 | $form_params = [ 99 | 'id_token' => $id_token, 100 | 'client_id' => $this->clientId, 101 | ]; 102 | 103 | $response = $this->getHttpClient()->post( 104 | 'https://api.line.me/oauth2/v2.1/verify', 105 | compact('headers', 'form_params') 106 | ); 107 | 108 | $payload = json_decode($response->getBody(), true); 109 | 110 | $this->email = data_get($payload, 'email'); 111 | } 112 | 113 | /** 114 | * {@inheritdoc} 115 | */ 116 | protected function getUserByToken($token) 117 | { 118 | $response = $this->getHttpClient()->get( 119 | 'https://api.line.me/v2/profile', 120 | [ 121 | 'headers' => [ 122 | 'Authorization' => 'Bearer '.$token, 123 | ], 124 | ] 125 | ); 126 | 127 | return json_decode($response->getBody(), true); 128 | } 129 | 130 | /** 131 | * {@inheritdoc} 132 | */ 133 | protected function mapUserToObject(array $user): User 134 | { 135 | return (new User)->setRaw($user)->map( 136 | [ 137 | 'id' => $user['userId'], 138 | 'nickname' => $user['displayName'] ?? '', 139 | 'name' => $user['displayName'] ?? '', 140 | 'email' => $this->email ?? '', 141 | 'avatar' => $user['pictureUrl'] ?? '', 142 | ] 143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /stubs/listeners/Line/FollowListener.php: -------------------------------------------------------------------------------- 1 | getSource(); 27 | $follow = $event->getFollow(); 28 | 29 | if ($source instanceof UserSource) { 30 | $id = $source->getUserId(); 31 | } 32 | 33 | info($source); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /stubs/listeners/Line/JoinListener.php: -------------------------------------------------------------------------------- 1 | getSource(); 29 | 30 | if ($source instanceof GroupSource) { 31 | $id = $source->getGroupId(); 32 | } 33 | 34 | info($source); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /stubs/listeners/Line/LeaveListener.php: -------------------------------------------------------------------------------- 1 | getSource(); 27 | 28 | if ($source instanceof GroupSource) { 29 | $id = $source->getGroupId(); 30 | } 31 | 32 | info($source); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /stubs/listeners/Line/MessageListener.php: -------------------------------------------------------------------------------- 1 | getMessage(); 33 | $this->token = $event->getReplyToken(); 34 | 35 | match ($message::class) { 36 | TextMessageContent::class => $this->text($message), 37 | StickerMessageContent::class => $this->sticker($message), 38 | }; 39 | } 40 | 41 | /** 42 | * @throws ApiException 43 | */ 44 | protected function text(TextMessageContent $message): void 45 | { 46 | Bot::reply($this->token)->text($message->getText()); 47 | } 48 | 49 | /** 50 | * @throws ApiException 51 | */ 52 | protected function sticker(StickerMessageContent $message): void 53 | { 54 | Bot::reply($this->token)->sticker( 55 | $message->getPackageId(), 56 | $message->getStickerId() 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /stubs/listeners/Line/UnfollowListener.php: -------------------------------------------------------------------------------- 1 | getSource(); 26 | 27 | if ($source instanceof UserSource) { 28 | $id = $source->getUserId(); 29 | } 30 | 31 | info($source); 32 | } 33 | } 34 | --------------------------------------------------------------------------------