DEV Community

Cover image for Building Instant Messaging App With Flask, Socketio, and Fauna
Bamimore-Tomi
Bamimore-Tomi

Posted on

Building Instant Messaging App With Flask, Socketio, and Fauna

Whatsapp, Messenger, Telegram, and other instant messaging apps make use of WebSockets. According to Wikipedia, WebSocket is a computer communications protocol, providing full-duplex communication channels over a single TCP connection.

A duplex communication system is a point-to-point system composed of two or more connected parties or devices that can communicate in both directions. In a full-duplex channel, both parties communicate simultaneously. Instant messaging applications implement the full-duplex concept via protocols like WebSockets.

In this post, we will be exploring how to build a serverless instant messaging application.

What Is a Serverless Application?

Serverless architecture, also known as function as a service, FaaS, is a software design pattern where applications are hosted by a third-party service, eliminating the developer's need for server software and hardware management. (cited from https://www.twilio.com/docs/glossary/what-is-serverless-architecture)

Advantages of Using a Serverless Database

Serverless databases reduce the effort put into managing the database resources. It allows you to focus on other essential parts of your application. Some other advantages of using a serverless database include:

  • Reduction in operational database cost
  • Improved database security
  • Improved scalability
  • Realtime database access
  • Quicker deployment time

Introduction to Fauna

Fauna is a serverless database. Fauna is a document database that offers GraphQL and Fauna Query Language (FQL) at its nucleus. You can store collections, indexes, and other databases ( multi-tenancy). Fauna can handle multiple data types. Fauna doesn't have strict schema requirements and has top-notch support for data relationships. You can check it out at: https://docs.fauna.com/fauna/current/

Setting Up your Fauna Database

First, you need to create an account with Fauna. You can do that at: https://dashboard.fauna.com/accounts/register

Creating a New Database

You need to create a database your Flask Application will interact with. First, click on the New Database button. It should look like the one in the image below.

create new fauna database screen

Then, enter the database name.

Create new fauna database form

Click on SAVE to create the database. You will see this:

fauna dashboard

Now, you have created a new database.

Creating a Fauna Collection

Click on the NEW COLLECTION button to create a collection. A collection is similar to tables in SQL that contain data with similar characteristics. Fauna organizes its data into collections and uses indexes to browse through it. Data in a collection is organized as documents.

Next, you fill out the details of your collection.

Create fauna collection screen

The collection name for this example will be users. You can leave the other fields with their default values. Click on SAVE. You will see something similar to this:

fauna database collection dashboard

Now, we have to create indexes for our collection.

Creating a Fauna Index

Indexes allow us to browse through our database. Navigate to Indexes and click on NEW INDEX. You have to fill out the details of the index.

Create new fauna index screen

Fill out the id in the Terms field. Terms specify the data the index is allowed to browse. Click the SAVE button.

You have now created a Fauna index.

Creating Your Database API Key

We need to create an API key to allow us to communicate with our database from Python securely. To generate the key, navigate to the Security tab on the left sidebar.

Create fauna database connection key

Generate the new key by clicking on the NEW KEY button.

You will be asked for the database to associate the key with, the key's role, and the optional name for the API key. Provide that. Then, click on the SAVE button.

display fauna database connection key

You will now see your API key (hidden here). Remember always to keep your key private.

Copy and store the keys where you can easily retrieve them.

Wiring it Up to Python


pip install faunadb

Enter fullscreen mode Exit fullscreen mode

After installing, run this sample code provided by Fauna: 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

If this runs successfully, it means you are done setting up Fauna.

Building the Application

We have now set up the serverless database for our application. We can get our hands dirty with some good stuff. You can clone the project's repository (git clone <https://github.com/Bamimore-Tomi/fauna-chat.git>). Go through the Readme.md file for instructions on how to use the web application.

Application Structure

The structure of the repository should look like this.
fauna-chat application structure

The .env was intentionally ignored. It contains app secrets.

Installing Project Requirements

For starters, you have to install the requirements using pip. Running this command in the project's directory will handle that for you.


pip install -r requirements.txt

Enter fullscreen mode Exit fullscreen mode

Simply running python main.py will get the server up and running. Let's run through the routes to see how they work.

Setting up Our Flask Routes

We initialize the Fauna client using client=FaunaClient(secret=os.getenv("FAUNA_KEY")). The Secret key is defined in the .env file. All requests to Fauna will be from the client object.


# main.py

app = Flask(__name__, template_folder='templates')

app.config["SECRET_KEY"] = "your-secret-key"

socketio = SocketIO(app)

Enter fullscreen mode Exit fullscreen mode

The main Flask app is instantiated with the app variable. In the socketio variable, we use SocketIO to enable bi-directional communications between the client and the server.

The following function is a custom login decorator. We will use it to decorate protected routes later in the file. If the user session is not set, they will be redirected to the login page.


def login_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if "user" not in session:
            return redirect(url_for("login"))
        return f(*args, **kwargs)

    return decorated

Enter fullscreen mode Exit fullscreen mode

The Register Route

The register view receives data from the HTML form and saves it to the database. It is good practice to always hash your passwords before storing them in your database. We used the hashlib library for that function. If this is successful, a new user and chat document is created.

The Login Route

The login view validates the credentials submitted by users. If this is successful, a session is set for the user. It is essential to understand how to query Fauna using the indexes you created.


user = client.query(q.get(q.match(q.index("user_index"), email)))

Enter fullscreen mode Exit fullscreen mode

When we created the user_index, we added the Term email (the terms field specifies which document fields can be searched ). Now I am querying the users' collection using the user_index created earlier.

Chat Route

The chat route handles several things. First, it fetches all ids in the user chats list.

chat_list = client.query(q.get(q.match(q.index("chat_index"), session["user"]["id"])))[
    "data"
]["chat_list"]
Enter fullscreen mode Exit fullscreen mode

We can also liken the chat_list to a friend list. A number of try and except blocks were used to create alternative action in case a query fails. For example:

try:
    chat_list = client.query(
        q.get(q.match(q.index("chat_index"), session["user"]["id"]))
    )["data"]["chat_list"]
except:
    chat_list = []
Enter fullscreen mode Exit fullscreen mode

If no one has been added to a user's chat list, we return an empty list.

At the function's beginning, the room_id variable is set from the optional rid url argument. The room id is a unique identifier generated for every chat room. According to socketIO, a room is an arbitrary channel that sockets can join and leave. It can be used to broadcast events to a subset of clients. In this case, a "subset of clients" consists of just two users.

New-Chat Route

This route enables a user to add other users to their chat list.

It confirms that the users are not trying to add themselves,

if new_chat == session["user"]["email"]:
    return redirect(url_for("chat"))
Enter fullscreen mode Exit fullscreen mode

a user that does not exist

try:
    # If user tries to add a chat that has not registered, do nothing
    new_chat_id = client.query(q.get(q.match(q.index("user_index"), new_chat)))
except:
    return redirect(url_for("chat"))
Enter fullscreen mode Exit fullscreen mode

A user that has already been added if new_chat_id["ref"].id() not in chat_list:.

Another vital operation that this route performs is generating the room id.


room_id = str(int(new_chat_id["ref"].id()) + int(user_id))[-4:]

Enter fullscreen mode Exit fullscreen mode

It adds the user id of both users and uses the last four digits as the room id for their chats.

Flask-SocketIO Events

We have seen the regular Flask routes in the application. We will not explore the Flask-Socketio Events in the application.

We have two events here. In an actual production application, you will likely have more events.

Join-Chat Event

The first join_private_chat event is used for adding a user to a specified room. When a client connects to a chat, the URL path will look like this: https://fauna-chat.herokuapp.com/chat/?rid=0488

The SocketIO client uses the rid to know who the user wants to chat with. The client now requests the server to add the user to that room.


var socket = io.connect('http://' + document.domain + ':' + location.port + '/?rid=' + {{ room_id }} );

socket.on( 'connect', function() {

 socket.emit('join-chat', {

 rid: '{{ room_id }}'

 } )

 } )

Enter fullscreen mode Exit fullscreen mode

Once this process is completed, the server now knows who to send a specific message to.

Outgoing Event

The chatting_event function is used when a user sends a new message. It simply saves the message to the database.

messages = client.query(q.get(q.match(q.index("message_index"), room_id)))

conversation = messages["data"]["conversation"]

conversation.append(
    {
        "timestamp": timestamp,
        "sender_username": sender_username,
        "sender_id": sender_id,
        "message": message,
    }
)

client.query(
    q.update(
        q.ref(q.collection("messages"), messages["ref"].id()),
        {"data": {"conversation": conversation}},
    )
)

Enter fullscreen mode Exit fullscreen mode

And broadcasts the message to the specified user.

socketio.emit(
    "message",
    json,
    room=room_id,
    include_self=False,
)
Enter fullscreen mode Exit fullscreen mode

Touring the Application

fauna-chat register page

fauna-chat login page
fauna-chat chat room page

Conclusion

In this article, we built an Instant Messaging app with Fauna's serverless database and Python. We learned how simple it is to incorporate Fauna into a Python application and had the opportunity to test out some of its most essential features and functions.

Please contact me via Twitter: @__forthecode if you have any questions.

Top comments (1)

Collapse
 
rsbruce profile image
Robert Bruce

This tutorial seems really promising but the problem is that you haven't given a list of all the collections, indexes and terms that we're going to need. Saying "Fill out the id in the Terms field" isn't very useful. As it stands at the moment I'm having to dig through the code to work out all the DB fields that I'm going to need. Until I sort that I can't run any of your code. It'd be great if you could update the tutorial, thanks.