Back to search

Build a live commenting system with auto moderation using Laravel

  • Shalvah Adebayo
August 30th, 2018
You will need PHP 7+ and Composer installed on your machine.

It’s 2018, and a lot of conversations happen over the Internet. It’s easy for people to forget to be nice when they’re sitting behind a screen and typing, as opposed to talking face-to-face. Many site admins employ some form of moderation to keep user behavior, such as comments, in check and ensure people play by the rules. This moderation could be manual (an admin logs in to review comments before approving) or automatic (an external service analyses comments and approves or rejects based on certain signals), or even a combination of both.

In this tutorial, we’ll build a blog which allows users to comment on posts. These comments will be sent to an external API for moderation, and comments which pass will be saved and displayed under the post in realtime, using Pusher Channels. Here’s a preview of the app in action:

laravel-automod-demo

You can find the source code of the complete application on GitHub. Let’s go!

Prerequisites

  1. PHP 7.1.3 or newer
  2. Composer.
  3. A Pusher account. Create one here.

Setting up

First, create a new Laravel project:

    composer create-project --prefer-dist laravel/laravel rcam

Open up the generated project folder (rcam). Set the value of DB_CONNECTION in your .env file to sqlite and remove all other lines that start with DB_.

    DB_CONNECTION=sqlite

Then create a file called database.sqlite in the database folder of your app.

Run the following command to add the zttp package. We’ll use it to make an API call to the moderation service:

    composer require kitetail/zttp

Now we’ll set up our Comment model and database migration. Run the following command:

    php artisan make:model -m Comment

Look for the comments migration file that was created in your database/migrations folder. Modify its contents so it looks like this:

    // 2018_xx_xx_xxxxxx_create_comments_table

    <?php

    use Illuminate\Support\Facades\Schema;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Database\Migrations\Migration;

    class CreateCommentsTable extends Migration
    {
        public function up()
        {
            Schema::create('comments', function (Blueprint $table) {
                $table->increments('id');
                $table->string('username');
                $table->string('text');
                $table->timestamps();
            });
        }

        public function down()
        {
            Schema::dropIfExists('comments');
        }
    }

Then we run migrations:

    php artisan migrate

Let’s add our route and view for the fake post and comments. Replace the contents of your routes/web.php with the following:

    // routes/web.php
    <?php

    Route::get('/', 'HomeController@home');

Create a file app/Http/Controllers/HomeController.php with the following content:

    // app/Http/Controllers/HomeController.php

    <?php

    namespace App\Http\Controllers;

    use App\Comment;

    class HomeController extends Controller
    {
        public function home()
        {
            // order comments from newest to oldest
            $comments = Comment::orderBy('id desc')->get();
            return view('home', ['comments' => $comments]);
        }
    }

Now create the file resources/views/home.blade.php with the following content:

    <!-- resources/views/home.blade.php -->

    <!doctype html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="csrf-token" content="{{ csrf_token() }}">

        <title>Rcam</title>

        <!-- Styles -->
        <style>
            html, body {
                background-color: #fff;
                color: #636b6f;
                font-family: sans-serif;
                padding: 20px;
            }

            input {
                border: 2px solid blue;
                font-size: 16px;
                padding: 5px;
            }

            button {
                font-size: 16px;
                padding: 5px;
            }
        </style>
    </head>
    <body>
    <h2>Post title</h2>
    This is a dummy post. There isn't really much to it, but it could be really fun if you're bored. Speaking of bored, did
    you hear the joke about the blackboard that had...oh, never mind. I'm not allowed to say that. Just drop a comment and
    be on your merry way. Wait, did I tell you this is a dummy post?

    <br><br>
    <div>
        <h3>Comments</h3>
        <form onsubmit="addComment(event);">
            <input type="text" placeholder="Add a comment" name="text" id="text" required>
            <input type="text" placeholder="Your name" name="username" id="username" required>
            <button id="addCommentBtn">Comment</button>
        </form>
        <div class="alert" id="alert" style="display: none;"></div>
        <br>

        <div id="comments">
            @foreach($comments as $comment)
                <div>
                    <small>{{ $comment->username }}</small>
                    <br>
                    {{ $comment->text }}
                </div>
            @endforeach
        </div>
    </div>

    </body>
    </html>

Moderating comments

Now let’s allow users to post comments. In the view we built above, we created a form for submitting comments. The onsubmit handler of that form is a function called addComment, so let’s implement that. Add this code to your resources/views/home.blade.php, just before the closing </body> tag:

    <!-- resources/views/home.blade.php -->

    <!-- Add jQuery -->
    <script src="https://code.jquery.com/jquery-3.3.1.min.js"
            integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
            crossorigin="anonymous"></script>
    <script>
        function displayComment(data) {
            let $comment = $('<div>').text(data['text']).prepend($('<small>').html(data['username'] + "<br>"));
            $('#comments').prepend($comment);
        }

        function addComment(event) {
            function showAlert(message) {
                let $alert = $('#alert');
                $alert.text(message).show();
                setTimeout(() => $alert.hide(), 4000);
            }

            event.preventDefault();
            $('#addCommentBtn').attr('disabled', 'disabled');
            let data = {
                text: $('#text').val(),
                username: $('#username').val(),
            };
            fetch('/comments', {
                body: JSON.stringify(data),
                credentials: 'same-origin',
                headers: {
                    'content-type': 'application/json',
                    'x-csrf-token': $('meta[name="csrf-token"]').attr('content'),
                    'x-socket-id': window.socketId
                },
                method: 'POST',
                mode: 'cors',
            }).then(response => {
                $('#addCommentBtn').removeAttr('disabled');
                if (response.ok) {
                    displayComment(data);
                    showAlert('Comment posted!');
                } else {
                    showAlert('Your comment was not approved for posting. Please be nicer :)');
                }
            })
        }
    </script>

Here, we’re using Fetch to post the comment to the backend. If a 200 OK response is received, we display the comment under the post. If not, that means the comment failed moderation, so we’ll tell the user to rephrase their comment.

You’ll notice we’re sending a couple of custom headers (x-csrf-token and x-socket-id). The first header is to satisfy Laravel’s CSRF protection, which ensures someone can’t be tricked into posting a comment to our site from a different site. You can read more about CSRF protection here. The second header is our Pusher socket ID, which tells the backend which client sent this request. It prevents the server from sending our own messages back to us. There’s more on that here. For now, it’s always going to be undefined, but we’ll come back to that later.

Next, add the route for making a comment to the bottom of your routes/web.php file:

    // routes/web.php

    Route::post('/comments', 'HomeController@addComment');

Next, we’ll add the method that handles this to our HomeController:

    // app/Http/Controllers/HomeController.php

    public function addComment()
    {
        $data = request()->post();
        Comment::moderate($data['text']);
        return Comment::create($data);
    }

Now open up the app/Comment.php file and replace its contents with the following:

    // app/Comment.php

    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;
    use Zttp\Zttp;

    class Comment extends Model
    {
        protected $guarded = [];

        public static function moderate($comment)
        {
            $response = Zttp::withoutVerifying()->post("https://commentator.now.sh", [
                'comment' => $comment,
                'limit' => -3,
            ])->json();
            if ($response['commentate']) {
                abort(400, "Comment not allowed");
            }
        }
    }

Here we’ve defined a moderate method. In it, we send the comment data to a comment moderation service located at https://commentator.now.sh. We’ve also specified a limit parameter of -3, which means that we’re willing to accept comments which have a score of -3 (mildly negative), but not any less. The response from the service contains a commentate parameter that is true if the comment scored below our limit. In such a case, we return a 400 Bad Request to the frontend and prevent the comment from being saved.

Displaying new comments in realtime

First, we’ll set up Pusher on the backend. Install the Pusher Laravel package and publish the config file by running the following commands:

    composer require pusher/pusher-http-laravel
    php artisan vendor:publish --provider="Pusher\Laravel\PusherServiceProvider"

Sign in to your Pusher dashboard and create a new app. Copy your app credentials from the App Keys section and add them to your .env file:

    PUSHER_APP_ID=your-app-id
    PUSHER_APP_KEY=your-app-key
    PUSHER_APP_SECRET=your-app-secret
    PUSHER_APP_CLUSTER=your-app-cluster

Note: Laravel sometimes caches old configuration, so for the project to see your new configuration values, you might need to run the command php artisan config:clear

Then add the following JavaScript to your view, just before the closing </body> tag:

    <!-- resources/views/home.blade.php -->

    <script src="https://js.pusher.com/4.2/pusher.min.js"></script>
    <script>
        var socket = new Pusher("your-app-key", {
            cluster: 'your-app-cluster',
        });
        // set the socket ID when we connect
        socket.connection.bind('connected', function() {
            window.socketId = socket.connection.socket_id;
        });
        socket.subscribe('comments')
            .bind('new-comment',displayComment);
    </script>

Replace your-app-key and your-app-cluster with the respective credentials as gotten from your Pusher app dashboard.

Now, let’s modify our addComment method on the backend so it triggers a new Pusher message when a comment is created successfully.

    // app/Http/Controllers/HomeController.php


    public function addComment()
    {
        $data = request()->post();
        Comment::moderate($data['text']);
        $comment = Comment::create($data);
        Pusher::trigger('comments', 'new-comment', $comment, request()->header('X-Socket-Id'));
        return $comment;
    }

You’ll need to import the Pusher class by adding use Pusher\Laravel\Facades\Pusher; at the top of the file.

And with that, we’re done. Start up your app by running php artisan serve, then visit http://127.0.0.1:8000. Try adding a few nice comments (“This is good”, “I don’t agree with this” ), and a few comments with some “bad” words (“This article is full of shit”, “Damn, this article sucks”). You should see the comments get moderated and then show up in realtime.

Conclusion

In this tutorial, we’ve combined Pusher Channels with an external comment moderation service to improve the quality of comments on our blog and the user experience for every reader. This is just one combination of services we can make to improve our user’s experience on our site or app. I hope you’ve enjoyed this! You can check out the source code of the completed application on GitHub.

  • Channels

© 2018 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 28 Scrutton Street, London EC2A 4RP.