Build A Group Chat With Admin Moderation In Laravel Using Twilio Programmable Chat

May 26, 2020
Written by
Chimezie Enyinnaya
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Build A Group Chat With Admin Moderation In Laravel Using Twilio Programmable Chat

In this tutorial, I’ll be showing you how to build a Laravel group chat application with admin moderation using Twilio Programmable Chat.

Prerequisites

In order to follow this tutorial, you will need the following:

What We’ll Be Building

For the purpose of this tutorial, we’ll be building a group chat, where an admin will be able to perform the following tasks:

  • Ban members in a group
  • Unban members in a group
  • Remove members from a group

Getting Twilio Credentials

Login to your Twilio dashboard and copy both your Account SID and Auth Token.

Twilio console credentials

Before you can start using the Twilio Chat API, you need to first create a chat service:

New Twilio Chat service

After you name and create your chat service, take note of your Service SID.

Chat service configuration

Lastly, you need to create an API key:

Twilio API Key

Also, take note of both your API Secret and API SID.

Getting started

To keep this tutorial concise, let’s clone a starter repo to your local machine, which you’ll build upon:

$ git clone https://github.com/ammezie/twilio-group-chat-admin-moderation.git
$ cd twilio-group-chat-admin-moderation
$ git checkout starter

Next, generate an APP_KEY:

$ php artisan key:generate

Create a database.sqlite file:

$ touch database/database.sqlite

Then update the .env file as below:

DB_CONNECTION=sqlite
DB_DATABASE=/absolute/path/to/database.sqlite

Finally,  run the migration:

$ php artisan migrate

With all the dependencies installed, let’s quickly review the cloned repo.

Your newly cloned repo has authentication set up by default and the User model has been updated to include a username field instead of the default name field. It also contains a ChatRoom Vue component with basic functionality of sending and displaying chat messages, as well as functionality for generating access tokens. You might want to check out my previous tutorial, which covers those in detail.

Finally, the Twilio PHP SDK and Programmable Chat JavaScript SDK have been installed as well.

Creating The Roles

Twilio Programmable Chat has an extensive and flexible roles and permission system. A chat service comes with four default roles and their respective permissions. A role can either be for a service or channel. Of these roles, two are for services while the remaining two are for channels.

For the purpose of this tutorial, we’ll be making use of only channel roles. The two default channel roles are Channel Admin and Channel Member. New channel members are assigned the Channel Member role by default.

In addition to the two channel roles, we’ll be creating a new channel role, which we’ll use to indicate banned channel members.

Navigate back to the recently created chat service , and select Roles and Permissions. Then click on the New Channel Role button to create a new channel role. Fill in the form as below with a new role, “channel banned user”:

New Chat Service Role

Take note of the SIDs of the three roles.

Next, add your keys/SIDs inside the project .env file:

// .env

TWILIO_AUTH_SID=YOUR_TWILIO_AUTH_SID
TWILIO_AUTH_TOKEN=YOUR_TWILIO_AUTH_TOKEN
TWILIO_API_SECRET=YOUR_TWILIO_API_SECRET
TWILIO_API_SID=YOUR_TWILIO_API_SID
TWILIO_CHAT_SERVICE_SID=YOUR_TWILIO_CHAT_SERVICE_SID
MIX_CHANNEL_ADMIN_ROLE_SID=CHANNEL_ADMIN_ROLE_SID
MIX_CHANNEL_MEMBER_ROLE_SID=CHANNEL_MEMBER_ROLE_SID
MIX_CHANNEL_BANNED_ROLE_SID=CHANNEL_BANNED_ROLE_SID

We are prefixing some of the environment variables with MIX_ because that’s the only way Laravel Mix will make the environment variables available to be used in your Vue.js code.

Creating Channel And Admin User

With the initial set up out of the way, let’s start working on the actual group chat functionality. We’ll start by creating a new channel, which we’ll call chatroom and an admin user to administer the group. Instead of creating UIs for these tasks, we’ll wrap them inside of a console command. Run the command below to create a new console command:

$ php artisan make:command InitChannel

This will create a new InitChannel.php file inside of app/Console/Commands. Open it up and replace its content with the following:

// app/Console/Commands/InitChannel.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Twilio\Rest\Client;
use App\User;


class InitChannel extends Command
{
    protected $signature = 'channel:init';
    protected $description = 'Initialize channel';

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
      $user = User::create([
        'username' => 'mezie',
        'email' => 'chimezie@adonismastery.com',
        'password' => bcrypt('password')
      ]);

      $twilio = new Client(env('TWILIO_AUTH_SID'), env('TWILIO_AUTH_TOKEN'));
      $channel = $twilio->chat->v2->services(env('TWILIO_CHAT_SERVICE_SID'))
                                  ->channels
                                  ->create([
                                      'friendlyName' => 'chatroom',
                                      'uniqueName' => 'chatroom',
                                      'createdBy' => $user->username
                                  ]);

      $twilio->chat->v2->services(env('TWILIO_CHAT_SERVICE_SID'))
                        ->channels($channel->sid)
                        ->members
                        ->create($user->username, [
                            'roleSid' => env('MIX_CHANNEL_ADMIN_ROLE_SID')
                        ]);

      return $this->info('Channel initialized');
    }
}

The code above  imports the Twilio PHP SDK and the User model. Then it defines the signature of the command and gives it a description. The actual command’s implementation is inside the handle method.

First, a new user is created. Then the channel is created and the newly created user is assigned as the creator of the channel. Next, the user is added as a member of the channel and their role is changed to the channel admin by setting the user’s roleSid to that of the admin role. Lastly, the appropriate success message is returned.

Now, you can run the channel:init command to initialize your channel:

$ php artisan channel:init

Displaying The Chatroom

Now, let’s add a way to access the chatroom. Add the code below inside web.php:

// routes/web.php

Route::get('/chatroom', 'ChannelController@show')->middleware('auth');

Because of the middleware, this route will only be accessible to logged in users.

Let’s create the ChannelController:

$ php artisan make:controller ChannelController

Then add the following code inside of the newly created controller at app/Http/Controllers/ChannelController.php:

// app/Http/Controllers/ChannelController.php

public function show()
{
  return view('chatroom');
}

Create a chatroom.blade.php file directly inside of the views directory and paste the code below into it:

// resources/views/chatroom.blade.php

@extends('layouts.app')

@section('content')
  <chat-room :user="{{ auth()->user() }}"></chat-room>
@endsection

We are rendering the ChatRoom component and passing the currently authenticated user as a prop.

Lastly, add a link to navigate to the chatroom page by adding a new link inside app.blade.php immediately after @else:

// resources/views/layouts/app.blade.php

<li class="nav-item">
  <a class="nav-link" href="/chatroom">Chatroom</a>
</li>

Joining The Chatroom

on-admin user need to be added to the channel automatically after registering. To accomplish this, we’ll make use of the registered() method, which is called once a user successfully registers. Add the following code inside of RegisterController.php:

// app/Http/Controllers/Auth/RegisterController.php

// add this at the top
use Twilio\Rest\Client;

protected function registered($user)
{
  $twilio = new Client(env('TWILIO_AUTH_SID'), env('TWILIO_AUTH_TOKEN'));

  try {
      $twilio->chat->v2->services(env('TWILIO_CHAT_SERVICE_SID'))
                        ->channels('chatroom')
                        ->members($user->username)
                        ->fetch();
  } catch (\Twilio\Exceptions\RestException $e) {
      $twilio->chat->v2->services(env('TWILIO_CHAT_SERVICE_SID'))
                        ->channels('chatroom')
                        ->members
                        ->create($user->username);
  }

  return redirect('/chatroom');
}

Using the $twilio client instance, an attempt is made to fetch a channel member with the newly registered username. If the member exists, they are fetched, otherwise, the code adds the user to the channel using the user's username. Finally, the user is redirected to the chatroom page.

Displaying Group MembersNow update the ChatRoom component to display a list of members of the chatroom:

// resources/js/components/ChatRoom.vue

data() {
  return {
      ... // previous code left out for brevity
      members: [],
      member: '',
  }
},
async initializeClient(token) {
  ... // previous code left out for brevity
  this.members = await this.channel.getMembers()
  this.member = await this.channel.getMemberByIdentity(this.user.username)


  this.channel.on('memberJoined', (member) => {
    this.members.push(member)
  })
}

Two new data properties were added to the data object. Then inside the initializeClient() method, the members of the channel were fetched along with the currently authenticated member. Lastly, a listener was added for the memberJoined event to keep the members list updated in real-time when a new member joins the chatroom on the channel, and add them to the list.

Now, let’s update the template. Replace <!-- Members list goes here --> with the following:

// resources/js/components/ChatRoom.vue

<ul class="list-group list-group-flush" v-if="members.length > 0">
  <li
      v-for="mem in members"
      :key="mem.sid"
      class="list-group-item d-flex justify-content-between align-items-center"
  >
    {{ mem.identity }}
  </li>
</ul>
<p v-else>No members</p>

If the channel has members, their identity is displayed, otherwise, the code displays an appropriate message.

Banning Members

Let’s add our first admin-only functionality, which is banning a member. Start by creating the endpoint using the snippet below inside roues/api.php:

// routes/api.php

Route::post('/members/{username}/ban', 'MemberController@ban');

The username/identity of the member to ban is passed to the path of the endpoint, which will be passed to the ban() method inside the MemberController.

Next, create the MemberController in your terminal using the following command:

$ php artisan make:controller MemberController

Replace its content with the following:

// app/Http/Controllers/MemberController.php

<?php

namespace App\Http\Controllers;

use Twilio\Rest\Client;

class MemberController extends Controller
{
    protected $twilio;

    public function __construct()
    {
        $this->twilio = new Client(env('TWILIO_AUTH_SID'), env('TWILIO_AUTH_TOKEN'));
    }

    public function ban($username)
    {
      $this->twilio->chat->v2->services(env('TWILIO_CHAT_SERVICE_SID'))
                              ->channels('chatroom')
                              ->members($username)
                              ->update([
                                  'roleSid' => env('MIX_CHANNEL_BANNED_ROLE_SID')
                              ]);
       
      return response()->json(['message' => 'Member banned']);
    }
}

The $twilio instance is defined as a class property, since we’ll be using it in multiple methods in the MemberController class. To ban a member, we will simply update the member role SID to that of the banned role SID.

Now, let’s head over to the ChatRoom component and add a button to ban a member. We’ll display the button next to a member’s identity. Add the following code just before </li>:

// resources/js/components/ChatRoom.vue

<div class="btn-group">
  <button
    type="button"
    class="btn btn-primary btn-sm"
    @click="banMember(mem.identity)"
    v-if="member.roleSid === adminRoleSid && mem.roleSid === memberRoleSid && user.username !== mem.identity"
  >Ban</button>
</div>

Once this button is clicked, a banMember() method will be called and pass the member’s identity to the method to ban. To make sure only an admin can perform this task, we verify that the currently authenticated member’s role SID equals that of the admin role SID. Also, we check that the member has not been banned already. Since we don’t want members to ban themselves, we check against that as well.

Next, let’s create the banMember method:

// resources/js/components/ChatRoom.vue

data() {
  return {
      ... // previous code left out for brevity
      isBanned: false,
      adminRoleSid: process.env.MIX_CHANNEL_ADMIN_ROLE_SID,
      memberRoleSid: process.env.MIX_CHANNEL_MEMBER_ROLE_SID,
      bannedRoleSid: process.env.MIX_CHANNEL_BANNED_ROLE_SID
  }
},
async initializeClient(token) {
  ... // previous code left out for brevity
  this.channel.on("memberUpdated", ({ member }) => {
    if (member.identity === this.user.username && member.roleSid === this.bannedRoleSid) {
        this.isBanned = true
    }
  })
},
methods: {
  ... // previous code left out for brevity
  async banMember(identity) {
    await axios.post(`/api/members/${identity}/ban`)
  },
}

A new data property called isBanned is created to indicate if a member has been banned or not, which defaults to false. Also, data properties are created for your respective roles with their values fetched from environment variables. Using axios, we make a POST request to the ban member endpoint. To update the UI in real-time, we listen for the memberUpdated event being fired. Inside the event handler, instead of just setting isBanned to true (which will apply to all users), we set isBanned to true only if the member is the currently authenticated user and the member role SID is indeed same as the banned role SID.

Lastly, let’s update the .card-footer as below:

// resources/js/components/ChatRoom.vue

<div class="card-footer">
  <div class="text-center" v-if="member.roleSid === bannedRoleSid || isBanned">
    You have been banned from sending messages.
  </div>

  <input
    type="text"
    v-model="newMessage"
    class="form-control"
    placeholder="Type your message..."
    @keyup.enter="sendMessage"
    v-else
  />
</div>

We only show the input field to send a message if the member has not been banned. Otherwise, we display an appropriate message.

Unbanning Members

We have seen how to ban a member. Let’s add a way to unban a member. As you might have guessed, this will be the complete opposite of what we have above. Again, let’s start by defining the endpoint:

// routes/api.php

Route::post('/members/{username}/unban', 'MemberController@unban');

Next, create the unban() method inside the MemberController:

// app/Http/Controllers/MemberController.php

public function unban($username)
{
  $this->twilio->chat->v2->services(env('TWILIO_CHAT_SERVICE_SID'))
                          ->channels('chatroom')
                          ->members($username)
                          ->update([
                              'roleSid' => env('MIX_CHANNEL_MEMBER_ROLE_SID')
                          ]);


  return response()->json([
      'message' => 'Member unbanned'
  ]);
}

Here, we simply updated the member’s role SID back to that of the member role SID.

Heading over to the ChatRoom component, let’s add the button to unban a member. We’ll add it immediately after the ban button:

<button
  type="button"
  class="btn btn-primary btn-sm"
  @click="unbanMember(mem.identity)"
  v-if="member.roleSid === adminRoleSid && mem.roleSid === bannedRoleSid && user.username !== mem.identity"
>Unban</button>

Once clicked, the unbanMember() method will be called.

Next, let’s create the unbanMember method in ChatRoom.vue:

// resources/js/components/ChatRoom.vue

async initializeClient(token) {
  ... // previous code left out for brevity
  this.channel.on("memberUpdated", ({ member }) => {
    ... // previous code left out for brevity
    if (member.identity === this.user.username && member.roleSid === this.memberRoleSid) {
      this.isBanned = false
    }
  })
},
methods: {
  ... // previous code left out for brevity
  async unbanMember(identity) {
    await axios.post(`/api/members/${identity}/unban`)
  }
}

A POST request is made to the /unban member endpoint. Just as we did with banning a member, inside the event handler of the memberUpdated event, we set isBanned to false only if the member is the currently authenticated user and the member role SID is indeed the same as the member role SID.

Removing Members From Group

The last admin functionality our chatroom will have is the ability to remove members from the group. For this, we’ll be making use of the Twilio Programmable Chat JavaScript SDK. Let’s start by adding the button to remove a member from the group. We’ll add it below the previous buttons:

// resources/js/components/ChatRoom.vue

<button
  type="button"
  class="btn btn-primary btn-sm"
  @click="removeMember(mem.identity)"
  v-if="member.roleSid === adminRoleSid && user.username !== mem.identity"
>Remove</button>

Once the button is clicked, a removeMember() method will be called. This method accepts the identity of the member to be removed.

Let’s create the method:

// resources/js/components/ChatRoom.vue

async initializeClient(token) {
  ... // previous code left out for brevity
  this.channel.on('memberLeft', (member) => {
    this.members = this.members.filter((mem) => mem.sid !== member.sid)
    
    if (member.identity === this.user.username) {
      window.location = '/home'
    }
  })
},
methods: {
  async removeMember(identity) {
    await this.channel.removeMember(identity)
  },
}

We call the removeMember method on the channel passing to it the identity of the member to be removed. To update the UI in real-time, we listen for the memberLeft event. Inside the event handler, we filter out the member that was removed from the members list. Lastly, if the removed member is the currently authenticated user, we simply redirect the user to the home route.

Before we wrap up this tutorial, let’s make sure only members can access the chatroom. We can achieve that using a middleware:

$ php artisan make:middleware MemberOnly

Then open up the newly created file at app/Http/Middleware/MemberOnly.php and replace its content with the following:

// app/Http/Middleware/MemberOnly.php

<?php

namespace App\Http\Middleware;

use Closure;
use Twilio\Rest\Client;

class MemberOnly
{
    public function handle($request, Closure $next)
    {
      $twilio = new Client(env('TWILIO_AUTH_SID'), env('TWILIO_AUTH_TOKEN'));

      try {
        $twilio->chat->v2->services(env('TWILIO_CHAT_SERVICE_SID'))
                          ->channels('chatroom')
                          ->members(auth()->user()->username)
                          ->fetch();


        return $next($request);
      } catch (\Twilio\Exceptions\RestException $e) {
        return redirect('/home');
      }
    }
}

If the user is a member of the channel, we allow the user to access the chatroom, otherwise, we redirect the user to the /home route.

Next, we need to register the middleware as a route middleware in app/Http/Kernel:

// app/Http/Kernel.php

protected $routeMiddleware = [
  ... // previous code left out for brevity      
  'memberOnly' => \App\Http\Middleware\MemberOnly::class
];

Let’s put the middleware to use. Update the /chatroom route inside web.php as shown below:

// routes/web.php

Route::get('/chatroom', 'ChannelController@show')->middleware('auth', 'memberOnly');

Now, not only is a user required to be authenticated to access the chatroom but also must be a member of the chatroom.

Testing Our Application

Let’s test what we’ve been building so far. First, let’s make sure our app is running:

$ php artisan serve

Then compile the JavaScript:

$ npm run dev

Now, open two browser windows. In the first, register and then login as the admin user and in the second, login as a normal user and try performing each of the tasks.

Conclusion

In this tutorial, we learned how to build a group chat room with admin moderation in Laravel using Twilio Programmable Chat. With the flexibility of Twilio Programmable Chat roles and permissions, we can build more advanced admin moderation functionality. To learn more about roles and permissions in Twilio Programmable Chat, check out the docs.

You can find the complete source for this tutorial on GitHub.

Chimezie Enyinnaya is a software developer and instructor. You can learn more about him here: