Building a simple WebChat

raymag

Carlos Magno

Posted on September 10, 2019

Building a simple WebChat

Hello there. In this post, you are going to learn how to build a simple webchat using python and web sockets. Since that's the case, you are going to need to have python installed on your computer.

This project source code is available on this github repository and you can see it online at Papo Reto.

If you don't have the flask package, you can install it with the following command: pip install Flask. You'll also need flask_socketio and eventlet, so you can run pip install flask_socketio and pip install eventlet to install them.

Now that you have everything installed, this is how the files and directories of our project will look like:

templates
 - index.html
static
   css
    - style.css
- main.py
Enter fullscreen mode Exit fullscreen mode

The core of our project is the main.py file, that's what will control our application. It will look like this:

#! -*- enconding: utf-8 -*-
from flask import Flask, render_template
from flask_socketio import SocketIO, emit

app = Flask(__name__, template_folder='templates', static_folder='static', static_url_path='/static/')
socketio = SocketIO(app)

@app.route('/')
def index():
    return render_template('index.html')

@socketio.on('client_message')
def receive_message (client_msg):
    emit('server_message', client_msg, broadcast=True)

if __name__ == '__main__':
    socketio.run(app)
Enter fullscreen mode Exit fullscreen mode

The first two lines of code import the packages and modules we are going to use in this project.

from flask import Flask, render_template
from flask_socketio import SocketIO, emit
Enter fullscreen mode Exit fullscreen mode

Flask is our web framework, and flask_socketio is a package that allows flask to use full-duplex low-latency communications protocols like websockets.

Then we instantiate our flask application and define where the templates and static files are going to be. We also instantiate our socketio object and pass the flask application as a argument.

app = Flask(__name__, template_folder='templates', static_folder='static', static_url_path='/static/')
socketio = SocketIO(app)
Enter fullscreen mode Exit fullscreen mode

We now define our default route which is going to return the index.html page:

@app.route('/')
def index():
    return render_template('index.html')
Enter fullscreen mode Exit fullscreen mode

When the user sends a message, it will emit the event client_message to the server, passing the nickname and the message as parameters. The server will then broadcast this message to every connected user:

@socketio.on('client_message')
def receive_message (client_msg):
    emit('server_message', client_msg, broadcast=True)
Enter fullscreen mode Exit fullscreen mode

Finally, we'll start to run our application with the following lines:

if __name__ == '__main__':
    socketio.run(app)
Enter fullscreen mode Exit fullscreen mode

That's it, our main.py file is now complete, and that take us to the index.html file. In this case, it will have 3 major parts: the message-box, the input-box and the nickname-box. The contents of this file will be:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
<title>Papo Reto</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js" integrity="sha256-yr4fRk/GU1ehYJPAs8P4JlTgu0Hdsp4ZKrx8bDEDC3I=" crossorigin="anonymous"></script>

</head>
<body>
<script type="text/javascript" charset="utf-8">
const socket = io();
socket.on('server_message', (data) => {
    let e = document.createElement('p');
    let sp = document.createElement('span');
    sp.innerHTML = data.nickname;
    e.appendChild(sp);
    e.innerHTML = e.innerHTML+'>> '+data.message;
    if(document.getElementById('message-box').children.length>20){
        document.getElementById('message-box').removeChild(document.getElementById('message-box').children[0]);
    }
    document.getElementById('message-box').appendChild(e);

    document.getElementById('message-box').scroll(0, document.getElementById('message-box').scrollHeight);
});
function htmlEntities(str) {
    return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function sendMessage(){
    socket.emit('client_message', {'nickname': htmlEntities(document.getElementById('nickname-input').value), 'message':  htmlEntities(document.getElementById('message-input').value)});
    document.getElementById('message-input').value = '';
}
function isPressingEnter(e){
    let k;
    if(window.event){
        k = e.keyCode;
        if(k===13){
            sendMessage();
        }
    }else if(e.which){
        k = e.which;
        if(k===13){
            sendMessage();
        }
    }
}
</script>

<h1 id="title">Papo Reto</h1>
<section id="chat-box">
    <section id="message-box">
    </section>

    <section id="input-box">
        <input type="text" autofocus onkeypress="return isPressingEnter(event)" required placeholder="Digite sua mensagem aqui" id="message-input">
        <button type="button" id="send-button" onclick="sendMessage()" >>></button> 
    </section>
</section>

<section id="nickname-box">
    <label id="nickname-label" for="nickname-input">Nickname: </label>
    <input type="text" id="nickname-input" autocomplete="off" value="Guest">
</section>

</body>
</html>
Enter fullscreen mode Exit fullscreen mode

We are using the SocketIO client side api to connect and take care of handling the websockets. So in this case, we are using a CDN to do so:

<script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js" integrity="sha256-yr4fRk/GU1ehYJPAs8P4JlTgu0Hdsp4ZKrx8bDEDC3I=" crossorigin="anonymous"></script>
Enter fullscreen mode Exit fullscreen mode

Now, with javascript, we need to define our socket object, that will connect with the server:

const socket = io();
Enter fullscreen mode Exit fullscreen mode

Since the websocket is on the same route, we don't need to pass anything as parameters.

When the user clicks on the send button, it will call the sendMessage function. But if the user types something on the input with the input-box id, it will call the isPressingEnter function, which will verify if the user is pressed enter. If so, it will also call sendMessage():

function isPressingEnter(e){
    let k;
    if(window.event){
        k = e.keyCode;
        if(k===13){
            sendMessage();
        }
    }else if(e.which){
        k = e.which;
        if(k===13){
            sendMessage();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The sendMessage function will then emit a client_message event to the server, passing the nickname and the message as data:

function sendMessage(){
    socket.emit('client_message', {'nickname': htmlEntities(document.getElementById('nickname-input').value), 'message':  htmlEntities(document.getElementById('message-input').value)});
    document.getElementById('message-input').value = '';
}
Enter fullscreen mode Exit fullscreen mode

We're also using the htmlEntities function to remove the HTML tags from the inputs:

function htmlEntities(str) {
    return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
Enter fullscreen mode Exit fullscreen mode

When the server receives the message, it will broadcast the message to everybody by emiting the server_message event. So we need to add this message to the message-box when the server_message event is received:

socket.on('server_message', (data) => {
    let e = document.createElement('p');
    let sp = document.createElement('span');
    sp.innerHTML = data.nickname;
    e.appendChild(sp);
    e.innerHTML = e.innerHTML+'>> '+data.message;
    if(document.getElementById('message-box').children.length>20){
        document.getElementById('message-box').removeChild(document.getElementById('message-box').children[0]);
    }
    document.getElementById('message-box').appendChild(e);

    document.getElementById('message-box').scroll(0, document.getElementById('message-box').scrollHeight);
});
Enter fullscreen mode Exit fullscreen mode

If you now, run the server with python main.py, you'll see our webchat is already working.
WebChat running without CSS

But since it's looking terrible, we'll add some css to it. Flask templates uses jinja as its template engine, so it's a little bit different to add our css file path to the index page. We can do so adding the following lines of code inside the head tag:

<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='./css/stylxe.css') }}">
Enter fullscreen mode Exit fullscreen mode

You can do as you please, but in this case, that's the contents of our CSS file:

*{
    margin: 0;
    padding: 0;
}
body{
    background: #202020;
    color: #eee;
    font-family: 'monospace';
    line-height: 1.4em; 
    display: flex;
    flex-direction: column;
    justify-content: space-around;
    align-items: center;
    padding: 15px;
}
#title{
    font-size: calc(20px + 2vw + 1vh);
    padding: 40px;
}
#chat-box{
    border: 2px solid #ccc;
    border-radius: 10px;
    box-shadow: 3px 4px 4px #111;
}
@media only screen and (max-width: 600px){
    #chat-box{
        width: 95vw;
    }
}
@media only screen and (min-width: 600px){
    #chat-box{
        width: 80vw;
    }
}
#message-box{
    width: 100%;
    height: 60vh;
    background: #444;
    overflow-y: scroll;
    padding: 5px;
    font-size: calc(10px + 1vh + 2vw);
}
#message-box p{
    width: 100%;
    line-height: 1.5em;
    overflow-wrap: break-word;
    padding: 5px 2px;
    margin: 2px 0;
    background: #4f4f4f;
}
#message-box span{
    color: yellow;
    font-weight: 900;
}

#input-box{
    width: 100%;
    height: 100%;
    display: grid;
    grid-template-columns: 90% 10%;
}
#message-input{
    height: 90%;
    font-size: calc(10px + 1vh + 2vw);
    border-radius: 0 0 0 6px;
}
#send-button{
    background: purple;
    color: #f0f0a0;
    height: 100%;
    font-size: calc(10px + 1vh);
    font-weight: 900;
}
#nickname-box{
    padding: 15px;
}
#nickname-input {
    border-radius: 5px;
    background: purple;
    color: white;
    padding: 2px;
    font-size: calc(10px + 1vh + 1vw);
}
#nickname-label{
    font-size: calc(10px + 1vh + 1vw);

}
Enter fullscreen mode Exit fullscreen mode

And now, that's the result:
Running WebChat

Thank you for your attention! That's one of the ways we can build a really simple webchat using python. Also, it's pretty easy to do so, but if you guys want, I can also show how to deploy it to heroku. So you can let it run online.

💖 💪 🙅 🚩
raymag
Carlos Magno

Posted on September 10, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Building a simple WebChat
webchat Building a simple WebChat

September 10, 2019