Some time ago I wrote an article about creating a Telegram bot, and there I promised to update it with a webhook method description, but never did. Now I finally got time to do that.

Telegram bot webhook

Эта статья на русском 🇷🇺.

What is it

As the documentation says, a bot can communicate with Telegram servers in two ways:

  1. getUpdates - pull: your bot constantly queries Telegram server for new messages;
  2. setWebhook - push: as soon as there are new messages, Telegram server sends those to your bot.

The difference can be illustrated as the following:

Telegram bot, getUpdates vs setWebhook

It is quite obvious that setWebhook method is a more rational one for every party. However, it has an implicit complexity: something has to accept messages from Telegram on bot’s side, so a web-server or its equivalent is required.

How to set it up

What you need to do:

  1. Get a domain name for your server and get a certificate for it (for example, from Let’s Encrypt). Documentation also states that self-signed certificate for a bare IP address will do as well, but I haven’t tried that;
  2. Implement the server part on the bot’s side (where Telegram will send messages to);
  3. Register your server part address at Telegram (set the webhook on your endpoint), so Telegram would know where to send messages to.

Certificate

Domain name and certificate were easy. I already had a domain, and certificate can be obtained following this instruction.

The option with self-signed certificate for direct IP address I suggest you to study yourself.

Server part

Server part was a bit more difficult. I modified my current pyTelegramBotAPI bot implementation using an example for AIOHTTP.

Install the required packages:

pip install pyTelegramBotAPI
pip install aiohttp
pip install cchardet
pip install aiodns

And here’s a short version of modified bot implementation:

import config
import telebot
from aiohttp import web
import ssl

WEBHOOK_LISTEN = "0.0.0.0"
WEBHOOK_PORT = 8443

WEBHOOK_SSL_CERT = "/etc/letsencrypt/live/YOUR.DOMAIN/fullchain.pem"
WEBHOOK_SSL_PRIV = "/etc/letsencrypt/live/YOUR.DOMAIN/privkey.pem"

API_TOKEN = config.token
bot = telebot.TeleBot(API_TOKEN)

app = web.Application()

# process only requests with correct bot token
async def handle(request):
    if request.match_info.get("token") == bot.token:
        request_body_dict = await request.json()
        update = telebot.types.Update.de_json(request_body_dict)
        bot.process_new_updates([update])
        return web.Response()
    else:
        return web.Response(status=403)

app.router.add_post("/{token}/", handle)

help_string = []
help_string.append("*Some bot* - just a bot.\n\n")
help_string.append("/start - greetings\n")
help_string.append("/help - shows this help")

# - - - messages

@bot.message_handler(commands=["start"])
def send_welcome(message):
    bot.send_message(message.chat.id, "Ololo, I am a bot")

@bot.message_handler(commands=["help"])
def send_help(message):
    bot.send_message(message.chat.id, "".join(help_string), parse_mode="Markdown")

# - - -

context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.load_cert_chain(WEBHOOK_SSL_CERT, WEBHOOK_SSL_PRIV)

# start aiohttp server (our bot)
web.run_app(
    app,
    host=WEBHOOK_LISTEN,
    port=WEBHOOK_PORT,
    ssl_context=context,
)

What’s going on here: we launch a mini-web-server, which listens to 8443 port and handles requests via specified endpoint comprised from the bot token. The latter (token) is used here as a unique enough identification so random dude from internet wouldn’t mess with our bot. So, the full endpoint address will look like this: https://YOUR.DOMAIN:8443/YOUR-TOKEN/.

Note also the differences between my snippet and the default code from repository:

  • fullchain.pem file is used instead of cert.pem;
  • the code for removing and setting the webhook is deleted.

Since I’m not executing the bot script under root user, my service started to fail with the following error:

python-bot[1824]: Traceback (most recent call last):
python-bot[1824]:   File "/usr/local/bin/bot/bot.py", line 142, in <module>
python-bot[1824]:     context.load_cert_chain(WEBHOOK_SSL_CERT, WEBHOOK_SSL_PRIV)
python-bot[1824]: PermissionError: [Errno 13] Permission denied
systemd[1]: telegram-bot.service: Main process exited, code=exited, status=1/FAILURE
systemd[1]: telegram-bot.service: Failed with result 'exit-code'.

Turns out this other user doesn’t have access to /etc/letsencrypt/, so he cannot open the certificate file. I tried to grand access to this folder to a new group, having included this user to its members:

groupadd letsencrypt
usermod -a -G letsencrypt userforbot
chgrp -R letsencrypt /etc/letsencrypt/

But he couldn’t open these files anyway, even simple ls was giving permission denied. So, either my Linux skills suck dick, or. In the end I simply set this user as an owner of the folder:

chown -R userforbot:letsencrypt /etc/letsencrypt/

After that there were no problems with the access and service started normally.

Registration

Now the most difficult part - register the bot endpoint at Telegram. It was difficult for me as I misunderstood the principle of forming the endpoint address, and also there were some issues with checking the certificate.

In order to register/set the webhook you need to send the following HTTP request (you can just open this URL in web-browser):

https://api.telegram.org/botYOUR-TOKEN/setWebhook?url=https://YOUR.DOMAIN:8443/YOUR-TOKEN/

While I was experimenting and studying the endpoint format, Telegram was sending me normal result:

{
    "description": "Webhook was set",
    "ok": true,
    "result": true
}

But then apparently it got sick of me, because I started to get the following result:

{
    "ok": false,
    "error_code": 504,
    "description": "Gateway Timeout"
}

Nevertheless, it turned out that such a result is not a problem as webhook was successfully set anyway, so there is no even need to wait for timeout, you can just cancel request after a couple of seconds.

To check the webhook status, send this request:

https://api.telegram.org/botYOUR-TOKEN/getWebhookInfo

If everything is fine, you should get this:

{
    "ok": true,
    "result": {
        "url": "https://YOUR.DOMAIN:8443/YOUR-TOKEN/",
        "has_custom_certificate": false,
        "pending_update_count": 0,
        "max_connections": 40
    }
}

As you can see, url field contains our endpoint.

However, right now I get this:

{
    "ok": true,
    "result": {
        "url": "https://YOUR.DOMAIN:8443/YOUR-TOKEN/",
        "has_custom_certificate": false,
        "pending_update_count": 0,
        "last_error_date": 1543762687,
        "last_error_message": "SSL error {error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed}",
        "max_connections": 40
    }
}

Which points to some problems with certificate. Despite this, bot works fine, so this error has no actual consequences. Although, if I replace fullchain.pem with cert.pem (like it was by default), then bot will stop working.

It is also worth to mention that if you set the webhook, then getUpdates method will stop working. To remove the webhook you need to send the same request you used for setting it, but this time send it without url parameter:

https://api.telegram.org/botYOUR-TOKEN/setWebhook

You should get the following answer:

{
    "ok": true,
    "result": true,
    "description": "Webhook was deleted"
}

This is it, not too complicated after all. If only official documentation (and various manuals from internet) would state such simple thing as that webhook is about having a web-server on the bot’s side, I would implement it ages ago. Certainly, such thing might seem obvious to experienced developers, but it wasn’t to me.