Block Spam Calls and Robocalls with PHP and Laravel

January 10, 2017
Written by
Jose Oliveros
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Paul Kamp
Twilion
Kat King
Twilion
Samuel Mendes
Contributor
Opinions expressed by Twilio contributors are their own

block-spam-robocalls-php-laravel

Block spam calls with PHP and Laravel

Spam, scams, and robocalls are at best annoying. For high-volume customer service centers, they can significantly impact the bottom line. Let’s leverage the Twilio Marketplace and our PHP skills to block spam callers, robocallers, and scammers.

Before getting started, you should already know how to handle incoming calls with Twilio.

Get a Spam Filtering Add-on

You can integrate third-party technologies without leaving the comfort of the Twilio API. You can access the Add-ons from your Twilio Console. Today, we’re going to look at two Voice Add-ons that can help us with this spam problem: Marchex Clean Call, and Nomorobo Spam Score.

Installing the Add-on

Once you’ve decided on the Add-on you’d like to use, click the Install button and agree to the terms. In our use case today, we want to make use of these Add-ons while handling incoming voice calls, so make sure the Incoming Voice Call box for Use In is checked and click Save to save any changes:

Marchex Clean Call Add On Twilio Console Screenshot- use in Incoming Voice Call box checked

Note the “Unique Name” setting. You need to use this in the code that you will write to read the Add-on’s results. In the code for this guide, we are sticking with the default names.

Check Phone Number Score in PHP and Laravel

When Twilio receives a phone call from your phone number, it will send details of the call to your webhook (more on how to configure that later). In your webhook code, you create a TwiML response to tell Twilio how to handle the call.

For spam-checking, our code needs to check the spam score of the number and deal with the call differently depending on whether the Add-on considers the caller to be a spammer or not. In our example code here, we’ll return a TwiML tag to send spammers packing and a TwiML tag to welcome legit callers.

The code is a simple Laravel application. The code that filters requests is conveniently implemented as Laravel Middleware.

Editor: this is a migrated tutorial. Clone the original code from: https://github.com/TwilioDevEd/block-spam-calls-php

<?php

namespace App\Http\Middleware;

use App\Http\Middleware\Exceptions\AddOnFailureException;
use Closure;
use Exception;
use Illuminate\Contracts\Routing\ResponseFactory;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;
use Monolog\Logger;
use Twilio\TwiML\VoiceResponse;


class ValidateVoiceRequest
{
    protected static $logger;

    public function __construct()
    {
        self::$logger = new Logger('ValidateVoiceRequest');
    }

    /**
     * Handle an incoming request.
     *
     * @param Request $request
     * @param Closure $next
     * @return mixed
     * @throws Exception
     */
    public function handle(Request $request, Closure $next)
    {
        $addOnsData = $this->objectToArray(
            $request->input('AddOns')
        );

        try {
            if (Arr::get($addOnsData, 'results.marchex_cleancall')){
                if ($this->isMarchexSpam($addOnsData)) {
                    return $this->rejectIncomingCall();
                }
            } elseif (Arr::get($addOnsData, 'results.nomorobo_spamscore')){
                if ($this->isNomoroboSpam($addOnsData)) {
                    return $this->rejectIncomingCall();
                }
            } elseif (Arr::get($addOnsData, 'results.whitepages_pro_phone_rep')){
                if ($this->isWhitePageSpam($addOnsData)) {
                    return $this->rejectIncomingCall();
                }
            } else {
                self::$logger->error('No Twilio AddOns configured.');
                return $this->rejectIncomingCall();
            }

        } catch (AddOnFailureException $e) {
            self::$logger->error($e->getMessage());
        }

        return $next($request);
    }

    /**
     * @param $object
     * @return mixed
     */
    private function objectToArray($object)
    {
        return @json_decode(json_encode($object), true);
    }

    /**
     * @param $string
     * @return ResponseFactory|Response
     */
    private function rejectIncomingCall() {
        $twiml = new VoiceResponse();
        $twiml->reject();
        return response($twiml)->header('Content-Type', 'text/xml');
    }

    /**
     * @param array $addOnsData
     * @return bool
     * @throws AddOnFailureException
     */
    private function isMarchexSpam(array $addOnsData) {
        $marchexData = Arr::get($addOnsData, 'results.marchex_cleancall');

        $recommendation = Arr::get(
            $marchexData,
            'result.result.recommendation'
        );

        if (Arr::get($marchexData, 'status') == 'failed') {
            throw new AddOnFailureException(Arr::get($marchexData, 'message'));
        }

        return $recommendation == 'BLOCK';
    }

    /**
     * @param $addOnsData
     * @return bool
     * @throws AddOnFailureException
     */
    private function isNomoroboSpam($addOnsData)
    {
        $spamscoreData = Arr::get(
            $addOnsData,
            'results.nomorobo_spamscore'
        );

        if (Arr::get($spamscoreData, 'status') == 'failed') {
            throw new AddOnFailureException(Arr::get($spamscoreData, 'message'));
        }

        $spamScore = (int)Arr::get($spamscoreData, 'result.score');

        return $spamScore >= 1;
    }

    /**
     * @param $addOnsData
     * @return bool
     * @throws AddOnFailureException
     */
    private function isWhitePageSpam($addOnsData)
    {
        $whitePagesReputationData = Arr::get(
            $addOnsData,
            'results.whitepages_pro_phone_rep'
        );

        if (Arr::get($whitePagesReputationData, 'status') == 'failed') {
            throw new AddOnFailureException(Arr::get($whitePagesReputationData, 'message'));
        }

        $reputationScore = (int)Arr::get(
            $whitePagesReputationData,
            'result.reputation_details.score'
        );

        return $reputationScore >= 50;
    }
}

How to Check Marchex Clean Call

Here’s an example of what Marchex Clean Call will return:

{
  "status": "successful",
  "message": null,
  "code": null,
  "results": {
    "marchex_cleancall": {
      "request_sid": "XRxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "status": "successful",
      "message": null,
      "code": null,
      "result": {
        "result": {
          "recommendation": "PASS",
          "reason": "CleanCall"
        }
      }
    }
  }
}

How to Check Nomorobo Spam Score

Here’s an example of what Nomorobo Spam Score will return:

{
  "status": "successful",
  "message": null,
  "code": null,
  "results": {
    "nomorobo_spamscore": {
      "request_sid": "XRxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "status": "successful",
      "message": null,
      "code": null,
      "result": {
        "status": "success",
        "message": "success",
        "score": 0
      }
    }
  }
}

Making a call blocking decision

In our example, unanimity is required for accepting a call. So we look at the advice from each add-on, and if even one of them tells us to block it, we'll reject the call.

<?php
use Illuminate\Http\Request;

Route::middleware('auth:api')->get('/user', function (Request $request) {
    return $request->user();
});

Route::middleware('validate.voice')->post('/voice', function (Request $request) {
    //Call has successfully passed spam screening.
    $twiml = new Twilio\Twiml();
    $twiml->say('Welcome to the jungle.',
        array('voice' => 'woman', 'language' => 'en-gb'));
    $twiml->hangup();
    return response($twiml)->header('Content-Type', 'text/xml');
});

Call Handling Options

Rejection Options

Using <Reject> is the simplest way to turn away spammers. However, you may want to handle them differently. The whole universe of TwiML is open to you. For example, you might want to record the call, have the recording transcribed using another Add-on, and log the transcription somewhere for someone to review.

<?php

namespace App\Http\Middleware;

use App\Http\Middleware\Exceptions\AddOnFailureException;
use Closure;
use Exception;
use Illuminate\Contracts\Routing\ResponseFactory;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;
use Monolog\Logger;
use Twilio\TwiML\VoiceResponse;


class ValidateVoiceRequest
{
    protected static $logger;

    public function __construct()
    {
        self::$logger = new Logger('ValidateVoiceRequest');
    }

    /**
     * Handle an incoming request.
     *
     * @param Request $request
     * @param Closure $next
     * @return mixed
     * @throws Exception
     */
    public function handle(Request $request, Closure $next)
    {
        $addOnsData = $this->objectToArray(
            $request->input('AddOns')
        );

        try {
            if (Arr::get($addOnsData, 'results.marchex_cleancall')){
                if ($this->isMarchexSpam($addOnsData)) {
                    return $this->rejectIncomingCall();
                }
            } elseif (Arr::get($addOnsData, 'results.nomorobo_spamscore')){
                if ($this->isNomoroboSpam($addOnsData)) {
                    return $this->rejectIncomingCall();
                }
            } elseif (Arr::get($addOnsData, 'results.whitepages_pro_phone_rep')){
                if ($this->isWhitePageSpam($addOnsData)) {
                    return $this->rejectIncomingCall();
                }
            } else {
                self::$logger->error('No Twilio AddOns configured.');
                return $this->rejectIncomingCall();
            }

        } catch (AddOnFailureException $e) {
            self::$logger->error($e->getMessage());
        }

        return $next($request);
    }

    /**
     * @param $object
     * @return mixed
     */
    private function objectToArray($object)
    {
        return @json_decode(json_encode($object), true);
    }

    /**
     * @param $string
     * @return ResponseFactory|Response
     */
    private function rejectIncomingCall() {
        $twiml = new VoiceResponse();
        $twiml->reject();
        return response($twiml)->header('Content-Type', 'text/xml');
    }

    /**
     * @param array $addOnsData
     * @return bool
     * @throws AddOnFailureException
     */
    private function isMarchexSpam(array $addOnsData) {
        $marchexData = Arr::get($addOnsData, 'results.marchex_cleancall');

        $recommendation = Arr::get(
            $marchexData,
            'result.result.recommendation'
        );

        if (Arr::get($marchexData, 'status') == 'failed') {
            throw new AddOnFailureException(Arr::get($marchexData, 'message'));
        }

        return $recommendation == 'BLOCK';
    }

    /**
     * @param $addOnsData
     * @return bool
     * @throws AddOnFailureException
     */
    private function isNomoroboSpam($addOnsData)
    {
        $spamscoreData = Arr::get(
            $addOnsData,
            'results.nomorobo_spamscore'
        );

        if (Arr::get($spamscoreData, 'status') == 'failed') {
            throw new AddOnFailureException(Arr::get($spamscoreData, 'message'));
        }

        $spamScore = (int)Arr::get($spamscoreData, 'result.score');

        return $spamScore >= 1;
    }

    /**
     * @param $addOnsData
     * @return bool
     * @throws AddOnFailureException
     */
    private function isWhitePageSpam($addOnsData)
    {
        $whitePagesReputationData = Arr::get(
            $addOnsData,
            'results.whitepages_pro_phone_rep'
        );

        if (Arr::get($whitePagesReputationData, 'status') == 'failed') {
            throw new AddOnFailureException(Arr::get($whitePagesReputationData, 'message'));
        }

        $reputationScore = (int)Arr::get(
            $whitePagesReputationData,
            'result.reputation_details.score'
        );

        return $reputationScore >= 50;
    }
}

Configuring a Phone Number Webhook

Now we need to configure our Twilio phone number to call our application whenever a call comes in. So we just need a public host for our application. You can serve it any way you like as long as it's publicly accessible or you can use ngrok to test locally.

Armed with the URL to the application, open the Twilio Console and find the phone number you want to use (or buy a new number). On the configuration page for the number, scroll down to "Voice" and next to "A CALL COMES IN," select "Webhook" and paste in the function URL. (Be sure "HTTP POST" is selected, as well.)

block-spam-calls-php/routes/api.php Voice Webhook config

Everything is set up now, you can pick up your phone and call your Twilio number. Hopefully, if you are not a spammer your call should be accepted and you should hear the greeting.

 

Testing a Blocked Call

You can quickly call your Twilio number to make sure your call goes through. However, how can we test a blocked spam result? The easiest way is to write some unit tests that pass some dummied up JSON to our controller action. For example, if we wanted to test a Nomorobo “BLOCK” recommendation, we could use the following JSON:

{
  "status": "successful",
  "message": null,
  "code": null,
  "results": {
    "nomorobo_spamscore": {
      "request_sid": "XRxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "status": "successful",
      "message": null,
      "code": null,
      "result": {
        "status": "success",
        "message": "success",
        "score": 1
      }
    }
  }
}

What’s Next?

As you can see, the Twilio Add-ons Marketplace gives you a lot of options for extending your Twilio apps. Next, you might want to dig into the Add-ons reference or perhaps glean some pearls from our other PHP tutorials. Wherever you’re headed next, you can confidently put spammers in your rearview mirror.