Last week, support for Telegram notifications was added to both Flare and Oh Dear. Let’s take a look at how both these services connect to Telegram. We’ll focus on how polling is used in the connection flow and how polling can be stopped once the necessary data has been received.
Explaining our use case
Flare is an exception tracker made for Laravel applications. Under the hood, it is built using Laravel / Inertia / React.
Let’s take a look at how people can connect to Telegram. At the Telegram notification preferences at Flare, users find the instructions. A bot needs to be invited to the Telegram room where notifications should be sent to. The bot is started by messaging a token.
Here’s how that looks like.
Don’t bother trying to use that token. It is randomly generated, based on some secret user data, and has a very short lifespan.
Whenever a command is sent to the @FlareAppBot
, Telegram will forward the command to Flare via a webhook. That webhook also contains a chatId
. This id identifies the unique chat room / channel used in Telegram. Flare will use this id to send notifications to the correct room on Telegram.
In the animation above, you see that a confirmation message is shown in Telegram as soon a token is accepted. That confirmation message is sent in the request on our server that handles that webhook.
But, you’ve maybe also noticed that the UI on Flare itself also changed. A message “Your Telegram chat has been connected successfully” is displayed. Let’s take a look at how our UI knows that a chatId
was sent.
The request that handles the webhook from Telegram will put the incoming chatId
in the cache. The cache key name will be generated based on something user specific, so no chatId
can be stolen by other users.
Let’s now focus on getting that chatId
from the cache to the UI. If you’re only interested in Livewire, you can skip the following paragraph on React.
Start and stop polling in React
The Flare UI is constantly polling an endpoint that takes a look if something is in the cache.
Here’s how that controller looks like:
<span class="line"><span>namespace</span><span> </span><span>AppHttpAppControllersNotificationDestinations</span><span>;</span></span>
<span class="line"></span>
<span class="line"><span>use</span><span> </span><span>AppDomainNotificationTelegramActionsTelegramInviteToken</span><span>;</span></span>
<span class="line"></span>
<span class="line"><span>class</span><span> </span><span>TelegramNewChatIdController</span></span>
<span class="line"><span>{</span></span>
<span class="line"><span> </span><span>public</span><span> </span><span>function</span><span> </span><span>__invoke</span><span>()</span></span>
<span class="line"><span> {</span></span>
<span class="line"><span> $token </span><span>=</span><span> </span><span>new</span><span> </span><span>TelegramInviteToken</span><span>(</span><span>current_user</span><span>());</span></span>
<span class="line"></span>
<span class="line"><span> $chatId </span><span>=</span><span> </span><span>''</span><span>;</span></span>
<span class="line"></span>
<span class="line"><span> $cacheKey </span><span>=</span><span> $token</span><span>-></span><span>cacheKey</span><span>();</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>if</span><span> (</span><span>cache</span><span>()</span><span>-></span><span>has</span><span>($cacheKey)) {</span></span>
<span class="line"><span> $chatId </span><span>=</span><span> </span><span>cache</span><span>()</span><span>-></span><span>get</span><span>($cacheKey);</span></span>
<span class="line"><span> </span></span>
<span class="line"><span> </span><span>cache</span><span>()</span><span>-></span><span>forget</span><span>($cacheKey);</span></span>
<span class="line"><span> }</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>return</span><span> </span><span>response</span><span>()</span><span>-></span><span>json</span><span>([</span><span>'new_chat_id'</span><span> </span><span>=></span><span> $chatId]);</span></span>
<span class="line"><span> }</span></span>
<span class="line"><span>}</span></span>
<span class="line"></span>
If we open up the browser’s dev tools, we see that we’re constantly polling for that chatId
.
When a new chatId
is received, the UI is updated with that confirmation message. Also, notice that after a chatId
is received, we stop polling. Let’s take a look at the part of the React component that is responsible for that behaviour.
<span class="line"><span>if</span><span> (telegramChatId) {</span></span>
<span class="line"><span> </span><span>return</span><span> (</span></span>
<span class="line"><span> <></span></span>
<span class="line"><span> <</span><span>Alert</span><span>></span></span>
<span class="line"><span> Your Telegram chat has been connected successfully!{</span><span>' '</span><span>}</span></span>
<span class="line"><span> {</span><span>!</span><span>hasInitialTelegramChatId.current </span><span>&&</span></span>
<span class="line"><span> </span><span>'Please submit this form to save your settings and start receiving notifications.'</span><span>}</span></span>
<span class="line"><span> </</span><span>Alert</span><span>></span></span>
<span class="line"></span>
<span class="line"><span> <</span><span>input</span><span> </span><span>type</span><span>=</span><span>"hidden"</span><span> </span><span>name</span><span>=</span><span>"configuration[chat_id]"</span><span> </span><span>value</span><span>=</span><span>{telegramChatId} /></span></span>
<span class="line"><span> </></span></span>
<span class="line"><span> );</span></span>
<span class="line"><span>}</span></span>
<span class="line"></span>
<span class="line"><span>return</span><span> (</span></span>
<span class="line"><span> <</span><span>div</span><span>></span></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>className</span><span>=</span><span>"flex flex-wrap"</span><span>></span></span>
<span class="line"><span> <</span><span>span</span><span>>You can get started by inviting</</span><span>span</span><span>>{</span><span>' '</span><span>}</span></span>
<span class="line"><span> <</span><span>CopyableCode</span><span> </span><span>className</span><span>=</span><span>"mx-1"</span><span>>@FlareAppBot</</span><span>CopyableCode</span><span>> to your Telegram chat, and issuing this</span></span>
<span class="line"><span> command:</span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"><span> <</span><span>CopyableCode</span><span> </span><span>className</span><span>=</span><span>"mt-2"</span><span>>/start@FlareAppBot {telegramInviteToken}</</span><span>CopyableCode</span><span>></span></span>
<span class="line"><span> <</span><span>Alert</span><span> </span><span>className</span><span>=</span><span>"mt-4"</span><span>></span></span>
<span class="line"><span> <</span><span>i</span><span> </span><span>className</span><span>=</span><span>"fas fa-spinner animate-spin mr-2"</span><span> /> Waiting for connection</span></span>
<span class="line"><span> </</span><span>Alert</span><span>></span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"><span>);</span></span>
<span class="line"></span>
You don’t need to understand the entire code. The important bit is that telegramChatId
is used to decide which message is displayed in the UI. The telegramChatId
is also put in a hidden form field that will get sent to the server when that form gets saved.
To poll for the chatId
, we have this code a bit higher up in the same component.
<span class="line"><span>useInterval</span><span>(</span></span>
<span class="line"><span> </span><span>async</span><span> () </span><span>=></span><span> {</span></span>
<span class="line"><span> </span><span>const</span><span> </span><span>response</span><span> </span><span>=</span><span> </span><span>await</span><span> axios.</span><span>post</span><span>(telegramEndpoint);</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>if</span><span> (response.data.new_chat_id) {</span></span>
<span class="line"><span> </span><span>setTelegramChatId</span><span>(response.data.new_chat_id);</span></span>
<span class="line"><span> }</span></span>
<span class="line"><span> },</span></span>
<span class="line"><span> </span><span>2000</span><span>, </span><span>// delay in milliseconds</span></span>
<span class="line"><span> </span><span>!</span><span>telegramChatId, </span><span>// set to false to stop polling</span></span>
<span class="line"><span>);</span></span>
<span class="line"></span>
Here you can see that we use a hand-rolled useInterval
function that takes a function that uses axios
to check if the chatId
was received. setTelegramChatId
is part of a React state hook that will set the telegramChatId
.
Let’s look at the code of that useInterval
function.
<span class="line"><span>import</span><span> { useEffect, useRef } </span><span>from</span><span> </span><span>'react'</span><span>;</span></span>
<span class="line"></span>
<span class="line"><span>export</span><span> </span><span>default</span><span> </span><span>function</span><span> </span><span>useInterval</span><span>(</span><span>callback</span><span>:</span><span> </span><span>Function</span><span>, </span><span>delay</span><span>:</span><span> </span><span>number</span><span>, </span><span>enabled</span><span>:</span><span> </span><span>boolean</span><span> </span><span>=</span><span> </span><span>true</span><span>) {</span></span>
<span class="line"><span> </span><span>const</span><span> </span><span>savedCallback</span><span> </span><span>=</span><span> </span><span>useRef</span><span><</span><span>Function</span><span>>(() </span><span>=></span><span> {});</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>useEffect</span><span>(() </span><span>=></span><span> {</span></span>
<span class="line"><span> savedCallback.current </span><span>=</span><span> callback;</span></span>
<span class="line"><span> }, [callback]);</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>useEffect</span><span>(() </span><span>=></span><span> {</span></span>
<span class="line"><span> </span><span>function</span><span> </span><span>tick</span><span>() {</span></span>
<span class="line"><span> savedCallback.</span><span>current</span><span>?.();</span></span>
<span class="line"><span> }</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>let</span><span> id</span><span>:</span><span> </span><span>any</span><span>;</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>if</span><span> (enabled </span><span>&&</span><span> delay </span><span>!==</span><span> </span><span>null</span><span>) {</span></span>
<span class="line"><span> id </span><span>=</span><span> </span><span>setInterval</span><span>(tick, delay);</span></span>
<span class="line"><span> }</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>return</span><span> () </span><span>=></span><span> </span><span>clearInterval</span><span>(id);</span></span>
<span class="line"><span> }, [delay, enabled]);</span></span>
<span class="line"><span>}</span></span>
<span class="line"></span>
We’re using a React effect here, which is used to handle side effects (in our case making API calls). Our callable that performs the axios request is wrapped in savedCallback.current
and made callable via the tick()
function. JavaScript’s native setInterval
is used to execute that tick
every a few seconds (specified by delay
).
The closure inside useEffect
returns () => clearInterval(id)
. This is called the clean up function. It will be executed whenever one of the dependancies given to useEffect
changes.
Whenever that axios call receives a response with a chatId
, telegramChatId
in the component that calls setInterval
will be set to something, causing enabled
to be set to false
in setInterval
. The dependencies of useEffect
change, causing that clearInterval
to be called. Pretty neat!
There’s a bit more going in the useInterval
, but let’s ignore that for now. The extra cruft is needed to make useInterval
work in hooks. If you want to know more details, read this extensive blog post on using setInterval
in React hooks by Dan Abramov.
Start and stop polling in Livewire
The UI of Oh Dear is built using Livewire. Previously, I shared how we use Livewire to build complex forms in Oh Dear.
Here’s how the Telegram notifications look like at Oh Dear. Just like on Flare, a user must copy/paste a specific token in a Telegram chat to get started. Notice that we are polling for an incoming chatId
. When the component detects a chatId
in the cache, a confirmation message is displayed, and the polling stops.
Here’s the Blade view of the TelegramChatIdComponent
Livewire component that does the polling and renders the message.
<span class="line"><span><</span><span>div</span><span> </span><span>class</span><span>=</span><span>"mb-4"</span><span>></span></span>
<span class="line"><span> </span><span>@if</span><span>(</span><span>!</span><span> $chatId)</span></span>
<span class="line"><span> <</span><span>div</span></span>
<span class="line"><span> </span><span>wire:poll.keep-alive</span><span>=</span><span>"pollForChatId()"</span></span>
<span class="line"><span> </span><span>class</span><span>=</span><span>"p-4 bg-orange-200 border border-orange-300 rounded"</span></span>
<span class="line"><span> ></span></span>
<span class="line"><span> Perform these steps in the Telegram group where you want to receive notifications:</span></span>
<span class="line"></span>
<span class="line"><span> <</span><span>ul</span><span> </span><span>class</span><span>=</span><span>"mt-2"</span><span>></span></span>
<span class="line"><span> <</span><span>li</span><span>>1. Add the Telegram bot "OhDearApp"</</span><span>li</span><span>></span></span>
<span class="line"><span> <</span><span>li</span><span>></span></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>class</span><span>=</span><span>"max-w-8"</span><span>></span></span>
<span class="line"><span> 2. Copy and paste this command</span></span>
<span class="line"></span>
<span class="line"><span> <</span><span>x-jet-input</span><span> </span><span>type</span><span>=</span><span>"text"</span><span> </span><span>readonly</span><span> </span><span>:value</span><span>=</span><span>"' /start@OhDearAppBot ' . $token"</span></span>
<span class="line"><span> </span><span>class</span><span>=</span><span>"mt-4 bg-orange-100 px-4 py-2 rounded font-mono text-sm border-orange-200 text-orange-900 w-full"</span></span>
<span class="line"><span> </span><span>autofocus</span><span> </span><span>autocomplete</span><span>=</span><span>"off"</span><span> </span><span>autocorrect</span><span>=</span><span>"off"</span><span> </span><span>autocapitalize</span><span>=</span><span>"off"</span></span>
<span class="line"><span> </span><span>spellcheck</span><span>=</span><span>"false"</span><span>/></span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"><span> </</span><span>li</span><span>></span></span>
<span class="line"><span> </</span><span>ul</span><span>></span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"><span> </span><span>@else</span></span>
<span class="line"><span> </span><span>@if</span><span>($isNewChatId)</span></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>class</span><span>=</span><span>"p-8 bg-green-200 border border-green-300 rounded text-green-900"</span><span>></span></span>
<span class="line"><span> You've connected Oh Dear to Telegram. You should now save this form.</span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"><span> </span><span>@else</span></span>
<span class="line"><span> <</span><span>div</span><span> </span><span>class</span><span>=</span><span>"p-8 bg-blue-200 border border-blue-300 rounded text-blue-900"</span><span>></span></span>
<span class="line"><span> Telegram has been connected.</span></span>
<span class="line"><span> </</span><span>div</span><span>></span></span>
<span class="line"><span> </span><span>@endif</span></span>
<span class="line"><span> <</span><span>input</span></span>
<span class="line"><span> </span><span>type</span><span>=</span><span>"hidden"</span></span>
<span class="line"><span> </span><span>name</span><span>=</span><span>"notification_destinations[</span><span>{{</span><span> $index </span><span>}}</span><span>][destination][chat_id]"</span></span>
<span class="line"><span> </span><span>value</span><span>=</span><span>"</span><span>{{</span><span> $chatId </span><span>}}</span><span>"</span><span>></span></span>
<span class="line"><span> </span><span>@endif</span></span>
<span class="line"><span></</span><span>div</span><span>></span></span>
<span class="line"></span>
Livewire has built-in support for polling. Polling can be started by adding the wire:poll
directive to an HTML element.
<span class="line"><span>wire:poll.keep-alive="pollForChatId()</span></span>
<span class="line"></span>
Our component will execute the pollForChatId()
function every two seconds (that’s Livewire’s default polling frequency).
By default, Livewire will stop polling whenever the browser or tab where a component is displayed is not active. In our case, the user will probably have switched to Telegram, but we still want to have the confirmation message rendered as soon as possible. That’s why we added the keep-alive
option, which makes sure that we keep polling, even if the browser is not active.
Let’s take a look at that pollForChatId()
function on the TelegramChatIdComponent
.
<span class="line"><span> </span><span>public</span><span> </span><span>function</span><span> </span><span>pollForChatId</span><span>()</span></span>
<span class="line"><span>{</span></span>
<span class="line"><span> $cacheKey </span><span>=</span><span> </span><span>$this</span><span>-></span><span>getInviteToken</span><span>()</span><span>-></span><span>cacheKey</span><span>();</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>if</span><span> ($chatId </span><span>=</span><span> </span><span>cache</span><span>()</span><span>-></span><span>get</span><span>($cacheKey)) {</span></span>
<span class="line"><span> </span><span>$this</span><span>-></span><span>isNewChatId </span><span>=</span><span> </span><span>true</span><span>;</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>$this</span><span>-></span><span>chatId </span><span>=</span><span> $chatId;</span></span>
<span class="line"></span>
<span class="line"><span> </span><span>cache</span><span>()</span><span>-></span><span>forget</span><span>($cacheKey);</span></span>
<span class="line"><span> }</span></span>
<span class="line"><span>}</span></span>
<span class="line"></span>
You can see that when we find a chatId
in the $cacheKey
of the cache, we set that value to $this->chatId
. Because we change something to the public properties of the component, the Livewire component will re-render.
In the Blade view, you might have noticed that we only render that div containing the wire:poll
directive whenever chatId
isn’t present.
<span class="line"><span>@if</span><span>(</span><span>!</span><span> $chatId)</span></span>
<span class="line"><span> <</span><span>div</span></span>
<span class="line"><span> </span><span>wire:poll.keep-alive</span><span>=</span><span>"pollForChatId()"</span></span>
<span class="line"><span> </span><span>class</span><span>=</span><span>"p-4 bg-orange-200 border border-orange-300 rounded"</span></span>
<span class="line"><span> ></span></span>
<span class="line"><span> </span><span>{{-- other stuff omitted for brevity --}}</span></span>
<span class="line"><span>@endif</span></span>
<span class="line"></span>
So with $chatId
now set to a value, that div won’t be rendered anymore. And because the div isn’t rendered anymore, Livewire will stop polling. That’s a pretty easy way to stop polling. To me, this is one of the many great details that Livewire creator Caleb Porzio added to Livewire. You don’t have to think about it. Livewire does the right thing.
In closing
I hope you’ve enjoyed reading both approaches on how to start and stop polling. If you want to see this in action for yourself, create a free account on either Flare or Oh Dear. Both services offer a free 10-day trial.
Flare is the best exception tracker service for Laravel apps. Whenever an exception occurs in your production environment, we can send you a notification so that the exception doesn’t go unnoticed.
Oh Dear is an all-in-one monitoring solution for your apps. It can notify you when your site is down, when your certificate is not ok, and whenever any of your app’s pages contain broken links / mixed content. It can also monitor your scheduled jobs, and public status page, (like this one from Laravel), can be created.
Both services can notify you via Telegram and via mail, SMS, Slack, Webhooks, and a couple more services.
Did you notice that the React, PHP, JS and Blade code snippets in this post were perfectly highlighted? This is done using our free spatie/laravel-markdown package, which uses Shiki under the hood. Be sure to also take a look at our many other open source packages. Pretty sure there’s something there for your next project.