Laravel Notifications - Telegram Bot
26 min read

Laravel Notifications - Telegram Bot

Programmatically configure and send Telegram Bot notification to your Users within your Laravel Application using a Laravel Notifications

Overview

I wanted to receive Telegram notifications when certain events within some of my Laravel Apps occurred. This guide shares what I have learnt and how I approached it and set it up.

I am going to make some assumptions; you are familiar with PHP and the Laravel framework as well as JavaScript and the VueJS Framework. However, with some development background you should be able to understand most of the lines of code I introduce and should be able to follow along.

I opted to use a Laravel Notification and a community built delivery channel to send Telegram notifications using the Telegram Bot API. I will implement it in an example Laravel Jetstream application using the Inertia stack. If you are not familiar with InertiaJS, Jeffery Way describes it as follows:

Inertia.js allows you to build single-page applications, while still allowing Laravel to be responsible for routing, controllers, authorization, policies, and more. Think of it like the glue that connects a server-side framework like Laravel, to a client-side framework like Vue.

Getting Started

As this is not a 101 on Laravel, I suggest looking at the Laravel Documentation for more information on the installation options

Install Laravel

This command will get us started:

# Install
laravel new laravel-telegram-bot --jet --stack inertia -q

# Navigate into Application root
cd laravel-telegram-bot

Serve the application...
I am using Laravel Valet for my development environment. There are a number of other options to serve a Laravel Application, however, I would suggest Laravel Sail if Valet is not an option for you.

Whatever you decide, once you are up and running you will be presented with a starting page similar to this when browsing to your new Laravel Application.

Laravel Application Page

Add Packages

In addition to installing Laravel with the InertiaJS stack, the installation only really requires the Telegram Notifications Channel for Laravel composer package.

composer require laravel-notification-channels/telegram

Telegram Bot

The main event. So let's start with creating a Telegram Bot and capturing a few details after.

Start a new chat with the @BotFather and create your new bot to obtain a Bot API Token.

Search for the BotFather

I think the team at Telegram made it fairly easy to use, and for the most part self-explanatory. I mean, we know what we are here for, so let's create a new bot using /newbot. Start the conversation to create a bot.

Once the BotFather congratulates you, you will be presented with your Bot's API Token and the bot URL (look for the link which starts with t.me/<bot name>). Open the project with your chosen IDE and save these to your .env file

TELEGRAM_BOT_URL=https://t.me/<Bot>
TELEGRAM_BOT_TOKEN=<YOUR BOT TOKEN HERE>

In addition, add a telegram-bot-api array in config/services.php. This will allow our application to read these values from the environment variables, we just added to the .env file

'telegram-bot-api' => [
    'bot_url' => env('TELEGRAM_BOT_URL'),
    'token' => env('TELEGRAM_BOT_TOKEN', 'YOUR BOT TOKEN HERE'),
],

With this we now have a Laravel App, the Package which will help us with notification installed, as well as our bot with some details of it captures.


Laravel Notification

Let's create a Laravel Notification and use the Telegram Notification Channel within it. We can follow the standard process of generating a Laravel Notification. Each notification you create is represented by a single class that is typically stored in the app/Notifications directory.

php artisan make:notification TelegramNotification

Although the documentation for the Telegram Notification Channel for Laravel explains this well, I have updated the class at app/Notifications/TelegramNotification.php as follows:

1. Specify the delivery channel in the via() method:

use NotificationChannels\Telegram\TelegramChannel;

public function via($notifiable)
{
    return [TelegramChannel::class];
}

2. Update the so-called message building methods. By default this is toMail which should also be renamed to toTelegram and updated as follows:

use NotificationChannels\Telegram\TelegramMessage;

public function toTelegram($notifiable)
{
    return TelegramMessage::create()
        ->to($notifiable->telegram_chat_id)
        ->content($this->message);
}

That is it, the to() is where the notification should be sent, this will be a Telegram Chat ID which is how Telegram identifies the chat you have open with a bot and the  content() method specifies what the Telegram message should contain.

3. Finally, let's also add a $message property and set it in the constructor of the class:

class TelegramNotification extends Notification
{
    // ...
    
    protected $message;

    public function __construct($message)
    {
        $this->message = $message;
    }

    // ...

}

Here is the full class for review:

laravel-telegram-bot/TelegramNotification.php at main · CryDeTaan/laravel-telegram-bot
Telegram Bot in Laravel. Contribute to CryDeTaan/laravel-telegram-bot development by creating an account on GitHub.

Laravel Notifiable Trait Explained

Notifications can be sent using the notify method from the Notifiable trait which is defined on the  App/Models/User model by default:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;
}

The notify method that is provided by this trait expects to receive a notification instance, in our case the TelegramNotification, and which we can implement as the send method in a new controller, let's call it the TelegramNotificationController.

Let's create this controller and add a send method.

# Create a new controller
php artisan make:controller TelegramNotificationController
use App\Notifications\TelegramNotification;

public function send(Request $request)
{
    $user = auth()->user();
    $user->notify(new TelegramNotification($request->notification));

    return back();

}

We will come back to this controller several times throughout this guide, but this does hook up a User with the Notifiable Trait to the Telegram Notification we created earlier.


Telegram Chat ID

Now, knowing that the to($notifiable->telegram_chat_id) method in the app/Notifications/TelegramNotification we created earlier is where the notification should be sent.

Update User Migration

We should set the telegram_chat_id on the user model by preparing the user migration by adding a column to the create method on the Schema facade

$table->string('telegram_chat_id')->nullable();

At this point it is a good idea to configure the database setting in the .env and run the migration command.

php artisan migrate

Telegram Chat ID Explained

There are a few things required here to set up the application to update the user's telegram_chat_id column with the Chat ID of the chat you have with a bot.

When starting a chat with a bot, that chat instance has a unique ID, this is the Chat ID. Not only do we need to capture this Chat ID in our application, but we also need to associate it with a user within our Laravel Application.

This can be confusing, I know I initially struggled to wrap my mind around it.

Let me further clarify this Chat ID concept by starting a chat with the bot and then interacting with it using the Telegram Bot API.

We can do this by making a request to the the getUpdates method by using the following URL:https://api.telegram.org/bot<token>/getupdates . NOTE the <token> was returned when the bot was created, which in my case is: 077474175:AAEqxEUGZ5edu0rOEg5tQNFPgmdTBLmQqio

📢  If at any point you need information about your bot, for example the bot URL, simply use the /mybots bot command in the BotFather chat view for further information on your bot(s).

Initially making this request will return an empty result array.

So, let's start a chat with the bot and see how this changes. Start a chat using the URL that was presented when the bot was created, in my case: https://t.me/laravel_telegram_test_bot.

Starting a chat with a bot will produce some results when making a request to the getUpdates method. There are a few interesting pieces of information here, but most importantly we can now get the Chat ID from the result array.


Deep Linking

Telegram User

Remember, this being our bot, only we should know the API Token for it, this is not something known by the user who will be using our Telegram Bot. Meaning that users may /start and/or interact with Telegram Bots, but only bot owners will be able to see the Chat IDs using the getUpdates bot API method as well as the details of the Telegram user who started to interact with our bot.

Laravel User

However, a user's details in Telegram may be different to the details in our Laravel Application. In fact, in this example Laravel Application, there is no concept of a username. The application is only concerned with name, surname and email address which can be used to distinguish a user. But from the last screenshot from the getUpdates output, the only available information from a Telegram chat perspective (in this case at least) is the username. And even if the example Laravel Application had a username field for a user, a user may choose to use different usernames on these two applications. Therefore, there is no way for us to be sure that the user's details in a Telegram message matches a user within our Laravel Application.

So how could we connect a user's Telegram account with their account within our Laravel Application.

Deep Linking Explained

Fortunately, Telegram bots have a Deep Linking mechanism that can aid us to programmatically connect a user's Telegram account to their account in our Laravel Application.

To make use of deep linking, a parameter(?start= or ?=startgroup) can be passed with the bot's URL, for example, https://t.me/laravel_telegram_test_bot?start=test.

The start parameter will open a one-on-one conversation with the bot, if the startgroup parameter is used, the user is prompted to select a group to add the bot to. In this example Laravel Application, I am making use of the start parameter, but you are free to make use of the startgroup parameter.

Your browser will prompt you to Open Telegram, notice the start parameter in the URL.

Starting a chat using this URL will have the resulting message contain the word test we specified as the ?start= parameter.

Now, the significance of this is that we have a value we can control from within our Laravel Application that can be unique to each request. So suppose we generate a unique value for a user and show them a button that links to the following URL: https://t.me/laravel_telegram_test_bot?start=vCH1vGWJxfSeofSAs0K5PA.

This will allow us to read the messages from the getUpdates method, and capture the Chat ID of the messages. Furthermore, using the unique value passed as the start parameter that appears in the text field, i.e. /start vCH1vGWJxfSeofSAs0K5PA, to lookup the user it has been associated with in our Laravel Application, thereby giving us a programmatic way to assign the Chat ID to a user within our Laravel Application.

Now that this provides us with a way to connect the user's Telegram account to their account on our Laravel Application, let's look at an alternative to the getUpdates method we have used so far.

Telegram Bot Webhook

Telegram currently support two ways of processing bot updates, the getUpdates API method we have used up to this point or a webhook. The getUpdates is a pull mechanism, where as the webhook is a push. This means that as soon as an update arrives, Telegram delivers it to application or bot for processing. This avoids having our application to ask Telegram for updates frequently and having the need to develop some kind of polling mechanism in our code.

Getting updates via a Telegram API Webhook is the preferred option when using deep linking with Telegram and all this means is that we would supply Telegram with an URL where Telegram posts updates to our application for processing.

Telegram access to Webhook

Now, the following is probably going to cause some confusion, and more so in a development environment, but Telegram needs to be able to connect and post updates to the specified Webhook URL of our application.

As I mentioned, I am using Laravel Valet which makes it easy to make an application accessible from the internet while in a development setting, i.e. behind some sort of NAT etc. Laravel Valet is using ngrok under the hood. So, if you are not using Valet to serve the application and you do not currently have a way to expose the application in a development setting to the internet, I suggest looking at ngrok. Or an alternative would be Expose from the team at BeyondCode which is a team I rate highly in the PHP Laravel community.

After sharing or exposing the Laravel Application using Laravel Valet to the internet, I am presented with a https URL that I can use to set the Telegram Bot's webhook.

We can be sure, at least with some certainty, that the Webhook request comes from Telegram by using a "secret" path as our Webhook URL, e.g. https://www.example.com/<token>. This Webhook URL can then be set by sending it as the ?url= parameter to the setWebhook method.

for example: https://api.telegram.org/bot2077474175:AAEqxEUGZ5edu0rOEg5tQNFPgmdTBLmQqio/setWebhook?url=https://8452-90-220-225-209.ngrok.io/telegram/webhook/Lf67MeNWr7kGAgsfGYv/

⚠️  Although nobody else knows the bot's webhook token, you can be pretty sure that the requests are from Telegram, however, it is also possible to set the webhook with a public key certificate so that the root certificate in use can be checked which is probably even better (I will discuss this in a future post).

It should be noted that there are other parameters that can be configured for the webhook, such as which update types you want your bot to receive or, as mentioned previously, supplying a certificate that can be checked during communication (which I will discuss in a future post).

Automatically setting a Webhook

Although setting a webhook in the way outlined above is not that complicated, I came up with a mechanism to do this with an Artisan Command.

This console command is perhaps a bit unnecessary to fully explain here, however, if you want to create this in your own application you can make a console command using the following artisan command and copy the following code to the app/Console/Commands/ConfigureTelegramWebhook.php console / artisan command.

artisan make:command ConfigureTelegramWebhook

From a high level overview the following happens:

  1. A webhook "secret" is generated
  2. This "secret" is updated in the .env
  3. The user is prompted for the base URL
  4. The webhook is constructed
  5. The Bot is set with the new webhook using the API setWebhook method

There are three values that are required in the .env to be updated if you want to use this Console Command, the first two should already be specified.

  1. The URL, you can copy it from the message from the BotFather and set a TELEGRAM_BOT_URL= key with, in my case,  https://t.me/laravel_telegram_test_bot
  2. Set a TELEGRAM_BOT_TOKEN= with the API Token, in my case 2017805637:AAFAB6wvnYC5aGzm07_hqUIVokMy82Z6WpA
  3. Set an empty TELEGRAM_BOT_WEBHOOK= key

The values in the .env should now look something like this:

TELEGRAM_BOT_TOKEN=2077474175:AAEqxEUGZ5edu0rOEg5tQNFPgmdTBLmQqio
TELEGRAM_BOT_URL=https://t.me/laravel_telegram_test_bot
TELEGRAM_BOT_WEBHOOK=

These keys should also be added in the telegram-bot-api array in config/services.php. Again, the token and bot_url keys should already have been specified earlier during the installation of the Laravel Notifications Channel.

'telegram-bot-api' => [
    'token' => env('TELEGRAM_BOT_TOKEN', 'YOUR BOT TOKEN HERE'),
    'bot_url' => env('TELEGRAM_BOT_URL'),
    'webhook' => env('TELEGRAM_BOT_WEBHOOK'),
],

Once this has been configured, the Telegram Bot's Webhook URL can be set using the telegram:configure-webhook artisan command.

⚠️ If you are making use of ngrok, or something similar during development, make sure to run this command every time when the exposed URL changes.

Webhook Route

It's great that we have set a webhook, but we'll need a route within our Laravel Application to handle incoming requests to the webhook that we configured previously.

We can update the routes/web.php file with the webhook URI and specify the store method in theTelegramNotificationController class.

use App\Http\Controllers\TelegramNotificationController;

Route::post('/telegram/webhook/'.config('services.telegram-bot-api.webhook'), 
[TelegramNotificationController::class, 'store']);

💡  Note that the <token>portion of the webhook is dynamic and specified automatically  in the .env as TELEGRAM_BOT_WEBHOOK= by the above mentioned Artisan command. This is after all only supposed to be shared with Telegram as previously explained, and having it static will only end up leaking it in places like source control repositories.

Very importantly, we also need to exclude this route from Laravel's CSRF protection. This is achieved by adding this route to the $except array in the app/Http/Middleware/VerifyCsrfToken.php class.

protected $except = [
    '/telegram/webhook/*',
];

Now that we have set the webhook route, let's add the store method in the TelegramNotificationController. There is more to come with this method, but for the moment, let's just log incoming requests to this webhook to a file to get an idea of what we are working with here.

I will add some initial checks to make sure that we are working with a Telegram message as there are other types of request coming from Telegram, we are only interested in the message type updates. You may notice that I perform a try catch but still return a 2xx on an exception. The reason for this is that Telegram will continue to try and send the updates unless it receives a 2xx and therefore, if the update is not what I am expecting on the webhook, I want to let Telegram know that I accepted the response and will deal with it in the application. This then means that Telegram no longer needs to care about it, and that way can forget this update and not try to resend it.

Update App/Http/Controllers/TelegramNotificationController as follow:

use Illuminate\Support\Facades\Log
use Exception;

/**
 * Store Telegram Chat ID from telegram webhook message.
 *
 * @param Request $request
 * @return \Illuminate\Http\Response
 */
public function store(Request $request)
{
    try {
        $messageText = $request->message['text'];
    } catch (Exception $e) {
        return response()->json([
            'code'     => $e->getCode(),
            'message'  => 'Accepted with error: \'' . $e->getMessage() . '\'',
        ], 202);
    }

    Log::build([
        'driver' => 'single',
        'path' => storage_path('logs/webhook.log'),
    ])->info($request->all());

    return response('Success', 200);
}

Send a new message to the Telegram bot or restart the chat using the /start feature. This will write the POST request to the storage/logs/webhook.log file whenever a message is received by our Telegram Bot, similar to this:

This confirms that we have correctly set up the webhook and that we are receiving message updates from the Telegram Bot API.

We can now start building our front-end to test notification with.


Notification Form

If you recall, we have already added a send method to the TelegramNotificationController class as follows:

use App\Notifications\TelegramNotification;

public function send(Request $request)
{
    $user = auth()->user();
    $user->notify(new TelegramNotification($request->notification));

    return back();

}

Let's build a form which we can use to send a notification that will reach this method.

Routes

First, let's start with the routes, we will have two routes:

  1. One that will render the view containing the form using a basic closure, and
  2. One that will receive a POST request containing the notification message from the form.

We will also wrap the second route in a route group and apply the default auth middleware provided by Laravel Fortify which is included in our Laravel Jetstream installation. This will be required later.

Route::middleware(['auth:sanctum', 'verified'])->get('/notification', function () {
    return Inertia::render('Notification');
})->name('notification');

Route::middleware(['auth:sanctum', 'verified'])->group(function () {
        Route::post('/telegram/notification', [TelegramNotificationController::class, 'send'])->name('send-notification');
});

Views - Notification Page

Laravel's Jetstream Inertia starter kit comes with an example Dashboard page that we can use as a template to create a Notification page. Let's copy the resources/js/Pages/Dashboard.vue file to resources/js/Pages/Notification.vue.

You will notice a Dashboard header section and a Welcome component that is imported and rendered. Change the Dashboard header to Notification and note the <welcome /> component on line 12; replace it with this form:

<div class="mt-5 md:mt-0 md:col-span-2">
    <form @submit.prevent="sendNotification">
        <div class="px-4 py-5 bg-white sm:p-6 shadow sm:rounded-tl-md sm:rounded-tr-md">
            <div class="col-span-6 sm:col-span-4">
                <jet-label for="notification" value="Notification" />
                <jet-textarea id="notification" type="text" class="mt-1 block w-full" v-model="form.notification" ref="notification"/>
                <jet-input-error :message="form.errors.notification" class="mt-2" />
                <p class="mt-2 text-sm text-gray-500">Write a few sentences as a notification.</p>
            </div>
        </div>

        <div class="flex items-center justify-end px-4 py-3 bg-gray-50 text-right sm:px-6 shadow sm:rounded-bl-md sm:rounded-br-md">
            <jet-button :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
                Send
            </jet-button>
        </div>
    </form>
</div>

Within the script section, make sure the imports and components match the following:

<script>
import {defineComponent} from 'vue'
import AppLayout from '@/Layouts/AppLayout.vue'
import JetTextarea from '@/Jetstream/Textarea.vue'
import JetInputError from '@/Jetstream/InputError.vue'
import JetLabel from '@/Jetstream/Label.vue'
import JetButton from '@/Jetstream/Button.vue'

export default defineComponent({
    components: {
        AppLayout,
        JetLabel,
        JetTextarea,
        JetInputError,
        JetButton,
    },

    data() {
        return {
            form: this.$inertia.form({
                notification: '',
            }),
        }
    },
    methods: {
        sendNotification() {
            this.form.post(route('send-notification'), {
                onSuccess: () => this.form.reset(),
            })
        },
    },

})
</script>

You can review this vue component here.

Firstly, a note on the imports. These are all components which ship with the Jetstream Inertia stack, apart from JetTextarea. This one I created based of the Jetstream Input component (resources/js/Jetstream/Input.vue).

Create a new vue component at this location resources/js/Jetstream/Textarea.vue and add the following:

<template>
    <textarea class="border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm" rows="8" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" ref="input" />
</template>

<script>
import { defineComponent } from 'vue'
export default defineComponent({
    props: ['modelValue'],
    emits: ['update:modelValue'],
    methods: {
        focus() {
            this.$refs.input.focus()
        }
    }
})
</script>

Finally, a note on the data() and methods: sections of our new Notification Form component which adds the logic to submit the form when the Send button is clicked.

  1. data() essentially stores the text from the text area in the notification data property within the Inertia form helper, and
  2. The sendNotification method will POST the form to the route we previously created.

Views - Navigation

Before we compile our frontend assets, let's add a navigation button to our Laravel Application, this can be done by making some changes to the resources/js/Layouts/AppLayout.vue file.

There are two sections that we can duplicate and make some changes to, on order to add the navigation to both desktop as well as mobile screen sizes.

Duplicate lines 22 to 24 and update it as depicted below:

Duplicate lines 148 to 150 and update it as depicted below:

Build

Now we can compile the frontend assets by running the following from the root of the project:

npm run dev

Test - Notification Form

Let's head to our Laravel Application and register a new user.

We can now navigate to the Notification tab and try to send a Notification.

This will not work. Remember that we have not yet set the Chat ID of the user we are currently logged in as. Let's manually set this, just to make sure everything works before we do so programmatically.

Set Chat ID - Manually

We have already seen our Chat ID a few times. First when we used the getUpdates method in our browser, as well as when we received updates via our webhook which was then written to the log file. Take note of this as we will need it to manually set the Chat ID for the user.

Open a Laravel Tinker session from your terminal

php artisan tinker

Find the user using the where clause, update my name with whatever you used when registering your user.

$user = User::where('name', 'CryDeTaan')->first();

Set the telegram_chat_id

$user->telegram_chat_id = 73856202 

Finally, save this value

$user->save(); 

Now try to send the notification once again, if all goes well you should receive a message in Telegram as can be seen below.


Programmatically Set Chat ID

The concept of deep linking has already been explained, so we can just go ahead and implement that logic now.

As mentioned before, I used Laravel Jetstream to scaffold the application, particularly useful for the user logic and the profile section this provides.

This can be seen after logging in and heading to the profile section from the dropdown to the top right of the screen.

Adding Profile Partial

These profile partials, are built up from the Jetstream components and can very easily be recreated or customised, so I decided to add another partial in the resources/js/Pages/Profile/Partials directory. You will see each of the profile panels match a partial in the Partials directory. The Vue component responsible for rendering these partials is in the parent directory and is called Show.vue.

We will create a new partial, TelegramNotificationsForm.vue, in the resources/js/Pages/Profile/Partials directory and use the Jetstream components to build our new profile panel. The following can be added to the newly created partial.

<template>
    <jet-action-section>
        <template #title>
            Telegram Notifications
        </template>

        <template #description>
            Add notifications to your account using Telegram.
        </template>

        <template #content>
            <h3 class="text-lg font-medium text-gray-900" v-if="telegramNotificationsEnabled">
                You have enabled Telegram Notifications.
            </h3>

            <h3 class="text-lg font-medium text-gray-900" v-else>
                You have not enabled Telegram Notifications.
            </h3>

            <div class="mt-3 max-w-xl text-sm text-gray-600">
                <p>
                    When enabling Telegram notifications, you will be taken to the Telegram website, or application and
                    asked to start a chat with the bot to receive notifications.
                </p>
            </div>

            <div class="flex items-center mt-5">
                <div v-if="! telegramNotificationsEnabled">
                    <jet-button @click="enableTelegramNotifications" type="button" :class="{ 'opacity-25': enabling }" :disabled="enabling">
                        Enable
                    </jet-button>
                </div>

                <div v-else>
                    <jet-danger-button @click="disableTelegramNotifications" :class="{ 'opacity-25': disabling }" :disabled="disabling">
                        Disable
                    </jet-danger-button>
                </div>
                <jet-action-message :on="disabling" class="ml-3">
                    Done.
                </jet-action-message>
            </div>
        </template>
    </jet-action-section>

</template>

<script>
import {defineComponent} from 'vue'
import JetActionSection from '@/Jetstream/ActionSection.vue'
import JetActionMessage from '@/Jetstream/ActionMessage.vue'
import JetDangerButton from '@/Jetstream/DangerButton.vue'
import JetButton from '@/Jetstream/Button.vue'

export default defineComponent({
    components: {
        JetActionSection,
        JetButton,
        JetDangerButton,
        JetActionMessage,
    },

})
</script>

I wanted to render this profile panel component just after the password change panel. To do this we can import our new panel as a component <telegram-notifications-form> in the resources/js/Pages/Profile/Show.vue parent component.  Take note of the additional prop 'telegramNotificationsEnabled' as well.

</template>
    <!-- ... Snip ... -->
    <div v-if="$page.props.jetstream.canUpdatePassword">
        <update-password-form class="mt-10 sm:mt-0" />
        <jet-section-border />
    </div>

    <div>
        <telegram-notifications-form :telegramNotificationsEnabled="telegramNotificationsEnabled" class="mt-10 sm:mt-0" />
        <jet-section-border />
    </div>

    <!-- ... Snip ... -->

</template>

<script>
<!-- ... Snip ... -->
import TelegramNotificationsForm from "@/Pages/Profile/Partials/TelegramNotificationsForm";

export default defineComponent({
    props: ['sessions', 'telegramNotificationsEnabled'],

	components: {
		<!-- ... Snip ... -->
		TelegramNotificationsForm,
	},
})
</script>

From your terminal, in the root of the project's directory, build the frontend assets.

npm run dev

Opening the Profile section should now include a Telegram Notification panel. We have not yet added any logic, but this does confirm we hooked it all up correctly to successfully render the panel in the profile section.

Backend Logic

From the frontend a request will be made by the user of our Laravel Application to obtain a URL with a unique value that we can use with the Telegram Deep Linking mechanism. So let's update our Routes and add a method to the TelegramNotificationController.

Add the /telegram/temp-url route to the existing Route group as follows:

Route::middleware(['auth:sanctum', 'verified'])->group(function () {
    // ...
    Route::get('/telegram/temp-url', [TelegramNotificationController::class, 'create'])->name('telegram-temp-url');
});

In the TelegramNotificationController add the create method as follows:

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;

class TelegramNotificationController extends Controller
{

    /**
    * Create a unique code for the user to activate Telegram notifications.
    *
    * @param Request $request
    * @return \\Illuminate\\Http\\JsonResponse
    */
    public function create(Request $request)
    {
        $telegramBotUrl = config('services.telegram-bot-api.bot_url');

        $userTempCode = Str::random(35);
        Cache::store('telegram')
            ->put($userTempCode, auth()->id(), $seconds = 120);

        // Telegram URL:
        // <https://t.me/ExampleBot?start=vCH1vGWJxfSeofSAs0K5PA>
        $telegramUrl = $telegramBotUrl . '?start=' . $userTempCode;

        return response()->json([
            'telegramUrl' => $telegramUrl,
        ]);
		
    }

    // ... 
}

This method will use our Telegram Bot's URL and construct a temporary Deep Link URL that includes a 35 character random string. This string is saved to a cache for 2 minutes (we will set this up next).

This temp url will be returned to the user and will be opened in a new tab. This will be covered more in the Frontend Logic section.

Caching

Laravel provides a caching mechanism which we can make use of to store the user's temporary codes. We can then retrieve the code from the cache to match the user's Telegram account with their account in our application. This, again, has been discussed in the Deep Linking mechanism section above.

I decided to create an additional cache store for this purpose that will make use of the Database driver. I do not foresee thousands of users requesting to enable Telegram Notifications on their accounts at the same time, so there is no need for using very fast data stores such as Memcached or Redis for caching.

Add the telegram store to the end of the stores array in config/cache.php.

<?php

use Illuminate\Support\Str;

return [
    'stores' => [
    	// ...
        
        'telegram' => [
       	    'driver' => 'database',
            'table' => 'telegram_cache',
            'connection' => null,
            'lock_connection' => null,
        ],
    ]
    
    'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache'),

];

As I mentioned before, I opted for the database drive, so a telegram_cache table needs to be set up which will contain the cached items. We can do this with a new migration.

php artisan make:migration create_telegram_cache_table

The Schema::create should be updated as follows.

Schema::create('telegram_cache', function (Blueprint $table) {
    $table->string('key')->primary();
    $table->mediumText('value');
    $table->integer('expiration');
});

We'll also need to set up a table to contain our application's cache locks.

Schema::create('telegram_cache_locks', function (Blueprint $table) {
    $table->string('key')->primary();
    $table->string('owner');
    $table->integer('expiration');
});

We need to specify these in the down() function of our migration as well.

public function down()
{
    Schema::dropIfExists('telegram_cache');
    Schema::dropIfExists('telegram_cache_locks');
}

The complete migration can be viewed here.

Run the new migration:

php artisan migrate

Let's add some frontend logic...

Frontend Logic

There are a few data properties we need to add to our TelegramNotificationsForm.vue component, together with some methods that will initiate the process.

Add the following (from props:) to the TelegramNotificationsForm.vue component's <scrip> section:

export default defineComponent({
    components: { 
    // ... Snip ...
    },

    props: ['telegramNotificationsEnabled'],

    data() {
        return {
            enabling: false,
            disabling: false,
        }
    },

    methods: {
        async enableTelegramNotifications() {
            this.enabling = true
            try {
                const response = await axios.get(route('telegram-temp-url'));
                window.open(response.data.telegramUrl, '_blank').focus();

            } catch (error) {
                // TODO: Display error to user.
                console.error(error);
            }
        },

    },

})

The logic is fairly simple, when clicking on the enable button, an axios request (available to us through the Jetstream installation) will be made to our backend.  This axios request is returned a temporary Deep Link URL that will open in a new browser tab.

From your terminal in the root of the project's directory, build the frontend assets one last time 🤞

npm run dev

Opening or Refreshing the Profile page and then clicking on the Enable button on the Telegram Notification panel should now open a new tab with a prompt to Open Telegram. Notice the start=<token> in the URL, this should now be familiar and is what is needed for the Deep Linking mechanism.

Once the Open Telegram option has been selected, Telegram will open a chat with our bot and from here you can start the bot by clicking on the Start button.

Storing Chat ID of User

When clicking the start button in the Telegram App, Telegram will send a POST request to the configured webhook that we discussed earlier. We have already created the store method in the TelegramNotificationController, we just need to implement the logic to take the unique value from the message and perform a lookup in the cache we have set to determine which user  the unique code is associated with.

Once the user is identified, the user's telegram_chat_id can be set from the message's Chat ID which will be used to deliver the notifications in the same way that was already seen when we manually set the telegram_chat_id previously.

The store method in the TelegramNotificationController should be updated with the following:

use App\Models\User;

/**
 * Store Telegram Chat ID from telegram webhook message.
 *
 * @param Request $request
 * @return \Illuminate\Http\Response
 */
public function store(Request $request)
{
    try {
        $messageText = $request->message['text'];
    } catch (Exception $e) {
        return response()->json([
            'code'     => $e->getCode(),
            'message'  => 'Accepted with error: \'' . $e->getMessage() . '\'',
        ], 202);
    }
    // Check if the message matches the expected pattern.
    if (!Str::of($messageText)->test('/^\/start\s[A-Za-z0-9]{35}$/')) {
        return response('Accepted', 202);
    }

    // Cleanup the string
    $userTempCode = Str::of($messageText)->remove('/start ');

    // Get the User ID from the cache using the temp code as key.
    $userId = Cache::store('telegram')->pull($userTempCode);
    $user = User::find($userId);

    // Get Telegram ID from the request.
    $chatId = $request->message['chat']['id'];

    // Update user with the Telegram Chat ID
    $user->telegram_chat_id = $chatId;
    $user->save();

    return response('Success', 200);
}

🔥  Remove the use Illuminate\Support\Facades\Log; from the controller and also delete the storage/logs/webhook.log file as well as these are no longer needed.

This will now programmatically set the user's Telegram Chat ID, and notifications can be sent using the Notification tab in the example Laravel Application.

There is one small thing that we still need to take care of before we can consider this to be complete. This is to give the user the ability to disable Telegram notifications.


Disable Telegram Notifications

Some of the work has already been done. If you look at the the Profile panel partial we added, resources/js/Pages/Profile/Partials/TelegramNotificationsForm.vue, you will notice that there is a section that does VueJS Conditional Rendering (v-if / v-else).

<div v-if="! telegramNotificationsEnabled">
    <jet-button @click="enableTelegramNotifications" type="button" :class="{ 'opacity-25': enabling }" :disabled="enabling">
        Enable
    </jet-button>
</div>

<div v-else>
    <jet-danger-button @click="disableTelegramNotifications" :class="{ 'opacity-25': disabling }" :disabled="disabling">
        Disable
    </jet-danger-button>
</div>

This conditional rendering is based on a prop, telegramNotificationsEnabled, which we are not yet sending to the frontend. We need to include this when the profile page is loaded, and we can achieve this by Customizing Jetstream's Page Rendering. We will use this feature to pass additional data to the profile page.

Let's need to update the boot method in the App\Providers\JetstreamServiceProvider class with the following closure:

use Illuminate\Http\Request;

class JetstreamServiceProvider extends ServiceProvider
{
    // ...

    Jetstream::inertia()->whenRendering(
        'Profile/Show',
        function (Request $request, array $data) {
            return array_merge($data, [
                'telegramNotificationsEnabled' => (bool)auth()->user()->telegram_chat_id,
            ]);
        }
    ); 

    // ...

}

Once this is configured, we can refresh the profile page and if the user already has a telegram_chat_id set, the Telegram Notification panel will show the disable button.

The only other thing that is needed here is to hook up the logic behind this disable button.

Frontend Logic

The resources/js/Pages/Profile/Partials/TelegramNotificationsForm.vue requires one additional method for this logic to function correctly.

Updated the methods: section with an additional method:

disableTelegramNotifications() {
    this.$inertia.delete(
        route('disable-telegram-notifications'),
        {
            preserveScroll: true,
            onSuccess: page => {
                this.disabling = true
                setTimeout(() => { this.disabling = false; }, 2000);
            },
        }
    )
},

In the terminal, from the root of the project's directory, build the frontend assets , this is really the last time.

npm run dev

Backend Logic

From a backend perspective, we need a route to accept this request from the frontend and add a destroy method in die the TelegramNotificationController to update the user by removing their telegram_chat_id.

Add the delete route to the existing Route group as follows:

Route::middleware(['auth:sanctum', 'verified'])->group(function () {
    // ...
    Route::delete('/telegram/notifications', [TelegramNotificationController::class, 'destroy'])->name('disable-telegram-notifications');
});

Update the TelegramNotificationController with the destroy method.

public function destroy()
{
    $user = auth()->user();
    $user->telegram_chat_id = null;
    $user->save();

    return back();

}

Conclusion

That should be it, if the currently logged in user had their Telegram_chat_id manually set as explained earlier, the button in the Telegram Notifications panel in the Profile page will be the Red disable button.

In either case, the Telegram Notification process is now automated, and clicking on the Enable button will open a new tab with a Deep Link URL. Opening Telegram from this tab will automatically capture the user's telegram_chat_id and the Notification tab can be used to test the sending of a message to the configure the Telegram Chat ID.

Conversely, clicking the Disable button will disable Telegram Notifications by removing their telegram_chat_id.

I hope this helps me in the future, especially when I need to enable Telegram Notification in one of my new Laravel Applications!

The full source code of this example Laravel Application can be found here:

https://github.com/CryDeTaan/laravel-telegram-bot


If you enjoyed the post, please consider to subscribe so that you receive future content in your inbox :)

Psssst, worried about sharing your email address and would rather want to hide it? Consider using a service I created to help with that: mailphantom.io

Also, if you have any questions, comments, or suggestions please feel free to Contact Me.