Kais DevBlog

More or less structred thoughts

Integrating a Telegram Bot with Structr (Part 2 - WebHooks)
Posted in Structr by Kai on Apr 09, 2021

In part 1 of this article, we got the basic Telegram integration going. As it often goes, in the initial version the easiest approach was used and now that we know a bit more, we want to implement a better approach. We would like to use the WebHooks feature of the Telegram API. This reduces load on our server and we also don't spam their servers with requests. Luckily with Structr and our previous code this is pretty easy. We are basically 90% there. In this article we will enable WebHooks in our project and figure out the remaining 10%.


Motivation

The getUpdates method (from the previous post) of getting updates from Telegram is pull (well, it's in the command name already: "getUpdates"). With WebHooks we don't need to request updates, Telegram pushes them to us. This removes the need for the scheduled job we created in the previous post and also makes one request per update so that we have to write less code. It should also speed up our response times. Previously we let the job run every 10 seconds which would theoretically result in an average delay of 5 seconds. Using WebHooks the updates are pushed to us and we can respond directly.

What we need

In principle, all we need is a internet-reachable address and a method to process the updates that Telegram will send to us. With the URL of that method we can setup the WebHook integration in the Telegram API. This is done by sending the command setWebhook to the Telegram API. The API documentation for setWebhook shows that it can't be that hard. Only the HTTPS URL is required. Everything else is optional. But we need to acknowledge that SSL is required and that the only allowed ports are 443, 80, 88, 8443. In that regard the getUpdates command was a bit easier to start with.

Step 1a: Let us start by creating the global schema method so we can figure out the URL to enable WebHooks. We'll call it receiveBotWebhookMessage. This is where Telegram will send the updates. The code is basically identical to the botGetUpdates method we created in the previous part. The only difference is that we only need to handle one messages per call. To make it a bit more interesting I added very rudimentary support for handling two bot commands (/start and /help). Every other message gets a generic reply.

I am aware that bot commands have to have the bot_command type but decided to omit this for brevity.

{
  let message = $.retrieve('message');

  if ($.empty(message)) {
   return; } let sendMessage = (userId, message) => { $.call('callBotMethod', { method: 'sendMessage', data: { chat_id: userId, text: message } }); }; let userId = message.from.id; let text = message.text; if (message.entities && message.entities.length > 0) { let command = text.slice(message.entities[0].offset, message.entities[0].length); switch (command) { case '/start': { sendMessage(userId, 'Hi, how can I help you? To begin you should go to https://kai.news'); break; } case '/help': { sendMessage(userId, 'For help, please visit https://kai.news'); break; } default: { sendMessage(userId, 'Unknown command! For more information go to https://kai.news'); break; } } } else { sendMessage(userId, 'Your message does not contain a command. Available commands are /start and /help'); } }

Step 1b: Telegram does not have a user account in our application. This has some implications:

  1. The method itself needs to be visible to public users - otherwise it would still not be callable from an anonymous context. In the current version of Structr (4.0-SNAPSHOT) we can simply tick a box while editing the method. In older version we would have to do this via the "Data" section and browse the type SchemaMethod for the correct one and set the visibility flag.
  2. The method needs to be callable by an unauthenticated user. For that purpose we need to create a Resource Access Grant which enables public users to make POST requests to our method. The signature of the grant is ReceiveBotWebhookMessage (notice the captial R)
  3. We need to keep in mind that we are in an anonymous context. In this context we probably do not have rights to write any data, or even view it. If we want to do anything in the database in reaction to a message, we need to do it in a privileged context.
  4. This raises a security concern. Anyone and everyone who knows the name of the method could now call it. This is actually what we want so Telegram can send us messages without having credentials to our application. We simply need to be aware of this and not write code which opens security holes. We can make this a bit more secure by appending a secret token as a GET parameter to the configuration url.
    ?secret_param=secret_token_value
    In the global schema method we could now test for this parameter and ignore the request if it is not present or different.
    if ($.request.secret_param !== 'secret_token_value') {
       $.log('webhook unauthorized');
       return;
    }


Step 2
: By sending the setWebhook command we can finish the process. We can do this via the callBotMethod function we created in the previous part. The only optional parameters I chose to add are max_connections and allowed_updates to keep the number of connections down - a detailed explanation of the available parameters can be found in the API documentation.

$.call('callBotMethod', {
   'method': 'setWebhook',
   'data': {
      url: 'https://YOUR.DOMAIN/structr/rest/receiveBotWebhookMessage',
      max_connections: 8,
      allowed_updates: ['message']
   }
});

To this request Telegram will send a reply which looks like this. It tells us that everything worked and that the Webhook was set.

{
   body={
      ok=true,
      result=true,
      description=Webhook was set
   },
   status=200
}

Now we can not use the getUpdates command anymore. It is mutually exclusive with WebHooks. If we ever want to go back to requesting updates we can either issue the above command with an empty url field or use the deleteWebhook command. 

That is it, we are finished. It is really quite simple. If we send our bot a message it will reply accordingly. To support actions and commands we only need to add a bit of code and handle everything as we see fit.

Final thoughts

Call me paranoid or call me security-minded. But should you choose to integrate Telegram in your Structr application following this article, you should probably use a different name for the global schema method and also for name (and value) of the secret token parameter. This does not add a substantial layer of security but it is the same story as standard passwords, standard accounts etc.