Create Your Own Phone Backup Service using PHP, WhatsApp API, DigitalOcean Spaces, and Laravel

August 30, 2019
Written by

Create a Phone Backup Service using WhatsApp.png

Have you ever had a video from a loved one that you wanted to keep forever? Maybe a picture that you wanted to save but didn’t want to add to your phone? Whatever the use case, it’s almost impossible to backup some types of media, like voicemail and contacts, without an app.

We're going to learn how to create a phone backup service that allows us to manually transfer data via WhatsApp to cloud storage, just by using our phone number.

We will accomplish this by creating a RESTful API in Laravel, connecting it to Twilio’s WhatsApp API, and adding cloud storage via DigitalOcean Spaces.

In order to complete this tutorial you will need the following:

At the time of writing, DigitalOcean does not provide an official PHP API for connecting to its object storage. So we will also utilize the Spaces API, an open-source wrapper that assists us in easily connecting to the DigitalOcean API in PHP. The Spaces API will be added to our project later via Composer.  Special thanks to Devang Srivastava for maintaining this repo.

Create a Backup Service Using a New Laravel Project

Begin this project by creating a RESTful API using the Laravel PHP framework. This API will communicate with the Twilio SMS API and deliver our media. If you haven’t already installed Laravel, you can do so by following the official documentation and installation guide.

After installation, run the following command to generate a new Laravel project:

$ laravel new phonebackup 
$ cd phonebackup

Add the Spaces API Dependency to Your Laravel Project

In preparation for the next step, the app we’re building will require the Spaces API in order to easily connect to our DigitalOcean Space. Before we set up the Spaces API, we need to add the Spaces API dependency to our Laravel project.

Now that you’re inside the phonebackup directory, run the following command inside of terminal using Composer:

$ composer require sociallydev/spaces-api:dev-master

This command added the Spaces API to our project as a dependency. Now, Laravel will use it to communicate with the DigitalOcean API and handle uploading of media received from the API endpoint.

Create a Twilio Account

Our backup service will use a Twilio’s WhatsApp API and sandbox number as a data entry point. The user will forward media from their phone to the sandboxed number. Once the number receives the SMS, it will be parsed in order to extract the attached media and upload the media into our Space.

If you don’t have a Twilio account, you can sign up for one now and receive a trial account to test your app. We will enable the WhatsApp sandbox later in this tutorial.

Create a DigitalOcean Space

This tutorial will work with any cloud storage system, including the local file system. However; we are using DigitalOcean’s Spaces, an S3-compatible object storage system able to store and serve large amounts of data with free CDN support. The following steps will illustrate how to create a DigitalOcean Space and connect it to your project.

Create a New Space

Login to your DigitalOcean account or create a new one (with a $50 credit). Create a new container by navigating to the Spaces dashboard and follow these five easy steps: 

DigitalOcean Spaces setup
  1. Select the datacenter that is closest to you for better deliverability.
  2. “Enable CDN” to increase the speed of content delivery
  3. For security, leave the “Restrict File Listing” option selected
  4. Choose a unique subdomain such as “phonebackup” + a unique identifier
  5. After clicking on “Create a Space” you will have successfully created a new container

Generate a DigitalOcean Access Key

Now that the Space has been created, navigate to the Applications & API page to generate a key/secret pair for secure access and transmittance of data to the Space.

Under “Spaces access keys” click on the “Generate New Key” button and fill in the required fields.

DigitalOcean Spaces API Key

After your key is generated, the secret will be temporarily displayed. Navigate to the .env file and add the following credentials:

DO_KEY="Insert ACCESS KEY here"
DO_SECRET="Insert ACCESS SECRET here"
DO_SPACE_NAME="Insert SPACE NAME here"
DO_SPACE_REGION="Generally sfo2 or nyc3"

Note: Your DO_SPACE_NAME is the unique name you previously created in Step 4. The DO_SPACE_REGION is available on the settings page of your Space in the “Endpoint” field.

Add Credentials to Laravel Config

Although the .env is being utilized, for better portability and support, we will exploit Laravel’s global configuration object to inject the credentials safely and consistently throughout the application. Laravel comes with configurations for various development environments automatically created for you.

For this project, our credentials will be injected into the config/services.php file which has already been predefined for third-party services.

   'digitalocean' => [
       'key'    => env('DO_KEY'),
       'secret' => env('DO_SECRET'),
       'space' => [
           'name'     => env('DO_SPACE_NAME'),
           'region' => env('DO_SPACE_REGION'),
       ],
   ]

Create the Endpoints to Upload Media

Now that setup of all services is complete, it’s time to write the scripts so we can capture and transfer our media.

Create the MediaController

Laravel by definition is a modern MVC framework and uses its built-in routing to create dynamic and fully extensible API routes. As an important next step, create the controller to support processing of incoming requests from our API and transferring the captured data to our Space.

To generate the MediaController, run the following command:

$ php artisan make:controller API/MediaController --api

You will notice that this command has created a file at app/Http/Controllers/API/MediaController.php.

Open this file.

Five methods have been created from this command, index() for listing all resources, store() for saving a single resource, show() for displaying a single resource, update() for updating a single resource, and destroy() for deleting a single resource. Essentially, CRUD operations are generated from the --api flag.

We are only focusing on the store() method because we are technically creating a new resource in our Space.

Modify the method as follows:

   /**
    * Store a newly created resource in storage.
    *
    * @param  \Illuminate\Http\Request  $request
    * @return \Illuminate\Http\Response
    */
   public function store( Request $request )
   {
       // Capture the sender's mobile number
       $mobile_number_formatted = str_ireplace( 'whatsapp:+
', '', $request->From );

       if ( ! $mobile_number_formatted ) {
           return [
               'error' => 'Cannot detect mobile number'
           ];
       }

       // Capture the media type of the attachment
       $media_type = $request->MediaContentType0;

       if ( ! $media_type || ! $request->MediaUrl0 ) {
           return [
               'error' => 'No media present in the SMS'
           ];
       }

       // Create the filename for the media sent
       $media_type_parts = explode( '/', $media_type );
       $filename_ext     = str_replace( [ 'jpeg', 'amr', 'x-vcard' ], [ 'jpg', 'mp3', 'vcf' ], $media_type_parts[1] );
       $filename         = $media_type_parts[0] . '-' . date( 'Y-m-d-His' ) . '.' . $filename_ext;

       // Set the folder name based on content type
       switch ( $filename_ext ) {
           case "mp3" :
               $folder = 'audio/';
               break;
           case 'jpg' :
               $folder = 'images/';
               break;
           default :
               $folder = '';
       }

       // Download the media locally
       $download = file_get_contents( $request->MediaUrl0 );

       // Assign the data to the specified $filename
       file_put_contents( $filename, $download );

       // Upload file to DigitalOcean Space
       $do_space = new \SpacesConnect( config('services.digitalocean.key'), config('services.digitalocean.secret'), config('services.digitalocean.space.name'), config('services.digitalocean.space.region') );
       $media_url = $do_space->UploadFile( $filename, 'public', $mobile_number . '/' . $folder . $filename );

       // Delete the file locally whether or not upload is successful
       unlink( $filename );

       // Send public link back to $mobile_number
       if ( isset( $media_url[ 'ObjectURL' ] ) ) {

           ?>
               <?xml version="1.0" encoding="UTF-8"?>
               <Response>
                   <Message><Body>Your file was successfully backed up! Here's a shareable link <?php echo $media_url[ 'ObjectURL' ]; ?></Body></Message>
               </Response>
           <?php
       }

       return [
           'status' => 'error',
           'error' => 'media failed to upload'
       ];
   }

In the code above we captured the sender's mobile number. The + prefix is removed since this value will create a subdirectory in our Space. After detecting the media type of the attachment, the filename is auto-generated based on the type, timestamp, and extension. Additionally, a folder relative to the media type is created.

Note: The switch statement can be modified to include any type of file you desire.

The contents of our attachment are downloaded to the local file system and assigned the generated filename. Once that process is complete, the contents are uploaded to your Space, the local file is deleted, and a success message is returned in TwiML with a request to send a response back to the sender’s mobile number.

Connect the MediaController to Routing

By default, when routing is mentioned, it assumes the public-facing, view-controller relationship for a Laravel application. However; for the purpose of what we’re creating, we’ll be focusing on the API routing as defined in routes/api.php which seamlessly generates RESTful endpoints.

Since the logic for MediaController is complete, we need to expose and connect the controller to the routing system.

The recommended endpoint that we’ll POST our request to is /media. Our endpoint will then transfer the request and all of its data to the store() method.

Simply add the following line of code to the routes/api.php file as seen below:

Route::post( 'media', 'API\MediaController@store' );

Connect the Endpoint to your WhatsApp Sandbox

In order to complete this project, we need to connect the Twilio Whatsapp Sandbox and the endpoint. Once this step is complete, we will be able to forward an attachment from our phone, where it will automatically upload to our DigitalOcean Space, and we will receive an automated SMS receipt with a public link to the file.

Our application lives within our local environment. Therefore it has no direct access from the Internet. Dynamically assign DNS to the localhost and provide a tunnel to communicate with our application using ngrok.

In your terminal and within the project folder, run the following command to serve your website:

$ php artisan serve

This command when successfully executed will serve your Laravel application at http://127.0.0.1:8000.

Laravel home screen

In another terminal window, run the following ngrok command to create a publicly accessible, temporary domain:

$ ngrok http 127.0.0.1:8000

Within this command, we’re telling ngrok to tunnel all traffic to our localhost at port 8000. After this runs successfully, a screen similar to the following will display:

ngrok screen

You’ll see above that my “Forwarding” address is http://8197e662.ngrok.io for unsecured traffic and https://8197e662.ngrok.io’ for https. Do not close this window until you are ready to terminate traffic to your local environment.

Within your Twilio console, navigate to your WhatsApp Sandbox under Programmable SMS. On the sandbox dashboard, navigate to the “Sandbox Configuration” section and under “When a message comes in” input the https ngrok address + /api/media’. For example, my full URL would be https://8197e662.ngrok.io/api/media. Save the updated settings.

Twilio WhatsApp Settings

To enable the sandbox to receive incoming messages, follow the instructions on the Learn tab. You will be able to send any message after your account is connected.

Ready to Test

Now that our WhatsApp sandbox is connected to our Laravel Application, we are ready to test the phone backup service. I will demonstrate how to do this with an iPhone, but the principle applies to all capable smartphones.

From WhatsApp, click on the share button to access your “Photo & Video Library”. After you select the photo or video that you desire to backup, click “Send”.

If there were no errors, you will receive an SMS response similar to the screenshot provided below.

Twilio WhatsApp screen on iPhone

Conclusion

I made this app because I have very important pictures and videos from loved ones that I want to keep “forever”, but in my own storage. I also have an iPhone and the backup services are somewhat hit-and-miss. I’ve even heard of people who have loved ones that have passed who hold on to their phones just to replay old videos and messages.

With this tutorial, you’ve learned how to ensure that special digital memories can be transferred from your phone in the event you no longer have access to it.

I’d love to see what you build with this and the modifications you make. If you need any assistance, don’t hesitate to hit me up on Twitter or shoot me an email.

Marcus Battle is Twilio’s PHP Developer of Technical Content. You can learn more about him on the blog.