We're hiring
Products

Channels

Beams

Chatkit

DocsTutorialsSupportCareersPusher Blog
Sign InSign Up
Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Sign InSign Up

Limit new users from posting in a chatroom for a specified wait time

  • Lanre Adelowo
April 4th, 2019
You will need Node 8+ installed on your machine.

Running and managing a community is hard. Maybe the most painful part of that is dealing with spam from humans and bots alike. There are many ways to deal with this:

  • Email verification. You verify the user has access to the provided email by requiring a link to be visited.
  • Moderation. The first few posts a user makes will not go live immediately but will require manual review after which subsequent posts will go live once the user posts them.
  • Require the user to have spent a reasonable period of time as a member before a message can be posted. If the period is set as 10 minutes, the user will only be able to create a message if they have spent more than 10 minutes as a member of the chat room. Discord for example calls this Tableflip-status verification as described here.

In this tutorial, we will implement the third option. You can find a demo of what the final application is going to look like below:

Prerequisites

  • NodeJS >=8
  • A Pusher Chatkit account and application
  • A way to serve HTML files. If you have Python (>=3) installed, that can be used else you will need to install http-server with npm install -g http-server.

This first thing to do is to create a directory that will house the project. We will be naming it pusher-tableflip-status, so you can go ahead to create that. We will also require two sub-directories called client and server. The command you can use for all of the above is:

    $ mkdir pusher-tableflip-status
    $ cd pusher-tableflip-status
    $ mkdir client server

Building the backend

For creating users and authenticating them, we need to create a server. To do that, we’ll make use of Express to create a NodeJS server. You will need to cd into the server folder and run the command npm init -y .

The next step of action is to install the additional dependencies required to build the server. Run the following command to do so:

    $ npm install express body-parser cors dotenv @pusher/chatkit-server -S

Once the above command succeeds, you will need to create a file named variables.env. This file will contain credentials required to connect to the Chatkit instance.

    CHATKIT_INSTANCE_LOCATOR=PUSHER_CHATKIT_INSTANCE_LOCATOR
    CHATKIT_SECRET_KEY=PUSHER_CHATKIT_SECRET_KEY

Remember to replace this with your own credentials.

The next thing to do is to create a new file called index.js as it will be the container for the server code. You can create it with:

    $ touch index.js

Copy and paste the following code in the newly created file:

    // pusher-tableflip-status/server/index.js

    require('dotenv').config({ path: 'variables.env' });

    const express = require('express');
    const bodyParser = require('body-parser');
    const cors = require('cors');
    const Chatkit = require('@pusher/chatkit-server');

    const app = express();

    const chatkit = new Chatkit.default({
      instanceLocator: process.env.CHATKIT_INSTANCE_LOCATOR,
      key: process.env.CHATKIT_SECRET_KEY,
    });

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

    const allUsers = [];

    app.post('/users', (req, res) => {
      let { username } = req.body;

      username = username.trim();

      chatkit
        .createUser({
          id: username,
          name: username,
        })
        .then(() => {
          const now = Date.now();

          allUsers.push({ name: username, timestamp: now });
          res.status(201).send({ created_at: now, skip_cooloff: false });
        })
        .catch(err => {
          if (err.error === 'services/chatkit/user_already_exists') {
            const user = allUsers.find(u => {
              return u.name === username;
            });

            if (user === undefined) {
              res.status(200).send({ created_at: 0, skip_cooloff: true });
              return;
            }

            res
              .status(200)
              .send({ created_at: user.timestamp, skip_cooloff: false });
          } else {
            res.status(err.status).json(err);
          }
        });
    });

    app.post('/authenticate', (req, res) => {
      const authData = chatkit.authenticate({
        userId: req.query.user_id,
      });
      res.status(authData.status).send(authData.body);
    });

    app.set('port', process.env.PORT || 5200);
    const server = app.listen(app.get('port'), () => {
      console.log(`Express running on port ${server.address().port}`);
    });

We have two routes on the server: the /users route takes a userId and creates a Chatkit user while the /authenticate route is meant to authenticate each user that tries to connect to our Chatkit instance and respond with a token (returned by chatkit.authenticate) if the request is valid.

Another thing that has been done here is to keep a record of the time each user registered, that will be useful as it will be used to inform the frontend if the user should have access to submitting a message or not.

We have used an in-memory store to save the data here. In a production system, it can be replaced with Redis or an actual database.

You can start the server on port 5200 by running node index.js in the terminal.

Building the frontend application

With the server done, we can now move to creating the client. Navigate to the client directory and create a new file, index.html. That can be done with the following command:

    $ touch index.html

In the newly created index.html, paste the following code:

    // pusher-tableflip-status/client/index.html

    <!DOCTYPE html>
    <html lang="en">

    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Chatbox</title>
      <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
    </head>

    <body style="padding-top: 100px; padding-bottom: 100px">
      <div class="container">
        <div class="row justify-content-center">
          <div class="col-md-8">
            <div class="card">
              <div class="card-body">
                <div id="join">
                  <form method="post" id="username-form">
                    <div class="form-group">
                      <div class='input-group'>
                        <input type='text' name='username' class="form-control" placeholder="Enter your username">

                        <div class='input-group-append'>
                          <button class='btn btn-primary'>Join</button>
                        </div>
                      </div>
                    </div>
                  </form>
                </div>

                <div id="chatbox" style="display: none">
                  <div class="row">
                    <div class="col-md-8">
                      <div class="card">
                        <div class="card-header">Chatbox</div>
                        <div class="card-body">
                          <dl id="messageList"></dl>
                          <hr>
                          <form id="sendMessage" method="post">
                            <div class='input-group'>
                              <input type='text' name='message' class="form-control" placeholder="Type your message...">

                              <div class='input-group-append'>
                                <button class='btn btn-primary'>Send</button>
                              </div>
                            </div>
                          </form>
                        </div>
                      </div>
                    </div>
                    <div class="col-md-4">
                      <div class="card">
                        <div class="card-header">Users Online</div>
                        <div class="card-body p-0">
                          <ul id="onlineUsers" class="list-group list-group-flush"></ul>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>

    <script src="https://unpkg.com/@pusher/chatkit-client"></script>
    <script src="https://unpkg.com/moment"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="app.js"></script>
    </body>
    </html>

Once done, in the client directory, you will also need to create another file called app.js. That can be done with:

    $ touch app.js

In app.js, we will handle communication with the backend server to authenticate the user and once authenticated, we will also connect to Chatkit for chat functionality.

    // pusher-tableflip-status/client/app.js

    const ROOM_ID = 'ROOM_ID';
    // A newly created user will only be able to post a message after this time
    const COOL_OFF_MINUTES = 5;

    const usernameForm = document.getElementById('username-form');

    usernameForm.addEventListener('submit', e => {
      e.preventDefault();

      const username = e.target.username.value;

      axios
        .post('http://localhost:5200/users', { username })
        .then(res => {
          const skipCooloff = res.data.skip_cooloff;
          let createdAt = 0;

          if (skipCooloff) {
            alert('This user is an existing user, there will be no cooloff ');
          } else {
            createdAt = res.data.created_at;
          }

          document.getElementById('join').style.display = 'none';
          document.getElementById('chatbox').style.display = 'block';

          const tokenProvider = new Chatkit.TokenProvider({
            url: 'http://localhost:5200/authenticate',
          });

          const chatManager = new Chatkit.ChatManager({
            instanceLocator: 'PUSHER_CHATKIT_INSTANCE_LOCATOR',
            userId: username,
            tokenProvider,
          });

          chatManager
            .connect()
            .then(currentUser => {
              currentUser
                .subscribeToRoom({
                  roomId: ROOM_ID,
                  hooks: {
                    onMessage: message => {
                      const { senderId, text } = message;

                      const messageList = document.getElementById('messageList');
                      const messageUser = document.createElement('dt');
                      const messageBody = document.createElement('dd');

                      messageUser.appendChild(document.createTextNode(senderId));
                      messageBody.appendChild(document.createTextNode(text));

                      messageList.appendChild(messageUser);
                      messageList.appendChild(messageBody);
                    },
                    onPresenceChanged: (state, user) => {
                      if (state.current === 'offline') {
                        const elem = document.getElementById(user.id);
                        if (elem !== null) {
                          elem.remove();
                        }
                        return;
                      }

                      if (currentUser.id !== user.id) {
                        addUserElement(user);
                      }
                    },
                  },
                  messageLimit: 100,
                })
                .then(() => {
                  currentUser.rooms[0].users.forEach(user => {
                    if (user.presence.state === 'online') {
                      addUserElement(user);
                    }
                  });

                  const sendMessage = document.getElementById('sendMessage');
                  sendMessage.addEventListener('submit', e => {
                    e.preventDefault();

                    if (!skipCooloff) {
                      const duration = moment.duration(
                        moment(Date.now()).diff(moment(createdAt))
                      );

                      if (duration.minutes() <= COOL_OFF_MINUTES) {
                        alert(
                          `You must be a member of this room for ${COOL_OFF_MINUTES} minutes before you can add a message`
                        );
                        return;
                      }
                    }

                    const message = e.target.message.value;

                    currentUser.sendMessage({
                      text: message,
                      roomId: ROOM_ID,
                    });

                    e.target.message.value = '';
                  });
                })
                .catch(error => console.error(error));
            })
            .catch(error => console.error(error));
        })
        .catch(error => console.error(error));
    });

    function addUserElement(user) {
      const onlineUsers = document.getElementById('onlineUsers');
      const singleUser = document.createElement('li');

      singleUser.className = 'list-group-item';
      singleUser.id = user.id;

      singleUser.appendChild(document.createTextNode(user.name));
      onlineUsers.appendChild(singleUser);
    }

Kindly remember to update the value of PUSHER_CHATKIT_INSTANCE_LOCATOR and ROOM_ID with the correct values

Perhaps, the most interesting piece of code here is:

    const duration = moment.duration(
        moment(Date.now()).diff(moment(createdAt))
    );

    if (duration.minutes() <= COOL_OFF_MINUTES) {
        alert(
            `You must be a member of this room for ${COOL_OFF_MINUTES} minutes before you can add a message`
        );
        return;
    }

In the above, we verify the user has had an account for more than the value of COOL_OFF_MINUTES. If yes, the user can make a post else the appropriate error message will be displayed.

To test the frontend application, If you want to make use of Python, the command you need to run is python -m http.server 8000. If you have installed http-server earlier, you should use http-server -p 8000. The commands should be executed in the client directory. Once it succeeds, you can visit http://localhost:8000 to view the application.

You will need to wait for 5 minutes before a message can be posted. To reduce the time, update the value of COOL_OFF_MINUTES to a lower number say 2.

Also note that since we handle the time associated with the user from the backend, the restriction will still be there even if the user opens up the chat room in incognito mode or another browser.

Conclusion

In this tutorial, we have implemented a way of preventing users and bots from mass-joining and posting unsolicited messages in our Chatkit room.

You can find the code for this tutorial on GitHub.

Clone the project repository
  • Chat
  • chatroom
  • JavaScript
  • Node.js
  • Chatkit

Products

  • Channels
  • Beams
  • Chatkit

© 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.