🎉 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

Monitor chat rooms for prohibited language and keywords using Vue

  • Gideon Onwuka

May 29th, 2019
You will need to have Node 8.9+, npm 3+ and ngrok installe on your machine.

Chat rooms can be highly toxic. They are the frequent haunt of trolls and spammers. But you can keep things in check by automatically scanning through sent messages for the inclusion of particular keywords.

In this tutorial, I’m going to show you how you can monitor your chat rooms for prohibited keywords. If a message contains a prohibited keyword, we’ll delete the message and replace it with “DELETED”.

Here is a preview of what we’ll be building in a moment:

The complete code for this tutorial is on GitHub.

Prerequisites

To follow along with this tutorial, you will need to:

- Have a basic knowledge of JavaScript.
- Have a basic understanding of [Vue.js](https://vuejs.org/).
- Have [Node.js](https://nodejs.org/) installed on your system (version 8.9 or above).
- Have npm installed on your system (version 3 or above).
- Have [ngrok](https://ngrok.com/) installed.

If you have all those installed, then let's get started!

Client setup - create a Vue project

Vue provides a CLI for scaffolding a new Vue project. First, you'll need to install the Vue CLI globally on your system (if you don't have it installed already). After that, we’ll create a new Vue project with the CLI commands.

Now, create a new Vue project by running the following commands in any convenient location on your system:

    # Install Vue CLI globally on your system
    $ npm install -g @vue/cli

    # Create a new Vue project (for the prompt that appears, press enter to select the default preset.)
    $ vue create chatkit-mod

    # Change your directory to the project directory
    $ cd chatkit-mod

    # Install Chatkit client SDK
    $ npm install @pusher/chatkit-client

    # Install axios - a library for making requests
    $ npm install axios

    # Run the app!
    $ npm run serve

Accessing the URL displayed in your terminal will take you to a Vue default page.

Create a Chatkit app

To get started with Chatkit, you’ll first need to create a Chatkit instance. Head to the dashboard, hit Create, then give your instance a name.

Once you’ve created your Chatkit instance, head to the Credentials tab and take note of your Instance Locator and Secret Key.

On the same Credentials tab, enable test token provider and then note your Test Token Provider Endpoint:

Note that the test token is not to be used for productions. You can read more on how you can create a token provider in your Node app here if you are going live.

Next, create a .env file in the root folder of the project and update your Chatkit keys to it:

    # ./.env

    VUE_APP_SERVER=http://localhost:3000
    APP_PORT=3000

    CHATKIT_SECRET_KEY=<YOUR_CHATKIT_SECRET_KEY>
    VUE_APP_CHATKIT_INSTANCE_LOCATOR=<YOUR_CHATKIT_INSTANCE_LOCATOR>
    VUE_APP_CHATKIT_TOKEN_ENDPOINT=<YOUR_CHATKIT_TOKEN_ENDPOINT>

Remember to replace <YOUR_CHATKIT_SECRET_KEY>, <YOUR_CHATKIT_INSTANCE_LOCATOR>, and <YOUR_CHATKIT_TOKEN_ENDPOINT> with your correct app keys you have noted above.

Create users

Next, create a number of users so we can use them to chat later.

Create rooms

Finally, create a number of rooms from your dashboard so we can use them for testing.

Set up the server

Next, create a new folder named server in the root folder of the project. Then, open up a new terminal and change your current directory to the server folder and then install the following dependencies that we’ll need for the server:

    # Install Express
    $ npm install express cors body-parser @pusher/chatkit-server path

The above dependencies include:

  • express A Node framework we are using to build our server
  • @pusher/chatkit-server Chatkit Node.js SDK
  • dotenv An npm package for parsing configs from .env files
  • cors, bodyParser, path

Create a new file named app.js in the server folder. Then create a Node app by adding the below code to server/app.js:

    // ./server/app.js

    const express = require('express');
    const cors = require('cors');
    const bodyParser = require('body-parser');
    const Chatkit = require('@pusher/chatkit-server');
    const resolve =  require("path").resolve;
    require('dotenv').config({path:  resolve(__dirname, "../.env")})

    const app = express(); // create an express app
    const port = process.env.APP_PORT

    // Initialises chatkit client
    const chatkit = new Chatkit.default({
        instanceLocator: process.env.VUE_APP_CHATKIT_INSTANCE_LOCATOR,
        key: process.env.CHATKIT_SECRET_KEY
    })

    app.use(cors());
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: true }));

    app.get('/', async (req, res) => {
        res.send({ hello: 'World!'});
    });

    app.listen(port, () => console.log(`Example app listening on port ${port}!`));

In the code above, after importing the packages that we have installed,

  • We created an Express app using const app = express();
  • Next, we initialisze the Chatkit SDK
  • Then finally, we created an endpoint - / (app.get('/',…) for testing to know if the app works. So, if you visit http://localhost:3000/, you will get a message - { hello: 'World!'}

Next, create a new endpoint for getting rooms that we have in our Chatkit app so we can list them in the chat.

Add the below code the server/app.js file:

    // ./server/app.js

    // [...]
    app.get('/get_rooms', (req, res) => {
        chatkit.getRooms({})
            .then(rooms => {
                res.status(200).send({
                    status: 'success',
                    data: rooms
                });
            })
            .catch(err => {
                res.status(200).send({
                    status: 'error',
                    message: err
                });
            })
    });
    // [...]

This will make a request to Chatkit's server to fetch the public rooms available in our app. It'll return a JSON containing an array of rooms if there is no error, otherwise, it will return a JSON data containing the error message.

Finally, start up the server:

    node app.js

If it starts successfully, you will see a message printed to the terminal - “Node app listening on port 3000!”

Building the chat interface

Now that we have both our client and server apps running, the next thing we’ll do is to create a simple chat interface so we can add the welcome action functionality.

Vue enables us to build reusable components which make up our app user interface. We’ll split the app UI into smaller components so we can build them separately:

For brevity’s sake, we’ll divide the app into four components:

  • Messages.vue - for listing messages
  • InputForm.vue - contains a form for sending message
  • Rooms.vue - for listing groups and contains a form for creating a new group
  • Login.vue - displays the login form

Create the Messages.vue, InputForm.vue, Login.vue and Rooms.vue files inside the src/components folder.

To keep the component file as minimal as possible, I have added all the CSS of the components to a single CSS file. Create the CSS file as App.css in the src folder of the project and then add the below styles to it:

    /* ./src/App.css */
    html,body{
        overflow: hidden; 
        width: 100%;
        height: 100%;
        box-sizing: border-box;
        background: #cfd3d3;
    }
    .main{
        width: 99%;
        height: 100vh;
        border: 1px solid green;
        display: grid;
        grid-template-columns: 1fr 4fr;
    }
    .message-area{
        display: grid;
        position: relative;
        grid-template-areas: "head"
                             "messages"
                             "input";
        height: 100vh;
        grid-template-rows: 50px auto 110px;
    }
    .room-wrapper{
       border: 1px solid gray;
       height: 100vh; 
       background: rgba(247, 250, 252, 0.945);
    }
    .search{
        display: block;
        padding: 6px;
    }
    .input-search {
        width: 93%;
        font-size: 17px;
        text-indent: 3px;
        outline: none;
        resize: none;
        flex-direction: row;
        padding: 10px;
        overflow:hidden;
        border:1px solid #556677;
    }
    .rooms{
        margin-top: 3px;
        overflow-y: auto;
        height: 92%;
    }
    .room {
       border-top: 1px solid gray;
       padding: 15px;
       cursor: pointer;
    }
    .room:hover{
        background: rgba(222, 226, 230, 0.952);
    }
    .input-area{
        grid-area: input;
        display: grid;
        grid-template-columns: 1fr;
        /* background: antiquewhite; */
        background: rgba(247, 250, 252, 0.945);
        padding: 20px;
    }
    .input{
        width: 100%;
    }
    .input-message {
        width: 98%;
        border-radius: 8px;
        border: gray;
        font-size: 17px;
        text-indent: 3px;
        display: block;
        outline: none;
        resize: none;
        overflow: auto;
        flex-direction: row;
        padding: 10px;
        overflow:hidden;
        border:3px solid #556677;
    }
    .message-header{
        grid-area: head;
        display: grid;
        grid-template-columns: 1fr 1fr;
        background: rgba(247, 250, 252, 0.945);
        border-bottom: 1px solid rgb(110, 106, 106);
    }
    .message-header-left{
        text-align: left;
        padding: 10px;
    }
    .message-header-right{
        text-align: right;
        padding: 10px;
    }
    .messages {
        grid-area: messages;
        overflow-y: auto;
        overflow-x: hidden;
        padding: 15px 30px;
        background: #929292ad;
    }
    .float-left {
        float: left;
        clear: both;
        background-color: white;
        color: rgba(28, 19, 64, 0.87);
    }
    .float-right {
        float: right;
        clear: both;
        color: #000000;
        background: #cbe4cbf2;
    }
    .message {
        border-radius: 3px;
        width: fit-content;
        padding: 7px;
        margin: 3px 0px;
        clear: both;
        max-width: 50%;
        min-width: 10%;
        word-wrap: break-word;
        font-size: 18px;
    }
    .chat-name{
       font-size: 17px;
       margin: 1px 0px 7px 0px;
       color: #177a2d;
    }
    .login {
        width: 500px;
        border: 1px solid #cccccc;
        background-color: #ffffff;
        margin: auto;
        margin-top: 30vh;
        box-sizing: border-box;
    }
    .input-form {
        margin-bottom: 9px;
        display: block;
    }
    .input{
        display: block;
        margin: 15px;
        width: 93%;
        height: 40px;
        outline: none;
        font-size: 17px;
        text-indent: 5px;
    }
    .submit{
        display: block;
        width: 95%;
        margin: auto;
        padding: 10px;
        margin-bottom: 20px;
        font-size: 17px;
        cursor: pointer;
    }

The Login component

We’ll be using the Single File Component structure for our components.

Add the Login component markup:

    <!-- ./src/components/Login.vue -->
    <template>
        <div class="login">
          <div class="form"> 
                <form @submit.prevent="login">
                    <input 
                        type="text" 
                        class="input" 
                        placeholder="Enter a username"
                        v-model="username"
                        required>
                    <button type="submit" class="submit" :disabled="status === 'processing'"> Login </button>
                </form>
          </div>
        </div>
    </template>

Here, we are making use of @``*submit*``.``*prevent* to prevent the page from reloading once a user submits the form and passed in the login function so it is called instead.

Next add the script for the Login component:

    <!-- ./src/components/Login.vue -->
    <script>
    export default {
      name: "login",
      props: ['status'],
      data () {
        return {
            username: "",
        }
      },
      methods: {
        login() {
            this.$emit('login', this.username)
            this.username = ""
        }
      }
     }
    </script>

Here, we define the login function that is called once the user submits the form. In the function, we emit an event named “login” so that we can process the login from the parent Component (App.vue) once the user submits the form.

The Rooms component

Next, add the Rooms component markup for the listing of rooms:

    <!-- ./src/components/Rooms.vue -->
    <template>
        <div class="room-wrapper">
            <div class="search">
                <input type="text" placeholder="search..." class="input-search">
            </div>
            <div class="rooms"> 
                <div class="room" 
                    v-for="(room, ind) in rooms"
                    :key="ind"
                    @click="joinRoom(room)"
                    :class="(activeRoom.id === room.id) ? 'active_room' :''"
                >
                    {{room.name}}
                </div>
            </div>
        </div>
    </template>

Using the v-for directive, we loop through the rooms available and list them. We also added an @``*click* event handler to listen to click events from the user when they want to join a room. Once a room is clicked, we call the joinRoom function.

Next, add the script of the Room's component:

    <!-- ./src/components/Rooms.vue -->
    <script>
    export default {
      name:'rooms',
      props: ['rooms'],
      data() {
        return {
          activeRoom: {}
        }
      },
      methods: {
        joinRoom(room) {
          this.activeRoom = room
          this.$emit('joinRoom', room)
        }
      }
    }
    </script>

    <style scoped>
      .active_room {
        background: rgba(197, 202, 207, 0.952);
      }
    </style>

Here, we define the joinRoom function, which we are calling on the template section. In the joinRoom function, we set the activeRoom state to the room that was just clicked. Then we emit an event named joinRoom to the parent component so we can add the user to the room.

The Messages component

Next, add the Messages component for the listing of messages:

    <!-- ./src/components/Messages.vue -->
    <template>
        <div ref="messages" class="messages">
            <div v-for="message in messages" :key="message.id">
                <div :class="['message-container message', messageDirection(message, 'float-')]"> 
                    <div class="chat-name"> {{ message.sender.name }} </div>
                    <template v-for="part in message.parts"> 
                        {{part.payload.content}}
                    </template>
                </div>
            </div>
        </div>
    </template>

Here, we loop through the messages that will be passed to this component and then list them. I have also added a float-left and float-right CSS that will position a message to the left and to the right respectively.

So the messageDirection function will determine if a message will be floated to the left or to the right of the page depending on who is sending the message.

Next, add the script section of the Messages component:

    <!-- ./src/components/Messages.vue -->
    <script>
    export default {
        name: "messages",
        props: ['messages', 'currentUser'],
        methods: {
          messageDirection(message, css='') {
            return (message.senderId !== this.currentUser.id) ? `${css}left` : `${css}right`
          }
        },
    }
    </script>

The InputForm component

Add the InputForm component markup for rendering the form for adding new messages:

    <!-- ./src/components/InputForm.vue -->
    <template>
        <div class="input-area"> 
            <div class="input">
                <textarea
                    v-if="activeRoom"
                    class="input-message" 
                    v-model="new_message" 
                    placeholder="Type a message" 
                    rows="1"
                    @keyup.shift.enter="resizeInput"
                    @keyup.exact.enter="sendMessage">
                </textarea>
                <div v-else style="text-align:center"> 
                    Click on a room to start chatting...
                </div>
            </div>
        </div>
    </template>

And then add the script section:

    <!-- ./src/components/InputForm.vue -->
    <script>
    export default {
      name: "input-from",
      props: ['activeRoom', ],
      data () {
        return {
            new_message: "",
        }
      },
      methods: {
        sendMessage(el) {
            if (!this.new_message) return;
            this.$emit('newMessage', this.new_message)
            this.new_message = ""
            el.target.style = 'height:auto;';
        },
        resizeInput(el) {
            if (el.target.scrollHeight < 80) {
                setTimeout(function() {
                    el.target.style = 'height:auto; padding:0';
                    el.target.style = 'height:' + el.target.scrollHeight + 'px';
                }, 0);
            }
        }
      }
    }
    </script>

The App component

This is the main component file that will house all the other components that we have created.

Next, replace the content of the App.vue file with the below:

    <!-- ./src/App.vue -->
    <template>
        <div id="app">
            <div v-if="!currentUser">
                <!-- The login component -->
                <Login @login="login" :status="status"/>
            </div>
            <div class="main" v-else>
                <!-- The rooms component -->
                <Rooms @joinRoom="subscribeToRoom" :rooms="rooms"/>
                <div class="message-area">
                    <div class="message-header"> 
                        <div class="message-header-left"> Group Name </div>
                        <div class="message-header-right">  @{{currentUser.id}} </div>
                    </div>
                    <!-- The messages component -->
                    <Messages 
                        :messages="messages" 
                        :currentUser="currentUser" 
                    />
                    <!-- The inputform component -->
                    <InputForm 
                        @newMessage="addMessage"
                        @joinedRoom="joinedRoom=true" 
                        :activeRoom="activeRoom"
                    />
                </div>
            </div>
        </div>
    </template>

The is the overall mark-up of our app grouped together.

Notice the components that we created earlier:

  • <Login… Remember, we emitted an event named *login* once the user submits the login form. Here, we are using the @login (short form of v-on:login) and thereby call a login function to process the login.
  • <Rooms… As with the Login component, we listen to the joinRoom event that will be triggered on the component using @``*joinRoom* and then handle the event by calling subscribeToRoom function.
  • <Messages… The same thing applies as with other components
  • <InputForm… The same thing applies as with other component

Next, add the script section of the App.vue file:

    // ./src/App.vue

    <script>
    import { ChatManager, TokenProvider } from '@pusher/chatkit-client'
    import axios from 'axios'

    import Messages from '@/components/Messages'
    import InputForm from '@/components/InputForm'
    import Rooms from '@/components/Rooms'
    import Login from '@/components/Login'

    import './App.css'

    export default {
      name: 'app',
      components: {
        Messages,
        InputForm,
        Rooms,
        Login
      },
      data() {
        return {
            messages: [],
            chatManager: null,
            currentUser: null,
            rooms: [],
            activeRoom: null,
            status: null
        }
      },
      methods: {
      },
    }
    </script>

Now, reload the app to confirm that the page renders properly.

Making the chat work

So far, we have our chat interface ready but we still can’t converse because we are yet to connect the app to Chatkit. We’ll be doing so next.

To start using the SDK, we first need to initialize it. Do so by adding the below function in the methods: {…} block in the App.vue file:

    // ./src/App.vue

    // [...]
        setupChatKit(username) {
          // Initialise the token provider
          const tokenProvider = new TokenProvider({
            url: process.env.VUE_APP_CHATKIT_TOKEN_ENDPOINT
          });

          // Initialise the chatkit manager
          const chatManager = new ChatManager({
            instanceLocator: process.env.VUE_APP_CHATKIT_INSTANCE_LOCATOR,
            userId: username,
            tokenProvider: tokenProvider
          });

          chatManager
            .connect()
            .then( currentUser => {
              this.currentUser = currentUser
              // Fetch rooms
              axios.get(`${process.env.VUE_APP_SERVER}/get_rooms`)
                .then(data => {
                  this.rooms = data.data.data
                })

            })
            .catch( error => {
              this.status = 'error'
            });
        },
    // [...]

When we want to initialize Chakit, we can call this function to do so.

Add the login function in the methods: {…} block in the App.vue file:

    // ./src/App.vue

    // [...] 
       login(username) {
            this.status = 'processing'
            this.setupChatKit(username)
        },
    // [...]

So if the user logs in, then we initialize the Chatkit app. Note that we are not doing any real authentication here. This is the best place to authenticate your users if you want to restrict access to your chat app.

Next, let’s add a function for subscribing to rooms. To receive notification from a room like when a new message is added to a room, we need to be subscribed to that room.

Add the below function in the methods: {…} block of the App.vue file:

    // ./src/App.vue

    // [...]
        subscribeToRoom(room) {
            this.messages = []
            this.currentUser
                .subscribeToRoomMultipart({
                    roomId: room.id,
                    hooks: {
                        onMessage: message => {
                            // Filter the message and remove duplicate.
                            // Deleted message is removed here...
                            this.messages = this.messages.filter(msg => msg.id !== message.id)
                            this.messages.push(message)
                        }
                    },
                    messageLimit: 40
                })

            this.activeRoom = room
        },
    // [...]

Next, add a function for adding new messages to the methods: {…} block of the App.vue file:

    // ./src/App.vue

    // [...]
        addMessage(message) {
            this.currentUser.sendSimpleMessage({
                    roomId: this.activeRoom.id,
                    text: message,
              })
        },
    // [...]

Monitor rooms for prohibited keywords

Good! At this point, users can converse in a room. What is left is to monitor rooms for prohibited keywords.

Now let’s define the prohibited keywords and check if a message contains the prohibited keyword. Add the below code to the Node server file - server/app.js:

    ./server/app.js

    // [...]
    function checkData (message) {

      // keywords that are not allowed on the chat
      // You can enter more here...
      const prohibited_keywords = [
        "shit",
        "resist",
        "hate"
      ];

      let regEx = new RegExp(`\\b(${prohibited_keywords.join('|')})\\b`, 'gi')

      return regEx.test(message)
    }
    // [...]

In the function above, we defined a variable - prohibited_keywords - that holds an array of keywords that we don’t want to see in chat rooms. You can add more keywords that you wish to monitor. Then we write a regular expression to check if the message contains the prohibited keywords or not. The function returns true if the message contains the prohibited keyword and false otherwise.

Next, add a function for deleting the message if it contains the prohibited keyword:

    // ./server/app.js

    // [...]
    function trbotDeleteMessage(messageId) {
      return chatkit.deleteMessage({
        id: messageId
      })
    }
    // [...]

This will irreversibly delete a message and replaces it with a generic tombstone - “DELETED”.

Chatkit’s webhook

A webhook is an HTTP POST request made to an endpoint of your choosing to notify you of some event. With the webhook, we can tell Chatkit to make a request to a given URL once a certain type of events happens.

For our use-case, we want to be notified when a new message is added to a room. We are most concerned with the Message Created event that will be triggered when a message is added to a room.

Create the webhook endpoint

Finally, let’s create an endpoint for the webhook.

When Chatkit makes a POST request to our endpoint, it sends along with it the details of the message that was deleted in this format.

Add the function for creating the /webhook endpoint:

    // ./server/app.js

    // [...]
    app.post("/webhook", async (req, res) => {
      const messages = req.body.payload.messages;
      const messageId = messages\[0\]['id']

      const checkKeyword = checkData(messages\[0\]["parts"]\[0\]["content"]);

      // Return response early - see https://pusher.com/docs/chatkit/webhooks#retry-strategy
      res.sendStatus(200);

      if (checkKeyword) {
        trbotDeleteMessage(messageId);
      }

    });
    // [...]

Here, when Chatkit makes a POST request to our endpoint, we extract the message_id from the data sent. Then we check the message if it contains the prohibited keyword. If the message contains the prohibited keyword, then we call the trbotDeleteMessage function to delete the message.

Enable the webhook

Although, we have our endpoint for translating message ready, which is accessible from http://localhost:3000/webhook. It can only be accessed from your local system. We need to expose the URL so it can be accessible by Chatkit which we can do using ngrok.

Open up ngrok and expose the URL:

    ./ngrok http 3000

Now note any of the Forwarding URLs which can now be accessed from anywhere.

Head to your Chatkit Dashboard and enable the webhook:

  • Click on the Settings tab.
  • Fill in the NAME as trbot or any convenient name.
  • Fill in the TARGET URL - the ngrok generated URL (eg: https://*.ngrok.io/webhook). Remember to add the /webhook after the URL.
  • Enter a WEBHOOK SECRET. You can use any text as we are not making use of it in this tutorial.
  • Then select the Message Created trigger option.

The webhook secret is for verifying requests to your webhook. This is for you to make sure the request is coming from Chatkit. You can read more on how to add this here.

Testing the app

Now let’s test the app to see that it works.

  • Restart the Vue and Node server
  • Open the Vue app in two or more tabs on your browser
  • Log in to the app with a user that you have created
  • Now send a message that contains a prohibited keyword

Conclusion

In this tutorial, you learned how to add welcome actions to your Chatkit app. We explored a way by which you can add welcome action for new users joining a room using the Chatkit webhook feature.

You can find the complete code of this tutorial on GitHub.

Clone the project repository
  • Chat
  • CSS
  • JavaScript
  • Node.js
  • Vue.js
  • Chatkit

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.