Sending Telegram messages with Laravel Notifications

Posted 15 Jun 2020

More a visual learner? Check out my video on this topic!

Laravel has a powerful notification system that comes with the ability to send notifications via mail, SMS (Nexmo), Slack, broadcasting to the browser or storing them in the database to display them in your web app. Thanks to its driver based design, the community added support for over 50 more services.

One driver that always sparked my interest was Telegram. I love the idea to send real time push notifications without the hassle to deal with native apps or paying for every sent sms. Luckily for us, there is already a ready to use notification Driver. The only problem: The documentation doesn't answer the question, how we get the unique chat ID. Thats what we are going to learn today.

First things first: Installation and preparation

We are going to need a working Laravel installation, the Telegram notification driver and an extra package to validate the response we get from the Telegram API about the current logged in user.

laravel new TelegramProject && cd TelegramProject
composer require laravel-notification-channels/telegram
composer require pschocke/laravel-telegram-login-widget

Next, we need to configure the Telegram notification driver. We need to talk to @botfather to get our bot API token and configure the notification driver to use this token:

// config/services.php
'telegram-bot-api' => [
    'token' => env('TELEGRAM_BOT_TOKEN', 'YOUR BOT TOKEN HERE')
    'name' => env('TELEGRAM_BOT_NAME', 'YOUR BOT_NAME')
],

Getting the User ID

We are now able to send notification to telegram, assuming we know the recipient user id. For me, that was the tricky part. How do we get the id? Luckily for us, Telegram offers a login widget that gives the user the ability connect his telegram account with our app.

We need two routes: One that displays the widget (of course the widget can be placed on any site) and one that handles the redirect from Telegram.

// routes/web.php
Route::get('/telegram/connect', 'TelegramController@connect')->name('telegram.connect');
Route::get('/telegram/callback', 'TelegramController@callback')->name('telegram.callback');

The connect route displays the login widget. there is not a lot of logic inside:

// App\Http\Controllers\TelegramController
public function connect {
    return view ('telegram.connect')
}

// resources/views/telegram/connect
<body>
    <script 
        async 
        type="application/javascript"
        src="https://telegram.org/js/telegram-widget.js?7"
        data-telegram-login="{{ config('services.telegram-bot-api.name') }}" 
        data-size="large" 
        data-auth-url="{{ route('telegram.connect') }}" 
        data-request-access="write"
    ></script>
</body>

Thats all we need to do. Telegram will render a call to action for the user to connect his account.

If the user clicks the button, he will be asked to login with his phone number (if he isn't already logged in to telegram) and to approve the access to his account for your app. After approving, he will be redirected to the route you've defined in the data-auth-url with his profile information. In this route you need to verify the response from Telegram and store the chat id.

// App\Http\Controllers\TelegramController
use pschocke\TelegramLoginWidget\Facades\TelegramLoginWidget;
// ...
public function callback(Request $request) {
    if (! $telegramUser = TelegramLoginWidget::validate($request)) {
        return 'Telegram Response not valid';
    }
    $telegramChatID = $telegramUser->get('id');
    // You need to store the chat ID to be able to use it later
    return 'Success!';
}

And thats it. You now know the chat ID. The $telegramUser also contains all other parameter that Telegram provided us: firstname, lastname, username, photourl and authdate.

Digging deeper

You now know how to get the chat ID from Telegram. If that's all you want, you can stop reading here. But if you want to know what's happening inside the TelegramLoginWidget::validate($request) function, continue reading.

The response from Telegram contains a hash parameter wich is a hexadecimal representation of all values sorted alphabetical, concatenated with '\n' and hashed with sha256 and your bot token. Your app needs to rebuild the same hash and compare it with the hash included in the response. Here is the basic logic:

private function checkHashes(Collection $collection) {
    $secret_key = hash('sha256', config('telegramloginwidget.bot-token'), true); 
    $data = $collection->except('hash'); 
    $data_check_string = $data->map(function ($item, $key) { 
        return $key.'='.$item; 
    })
        ->values() 
        ->sort() 
        ->implode("\n"); 
    $hash = hash_hmac('sha256', $data_check_string, $secret_key); 
    if (strcmp($hash, $collection->get('hash')) !== 0) { 
        throw new HashValidationException; 
    }
    return $data; 
}

If you have any questions, hit me up on twitter @schockepatrick.