How to: Telegram bot with python

How to: Telegram bot with python

Create a Telegram bot using python, Tornado, and deployed in Render

·

12 min read

Overview

In this article, we are going to guide you through creating a Bot for Telegram in Python using Tornado and deploying it to Render.

Requirements

You will need to have created and working accounts for the following services:

  1. Have a Telegram account

  2. Have a Github account

  3. Have a Render account

Once you have accounts for each one we are going to create our Telegram bot, then create our Tornado backend, and finally deploy to Render.

Telegram

This is the first step of the process, where we create our bot. We are going to search for the BotFather. Once we find it, and we create a chat with it, we are going to type /start. This will show us a prompt of what commands we have available. You will notice the /newbot command for creating... new bots!

Once you call the /newbot command, it will prompt you for a name to assign to the new bot. In this case give it a unique name you want to use to describe your bot (e.g. tele tubbier, awsem-o, ...). Once the name is accepted for being available, it will then ask you to choose a username that will end with bot. Again, I would use something similar to the name (e.g. tele_tubbier_bot, awsem-o_bot, ...). Once again, if the username is available, it will generate a token to access the HTTP API (crossed out in red) and you need to save the token.

It is a good practice to put the token in your environment variables, in Windows click on the start menu, and search for "Edit environment variables for your account". Then click on the button"New..." under the User variables for XXXX. In the image below I show you the name I use on the environment variable, and the value for it should be the access token we just copied.

This will be used again when we deploy to Render. At this point, we should be all set with the Telegram bot API, next we move on to work on our Tornado backend. We are going to follow the same steps to create TELE_BOT_NAME, where the variable value will be the name of the bot, and APP_URL_LINK we will assign the URL for the webhook. We will set this value when we are ready for deployment in Render.

Tornado

We are going to implement the Python code using the Tornado asynchronous framework. Why? Because this open-source asynchronous framework developed by FriendFeed and acquired by Facebook scales incredibly well! Next, we will need an API for Telegram, there are several Telegram libraries available for Python. However, we will be using the pyTelegramBotAPI for its ease of use. Now that we know what we will be using, we will set our requirements, set up the local virtual environment, create our Tornado 6.3 application, create our webhook into the Telegram Bot API, and finally Test our work locally.

Requirements

We are going to add the following libraries: tornado, requests, and pyTelergramBotAPI to a requirements.txt file as shown below:

tornado>=6.2
requests
pyTelegramBotAPI

Setting up your local environment

In Windows we will create the following local environment for your project:

PS C:\dev\medium\tornado-tele-bot> python -m venv .venv
PS C:\dev\medium\tornado-tele-bot> .\.venv\Scripts\activate
(.venv) PS C:\dev\medium\tornado-tele-bot> pip install -r .\requirements.txt

TIP: For Linux or MacOS please refer to my previous article on deploying to Render.

Tornado v6.3

With our virtual environment setup, we are ready to create our Tornado backend. We are going to use and update to version 6.3 of the Python Tornado asynchronous framework. If you have never used Tornado before, know that is an easy-to-scale solution developed by FriendFeed and later acquired by Facebook.

We are now going to create a file and name it "app.py" where we will create our Tornado application. We will create a "static", and a "templates" folder in the directory. Then, we will also create a README.md and a mastermind.py file. Our folder structure should look as in the image below:

Then we will import the following libraries and setup global settings:

import os
import signal
import asyncio
from typing import Optional, Awaitable

import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web

WEBHOOK_SECRET = "setwebhook"
WEBHOOK_PORT = 8000

tornado.options.define("port", default=WEBHOOK_PORT, help="run on the given port", type=int)

We will use the 8000 port as our default. Also, we will set the name for the route that we want to use for creating the webhook for our Telegram API. This route we want to obfuscate, hence the name WEBHOOK_SECRET.

Now we are going to create our RequestHandlers, which is how Tornado maps its routes and executes POST/DELETE/PUT/GET Rest calls. We are creating a custom BaseHandler to inherit in our Handlers. We will create three handlers:

  1. ErrorHandler - handles our basic error messages when trying to access a non-valid route.

     class ErrorHandler(tornado.web.ErrorHandler, BaseHandler):
         """
         Default handler gonna to be used in case of 404 error
         """
         pass
    
  2. MainHandler - handles the root address, and shows that the server is running.

     class MainHandler(BaseHandler):
         def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]:
             pass
    
         def get(self):
             self.write("Tornado v6.3 app is running!")
             self.finish()
    
  3. WebhookSrv - handles the telegram messages through a POST that contains the JSON data in the body of the request.

     class WebhookServ(BaseHandler):
         def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]:
             pass
    
         def get(self):
             self.write("Webhook server handler")
             self.finish()
    
         def post(self):
             if "Content-Length" in self.request.headers and \
                 "Content-Type" in self.request.headers and \
                 self.request.headers['Content-Type'] == "application/json":
                 json_data = self.request.body.decode("utf-8")
                 self.write(json_data)
                 self.finish()
             else:
                 self.write("What are you doing here?")
                 self.finish()
    

Finally, we are going to create our Tornado application:

def make_app():
    settings = dict(
        cookie_secret=str(os.urandom(45)),
        template_path=os.path.join(os.path.dirname(__file__), "templates"),
        static_path=os.path.join(os.path.dirname(__file__), "static"),
        default_handler_class=ErrorHandler,
        default_handler_args=dict(status_code=404)
    )
    return tornado.web.Application([
            (r"/", MainHandler),
            (r"/" + WEBHOOK_SECRET, WebhookServ)
        ], **settings)

async def main():
    print("starting tornado server..........")
    app = make_app()
    app.listen(tornado.options.options.port)
    await asyncio.Event().wait()

if __name__ == '__main__':
    asyncio.run(main())

Lastly, our matermind.py file will contain the simplest logic shown in the code below. This is meant to be a stub for adding logic, I will develop this further with a behavior tree using the py_trees library.

def get_response(msg):
    """
    you can place your mastermind AI here
    could be a very basic simple response like "this is my response"
    or a complex LSTM network that generate appropriate answer
    """
    return "this is my response"

The full source code is below:

Running the above code will show the Tornado server up and running.

This concludes the setup for our Tornado application setup, next we are going to tie it with the webhook to interact with the Telegram API.

Webhook

What is a Telegram Webhook? Is a way for "different applications and platforms to communicate and share data through messages". Why do we want a webhook? It makes it so that our bot does not need to poll, by that we mean it will not query the Telegram API for a message, but that the Telegram will push messages to our specified URL through the webhook.

NOTE: The alternative to the webhook is to get updates using the Telegram API, this is a pull method to get the data (the bot would need to poll), while the webhook solution is pushing data.

Now that we know what a webhook is, we are going to use it and update our base Tornado v6.3 backend application. We are going to import the telebot library, then we are going to get our TELE_BOT, TELE_BOT_NAME, and our APP_URL_LINK environment variables by using the os import. We are also going to set our secret route variable for the webhook, the port variable, and the webhook URL variable. Finally, we are going to create our TeleBot with the API_TOKEN as an initialization parameter and assign it to the bot variable. The code below shows what we are adding on line 11 of the previous gist.

import telebot

API_TOKEN = os.environ['TELE_BOT']
BOT_NAME = os.environ['TELE_BOT_NAME']
WEBHOOK_HOST = os.environ['APP_URL_LINK']
WEBHOOK_SECRET = "setwebhook"
WEBHOOK_PORT = 8000
WEBHOOK_URL_BASE = "{0}/{1}".format(WEBHOOK_HOST, WEBHOOK_SECRET)

bot = telebot.TeleBot(API_TOKEN)

Next, we are going to update our WebbookServ class by adding the processing of the messages by the bot on the POST request. The code below shows the updated POST method:

    def post(self):
        if "Content-Length" in self.request.headers and \
            "Content-Type" in self.request.headers and \
            self.request.headers['Content-Type'] == "application/json":
            json_data = self.request.body.decode("utf-8")
            update = telebot.types.Update.de_json(json_data)
            bot.process_new_updates([update])
            self.write("")
            self.finish()
        else:
            self.write("What are you doing here?")
            self.finish()

Now we need to update the make_app() function, as we need to set up the webhook for the Telegram bot we created. First, we need to remove the webhook, this makes sure there is only one webhook assigned to the bot. Finally, we will use the previously declared WEBHOOK_URL_BASE variable, and pass it as the url parameter to the set_webhook method as shown below.

def make_app():
    bot.remove_webhook()
    bot.set_webhook(url=WEBHOOK_URL_BASE)
    settings = dict(
        cookie_secret=str(os.urandom(45)),
        template_path=os.path.join(os.path.dirname(__file__), "templates"),
        static_path=os.path.join(os.path.dirname(__file__), "static"),
        default_handler_class=ErrorHandler,
        default_handler_args=dict(status_code=404)
    )
    return tornado.web.Application([
            (r"/", MainHandler),
            (r"/" + WEBHOOK_SECRET, WebhookServ)
        ], **settings)

Lastly, we are going to add processing for three commands, and processing for non-commands when the message is longer than two words. Using the annotation @bot.message_handler we are going to assign the commands parameter to an array of strings. Each string contains a command, and one function can represent more than one command if needed. My example below shows how to map the help and start commands to the send_welcome function. How this works is the user will type /start and this will fire off the send_welcome function and return a message to the user. Next, we will add a greet function for the /greet command as well. Lastly, we are going to add the echo_msg function, this one will instead of the "commands" parameter we are going to use the "func" parameter to map it to another function that validates the input and returns True or False. If it validates it will return True, then the message is processed by the echo_msg function. The function validates to True by splitting the message into words, and checking for the length of the number of words to be more than one. The code below shows the code added after the WebhookServ class.

# Handle '/start' and '/help'
@bot.message_handler(commands=['help', 'start'])
def send_welcome(message):
    bot.reply_to(message,
                 ("Hi there, I am EchoBot.\n"
                  "I am here to echo your kind words back to you."))

@bot.message_handler(commands=['greet'])
def greet(message):
    bot.send_message(message.chat.id, "Hey how's it going?")

def msg_definition(message):
    request = message.text.split()
    if len(request) < 2: 
        return False
    else:
        return True

@bot.message_handler(func=msg_definition)
def echo_msg(message):
    print(f"message: {message}")
    bot.send_message(message.chat.id, f"message: {message.text}")

EXTRA: you can add better logic here by calling the mastermind.get_response(msg) function and return the processed result in the message. This way we can keep our AI outside the Tornado application code.

The full gist of the updated code for our app.py is shown below.

There you have it, a full working solution for a basic Telegram bot in python using the Tornado library. We are going to test locally next, then deploy, and finally test the live bot.

Testing

We are going to navigate to our application with our virtual environment working, and run python .\app.py from the terminal (in my case the Windows PowerShell) and it will show no errors, and a message letting me know the server is starting as shown below.

Now that the server is running, we will open up the localhost:8000 address in our browser and it should show the following:

That is all for making sure our application runs locally without any errors.

Render

After you have set up your Render account, please follow the sets from this previous article https://swiftuser.hashnode.dev/heroku-removed-their-free-tier-render-rises. Once you have your render account set up, we want to create our project. In my case, I created a Web Service and named it ttb. The URL provided for my service was ttb.onrender.com (this value is what goes on the APP_URL_LINK environment variable later on). We are also going to connect to the Github repo you are using to host your code (you can also host your code in Gitlab). Please follow the previous article on Render.

NOTE: Render, like Heroku and Azure, the free tiers do have a warm-up/start-up time. What this means, is that when an application first queries a sleeping app service it will take some time to return the results. Once the service has "awoken" it will reply right away. To avoid this, it is good to move up to a paid plan when a Beta product has been established with users.

Environment Variables

Now we are going to navigate to the environment variables, and we will use the environment variables TELE_BOT, TELE_BOT_NAME, and APP_URL_LINK. The values for these environment variables will be the TOKEN for the bot's API, the name of the bot, and the URL on the hosted web service (in this case it ill be ttb.onrender.com).

We also want to make sure that under the Settings section, we have Build Command and Start Command as shown in the image.

Once these steps are all set up you should be ready to go deploy.

Deploying

Now we are ready to deploy! In this step, we need to go into the Logs tab. That way we can see if anything goes wrong. Then we want to go to Manual Deploy \> Deploy latest commit from the top right dropdown. This will copy your code from your repo, in my case the Github an01f01/tornado-tele-bot repo; then execute the pip install -r requirements.txt to get all the required libraries; and finally, it will execute the python app.py to run the server. The image below shows a running log window from Render.

TIP: If you can deploy and run your Tornado python application to Render, you can use the same repository to deploy to an App Service in Azure! Just use a Python 3 version that is compatible, and make sure the startup script points to "python app.py" like we just did in Render.

Live Testing

After deployment, the last thing we need to do is live test! So we are going to search for Render Tele Bot in my case (in your case it should be the name of your bot, you can search for mine as well), and it should show up in the search as follows:

Click on the bot, and test the following commands: /start, /help, /greet, a one word sample (e.g. Test), and a two+ words/sentence (e.g. Test Two). The replies should be as in the following image:

If you get the same as I do, then you should be all set with your first Telegram bot in Tornado hosted by Render.

NOTE: you can notice that the validation is working since we provide a one-word message, which fails validation and nothing is returned; a two+ words test returns the "message: two+ words". Why is this important? Because we can expand it to build a dialog tree or do some interesting processing in a behavior tree and treat the bot as an NPC of a game for your purpose/company/product.

Conclusion

Congrats on making it to the end, if you have stayed until the end, you should have a working Telegram bot, written in Python using the Tornado asynchronous framework, and deployed in Render! This was not short but has gotten you 80% of the way there to make the bot you want to make. I provided reference links below, some of them extend the functionality in python (not within Tornado) but similar enough to allow you to consume/provide audio, video, emojis, and images.

The full source code for the Telegram Bot working with Tornado and deployable to Render can be found on this repo https://github.com/an01f01/tornado-tele-bot

References

Did you find this article valuable?

Support Alessandro by becoming a sponsor. Any amount is appreciated!