DEV Community

loading...

Building a Telegram Bot with Python and Fauna

lordghostx profile image LordGhostX Updated on ・15 min read

A while ago, I stumbled upon the term "Serverless Databases" and got to know about Fauna. Out of curiosity, I decided to try it out and I was honestly impressed by its simplicity and the whole concept of serverless databases. I loved that Fauna had a library that made it compatible with the Python programming language, along with libraries for several other languages too.

The purpose of this article is to introduce the reader to the world of serverless databases with Fauna, and explore its CRUD functionalities by building a Telegram Bot with Python to help track our everyday tasks (more or less a todo list bot).

So we will be creating bot users in Fauna, adding tasks, retrieving tasks, updating tasks and deleting tasks.

What is a Serverless Database?

A serverless database is one where all maintenance and operational responsibility is outside of a developer or application’s concern, and all traffic/CRUD is accommodated with on-demand scaling. This reduces the load and effort put into managing the database resources, especially when the application begins to scale to support a large number of users.

There are a lot of advantages of picking a serverless database for your application aside from reducing work in managing database resources. They include:

  • Database operational cost reduction
  • Real-time database access
  • Increase in productivity (since the burden of database management is removed)
  • Infinite scalability
  • Improved database security

A Brief Introduction to Fauna

Of course, we now know Fauna is a serverless database, but how does it store and handle data? At its core, Fauna is a document database that offers two interfaces, GraphQL and the Fauna Query Language (FQL). Databases can store collections, indexes, and even other databases (yay multi-tenancy!). Within collections are documents, which by default, don’t have strict schema requirements. Fauna is capable of handling a variety of data types (e.g. temporal) but is particularly impressive for having first-class support for relational data. We’ll be exploring most of this together momentarily, but if you’re eager to learn even more, check out the Fauna documentation!

Getting Started with Fauna

The first thing you need to get started with Fauna is to create an account on the official website. You can do that using either your email address or your github or netlify account here: https://dashboard.fauna.com/accounts/register

Creating a Fauna Database

The first thing we’ll do on Fauna is create an actual database, which the application will interact with and where the data will be stored. To do this click on the “NEW DATABASE” button and you will be presented with a screen similar to the one below.

create database image

On this screen, you will be asked for the database name, whether you want to pre-populate the database and there’s a tip for you if you are migrating from another database to Fauna. For now, we will just enter the database name and press the “SAVE” button. Then you will be presented with a screen like the one below.

db overview image

Creating a Fauna Collection

Fauna organizes its data into collections and uses indexes to browse its data. A collection is similar to SQL tables that contain data with similar characteristics e.g users collection that contain information about users in the database. Before we can start storing actual data in the Fauna database, we need to first create a collection. This is done by pressing the “NEW COLLECTION” button.

create collection image

You will then be provided with a form to fill in details about the collection. You will be asked for the collection name (we will be using “users” for this example) then you will be asked for History Days and TTL. The History Days is used to define the number of days Fauna should retain a historical record of any data in that particular collection while the TTL serves as an expiry date for data in the collection. For example, if the TTL is set to 7, any data stored in that collection will be automatically deleted 7 days after its last modified date.

users collection image

After entering the name of the collection and hitting the save button, you will be presented with a screen like the one above. In Fauna, data in a collection are referred to as documents. Documents are similar to table rows in SQL based databases.

Creating a Fauna Index

We also need to create an index that will allow us to browse through data that is stored in our database. To create one, navigate to the DB Overview tab on the Fauna sidebar (left side of the screen) then click the “NEW INDEX” button.

creating index image

You will be presented with a form where you will be asked to select the collection to link the index to and provide a name. Also, specify the Terms for the index, these are specific data the index is allowed to browse. For our example, we will be using “id”. Once you’ve filled this click the “SAVE” button.

Integrating Fauna with Python

Creating a Fauna Database API key

Before we begin building a Python app that uses Fauna, we need to create an API key that would allow our application to easily communicate with our database. To create an API key, we need to navigate to the security tab on the Fauna sidebar (on the left side of the screen).

create key image

You will be presented with a screen similar to the one above, proceed to generate a key by clicking the “NEW KEY” button.

assign key image

You will then be asked for the database to associate the key with, the role of the key, and an optional name for your API key. Once you’ve filled in this information hit the “SAVE” button.

key created image

Once you have done this, you will be presented with your API key (hidden here for privacy reasons). The key should be copied as soon as it is generated then stored somewhere you can easily retrieve.

Communicating to Fauna with Python

Now, we want to install the Python driver for Fauna. Luckily it’s available via the Python package manager (pip) and that saves us a lot of stress instead of having to build it via the Github repo.

$ pip install faunadb
Enter fullscreen mode Exit fullscreen mode

After this is installed, we are going to run the sample code provided in Fauna Python driver docs https://docs.fauna.com/fauna/current/drivers/python.html

from faunadb import query as q
from faunadb.objects import Ref
from faunadb.client import FaunaClient

client = FaunaClient(secret="your-secret-here")

indexes = client.query(q.paginate(q.indexes()))

print(indexes)
Enter fullscreen mode Exit fullscreen mode

Building a Telegram Bot with Python

What is Telegram?

Telegram is one of the top social media platforms for messaging. It’s core functionalities include sending messages to other Telegram users, creating group chats, calling contacts, and sending files and stickers. You can learn more about Telegram here https://telegram.org

What is a Telegram Bot?

Telegram Bots are accounts that are operated by software and not actual people. They can do anything from teaching, playing games, acting as search engines, broadcasting messages, serving as reminders, connecting and integrating with other services, or even pass commands to Internet Of Things (IOT) devices. @GroupButler_bot is a very popular Telegram Bot for managing Telegram groups.

group butler bot

Getting Started

Step 1: Having a Conversation with BotFather

Sign In to your Telegram account then search for @botfather and start a conversation with the account. PS: BotFather is also a Telegram Bot

bot father search
bot father start

Step 2: Creating the Telegram Bot Interface with BotFather

We will now use the /newbot command to create a new Telegram Bot. Creating the Telegram Bot with BotFather means you give it a name and assign a username to it.

creating new bot
submit bot info

After supplying the name and username for your Telegram Bot, BotFather will give you the API token that will be used to interact with the Bot account via the Telegram API.

getting bot token

Step 3: Powering Our Bot with Python

The first thing we will do is install the required Python libraries for building Telegram Bots. Of course, we can use the Telegram API directly following the official docs but Python already simplifies this by providing libraries we can easily integrate.

$ pip install telegram
$ pip install python_telegram_bot
Enter fullscreen mode Exit fullscreen mode

The next thing we want to do is write a script that receives any message sent to our bot.

import telegram
from telegram.ext import Updater
from telegram.ext import CommandHandler

telegram_bot_token = "your-telegram-token"

updater = Updater(token=telegram_bot_token, use_context=True)
dispatcher = updater.dispatcher


def start(update, context):
    print("Hello World!")


dispatcher.add_handler(CommandHandler("start", start))
updater.start_polling()
Enter fullscreen mode Exit fullscreen mode

Also, we need to create an updater and dispatcher for our Telegram Bot using the telegram python module. The updater class tracks and monitors messages sent to the bot and delivers it to the dispatcher, while the dispatcher handles and processes the received message.

The next thing we need to do is set a command handler for our dispatcher, a command handler is a function that is called when a certain command is used in our bot. A Telegram Bot command is any message that starts with “/” such as: /start /help /info. There are other kinds of handlers to add to the dispatcher such as image handler, text handler, regex handler and many more. We are starting with a command handler because that is the first thing every user enters into a Telegram Bot.

When a command handler is triggered, the updater passes two parameters (update, context) which contain all the information about the message request. We want our handler for the start command to print the text “Hello World!” in our console whenever it is triggered.

After setting our handlers, we would then initiate our updater to start monitoring messages coming from Telegram and triggering the dispatcher using the start_polling() method. Let’s save our file as bot.py and start monitoring messages from Telegram.

$ python bot.py
Enter fullscreen mode Exit fullscreen mode

After running our script, let’s send a message to our bot and see if “Hello World!” is printed in the console.
If you haven’t already done this, we need to search for our Telegram bot with the same username supplied to BotFather earlier.

searching for our bot
starting our bot
getting hello message

Our command handler was triggered successfully and we got our hello message, let’s tweak this a little to send the user the text “Hello World” instead of printing on our console.

def start(update, context):
   chat_id = update.effective_chat.id
   context.bot.send_message(chat_id=chat_id, text="Hello World")
Enter fullscreen mode Exit fullscreen mode

To send messages from our bot back to the user, we need to use the send_message method of the context.bot class which takes in a parameter called chat_id. Telegram assigns a unique identifier to every user accessible with the name chat_id which can be used by bots to send messages or make references.

When we run the script again with our updates and send the start message again we should get a response something similar to the image below.

sending bot messages

Making Queries to Fauna

Saving data with Fauna

Now let’s move on to integrating our Telegram Bot with Fauna, the first thing we want to experiment with is saving the details of users who start our Telegram Bot.

Next thing we do is import the necessary libraries needed for Fauna to operate and merge with our existing code.

import telegram
from telegram.ext import Updater
from telegram.ext import CommandHandler
from faunadb import query as q
from faunadb.objects import Ref
from faunadb.client import FaunaClient

telegram_bot_token = "your-telegram-token"
fauna_secret = "your-secret-here"

updater = Updater(token=telegram_bot_token, use_context=True)
dispatcher = updater.dispatcher
client = FaunaClient(secret=fauna_secret)


def start(update, context):
    chat_id = update.effective_chat.id
    context.bot.send_message(chat_id=chat_id, text="Hello World")


dispatcher.add_handler(CommandHandler("start", start))
updater.start_polling()
Enter fullscreen mode Exit fullscreen mode

The next thing we want to do is store all necessary information about users who start a conversation with the Telegram Bot.

import pytz
from datetime import datetime
Enter fullscreen mode Exit fullscreen mode

We are going to be importing the datetime and pytz libraries so we can save timestamps to the database we created earlier as Fauna only accepts offset-aware datetimes.

def start(update, context):
    chat_id = update.effective_chat.id
    first_name = update["message"]["chat"]["first_name"]
    username = update["message"]["chat"]["username"]

    user = client.query(q.create(q.collection("users"), {
        "data": {
            "id": chat_id,
            "first_name": first_name,
            "username": username,
            "last_command": "",
            "date": datetime.now(pytz.UTC)
        }
    }))
    context.bot.send_message(chat_id=chat_id, text="Welcome to Fauna TO-DO, your details have been saved 😊")
Enter fullscreen mode Exit fullscreen mode

Next, we are making some changes to our start handler, once it is triggered we will first extract the user’s first name and Telegram username. Then we make a query to our users collection we created earlier and save our users’ details. Then round it up by sending the user a welcome message.

We should also add some checks to our users’ registration so we don’t register a user more than once. Before we save the user details we should check if they are already registered in the database and only save their details if they don’t.

def start(update, context):
    chat_id = update.effective_chat.id
    first_name = update["message"]["chat"]["first_name"]
    username = update["message"]["chat"]["username"]

    try:
        client.query(q.get(q.match(q.index("users"), chat_id)))
    except:
        user = client.query(q.create(q.collection("users"), {
            "data": {
                "id": chat_id,
                "first_name": first_name,
                "username": username,
                "last_command": "",
                "date": datetime.now(pytz.UTC)
            }
        }))
    context.bot.send_message(chat_id=chat_id, text="Welcome to Fauna TO-DO, your details have been saved 😊")
Enter fullscreen mode Exit fullscreen mode

We have updated our start command handler to first query the database if our user exists, if it does - send the greeting message immediately, if it doesn't save the user and send the greeting. Let’s move forward and add a command for adding items to our todo list.

Before writing code, we need to create an index to browse the data in the collection we created earlier.

create index

After this step, let’s write a new command handler for adding todo tasks and include it in our dispatcher.

def add_todo(update, context):
    chat_id = update.effective_chat.id

    user = client.query(q.get(q.match(q.index("users"), chat_id)))
    client.query(q.update(q.ref(q.collection("users"), user["ref"].id()), {"data": {"last_command": "add_todo"}}))
    context.bot.send_message(chat_id=chat_id, text="Enter the todo task you want to add 😁")


dispatcher.add_handler(CommandHandler("add_todo", add_todo))
Enter fullscreen mode Exit fullscreen mode

What I did here was to set the value of the last_command to add_todo then send the user a message telling them to write what they want to add.

add todo image

The next thing we will need to do is add a message handler to the bot. A message handler is a kind of Telegram dispatcher handler for processing text-based messages. To create a message handler we need to import the required classes.

from telegram.ext import Updater
from telegram.ext import CommandHandler
from telegram.ext import MessageHandler, Filters 
Enter fullscreen mode Exit fullscreen mode

Then we define the actual message handler that saves our todo task into Fauna.

def echo(update, context):
    chat_id = update.effective_chat.id
    message = update.message.text

    user = client.query(q.get(q.match(q.index("users"), chat_id)))
    last_command = user["data"]["last_command"]

    if last_command == "add_todo":
        todo = client.query(q.create(q.collection("todo"), {
            "data": {
                "user_id": chat_id,
                "todo": message,
                "completed": False,
                "date": datetime.now(pytz.UTC)
            }
        }))
        client.query(q.update(q.ref(q.collection("users"), user["ref"].id()), {"data": {"last_command": ""}}))
        context.bot.send_message(chat_id=chat_id, text="Successfully added todo task 👍")
Enter fullscreen mode Exit fullscreen mode

Let’s not forget to add the message handler to the dispatcher too.

dispatcher.add_handler(MessageHandler(Filters.text, echo))
updater.start_polling()
Enter fullscreen mode Exit fullscreen mode

Restarting our bot and sending a message should let the bot respond to me with a success message. We can also monitor our database from our Fauna dashboard.

added todo task
viewing data in dashboard

Reading data from Fauna

Now we should add a command that retrieves all our todo tasks along with its status and display to the user.

def list_todo(update, context):
   chat_id = update.effective_chat.id

   task_message = ""
   tasks = client.query(q.paginate(q.match(q.index("todo"), chat_id)))
   for i in tasks["data"]:
       task = client.query(q.get(q.ref(q.collection("todo"), i.id())))
       if task["data"]["completed"]:
           task_status = "Completed"
       else:
           task_status = "Not Completed"
       task_message += "{}\nStatus: {}\n\n".format(task["data"]["todo"], task_status)
   if task_message == "":
       task_message = "You have not added any task, do that with /add_todo 😇"
   context.bot.send_message(chat_id=chat_id, text=task_message)
Enter fullscreen mode Exit fullscreen mode

Then add this to our dispatcher

dispatcher.add_handler(CommandHandler("list_todo", list_todo))
Enter fullscreen mode Exit fullscreen mode

After restarting our script and triggering the /list_todo command on Telegram we should get a response similar to the image below.

list task image

Updating data in Fauna

Now would be a good time to add a function that updates our todo data, updating, in this case, would be changing a task status from not completed to completed and vice versa. We would be using the Ref ID of our todo tasks to track which task to update. To make this a little easier for the user, let’s create a command text that will be included in our /list_todo message.

def list_todo(update, context):
   chat_id = update.effective_chat.id

   task_message = ""
   tasks = client.query(q.paginate(q.match(q.index("todo"), chat_id)))
   for i in tasks["data"]:
       task = client.query(q.get(q.ref(q.collection("todo"), i.id())))
       if task["data"]["completed"]:
           task_status = "Completed"
       else:
           task_status = "Not Completed"
       task_message += "{}\nStatus: {}\nUpdate Link: /update_{}\n\n".format(task["data"]["todo"], task_status, i.id())
   if task_message == "":
       task_message = "You have not added any task, do that with /add_todo 😇"
   context.bot.send_message(chat_id=chat_id, text=task_message)
Enter fullscreen mode Exit fullscreen mode

Now when we trigger the /list_todo command we get a response similar to the image below.

added update

After this, we need to create a function called update_todo which would use regex to find any text that starts with /update_{todo ID} then update it.

dispatcher.add_handler(MessageHandler(Filters.regex("/update_[0-9]*"), update_todo))
dispatcher.add_handler(MessageHandler(Filters.text, echo))
Enter fullscreen mode Exit fullscreen mode

We used the same message handler from before but with a regex handler this time around. The regex string /update_[0-9]* will return true if a string starts with /update_ then is followed by numbers of any given length. PS: I noticed the regex handler won't be triggered if you place it below the echo handler that accepts the text filter.

def update_todo(update, context):
   chat_id = update.effective_chat.id
   message = update.message.text
   todo_id = message.split("_")[1]

   task = client.query(q.get(q.ref(q.collection("todo"), todo_id)))
   if task["data"]["completed"]:
       new_status = False
   else:
       new_status = True
   client.query(q.update(q.ref(q.collection("todo"), todo_id), {"data": {"completed": new_status}}))
   context.bot.send_message(chat_id=chat_id, text="Successfully updated todo task status 👌")
Enter fullscreen mode Exit fullscreen mode

In our update_todo function, we first extracted the chat_id of our user and the todo_id of the task they want to update, then we checked the status of the task and it changed to completed - if it’s not and vice versa. The full user flow should resemble the image below.

update todo task

Deleting data from Fauna

We also need functionality to delete tasks from our database, what’s a CRUD app without the D anyway. Fauna provides APIs for this so let’s jump into the code. We would be using the same style from our update function for deleting too.

task_message += "{}\nStatus: {}\nUpdate Link: /update_{}\nDelete Link: /delete_{}\n\n".format(task["data"]["todo"], task_status, i.id(), i.id())
Enter fullscreen mode Exit fullscreen mode

First, we add the delete command to our list_todo rendering so we get a response similar to the one below.

added delete task

Then we define a regex handler to track messages that begin with /delete_ and is followed by our to-do ID. Don’t forget to place this above the echo handler.

dispatcher.add_handler(MessageHandler(Filters.regex("/delete_[0-9]*"), delete_todo))
dispatcher.add_handler(MessageHandler(Filters.text, echo))
Enter fullscreen mode Exit fullscreen mode

Then we create our delete_todo function with the same style as our update but with deleting functionality.

def delete_todo(update, context):
   chat_id = update.effective_chat.id
   message = update.message.text
   todo_id = message.split("_")[1]

   client.query(q.delete(q.ref(q.collection("todo"), todo_id)))
   context.bot.send_message(chat_id=chat_id, text="Successfully deleted todo task status 👌")
Enter fullscreen mode Exit fullscreen mode

finished delete task

Now that wraps up our todo app in the form of a Telegram Bot with Python and Fauna. The user flow of the entire app should look like the image below. I also made some changes to the bot messages, the python source code is available at the end of the tutorial.

full bot UI

Conclusion

By integrating Fauna with Python, we were able to build a todo app using the Telegram Bot API with minimal effort. We also saw how easy it was to create and manage serverless databases with Fauna.

The full source code of the app can be found here on github. Trying out Fauna was very interesting and I can’t wait to see the amazing things you build with it. If you have any questions, don't hesitate to hit me up on Twitter: @LordGhostX

Discussion

pic
Editor guide
Collapse
darkart108 profile image
Ritesh Yadav

Hello @lordghostx it was a very nice blog!! I am following all the steps of setting up the bot but I confuse in creating a FaunaDb database collection!! please right me if I am wrong

  1. We need to create one collection of name users and then the index with that users
  2. We need to create new collection name todo and the index with that todo with terms as data.user_id

please right me if I am wrong!!

Collapse
lordghostx profile image
LordGhostX Author

Yes you're right, our app has 2 collections which are "users" and "todo"

The users collection stores information about our user such as their chat_id and username while the todo collection stores tasks created by users.

In Fauna, we need to create different collections and an index for each that's why we have different ones.

Collapse
darkart108 profile image
Ritesh Yadav

Ok @lordghostx , so I have created two collections one for users and another for todo, and also created their respective index,
and in the todo index, I have added data.user_id in terms Is it right? because it is giving me connection aborted error when I execute [/add_todo] cmd and rest all cmd are working fine.
Please correct me if it is wrong I think I am wrongly create database :(

Collapse
lordghostx profile image
LordGhostX Author

Hi, I had to clone the repository and setup Fauna all over but I was unable to recreate your error.

I want to ask, when you were creating the first index did you set the name of the index to users and it's terms to id as shown here

article screenshot
fauna screenshot

Thread Thread
darkart108 profile image
Ritesh Yadav

@lordghostx I am uploading the images of both collection and index please take a look and correct it if it is wrong!!

The cmd /start and /list_todo are working fine.

Problem is with /add_todo

The error I am getting:

File "bot.py", line 42, in add_todo
    user = client.query(q.get(q.match(q.index("users"), chat_id)))
raise ConnectionError(err, request=request)
requests.exceptions.ConnectionError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))
Enter fullscreen mode Exit fullscreen mode

Collections
Alt

Indexs
Alt

Alt

Code is the same as you have in your git repo!!

Thread Thread
lordghostx profile image
LordGhostX Author

Hi,

From the look of things I can see the error occurs when you are trying to fetch the current user saved in your database and it seems the Fauna API is not giving you a response.

You're setting up the database well and the code runs fine. Can you delete and create a new Fauna API key then try running the code again, also don't forget a user account needs to be created first using /start if you haven't done that already. If the problem persists send me a message on Twitter so we don't fill up the comment section with back and forth messages. Thanks

Collapse
iamnotstatic profile image
Abdulfatai Suleiman

Thanks for this, very helpful