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.
Then, enter the database name.
Click on SAVE
to create the database. You will see this:
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.
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:
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.
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.
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.
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
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)
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.
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
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)
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
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)))
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"]
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 = []
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"))
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"))
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:]
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 }}'
} )
} )
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}},
)
)
And broadcasts the message to the specified user.
socketio.emit(
"message",
json,
room=room_id,
include_self=False,
)
Touring the Application
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)
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.