DEV Community

Cover image for How to build a fully-fledged telegram bot in Python
Edun Rilwan
Edun Rilwan

Posted on

How to build a fully-fledged telegram bot in Python

Introduction

Chatbots are gradually becoming an integral part of our lives. These automated agents allow users to solve problems quickly by engaging them in real-time. I personally refer to them as online-buddies.

One of the simplest ways of building a chatbot is through the Telegram messaging app. Telegram provides an easy way to build chatbots through their API.

In this article, you will understand how telegram bots work and how to create a Telegram bot using Python.

By the end of this article, you would have built a fully-fledged currency exchange telegram bot with Python.

This telegram bot allows users to get the exchange rate between two or more currencies in real time. It will also store registered users’ data in a SQL relational database.

NB: This is not your regular kind of bot. You will build a fully-fledged telegram bot in Python that uses a relational database.

The following are the features of the bot:

  • The bot registers new users and stores their data in a relational database.
  • Only registered users can get the exchange rate between two or more currencies.
  • Users can select a favorite base currency and multiple target currencies they would like to get updates on.
  • The bot sends daily updates on latest exchange rates based on a user's base currency and their target currencies.
  • Users can activate or deactivate the daily updates.

The purpose of this tutorial is to help you learn how to build a fully-fledged Telegram bot in Python. In the process, you will also learn concepts such as OOP, decorators, etc. that will help you write cleaner and reusable code in Python.

Below is a live demo of the bot in action:

A fully fledged currency exchange bot - YouTube

This is a live demo of a database driven telegram bot built with Python.The full code of the bothttps://github.com/Omotunde2005/currency_exchange_bot

favicon youtube.com

Requirements

Below is a list of tools and libraries required for this tutorial:

Python: Python must be installed on your computer. Preferably, python 3.8+. You must also have a basic knowledge of Python.

python-telegram-bot library (PTB): PTB library would be used to build the telegram bot. It is a feature-rich wrapper for the Telegram bot API in Python. To install this library, run the command below:

pip install python-telegram-bot
Enter fullscreen mode Exit fullscreen mode

Python-telegram-bot[job_queue]: job_queue is a package within the PTB library. It is used for setting up cron-jobs such as reminders, etc. in a telegram bot. Run the command below to install it:

pip install python-telegram-bot[job_queue]
Enter fullscreen mode Exit fullscreen mode

sqlalchemy library: sqlalchemy is a python library used for creating a relational database. Run the command below to install this library:

pip install sqlalchemy
Enter fullscreen mode Exit fullscreen mode

Requests library: This library is used for making HTTP requests in python. In this tutorial, it would be used to send HTTP requests to the currency exchange API. Run the command below to install it:

pip install requests
Enter fullscreen mode Exit fullscreen mode

A telegram bot: This can be created on the telegram app. It lets you create and customize the theme and design of your chatbot. Once created, you would be given an api-token that would let you authenticate with the API.

Abstract API api-key: Abstract API offers a suite of 12+ REST APIs. The currency exchange API is one of these APIs. You need an api-key to be able to use the API. You can only get one when you sign up with Abstract API.

Create a Telegram bot

As part of the requirements for this tutorial, the following is a step-by-step process to create and design a Telegram bot:

  1. Open the Telegram app.
  2. Enter the name BotFather in the search bar.
  3. Click on the BotFather profile like the one in the image below:

An Image of Telegram BotFather's profile

  1. Click the start button in the new chat that opens.

A screenshot of the start command in a Telegram bot

  1. Follow the directions of the BotFather to create and design your Telegram bot.

A screenshot of steps for editing a Telegram bot

BotFather is the father of all Telegram bots. It was created by Telegram to help developers create chatbots on the platform.

Create an Abstract API account

Abstract API offers a single api-key for registered users to authenticate with any of their APIs. Below is a step-by-step process to create an Abstract API account:

  1. Proceed to their sign-up page.
  2. Enter your details and confirm you’re not a robot.

Abstract API sign-up page

  1. Click the Continue button to complete your registration.

A confirmation link will be sent to your mail. This link will confirm your registration and redirect you to their login page.

Once you login, you will have access to your dashboard.

Get your Abstract API key

Image description on how to get Abstract API key

Follow the steps below to get your api-key:

  1. Hover your cursor around the left side of your dashboard.
  2. Select Exchange Rates under APIs to lookup.
  3. Click Try it out on the new page.
  4. Locate the Primary key label. You will find your api-key there.

Build the Telegram bot

There are various libraries used for building a Telegram bot in Python. One of which is the python-telegram-bot (PTB) library.

Its use of asynchronous programming enables your bot to handle multiple requests at a time without blocking the other.

Also, it has a bunch of other features for building a fully functional Telegram bot. You will explore most of these features in this tutorial.

Open your code editor. Ensure you have created a project and a virtual environment that has all the required libraries installed in it.

Initial application setup

The first step is to configure the bot settings. Below is the code for the application setup:

from telegram.ext import ApplicationBuilder


# To ensure errors are properly logged
logging.basicConfig(
   format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
   level=logging.INFO
)

# The "main block"
if __name__ == "__main__":

   TOKEN = 'your-api-token'

   # An application instance
   application = ApplicationBuilder().token(TOKEN).build()

Enter fullscreen mode Exit fullscreen mode

In the code above, the logginglibrary is used to ensure that errors from the bot are logged with correct descriptions so users can easily track and resolve them.

Also, an application instance is created with the ApplicationBuilder() class of the PTB library. It uses the .token() and .build() method to create an application instance that authenticates your bot with Telegram API.

The .token() method accepts a compulsory TOKEN argument, that is, your telegram api-token.

This initial setup is required for every bot (also known as application) built with the PTB library.

Create the start command

The first command available to users in a Telegram bot is the /start command. Commands are instructions given by users to the bot.

Therefore, you need to create a function that responds to this command.

Follow the steps below to create a function for the /start command:

  1. Import the following modules from PTB library
import telegram
from telegram import Update
from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, filters
Enter fullscreen mode Exit fullscreen mode
  1. Write the function for the start command
...
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):

   # Responds to a user
   await context.bot.send_message(
       chat_id=update.effective_chat.id,
       text="Welcome to the currency exchange bot.\n\n"
            "What is your base currency? Enter your answer using the example
below as a guide:\n\n"
            "/baseCurrency\n"
            "your base currency, e.g USD, GBP\n\n"
   )
...
Enter fullscreen mode Exit fullscreen mode

The start() function is an asynchronous function that accepts two arguments namely:

  • update: This argument has a type hint of the Update class in PTB library. It contains updates like new messages from the bot. It also includes data about the sender, such as: name, chat_id, etc.

  • context: This argument has a type hint of the ContextTypes.DEFAULT_TYPE module in PTB. It includes all actions the bot can perform, such as sending messages, audios, replying to messages, etc.

These two arguments are compulsory for every function in the PTB library.

The context.bot.send_message() method is used to send messages to users. It accepts two compulsory arguments which include the following:

  • chat_id: A unique identifier for the user/chat to send the message to.
  • text: The textual content of the message.

The start() function is invoked when a user starts the bot. In the case of the currency exchange bot, it asks a few questions from them to ensure a personalized experience.

This question is added to the .send_message() method in the start() function above.

Register the start command

Handlers in the PTB library provide a way of linking each command to a function. Each function is executed when its linked command is invoked.

For example, when building a Django web app, you need to create url paths and a view function for each path. When a user visits any url, its associated view function is executed.

Add a handler for the start() function below the application instance and start the bot:

...
if __name__ == "__main__":

   TOKEN = 'your_api_token'
   application = ApplicationBuilder().token(TOKEN).build()

   # Creates a command handler
   start_handler = CommandHandler('start', start)

   # Adds the handler to the bot
   application.add_handler(start_handler)

   # start the bot. 
   application.run_polling()
Enter fullscreen mode Exit fullscreen mode

The CommandHandler() class is used to link a command and a function together. It accepts two arguments — the command name and the function.

You register the handler with the application using the .add_handler() method.

The application.run_polling() method keeps the bot running and constantly polls the Telegram bot API for new alerts on your bot. It should be at the bottom end in the main block.

Finally, run the script file to start the bot.

Open your Telegram bot and click on the start button in the chat. The bot will respond with the message that was set earlier in the start() function. This means the bot setup was successful. Below is an example:

Using the start button in a Telegram bot

Learn more about setting up an application and creating functions in this sample tutorial of the PTB documentation.

Create a database connection

A database is required to store users' data. The sqlalchemy library in Python will be used to set up the database.

For this tutorial, you will use a local database file which is suitable for a development environment.

Follow the steps below to set up a database:

  1. Create a new file named database.py
  2. Open the file and add the code below:
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os
# The database url
SQLALCHEMY_DATABASE_URL = "sqlite:///users.db"
# Creates a database engine
engine = create_engine(
   SQLALCHEMY_DATABASE_URL
)

# Handles database sessions
SessionLocal = sessionmaker(bind=engine)

Base = declarative_base()

Enter fullscreen mode Exit fullscreen mode

The following is an analysis of the database.py file:

  • SQLALCHEMY_DATABASE_URL: This refers to the database link or URL. Since you would be using a local database file, the URL would be a local file name. For this tutorial, the database file is titled users.db

  • create_engine(): This is a method in sqlalchemy that is used to create connections with a database. It takes the database URL as an argument.

  • SessionLocal: SessionLocal object is used for creating sessions in SQLAlchemy. It is used to manage transactions and serve as a gateway to the database.

  • Base: This line creates a base class for declarative class definitions. In SQLAlchemy, a declarative base is a base class for declarative class definitions, which are used to define database models.

Create a database model

The next step is to create a User database model. Follow the steps below to create a database model:

  1. Create a new file named models.py
  2. Add the code below to this file:
from sqlalchemy import Boolean, Column, Integer, String
from database import Base

class User(Base):
   __tablename__ = "users"

   id = Column(Integer, primary_key=True, index=True)
   chat_id = Column(Integer, unique=True, nullable=False)
   base_currency = Column(String, unique=False, nullable=False)
   currency_pairs = Column(String, unique=False, nullable=False)
   receive_updates = Column(Boolean, nullable=False, unique=False, default=False)
Enter fullscreen mode Exit fullscreen mode

The _User _model contains the following columns:

  • chat_id: A long set of integers such as, 257945683925 that is unique to each chat or user.

  • base_currency: This column is for a user's favorite base currency such as, USD, CAD, etc.

  • target_currencies: This refers to a list of other currencies the user would like to get daily updates on, relative to their base currency. For example, a user can select USD as their base currency and GBP, CAD, EUR as their target currencies. This means that the user would like to get daily updates on how much 1USD is, when converted to GBP, CAD, and EUR.

  • receive_updates: This is a boolean field that records if the user would like to receive updates or not.

Create the database file

Follow the steps below to create a new database file:

  1. Open the main.py file.
  2. Add the following import statements:
from database import Base, SessionLocal, engine
from models import User
...
Enter fullscreen mode Exit fullscreen mode
  1. Add the code below after the logging.basicConfig() function.
...
Base.metadata.create_all(bind=engine)
db = SessionLocal()
...
Enter fullscreen mode Exit fullscreen mode
  1. Run the script file.

The steps above will create a database file and add a _user _ table to it. An instance of the database session is assigned to the db variable. It is used to perform CRUD (create, read, update, delete) operations with the database.

A new file named users.db would be created in your project folder.

Create a CurrencyExchange class

So far, you have learnt the following:

  • How to create a telegram bot and get an api-token.
  • How to create a command and a corresponding function in PTB.
  • How to register a command with a function through handlers.
  • How to create a database connection.
  • How to add a database model in Python.
  • How to create a relational database file in Python.

The currency exchange API is a REST API with different endpoints. In order to prevent repetition and enhance readability, it is necessary that you bundle all the endpoints into a Python class.

This is a concept in Object-Oriented Programming (OOP). You can check out my complete guide on OOP for beginners.

The class will be named CurrencyExchange.

Follow the steps below to create the CurrencyExchange class:

  1. Create a new file named exchange.py
  2. Add the code below to create the class:
import requests

class CurrencyExchange:

   def __init__(self, api_key):
       self.key = api_key
       self.codes = ["ARS", "AUD", "BCH", "BGN", "BNB", "BRL", "BTC", 
                     "CAD", "CHF", "CNY", "CZK", "DKK", "DOGE", "DZD", 
                     "ETH", "EUR", "GBP", "HKD", "HRK", "HUF", "IDR", 
                     "ILS", "INR", "ISK", "JPY", "KRW", "LTC", "MAD", 
                     "MXN", "MYR", "NOK", "NZD", "PHP", "PLN", "RON", 
                     "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "XRP", 
                     "ZAR", "USD"]

   # Performs one-to-one exchange
   def single_exchange(self, params):
       url = "https://exchange-rates.abstractapi.com/v1/convert"
       params['api_key'] = self.key
       res = requests.request("GET", url, params=params)
       return res.json()

   # Performs one-to-many exchange
   def multiple_exchange(self, params):
       url = "https://exchange-rates.abstractapi.com/v1/live"
       params['api_key'] = self.key
       res = requests.request("GET", url, params=params)
       return res.json()

   # validates a single currency code
   def is_valid_currency(self, currency):
       if currency in self.codes:
           return True
       else:
           return False

   # validates multiple currencies
   def is_valid_currencies(self, currencies_list):

       # uses all() function to loop through currencies list
       if all(currency in self.codes for currency in currencies_list):
           return True
       else:
           return False

Enter fullscreen mode Exit fullscreen mode

The CurrencyExchangeclass has two attributes - an api_key and currency codes which are supported by the bot.

The single_exchange() method is for exchange rate between two currency pairs. It uses the convert endpoint of the currency exchange API.

The multiple_exchange() method is for rates between a single base currency and multiple target currencies. It uses the live endpoint of the currency exchange API.

The last two methods namely is_valid_currencies() and is_valid_currency() will be used to validate user inputs. That is, single and multiple currency codes respectively.

Read the API documentation to learn more about these endpoints.

Add new functions and commands

The next step is to add new commands and their corresponding functions to the bot.

Create function for base currency input

This function receives and records users' base currency. Users have been instructed in the start() function to submit a base currency code through the /baseCurrency command.

Commands in the PTB library accept arguments as user inputs. It is like a URL with query parameters.

Below is the function for the /baseCurrency command:

...

async def record_base_currency(update: Update, context: ContextTypes.DEFAULT_TYPE):
   """ Record Base Currency """

   # user input
   argument = "".join(context.args)
   base_currency = str(argument).upper()

   # validates the user input(currency code)
   if currency_exchange.is_valid_currency(base_currency):
       context.user_data["base_currency"] = base_currency
       await context.bot.send_message(
           chat_id=update.effective_chat.id,
           text="Kindly list your favorite target currencies. That is, a list of currencies you want to get updates on relative to your base currency."
                " You can select as many as you want\n"
                "<b>Each currency should be separated by a comma(,)</b>\n\n"
                "Use the example below as a guide:\n\n"
                "/targetCurrencies\n"
                "USD,CAD,GBP",
           parse_mode=telegram.constants.ParseMode.HTML
           )
   else:
       # alerts user if currency is not supported 
       await context.bot.send_message(
           chat_id=update.effective_chat.id,
           text=f"This bot does not support {base_currency} currency"
       )

...
Enter fullscreen mode Exit fullscreen mode

Arguments in PTB are stored in a Python list called context.args.

In the record_base_currency() function, this list is unpacked and converted to a string. This string, which is the user's base currency, is converted to uppercase and stored in the context dictionary.

A context dictionary is used to temporarily store information within the bot’s memory. It is of two types:

  • The bot context dictionary: It stores information temporarily in memory and it can be accessed from each user's chat. More like a private storage.

  • The user context dictionary: It stores information temporarily in memory, and it can only be accessed in a single user's chat. More like a private storage.

Instead of adding user's input directly to the database, you can easily add it to the context dictionary. This helps in the following ways:

  • It helps to limit frequently queries to the database.

  • In case the user fails to complete registration, you do not have a redundant or incomplete record in the database.

If the user successfully completes his/her registration, you can then add everything to the database.

The bot queries the user for their target currencies. Each user input is verified to ensure that it is supported by the API.

PTB library allows the bot to send HTML messages. Telegram API has a list of supported HTML tags.

When sending HTML messages, it is important that you parse the message by adding the parse_mode argument to the context.bot.send_message() method.

Below is an example of the /baseCurrency command:

Screenshot that shows how to submit a base currency

Create function for target currencies input

In the previous step, the user is prompted to submit their target currencies. The record_target_currencies() function will be used to validate and record the data submitted by the user.

Follow the steps below to create this function:

  1. Open the main.py file

  2. Import the following classes

from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup
Enter fullscreen mode Exit fullscreen mode
  1. Add the new function below:
...
async def record_target_currencies(update: Update, context: ContextTypes.DEFAULT_TYPE):
   # user input
   arguments = "".join(context.args)

   currency_pairs = arguments.split(",")

   # validates the target currencies
   if currency_exchange.is_valid_currencies(currency_pairs):
       context.user_data["target_currencies"] = arguments

       keyboard = [[InlineKeyboardButton('Yes', callback_data='yes')],
                   [InlineKeyboardButton('No', callback_data='no')]]

       await update.message.reply_text(
           text="<b>Would you like to receive daily updates on selected currencies?</b>",
           parse_mode=telegram.constants.ParseMode.HTML,
           reply_markup=InlineKeyboardMarkup(keyboard)
       )

   else:
       await context.bot.send_message(
           chat_id=update.effective_chat.id,
           text=f"Error occurred! Ensure that each currency is supported by the bot."
       )
...
Enter fullscreen mode Exit fullscreen mode

In the code above, the record_target_currency() function adds the user input directly to the context dictionary. The function also validates a user input before adding it to the context dictionary.

If submission is successful, the user is asked if they want to receive daily updates. The InlineKeyboardButton() and InlineKeyboardMarkup() classes are used to create a list of clickable options for a user to select from.

Below is a sample of the /targetCurrencies command and the bot's response:

Screenshot that shows how to submit target currencies

Create function to complete user registration

In the previous function, the user is presented with a list of options to choose from.

The complete_registration() function receives the user's selection. It enters this selection alongside the previous inputs submitted by the user into the database.

This marks the end of the user registration. Below is the code for this function:

async def complete_registration(update: Update, context: ContextTypes.DEFAULT_TYPE):
   """ User completes registration """
   query = update.callback_query.data

   # checks for incomplete registration
   try:
       target_currencies = context.user_data.get("target_currencies")
       base_currency = context.user_data.get("base_currency")
   except KeyError:
       await context.bot.send_message(
           chat_id=update.effective_chat.id,
           text="<b>There's an error with your registration. Kindly restart by submitting your base currency</b>\n"
                "Use the example below as a guide:\n\n"
                "/baseCurrency\n"
                "your base currency, e.g USD, GBP\n\n",
           parse_mode=telegram.constants.ParseMode.HTML
       )
   else:
       if query == "yes":
           receive_updates = True
       else:
           receive_updates = False

       new_user = User(
           chat_id=update.effective_chat.id,
           base_currency=base_currency,
           currency_pairs=target_currencies,
           receive_updates=receive_updates
       )
       db.add(new_user)
       db.commit()

       options = [["Activate Updates 🚀", 'Deactivate updates'], ['Bot Manual 📗']]

       key_markup = ReplyKeyboardMarkup(options, resize_keyboard=True)
       await context.bot.send_message(text="<b>You have successfully completed your registration</b>",
                                      reply_markup=key_markup,
                                      chat_id=update.effective_chat.id,
                                      parse_mode=telegram.constants.ParseMode.HTML)

Enter fullscreen mode Exit fullscreen mode

In the code above, all the previous entries are retrieved and added to the database. The user gets a success message and three buttons are added to the bot. This would allow the user to perform certain tasks quickly. Below is an image of the three buttons:

Screenshot of buttons in a Telebgram bot

In some cases, it is possible the user omitted a step in the registration process.

For example, if a user has a prior knowledge of how this bot works, he/she could decide to start the bot and enter the target_currencies first and skip the base_currency part.

If by the end of the registration, you try to access the value for the base_currency from the context dictionary, it will trigger a KeyError because it doesn't exist. This function handles this scenario.

Create function for messages

Users now have full access to the bot. You need to add a function that handles the user's direct messages to the bot.

For this tutorial, the only direct messages the bot can respond to are those from the 3 buttons. If you click on any of them, a direct message will be sent to the bot.

Below is the function that handles direct messages to the bot:

...
async def direct_messages(update: Update, context: ContextTypes.DEFAULT_TYPE):
   """ Direct messages """
   message = update.message.text
   user = db.query(User).filter_by(chat_id=update.effective_chat.id).first()

   if message == "Bot Manual 📗":
       await context.bot.send_message(
           text="<i>Click <b>Activate Updates</b> to activate daily updates</i>\n\n"
                "<i>Click <b>Deactivate Updates</b> to deactivate updates</i>\n\n"
                "<i>Click <b>Bot Manual</b> to learn how to use the bot</i>\n\n"
                ""
                "<i>To find the exchange rate between a base currency and multiple target currencies, use the command "
                "below:\n\n</i>"
                "/multipleExchange\n"
                "USD/CAD/EUR\n\n"
                "Put your base currency first and other currencies should follow. Separate them with a forward "
                "slash(/)\n\n"
                ""
                "<i>To find the exchange rate between a base currency a single target currency, use the command "
                "below:</i>\n\n"
                "/singleExchange\n"
                "USD/GBP\n\n"
                "Put your base currency first and the target currency should follow. Separate them with a forward "
                "slash(/).\n\n"
                ""
                "<i>To find the exchange rate between a base currency a single target currency with a base amount, "
                "use the command below:</i>\n\n"
                "/exchangeRate\n"
                "USD/CAD @ 50\n\n"
                "Put your base and target currency together and signify the base amount with the @ symbol",
           chat_id=update.effective_chat.id,
           parse_mode=telegram.constants.ParseMode.HTML)

   elif message == "Activate Updates 🚀":
       user.receive_updates = True
       db.commit()
       await context.bot.send_message(
           chat_id=update.effective_chat.id,
           text="<i>You have successfully activated daily exchange rate updates</i>",
           parse_mode=telegram.constants.ParseMode.HTML
       )

   elif message == 'Deactivate updates':
       user.receive_updates = False
       db.commit()
       await context.bot.send_message(
           chat_id=update.effective_chat.id,
           text="<i>You have successfully deactivated daily exchange rate updates</i>",
           parse_mode=telegram.constants.ParseMode.HTML
       )

   else:
       await context.bot.send_message(
           chat_id=update.effective_chat.id,
           text="<i>This bot is not able to respond to your messages for now</i>",
           parse_mode=telegram.constants.ParseMode.HTML
       )
...
Enter fullscreen mode Exit fullscreen mode

In the code above, each button click is handled by the bot. The Bot manual sends a guide on how to use the bot, the Activate updates activate daily updates, and Deactivate updates deactivates daily updates from the bot. You can add as many buttons as you like.

Create function for single exchanges

The function of this bot is to help users get exchange rates in real time. It does this in 3 ways. They include:

  • single exchange - that is, between two currencies. For example: USD-CAD.

  • multiple exchange - that is, between one currency and other multiple currencies. For example: USD to CAD/BTC/AUD, etc.

  • arbitrary exchange - that is, exchange between two currencies while stating a base amount. For example: 50 USD to CAD.

Using the single_exchange() method of the CurrencyExchange class, the function below allows users to find single exchange rates:

...
async def single_exchange_rate(update: Update, context: ContextTypes.DEFAULT_TYPE):
   """ Exchange Currencies """
   currency_pairs = "".join(context.args)

   # Ensures the currency codes are split by a slash(/)
   try:
       base_currency = currency_pairs.split("/")[0].upper()
       target_currency = currency_pairs.split("/")[1].upper()
   except IndexError:
       await context.bot.send_message(
           chat_id=update.effective_chat.id,
           text=f"Error occurred! You must enter two currency codes separated by a forward slash(/)"
       )
   else:
       # validates currency codes
       if currency_exchange.is_valid_currency(base_currency) and currency_exchange.is_valid_currency(target_currency):
           params = {
               "base": base_currency,
               "target": target_currency
           }

           response = currency_exchange.single_exchange(params=params)
           await context.bot.send_message(
               chat_id=update.effective_chat.id,
               text=f"<b>Base Currency:</b> {base_currency}\n"
                    f"<b>Target Currency:</b> {target_currency}\n"
                    f"<b>Exchange Rate</b>: {response['exchange_rate']}\n\n"
                    f"<i>This means that 1 {base_currency} is equal to {response['exchange_rate']} {target_currency}</i>",
               parse_mode=telegram.constants.ParseMode.HTML
           )

       else:
           await context.bot.send_message(
               chat_id=update.effective_chat.id,
               text=f"Error occurred! Ensure that both currencies are supported by the bot."
           )
...
Enter fullscreen mode Exit fullscreen mode

The user is instructed to use the /singleExchange command to make a request and separate the two currency codes by a slash (/). This input is split by the slash and assigned to two different variables.

They are passed as parameters into the single_exchange() method. The response is styled with HTML and sent to the user.

Using error handling, the function ensures that the user input is in the right format and contains currency codes that are supported by the API.

The image below shows how to send a single exchange command:

Screenshot on how to find a one-to-one exchange rate using Telegram bots

Create function for multiple exchanges

This section shows you how to handle multiple or one-to-many exchange rates for users. Users are to separate the currency codes by a comma, with the base currency added first.

Below is the code below for this fucntion:

...
async def multiple_exchange_rate(update: Update, context: ContextTypes.DEFAULT_TYPE):
   """ Exchange Currencies """

   # user input
   argument = "".join(context.args)
   currency_pairs = argument.split("/")


   # ensures currency codes are split 
   try:
       base_currency = currency_pairs[0]
       target_currencies_list = currency_pairs[1:]
   except IndexError:
       await context.bot.send_message(
           chat_id=update.effective_chat.id,
           text=f"Error occurred! Ensure that there are more than two currencies separated by a forward slash(/)."
       )
   else:
       # validates currency codes.
       if currency_exchange.is_valid_currencies(target_currencies_list):
           print(target_currencies_list)
           target_currencies = ",".join(target_currencies_list)

           # API request parameters
           params = {
               "base": base_currency,
               "target": target_currencies
           }
           # API response
           response = currency_exchange.multiple_exchange(params=params)
           result = []
           for currency in target_currencies_list:
               rate = response["exchange_rates"][currency]
               result.append(f"<b>{currency}</b> = {rate}\n")

           await context.bot.send_message(
               chat_id=update.effective_chat.id,
               text="".join(result),
               parse_mode=telegram.constants.ParseMode.HTML
           )

       else:
           await context.bot.send_message(
               chat_id=update.effective_chat.id,
               text=f"Error occurred! Ensure this bot supports the currencies you entered."
           )
...
Enter fullscreen mode Exit fullscreen mode

This function is similar to the single_exchange() function, except that it queries multiple currency codes at a time. They handle exceptions the same way but with different methods of the CurrencyExchange class.

Below is an example of how to find multiple exchange rates:

Screenshot of how to find multiple exchange rates using a Telegram bot

Create function for arbitrary exchanges

This is the final exchange type. It allows users to state a base amount for an exchange.

The function below allows users to perform arbitrary exchange:

...
async def arbitrary_exchange(update: Update, context: ContextTypes.DEFAULT_TYPE):
   """ Exchange Currencies """
   arguments = "".join(context.args)
   split_arguments = arguments.split("@")

   try:
       base_currency = split_arguments[0].split("/")[0]
       target_currency = split_arguments[0].split("/")[1]
       base_amount = float(split_arguments[1])

   except IndexError:
       await context.bot.send_message(
           chat_id=update.effective_chat.id,
           text=f"Please enter values in the correct format"
       )

   except ValueError:
       await context.bot.send_message(
           chat_id=update.effective_chat.id,
           text=f"Please enter a valid number"
       )
   else:
       if currency_exchange.is_valid_currency(base_currency) and currency_exchange.is_valid_currency(target_currency):
           params = {
               "base": base_currency,
               "target": target_currency,
               "base_amount": base_amount
           }

           response = currency_exchange.single_exchange(params=params)
           await context.bot.send_message(
               chat_id=update.effective_chat.id,
               text=f"<b>Base Currency:</b> {base_currency}\n"
                    f"<b>Target Currency:</b> {target_currency}\n"
                    f"<b>Exchange Rate:</b> {response['exchange_rate']}\n\n"
                    f"<i>This means that {response['base_amount']} {base_currency} is equal to "
                    f"{response['converted_amount']} {target_currency}</i>",
               parse_mode=telegram.constants.ParseMode.HTML
           )
       else:
           await context.bot.send_message(
               chat_id=update.effective_chat.id,
               text=f"Error occurred! Ensure this bot supports the currencies you entered."
           )
...
Enter fullscreen mode Exit fullscreen mode

The code above handles two exceptions. The first one is an IndexError which may occur if the user did not enter two currency codes split by a slash (/), and the other which handles a ValueError if the base_amount is not a valid number.

If there are no errors, the currency codes are validated to ensure they are acceptable by the API.

Below is an image that shows how a user can perform an arbitrary exchange:

Image description

Create and register all handlers

Congratulations on getting this far. The next step is to add each function to a handler and test the bot.

The code below shows you how to achieve this:

...
# The "main block"
if __name__ == "__main__":

   # direct messages handler
   message_handler = MessageHandler(filters.TEXT & (~filters.COMMAND), 
   direct_messages)

   base_currency_handler = CommandHandler('baseCurrency', 
   record_base_currency)
   target_currency_handler = CommandHandler('targetCurrencies', 
   record_target_currencies)

   exchange_handlers = [CommandHandler('singleExchange', 
                              single_exchange_rate),
                        CommandHandler("multipleExchange", 
                              multiple_exchange_rate),
                        CommandHandler("exchangeRate", 
                              arbitrary_exchange)]

   # Option list handlers
   callback_handlers = [CallbackQueryHandler(complete_registration, 
                          'yes'),
                       CallbackQueryHandler(complete_registration, 
                          'no')]

   # Add handlers to the bot
   application.add_handler(message_handler)
   application.add_handler(base_currency_handler)
   application.add_handler(target_currency_handler)
   application.add_handlers(callback_handlers)
   application.add_handlers(exchange_handlers)
   ...
Enter fullscreen mode Exit fullscreen mode

Each function is added to a handler. The CallbackQueryHandler() is used to handle the select options created earlier in this tutorial.

Run the bot to test the progress so far. The bot should respond to all your messages and queries.

Protect the bot from unauthorized access.

Currently, the bot is accessible by anyone, both registered and unregistered users. This is the final defect of the bot.

If an unregistered user tries to enter a command in the bot, the bot should respond by telling them to register first.

Python decorators to the rescue

Python decorators are wrapper functions that accept other functions or classes as arguments. They are useful for validations in web apps.

For example, if a condition is satisfied, the wrapped function is returned and executed. Else, a warning message is sent or any other action performed.

For this tutorial, two decorators will be created. They are as follows:

1. A decorator for new users:
This decorator protects a function and makes it accessible by new users only. This is the code below:

def for_new_users(f):
   @wraps(f)
   async def wrapper_function(update: Update, context: ContextTypes.DEFAULT_TYPE):
       # Get a single user by chat_id
       user = db.query(User).filter_by(chat_id=update.effective_chat.id).first()

       # Checks if user exists
       if user:
           await context.bot.send_message(
               chat_id=update.effective_chat.id,
               text="This endpoint is for new and unregistered users."
           )
       else:
           await f(update, context)
   return wrapper_function
Enter fullscreen mode Exit fullscreen mode

In this decorator, the code checks a user's chat_id against the database. If the chat_id exists, the user is sent a warning message and denied access to the function. If otherwise, the user is allowed to continue with his/her registration.

2. A decorator for registered users:
This decorator protects a function and makes it accessible by registered users only. This is the code below:

def for_registered_users(f):
   @wraps(f)
   async def wrapper_function(update: Update, context: ContextTypes.DEFAULT_TYPE):
       # Get a single user by chat_id
       user = db.query(User).filter_by(chat_id=update.effective_chat.id).first()

       # Checks if user exists
       if not user:
           await context.bot.send_message(
               chat_id=update.effective_chat.id,
               text="This endpoint is for registered users. Kindly register to use this function"
           )
       else:
           await f(update, context)

   return wrapper_function
Enter fullscreen mode Exit fullscreen mode

In this decorator, the code checks a user's chat_id against the database. If the chat_id exists, the user is allowed to continue with the bot. If otherwise, the user is sent a warning and instructed to complete his/her registration to access the function.

This way, registered users do not have to recreate a profile with the same account again.

Add the decorators to each function

Protect each function by adding the decorators as below:

@for_new_users
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
# Rest of the code
Enter fullscreen mode Exit fullscreen mode

Assign a suitable decorator for each function. Check the full code on GitHub to see how the decorators are added to each function.

Set up daily updates

The final feature of the bot is to send personalized daily updates to users who opt-in for daily updates.

The job_queue package of the PTB library is used to set up cron jobs in Telegram bots.

Below is a function named daily_updates() that sends the personalized updates:

async def daily_updates(context: ContextTypes.DEFAULT_TYPE):
   all_users = db.User.all()

   # List of users who opt-in for daily updates
   will_receive_updates = [user for user in all_users if user.receive_updates is True]

   context.bot_data['cached_rates'] = {}

   # Function that sends updates
   async def send_update(user):
       base_currency = user.base_currency.upper()
       params = {
           "base": base_currency
       }
       chat_id = user.chat_id
       currency_pairs = user.currency_pairs.split(",")
       response = ""

       # Checks if an updates exists in the context dictionary
       try:
           cached_response = context.bot_data['cached_rates'][base_currency]
       except KeyError:
           response = currency_exchange.multiple_exchange(params)
       else:
           response = cached_response

       update_message = f"<b>Latest update on exchange rates relative to {base_currency}</b>\n" \
                        f"This means that 1 {base_currency} is equal to the following in different currencies:\n\n" \
                        f""
       for currency in currency_pairs:
           exchange_rate = response['exchange_rates'][currency.upper()]
           update_message += f"<b>{currency}</b>: {exchange_rate}\n"

       context.bot_data['recent_exchange_rates'][base_currency] = response
       await context.bot.send_message(
           chat_id=chat_id,
           text=update_message,
           parse_mode=telegram.constants.ParseMode.HTML
       )
   for user in will_receive_updates:
       await send_update(user)
       time.sleep(2)

   # Clears cached updates
   context.bot_data.clear()
Enter fullscreen mode Exit fullscreen mode

In the function above, users who want daily updates are separated from others and added to a list.

Within this function, there is another asynchronous function that queries the API based on each user’s base and target currencies.

Each successful query is temporarily stored(cached) in the context dictionary of the bot. This is useful in situations where users might have similar base currency.

Instead of sending a request to the API every time, you can easily access it from within the bot and render results to users.

For example — John and Janet both have the same base currency which is USD. If John receives his update first, instead of sending a new request to the API for Janet’s update, you can easily use John’s own which has been saved to memory since they both have the same base currency.

If the next user has a different base currency, a new request is sent to the API and the result is also saved to memory in case there’s another user with the same base currency.

Finally, the cached data is cleared after all updates have been sent.

Caching API responses are not accepted by all APIs. In fact, it is considered a crime for some. In the case of the Exchange API, cached responses are not useful after a long time. This is because exchange rates fluctuate and change periodically. Therefore, using cached results would mean that your bot will not provide accurate exchange rates. It is only useful for situations like this where you have to send updates to users at once.

Create a cron job and register the updates function to the application by following the steps below:

  1. Import the datetime module.

  2. Add the code below in the “main block".

...
# The "main block"
if __name__ == "__main__":
   ...

   time_format = '%H:%M'
   reminder_time_string = '21:13'
   daily_job = application.job_queue
   datetime_obj = datetime.strptime(reminder_time_string, time_format)
   daily_job.run_daily(daily_updates, time=datetime_obj.time(), 
   days=tuple(range(7)))

   ...
Enter fullscreen mode Exit fullscreen mode

In the code above, a daily cron job is created with the application.job_queue instance.

It is automatically set at UTC timing and it runs based on the specified time of the reminder_time_string.

It uses the Hours:Minutes(e.g 05:00) time format.

Conclusion: Next steps

The bot is up and running and delivers real time exchange rates to users. I'm sure you enjoyed building this project.

You learned how to apply some python concepts that help you write a clear and organized code.

What Next?

What's the joy of building something this beautiful without it being used by people?

That's why I’ve decided to create a new tutorial that will show you how to add a live postgresql database to the bot and host it live for others to use.

Follow me on LinkedIn to be the first to know when the new tutorial is out.

Frequently asked questions (FAQs)

How can I host the Telegram bot for free?

Yes. There are free hosting services where you can host your Telegram bot. However, this particular bot requires a persistent memory and state persistence. It also runs and stores cron-jobs on them, hence you will need a more sophisticated solution.

A new article on how to solve this will be ready soon.

Can I host this Telegram bot on Google Cloud functions?

No. Google Cloud functions is suitable for hosting applications that does not require persistence or are stateless. They are only triggered when an action or event occurs in an application.

Which other Python libraries can be used to build a Telegram bot?

There are numerous Python libraries that can be used to build telegram bots. Here is a list of top 10 Python libraries for developing Telegram bots

Top comments (1)

Collapse
 
artyomovs profile image
artyomovs

Hey man. Great article! Created something similar (but for budget planning) and toke some ideas from your example, thanks a lot.