🎉 New! Web Push Notifications for Chatkit. Learn more in our latest blog post.
Hide
Products
chatkit_full-logo

Extensible API for in-app chat

channels_full-logo

Build scalable realtime features

beams_full-logo

Programmatic push notifications

Developers

Docs

Read the docs to learn how to use our products

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Sign in
Sign up

Build a chat app in Flask and Vue with sentiment analysis - Part 3: Live chat with sentiment analysis

  • Gideon Onwuka

September 5th, 2018
You will need Node 8.9+ and Python 3.6+ installed on your machine.

In this third tutorial, we will be implementing live chat and sentiment analysis.

If you haven’t followed the previous parts, you can catch it up here:

Chatting over channels

Pusher Channels provides us with realtime functionalities. It has a publish/subscribe model where communication happens across channels. There are different types of channel, which we can subscribe to - public channel, private channel, presence channel and the encrypted channel.

For our app, we will make use of the private channel since the chat messages need to be only accessible by the two users involved. This way we can authenticate a channels’ subscription to make sure users subscribing to it are actually authorized to do so. When naming your private channel, it needs to have a prefix of “private-”.

The flow

Once a user logs in, we'll redirect the user to the chat page. Then we subscribe this user to a private channel - private-notification-<the_user_id>, where is the actual ID of the logged in user. This channel will be used to send notifications to the user. So that means every logged in user will have a private notification channel where we can notify them anytime we want to.

After subscribing to the notifications channel (“private-notification-”), we will start to listen for an event we will name new_chat. We'll trigger this event once a user clicks on another user they want to chat with. Also, we'll send along data that looks like below when triggering this event:

    {
      from_user, // ID of the user initiating the chat
      to_user, // ID of the other user
      from_user_notification_channel,// notificaction channel for the user intiating the chat
      to_user_notification_channel, // notificaction channel of the other user
      channel_name, // The channel name where both can chat
    }

In the data above, we have:

  • from_user — The user that triggered the event (the user starting the conversation).
  • to_user — The other user.
  • from_user_notification_channel — Notification channel for the user initiating the chat (for example private-notification-1).
  • to_user_notification_channel — Notification channel for the other user (for example private-notification-2).
  • channel_name — The channel where both can exchange messages.

The notification channels for users is unique since we are making use of their IDs.

How do we generate the channel_name?

We need a way to generate a channel name for the two users since they need to be on the same channel to chat. Also, the name should not be re-used by other users. To do this we’ll use a simple convention to name the channel - "private-chat__" (for example "private-chat_1_2"). Once we get the channel we’ll store it in the channels table. So subsequently, before we generate a new channel name, we’ll query the database to check if there is an already generated channel for the users and use that instead.

After getting the channel_name, we’ll notify the other user (to_user_notification_channel) by triggering the new_chat event.

Once a user receives the new_chat event, we’ll subscribe that user to the channel name we got from the event and then start to listen for another event we’ll name new_message on the channel we just subscribed to. The new_message event will be triggered when a user types and submits a message.

This way, we’ll be able to subscribe users to channels dynamically so they can receive messages from any number of users at a time. Let’s go ahead and write the code.

Coding the Server

First, initialize Pusher’s Python library by adding the following code to the api/app.py file right after the app = Flask(__name__) line of code:

    # api/app.py

    # [...]

    pusher = pusher.Pusher(
        app_id=os.getenv('PUSHER_APP_ID'),
        key=os.getenv('PUSHER_KEY'),
        secret=os.getenv('PUSHER_SECRET'),
        cluster=os.getenv('PUSHER_CLUSTER'),
        ssl=True)

    # [...]

We already have our login and register endpoint ready from part two. We still need to create several endpoints:

  • /api/request_chat we will use this endpoint to generate a channel name where both users can communicate.
  • /api/pusher/auth the endpoint for authenticating Pusher Channels subscription
  • /api/send_message the endpoint to send message across users.
  • /api/users endpoint for getting all users from the database.
  • /api/get_message/<channel_id> we’ll use this endpoint to get all messages in a particular channel.

Request chat

We’ll make a request to the /api/request_chat endpoint to generate a channel name when users want to chat.

Recall, every user on our chat will have their private channel. To keep things simple, we used "private-notification_user_" to name the channel. Where is the ID of that user in the users table. This way every users will have a unique channel name we can use to notify them.

When users want to chat, they need to be on the same channel. We need a way to generate a unique channel name for both of them to use. This endpoint will generate such channel as "private-chat__", where from_user is the user ID of the user initiating the chat and to_user is the user ID of the other user. Once we generate the channel name, we will store it to our channels table. Now if the two users want to chat again, we don't need to generate a channel name again, we'll fetch the first generated channel name we stored in the database.

After the first user generates the channel name, we’ll notify the other users on their private channel, sending them the channel name so they can subscribe to it.

Add the below code to api/app.py to create the endpoint:

    # api/app.py
    [...]
    @app.route('/api/request_chat', methods=["POST"])
    @jwt_required
    def request_chat():
        request_data = request.get_json()
        from_user = request_data.get('from_user', '')
        to_user = request_data.get('to_user', '')
        to_user_channel = "private-notification_user_%s" %(to_user)
        from_user_channel = "private-notification_user_%s" %(from_user)

        # check if there is a channel that already exists between this two user
        channel = Channel.query.filter( Channel.from_user.in_([from_user, to_user]) ) \
                               .filter( Channel.to_user.in_([from_user, to_user]) ) \
                               .first()
        if not channel:
            # Generate a channel...
            chat_channel = "private-chat_%s_%s" %(from_user, to_user)

            new_channel = Channel()
            new_channel.from_user = from_user
            new_channel.to_user = to_user
            new_channel.name = chat_channel
            db_session.add(new_channel)
            db_session.commit()
        else:
            # Use the channel name stored on the database
            chat_channel = channel.name

        data = {
            "from_user": from_user,
            "to_user": to_user,
            "from_user_notification_channel": from_user_channel,
            "to_user_notification_channel": to_user_channel,
            "channel_name": chat_channel,
        }

        # Trigger an event to the other user
        pusher.trigger(to_user_channel, 'new_chat', data)

        return jsonify(data)
    [...]

In the preceding code:

  • First of all, we created a route named /api/request_chat where users can get a channel name where they can chat.
  • We also protected the route to check for JWT token using @jwt_required.
  • Next, we get the ID of the user initiating the chat and the ID of the other participating user.
  • Next, we check if there is already a chat channel created for the two users in the database. If the channel name already exists, we return the channel else we generate a new channel for the users then save it to the database.
  • Then using pusher.trigger(), we trigger an event named new_chat to the other user’s private channel.
  • Finally, we return a JSON object containing the details of the channel name created.

Authenticate Channel subscriptions

Since we are using a private channel, we need to authenticate every user subscribing to the channel. We’ll make a request to the /api/pusher/auth endpoint to authenticate channels.

Add the below code to create the endpoint to authenticate channels in api/app.py.

    # api/app.py
    [...]
    @app.route("/api/pusher/auth", methods=['POST'])
    @jwt_required
    def pusher_authentication():
        channel_name = request.form.get('channel_name')
        socket_id = request.form.get('socket_id')

        auth = pusher.authenticate(
            channel=channel_name,
            socket_id=socket_id
        )

        return jsonify(auth)
    [...]

Pusher will make a request to this endpoint to authenticate channels, passing along the channel name and socket_id of the logged in user. Then, we call pusher.authenticate() to authenticate the channel.

Sending messages

When a user sends a message, we’ll save the message to the database and notify the other user. We’ll make a request to the /api/send_message endpoint for sending messages.

Add the following code to api/app.py.

    # api/app.py
    [...]
    @app.route("/api/send_message", methods=["POST"])
    @jwt_required
    def send_message():
        request_data = request.get_json()
        from_user = request_data.get('from_user', '')
        to_user = request_data.get('to_user', '')
        message = request_data.get('message', '')
        channel = request_data.get('channel')

        new_message = Message(message=message, channel_id=channel)
        new_message.from_user = from_user
        new_message.to_user = to_user
        db_session.add(new_message)
        db_session.commit()

        message = {
            "from_user": from_user,
            "to_user": to_user,
            "message": message,
            "channel": channel
        }

        # Trigger an event to the other user
        pusher.trigger(channel, 'new_message', message)

        return jsonify(message)
    [...]
  • We created a POST request route which expects some data to be sent along:
    • from_user - The user sending the message.
    • to_user - The other user on the chat receiving the message.
    • message - The chat message.
    • channel - The channel name where both of the users are subscribed to.
  • Next, we save the data to the database using the Message() class.
  • Then finally, we trigger an event named new_message to the channel name that will be sent from the request data and then return the information as JSON.

Get all users

We’ll make a request to the /api/users endpoint to get all users. Add the below code to api/app.py:

    # api/app.py
    [...]
    @app.route('/api/users')
    @jwt_required
    def users():
        users = User.query.all()
        return jsonify(
            [{"id": user.id, "userName": user.username} for user in users]
        ), 200
    [...]

Get messages from a channel

We’ll make a request to the /api/get_message/<channel_id> endpoint to get all messages sent in a channel. Add the below code to api/app.py:

    # api/app.py
    [...]
    @app.route('/api/get_message/<channel_id>')
    @jwt_required
    def user_messages(channel_id):
        messages = Message.query.filter( Message.channel_id == channel_id ).all()

        return jsonify([
            {
                "id": message.id,
                "message": message.message, 
                "to_user": message.to_user,
                "channel_id": message.channel_id,  
                "from_user": message.from_user, 
            } 
            for message in messages
        ])
    [...]

Coding the Client

Authenticate users

On our current view, we have the login form and the chat interface visible at the same time. Let’s make the login form only visible when the user is not logged in.

To fix it, add a condition to check if the user is authenticated in src/App.vue:

    // ./src/App.vue

    [...]
    <Login v-if="!authenticated" v-on:authenticated="setAuthenticated" />
    <b-container v-else>
    [...]

We are using a v-if directive to check if authenticated is false so we can render the login component only. Since authenticated is not defined yet, it will resolve to undefined which is false, which is ok for now.

Load up the app on your browser to confirm that only the login form is visible.

Next, update the src/components/Login.vue component with the below code to log users in:

    // ./src/components/Login.vue

    [...]
    <script>
    export default {
      name: "Login",
      data() {
        return {
          username: "",
          password: "",
          proccessing: false,
          message: ""
        };
      },
      methods: {
        login: function() {
          this.loading = true;
          this.axios
            .post("/api/login", {
              username: this.username,
              password: this.password
            })
            .then(response => {
              if (response.data.status == "success") {
                this.proccessing = false;
                this.$emit("authenticated", true, response.data.data);
              } else {
                this.message = "Login Faild, try again";
              }
            })
            .catch(error => {
              this.message = "Login Faild, try again";
              this.proccessing = false;
            });
        }
      }
    };
    </script>
    [...]

In the preceding code:

  • We are making a POST request to /api/login to authenticate our users.
  • If the login was successful, we’ll emit an event named authenticated so we can act on it in the src/App.vue file. We also passed some data in the event:
    • true - to indicate the login was successful
    • response.data.data - contains details of the logged in user

Next, add some state of the src/App.vue file in the <script> section:

    // ./src/App.vue

    [...] 
      data: function() {
        return {
          messages: {},
          users: [],
          active_chat_id: null,
          active_chat_index: null,
          logged_user_id: null,
          logged_user_username: null,
          current_chat_channel: null,
          authenticated: false
        };
      },
    [...]

So that the entire <script> section looks like below:

    // ./App.vue

    import MessageInput from "./components/MessageInput.vue";
    import Messages from "./components/Messages.vue";
    import NavBar from "./components/NavBar.vue";
    import Login from "./components/Login.vue";
    import Users from "./components/Users.vue";
    import Pusher from "pusher-js";

    let pusher;

    export default {
      name: "app",
      components: {
        MessageInput,
        NavBar,
        Messages,
        Users,
        Login
      },
      data: function() {
        return {
          authenticated: false,
          messages: {},
          users: [],
          active_chat_id: null,
          active_chat_index: null,
          logged_user_id: null,
          logged_user_username: null,
          current_chat_channel: null
        };
      },
      methods: {},
    };

We defined some default states of data which we will use. For example, we’ll use the authenticated: false state to check if a user is authenticated or not.

Recall that in the Login component, we emitted an event when a user logs in successfully. Now we need to listen to that event on the src/App.vue component so as to update the users states.

Add a function to set authenticated users information to src/App.vue in the methods block:

    // ./src/App.vue

    [...]
      data: function() {
        return {
          authenticated: false,
          messages: {},
          users: [],
          active_chat_id: null,
          active_chat_index: null,
          logged_user_id: null,
          logged_user_username: null,
          current_chat_channel: null
        };
      },
      methods: {
        async setAuthenticated(login_status, user_data) {

          // Update the states
          this.logged_user_id = user_data.id;
          this.logged_user_username = user_data.username;
          this.authenticated = login_status;
          this.token = user_data.token;

          // Initialize Pusher JavaScript library
          pusher = new Pusher(process.env.VUE_APP_PUSHER_KEY, {
              cluster: process.env.VUE_APP_PUSHER_CLUSTER,
              authEndpoint: "/api/pusher/auth",
              auth: {
                headers: {
                  Authorization: "Bearer " + this.token
                }
              }
          });

          // Get all the users from the server
          const users = await this.axios.get("/api/users", {
            headers: { Authorization: "Bearer " + this.token }
          });

          // Get all users excluding the current logged user
          this.users = users.data.filter(
            user => user.userName != user_data.username
          );

        },
      },
    };
    [...]

In the code above:

  • We created a new function named setAuthenticated which accepts the information we passed along when emitting the authenticated event in the Login.vue file.
  • After updating the component state with the logged in user information, we made a request to /api/users to get all registered users.
  • Then we initialize Pusher JavaScript library
  • Finally, we remove the current log users from the users list we got and then update the users state.

Finally, pass down the users we fetched to the Users.vue component. Update the Users component in src/App.vue:

    // ./src/App.vue
    [...]
    <Users :users="users" v-on:chat="chat" />
    [...]

Here we passed the users list down to the Users.vue component so we can render them. Also, using the v-on directive we listen for an event chat which will be triggered from Users.vue whenever a user is clicked to start up a chat.

Subscribe the user to a channel

Add the below code to the setAuthenticated function in src/App.vue to subscribe the user to a channel when they are logged in:

    // ./src/App.vue

    [...]
      methods: {
        async setAuthenticated(login_status, user_data) {
          [...]
          var notifications = pusher.subscribe(
            `private-notification_user_${this.logged_user_id}`
          );

          notifications.bind("new_chat", data => {
            const isSubscribed = pusher.channel(data.channel_name);
            if (!isSubscribed) {
              const one_on_one_chat = pusher.subscribe(data.channel_name);

              this.$set(this.messages, data.channel_name, []);

              one_on_one_chat.bind("new_message", data => {
                // Check if the current chat channel is where the message is coming from
                if (
                  data.channel !== this.current_chat_channel &&
                  data.from_user !== this.logged_user_id
                ) {
                  // Get the index of the user that sent the message
                  const index = this.users.findIndex(
                    user => user.id == data.from_user
                  );
                  // Set the has_new_message status of the user to true
                  this.$set(this.users, index, {
                    ...this.users[index],
                    has_new_message: true
                  });
                }

                this.messages[data.channel].push({
                  message: data.message,
                  from_user: data.from_user,
                  to_user: data.to_user,
                  channel: data.channel
                });
              });
            }
          });

        },
      },
    };
    [...]
  • First, we subscribe the user to their private channel using var notifications = pusher.subscribe(… once they log in.
  • Next, we bind that channel to an event we named new_chat so we can get a notification when a user is requesting for a new chat.
  • Then if there is any new chat request, we’ll subscribe that user to the channel sent along and also bind that channel to a new event named new_message.
  • Finally, if there is a message coming to the event - new_message, we append the message to the “messages” property in the data component. Also, if the user is not currently chatting on the channel where they received the message, we’ll notify them of the message.

Get all messages in a channel

Add a function to fetch all messages in a chat channel to src/App.vue in the methods block:

    // ./src/App.vue
    [...]
        getMessage: function(channel_name) {
          this.axios
            .get(`/api/get_message/${channel_name}`, {
              headers: { Authorization: "Bearer " + this.token }
            })
            .then(response => {
              this.$set(this.messages, channel_name, response.data);
            });
        },
    [...]

The chat function

We'll call the function when a user clicks on another user they want to chat with to prepare the chat channel.

Add the below code to the methods block of src/App.vue

    // ./src/App.vue

    [...]
        chat: function(id) {
          this.active_chat_id = id;

          // Get index of the current chatting user...
          this.active_chat_index = this.users.findIndex(
            user => user.id == this.active_chat_id
          );

          // Set the has_new_message status of the user to true
          this.$set(this.users, this.active_chat_index, {
            ...this.users[this.active_chat_index],
            has_new_message: false
          });

          this.axios
            .post(
              "/api/request_chat",
              {
                from_user: this.logged_user_id,
                to_user: this.active_chat_id
              },
              { headers: { Authorization: "Bearer " + this.token } }
            )
            .then(response => {
              this.users[this.active_chat_index]["channel_name"] =
                response.data.channel_name;

              this.current_chat_channel = response.data.channel_name;

              // Get messages on this channel
              this.getMessage(response.data.channel_name);

              var isSubscribed = pusher.channel(response.data.channel_name);

              if (!isSubscribed) {
                var channel = pusher.subscribe(response.data.channel_name);

                this.$set(this.messages, response.data.channel_name, []);

                channel.bind("new_message", data => {
                 //Check if the current chat channel is where the message is comming from
                  if (
                    data.channel !== this.current_chat_channel &&
                    data.from_user !== this.logged_user_id
                  ) {
                    // Set the has_new_message status of the user to true
                    this.$set(this.users, this.active_chat_index, {
                      ...this.users[this.active_chat_index],
                      has_new_message: true
                    });
                  }

                  this.messages[response.data.channel_name].push({
                    message: data.message,
                    from_user: data.from_user,
                    to_user: data.to_user,
                    channel: data.channel
                  });
                });
              }
            })
            .catch(function(error) {
              console.log(error);
            });
        },
    [...]
  • We make a request to /api/request_chat to get the channel name for the chat session.
  • Next, we update the state of the current_chat_channel with the channel returned using: this.current_chat_channel = response.data.channel_name;
  • Then we subscribe the user to the channel name returned and then bind the channel to an event we named new_message. Once we receive a new message, we add the message to the messages state.
  • Also, in the bound new_message event, we check if the message received is between the current chat channel, else we display an alert notifying the user that they have a new message from another user.

We are already passing the messages to the Messages.vue component so any new message will be rendered on the page dynamically. Take a look at the Messages component in src/App.vue:

    <Messages 
      v-else 
      :active_chat="active_chat_id" 
      :messages="messages[current_chat_channel]"
    />

Sending messages

Now add the function for sending messages to src/App.vue:

    // ./src/App.vue
    [...]
        send_message: function(message) {
          this.axios.post(
            "/api/send_message",
            {
              from_user: this.logged_user_id,
              to_user: this.active_chat_id,
              message: message,
              channel: this.current_chat_channel
            },
            { headers: { Authorization: "Bearer " + this.token } }
          );
        },
    [...]

We’ll call this function whenever a user submits a message.

Take a look at the MessageInput.vue component which is the component for sending messages. You will notice that after the user submits a message, we trigger an event named send_message passing along the message text.

Now we will listen to the event and send the message to the server once we get the event. Update the MessageInput component in the <template> section of src/App.vue:

    [...]
    <MessageInput v-on:send_message="send_message" />
    [...]

Here, we listen for the event using the v-on directive and then call the function we just added (send_message) once we get the event.

Test out the chat by opening the app in two different tabs on your browser.

Get sentiments from messages

To get the sentiment from messages, we’ll use the TextBlob Python library which provides a simple API for common natural language processing (NLP).

Install TextBlob

From your terminal, make sure you are in the api folder. Also, make sure your virtualenv is activated. Then execute the below function.

    # Install the library
    $ pip install -U textblob

    # Download NLTK corpora
    $ python -m textblob.download_corpora lite

This will install TextBlob and download the necessary NLTK corpora (trained models).

Import TextBlob to api/app.py:

    from textblob import TextBlob

Add a function to get the sentiment of a message to api/app.py

    # ./api/app.py

    def getSentiment(message):
            text = TextBlob(message)
            return {'polarity' : text.polarity }

The sentiment property returns a tuple of the form (polarity, subjectivity) where polarity ranges from -1.0 to 1.0 and subjectivity ranges from 0.0 to 1.0. We will only use the polarity property.

Next, include the sentiment on the return statement in the user_messages function in api/app.py:

    [...]
        return jsonify([
            {
                "id": message.id,
                "message": message.message,
                "to_user": message.to_user,
                "channel_id": message.channel_id,
                "from_user": message.from_user,
                "sentiment": getSentiment(message.message)
            }
            for message in messages
        ])
    [...]

And also update the data we trigger to Pusher in the send_message function in api/app.py:

    [...]
        message = {
            "from_user": from_user,
            "to_user": to_user,
            "message": message,
            "channel": channel,
            "sentiment": getSentiment(message)
        }
    [...]

Now we have the sentiment of text. Let’s display the related emoji beside messages in the view.

Next update the code in src/components/Messages.vue to display the emoji sentiment:

    [...]
    <template>
       <div>
        <div v-for="(message, id) in messages" v-bind:key="id"> 
            <div class="chat-message col-md-5" 
              v-bind:class="[(message.from_user == active_chat) ? 'to-message' : 'from-message offset-md-7']">
              {{message.message}}
              {{ getSentiment(message.sentiment.polarity) }}
            </div> 
        </div>
       </div>
    </template>
    <script>
    export default {
      name: "Messages",
      data() {
        return {
          happy: String.fromCodePoint(0x1f600),
          neutral: String.fromCodePoint(0x1f610),
          sad: String.fromCodePoint(0x1f61f)
        };
      },
      methods: {
        getSentiment(sentiment) {
          if (sentiment > 0.5) {
            return this.happy;
          } else if (sentiment < 0.0) {
            return this.sad;
          } else {
            return this.neutral;
          }
        }
      },
      props: {
        messages: Array,
        active_chat: Number
      }
    };
    </script>
    [...]

Here, we defined the emotions for each sentiment score.

Then finally update the bound event for new_message to include the sentiment data. Update src/App.vue as below in the setAuthenticated function:

    [...]
    channel.bind("new_message", data => {
      [...]
      this.messages[data.channel].push({
        message: data.message,
        sentiment: data.sentiment,
        from_user: data.from_user,
        to_user: data.to_user,
        channel: data.channel
      });
    });
    [...]

And also on the bound event in chat function to include the sentiment data in src/App.vue file:

    [...]
    one_on_one_chat.bind("new_message", data => {
      [...]
      this.messages[response.data.channel_name].push({
        message: data.message,
        sentiment: data.sentiment,
        from_user: data.from_user,
        to_user: data.to_user,
        channel: data.channel
      });
    });
    [...]

And that’s it! congrats. If you test the app again, you will see the sentiments of each chat messages.

Note: If you are having issue with displaying the emoji in your browsers, you might want to use the latest version of Chrome or Mozilla to display it.

Conclusion

In this tutorial of the series, we have successfully built a one-to-one private chat with sentiment analysis using Pusher Channels to add realtime functionality.

You can get the complete code on GitHub.

Clone the project repository
  • Flask
  • JavaScript
  • Python
  • Social
  • Social Interactions
  • Vue.js
  • Channels

Products

  • Channels
  • Chatkit
  • Beams

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.