Kais DevBlog

More or less structred thoughts

Integrating a Telegram Bot with Structr (Part 1)
Posted in Structr by Kai on Feb 16, 2021

Push notifications are (kind of) awesome! But unless you want to go the extra mile and create a native mobile application for you web application, you are often stuck with emails. That is not necessarily a bad thing though, emails are robust, everyone has an email address and it is unintrusive most of the time. But sometimes you really want (or even need push notifications). Using a messenger app with an API is a simple solution - in this post I'll take a quick look at how to integrate a Telegram Bot with Structr.


It is surprisingly easy to create a Telegram Bot and to use it with Structr. A few quick steps and we are done with a (admittedly rudimentary) bot. For this post I will only get into the details on how to use the bot as a direct-message bot.

Step 1: Registering the bot is done via the Botfather... which is a bot itself. After a couple of guided steps we have a bot and an API token. The short but full explanation can be found here: https://core.telegram.org/bots/#6-botfather

Our (example) token: 110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw

Step 2: To have this token readily available we'll add it as a custom key to structr.conf

my.telegram.bot.api.token = 110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw

Step 3: We define a global schema method callBotMethod which is responsible for communication with the bot.

{
    let method = $.retrieve('method');
    let data   = $.retrieve('data');
    
// retrieve token from config
  let token = $.config('my.telegram.bot.api.token');
    let url   = 'https://api.telegram.org/bot' + token + '/';
    
    $.addHeader('Content-Type', 'application/json');
    if ($.empty(data)) {
        return $.POST(url + method, '');
    } else {
        return $.POST(url + method, JSON.stringify(data));
    }
}

We can, for example send messages to any user or chat using the sendMessage bot method like this:

$.call('callBotMethod', {
      method: 'sendMessage',
    data: {
           chat_id: '@username',
          text: 'Hi @username, I am a bot!'
      }
});

In the next step we will simply react to every message sent to our bot.

Step 4: We also create a method botGetUpdates to fetch the messages sent to the bot using the getUpdates method and simply mirror each message (text only) back to the sender. After sending all replies we confirm the messages so that we do not receive them again.

{
    // fetch all updates from the Bot API
    let updates = $.call('callBotMethod', {
        method: 'getUpdates'
    });

    let updatesArray = updates.body.result;
    if (updatesArray.length === 0) {
        return;
    }

    $.log(updatesArray.length, ' new messages');

    // store max seen update id for later
    let maxSeenUpdateId = 0;

    for (let update of updatesArray) {
        let updateId     = update.update_id;
        maxSeenUpdateId  = Math.max(updateId, maxSeenUpdateId);
        let userId       = update.message.from.id;
        let chatId       = update.message.chat.id;
        let userName     = update.message.from.first_name;
        let text         = update.message.text;
        
        let reply = 'Hi ' + userName + '! Your message was: >>' + text + '<<';

        // messages have a max length of 4096
        reply.slice(0, 4096);
        
        // mirror the message back as a direct message
        $.call('callBotMethod', {
            method: 'sendMessage',
            data: {
                chat_id: userId, // send as direct message
                text: reply
            }
        });
    }
    
    // confirm updates so we do not receive them again
    $.call('callBotMethod', {
        method: 'getUpdates',
        data: {
            offset: (maxSeenUpdateId + 1)
        }
    });
}

This naive implementation only supports text messages - no stickers or other media. But it just serves as illustration what can be done.

Step 5: We automate the process using a cron job by configuring it in structr.conf and restarting the CronService or Structr itself. This configuration will run the botGetUpdates method every 10 seconds.

cronservice.tasks = botGetUpdates
botGetUpdates.cronExpression = */10 * * * * *

Result: We now have a very rudimentary bot which parrots every message to the sender. This is probably at the very least one step away of 'awesome', but it is also where we can be creative. We can start small and send messages to ourself when a user registers an account, or send messages during any lifecycle method, or when any event occurs.

Outlook: Long polling of new messages is not my favorite way of getting updates but it is very nice for local development. Luckily the Telegram Bot API supports WebHooks. The moment the project is on a web server we will integrate that. This will happen in part 2 - along with some custom commands which allow us to be even more creative in things we can do with the bot.

 

Further reading: