Toot with Laravel, Vonage and Mastodon
Published on March 20, 2023

You may have heard of Mastodon in recent months, given changes that are happening within the world of Social Media, but what is it, and what can I do with it?

In this article, I'm going to use Vonage, Laravel, and Expose to receive an incoming SMS sent from your phone to 'toot' it out on a Mastodon account.

What is Mastodon?

Mastodon is a Social Media platform that is open source. In terms of its behaviour and UI, it's very similar to Twitter (the equivlent to a "tweet" is a "toot", for instance). There are, however, major differences; I've mentioned that it's open source, but it also works as a "decentralised" network. This aspect of it is what makes it unique: the idea is that you can spin up your own Mastodon instance, that is connected to "the Fediverse" (i.e. all the other Mastodon servers). The end result is that you, as a user, can choose a Mastodon instance to sign up to, but can see content from all other users across the platform.

To the surprise of probably nobody, the signup rate of Mastodon has been phenomenal: in November 2022 alone, it welcomed a million new users.

I suppose it's pretty poetic that Mastodon's mascot is an Mastodon, a distant relative of the elephant, given that I am firstly the PHP specialist in the Vonage Developer Relations Team, and secondly a user on Mastodon's PHP community server. So let's get started: firstly we want some Laravel scaffolding to fire off a request to the Mastodon API to create a new post.

Booting Up Laravel

It doesn't make much sense to write a whole paragraph on how to install Laravel when the documentation is pretty comprehensive, so let's work on building a command to fire off a Mastodon API request. Use the console to make a new command:

php artisan make:command PostToMastodonCommand

In the new command, I'm going to edit the signature (which is the command name to run from the console) and description.

protected $signature = 'app:post-to-mastodon';
protected $description = 'Post a default message to Mastodon';

To test execution, I'm going to write a placeholder string and then dump it out when running a command. Here's how the handle() method looks:

public function handle(): void  
{  
    $exampleMessage = "Hey! I'm writing a blog post on integrating Mastodon, Vonage, and Laravel.  
    If you see this, I've just fired an artisan command to the API. Nice.";  
  
    $this->info($exampleMessage);
}

Run the command in your terminal:

php artisan app:post-to-mastodon

Out comes the message in the terminal. Nothing particularly special here, but now it's time to configure Mastodon.

Mastodon Configuration

We're going to configure Mastodon for Laravel using Laravel conventions: the only thing we actually need to send a Toot is an access token (API keys for more advanced operations require full keypairs or rotation of keys because access has been configured via. OAuth).

Our first stop is getting the access key. In your Mastodon. Head to your preferences panel while logged in. You should be able to see a Development tab:

nullnull

Create a new application with read access in the scope (this comes as default). Once created, what you are looking for is under Your access token:

Screenshot of the Mastodon Application settingsScreenshot of the Mastodon Application settings

This is the value we'll need in the app. Copy this, then head back to your code.

In your Laravel config file, create a new file named mastodon.php and write the following code:

<!--?php  
  
return [  
  
    /*  
    |----------------------------------------------------------------
    | Mastodon    
    |----------------------------------------------------------------
    |
    |
    */    
    
    'access_token' =--> env('MASTODON_ACCESS_TOKEN')  
];

The config will now read your access token from your .env file, so copy your included Laravel .env.example (if you haven't already) and create your .env file. In there, add our new environment variable:

MASTODON_ACCESS_TOKEN=2sd09g-0h9hs-09ts-0risd-f9j4-s9d0g9s-0h8

Fire a Toot with the Command

Our last part is to hook up the config variable into the command and send it off to our Mastodon instance. In the PostToMastodonCommand handler, add the following code:

$response = Http::asForm()->withToken(config('mastodon.access_token'))->post('https://mymastodon.social/api/v1/statuses', [  
    'status' => $exampleMessage  
]);  
  
$this->info($response->body());

Laravel's Http facade gives us a pretty fluid way to send the request off without having to fiddle with configuring a Client such as Guzzle directly. Breaking down the request, we have the following components:

  • asForm(): Mastodon's API for this endpoint requires the data in the request to be sent as multipart/form-data.

  • withToken() is a neat function to automatically add a Bearer token to the Authorization of the request.

  • config(mastodon.access_token) retrieves our token out of the newly created config file

  • post() is the type of HTTP request we need to make

Fire the command into the console:

php artisan app:post-to-mastodon

and hopefully, you'll get an HTTP200 response back, and your Toot should appear in Mastodon.

What if we... sent an SMS to Toot instead?

There is absolutely no reasoning behind this, apart from "why not?". We could send a text to a number, and have it toot out the contents if we wanted to... so, let's do that!

DT API Account

To complete this tutorial, you will need a DT API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the DT API Dashboard.

You'll need two things here: firstly, a new application created in the dashboard, and secondly a Vonage number tied to the application.

Head to the Applications tab in your Vonage Dashboard, and hit Create Application:

Screenshot of the Vonage Dashboard when logged inScreenshot of the Vonage Dashboard when logged in

Our new application is going to use the Messages API to receive SMS messages, which will then fire off webhooks for our Laravel application to listen to. Toggle the Messages capability, and fill the Inbound URL/StatusURL with a placeholder for the time being; we'll revisit these shortly.

Screenshot of a Vonage Application in the Dashboard, highlighting the webhook settings for MessagesScreenshot of a Vonage Application in the Dashboard, highlighting the webhook settings for Messages

We'll need a number to connect incoming messages to the application's webhooks. Create your application, and then you should see the option to Buy more Numbers. It's this process where you can select a number, purchase it and then link it to the application. Your end result in the application dashboard should look like this:

Screenshot of the Numbers configuration for a Vonage Application on the DashboardScreenshot of the Numbers configuration for a Vonage Application on the Dashboard

Routing our Laravel Application

The URL specified in the 'InboundURL' field on the dashboard is where our webhook is going to be sent, but we've not opened a route yet in our Laravel Application.

The webhook is designed to interact with our app at an API level rather than the web routing (which would be to load pages, dashboards, etc.). For this reason, we want our new route to be in routes/api.php. Open that up, then add the route:

Route::post('/incoming', IncomingSMSController::class);

Hang on, IncomingSMSController doesn't exist yet! You would be correct: let's create it in the terminal:

php artisan make:controller IncomingSMSController

Our new controller is going to handle the incoming webhook request, and then use the existing code we used to fire a Toot off to Mastodon. The controller looks like this:

<!--?php  
  
namespace App\Http\Controllers;  
  
use Illuminate\Http\Request;  
use Illuminate\Support\Facades\Http;  
use Symfony\Component\HttpFoundation\Response;  
  
class IncomingSMSController extends Controller  
{  
    public function __invoke(Request $request)  
    {        $body = json_decode($request--->getContent(), true, 512, JSON_THROW_ON_ERROR);  
        $message = $body['text'];  
  
        $response = Http::asForm()->withToken(config('mastodon.access_token'))->post('https://mymastodon.social/api/v1/statuses', [  
            'status' => $message  
        ]);  
  
        return response($response->body(), Response::HTTP_CREATED);  
    }}

So, what we're doing here is replacing the placeholder message we sent to Mastodon originally in the command with the incoming SMS text contents.

There is one major missing piece here: Vonage needs a live URL to send that webhook to. So, how do we do that?

In previous examples I've written, I've turned to ngrok as an excellent tunnel application to expose your local stack to the internet. However, recently I have started using something else

Expose to the Rescue!

Expose from Beyond Code essentially behaves the same as ngrok; It's a tunnel application that allows your local web application to be exposed to the internet. You can install it in several ways, but arguably the simplest way will be via. composer require global:

composer global require beyondcode/expose

You'll want to add the executable to your operating system's path - check out the docs here to see how to do that.

Expose will need configuring with an access token - you can follow the steps here to create a token and assign it to your copy.

We need to take two steps: firstly, fire up the local server in your Laravel app:

php artisan serve

And secondly, we start Expose which will give us a domain for our app (Laravel's built-in server defaults to 8000 as the port):

expose share http://127.0.0.1:8000

All being well, you should now see the Expose dashboard:

Screenshot of Expose running on the command lineScreenshot of Expose running on the command line

You can see that there is a dashboard URL: Expose runs a UI on port 4040. If you navigate to it, you'll be presented with a pretty nice set of tools for debugging:

Screenshot of the Expose GUI waiting for connectionsScreenshot of the Expose GUI waiting for connections

We'll keep this window so we can see our webhook coming in. Our final bit of configuration is to take our new public HTTPS URL and paste it back into the application settings in the Vonage Dashboard:

Screenshot inputting the Expose web URL into the Vonage Application message webhook settingsScreenshot inputting the Expose web URL into the Vonage Application message webhook settings

The only important thing here for our use case is that the Inbound URL matches the Laravel route we defined. The Status URL isn't important here as this is for deeper integrations that listen out for any changes, delivery failures, and network issues.

Txt me!

Everything is wired together: send an SMS with what you want to display in Mastodon, and watch our setup do its thing!

Screenshot of Expose with the incoming webhook recorded in the dashboardScreenshot of Expose with the incoming webhook recorded in the dashboard

And here's our toot!

Screenshot of the resulting Mastodon Toot sent out from our ApplicationScreenshot of the resulting Mastodon Toot sent out from our Application

Conclusion

Mastodon's API is fully open by design, without 'premium features' due to the nature of the software being FOSS-first. Bearing that in mind, there is pretty much no limit to what you can do here - I wanted this to be a brief introduction, but in terms of proof-of-concept I've already got voice transcriptions using the Vonage Voice API to also Toot out recordings.

Thought of something else you could do with Laravel, Mastodon, and Vonage? Hit me up, I'm always up for experimenting. Come join in the conversation on our Vonage Community Slack or hit us up on Twitter.

James SecondeSenior PHP Developer Advocate

A trained actor with a dissertation on standup comedy, I came into PHP development via. the meetup scene. You can find me speaking and writing on tech, or playing/buying odd records from my vinyl collection.

Ready to start building?

Experience seamless connectivity, real-time messaging, and crystal-clear voice and video calls-all at your fingertips.