DEV Community

loading...
Cover image for Building a simple WebChat

Building a simple WebChat

raymag profile image Carlos Magno ・6 min read

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

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)

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

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)

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

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

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)

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

if __name__ == '__main__':
    socketio.run(app)

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>

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>

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

const socket = io();

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();
        }
    }
}

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 = '';
}

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;');
}

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);
});

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') }}">

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);

}

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.

Discussion (4)

pic
Editor guide
Collapse
akshaymadhan profile image
Collapse
raymag profile image
Carlos Magno Author

You're welcome o/

Collapse
abdulrahman_ali profile image
Abdulrahman Ali

I loved it! Thanks :)

Collapse
raymag profile image
Carlos Magno Author

I'm glad you liked it o/