Last active
October 2, 2023 10:53
-
-
Save breadthe/2787c4f6d6ac805ef9eb698a91b6a750 to your computer and use it in GitHub Desktop.
Laravel Strava webhooks based on https://github.com/RichieMcMullen/laravel-strava/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
https://developers.strava.com/ | |
https://developers.strava.com/docs/webhooks/ | |
https://developers.strava.com/docs/webhookexample/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#... | |
STRAVA_PUSH_SUBSCRIPTIONS_URL="https://www.strava.com/api/v3/push_subscriptions" | |
# The app webhook callback URL that Strava uses to fire GET/POST events | |
STRAVA_WEBHOOK_CALLBACK_URL="https://www.yoursite.com/webhook" | |
# A random string generated by my app that Strava uses to verify the request | |
STRAVA_WEBHOOK_VERIFY_TOKEN=123ABCdefXYZ4567 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// app/Providers/AppServiceProvider.php | |
namespace App\Providers; | |
use App\Services\StravaWebhookService; | |
use Illuminate\Support\ServiceProvider; | |
class AppServiceProvider extends ServiceProvider | |
{ | |
public function register() | |
{ | |
$this->app->singleton(StravaWebhookService::class, function ($app) { | |
return new StravaWebhookService(); | |
}); | |
} | |
// ... | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// config/services.php | |
return [ | |
// ... | |
'strava' => [ | |
'push_subscriptions_url' => env('STRAVA_PUSH_SUBSCRIPTIONS_URL'), | |
'webhook_callback_url' => env('STRAVA_WEBHOOK_CALLBACK_URL'), | |
'webhook_verify_token' => env('STRAVA_WEBHOOK_VERIFY_TOKEN'), | |
], | |
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// app/Services/StravaWebhookService.php | |
namespace App\Services; | |
use Illuminate\Http\JsonResponse; | |
use Illuminate\Http\Response; | |
use Illuminate\Support\Facades\Http; | |
use Illuminate\Support\Facades\Log; | |
class StravaWebhookService | |
{ | |
private string $url; | |
private string $client_id; | |
private string $client_secret; | |
private string $callback_url; | |
private string $verify_token; | |
public function __construct() | |
{ | |
$this->url = config('services.strava.push_subscriptions_url'); | |
$this->client_id = config('ct_strava.client_id'); | |
$this->client_secret = config('ct_strava.client_secret'); | |
$this->callback_url = config('services.strava.webhook_callback_url'); | |
$this->verify_token = config('services.strava.webhook_verify_token'); | |
} | |
public function subscribe(): int|null | |
{ | |
$response = Http::post($this->url, [ | |
'client_id' => $this->client_id, | |
'client_secret' => $this->client_secret, | |
'callback_url' => $this->callback_url, | |
'verify_token' => $this->verify_token, | |
]); | |
if ($response->status() === Response::HTTP_CREATED) { | |
return json_decode($response->body())->id; | |
} | |
Log::channel('strava')->error(json_encode($response->body()), [$response->status()]); | |
return null; | |
} | |
public function unsubscribe(): bool | |
{ | |
$id = app(StravaWebhookService::class)->view(); // use the singleton | |
if (!$id) { | |
return false; | |
} | |
$response = Http::delete("$this->url/$id", [ | |
'client_id' => $this->client_id, | |
'client_secret' => $this->client_secret, | |
]); | |
if ($response->status() === Response::HTTP_NO_CONTENT) { | |
return true; | |
} | |
Log::channel('strava')->error(json_encode($response->body()), [$response->status()]); | |
return false; | |
} | |
public function view(): int|null | |
{ | |
$response = Http::get($this->url, [ | |
'client_id' => $this->client_id, | |
'client_secret' => $this->client_secret, | |
]); | |
if ($response->status() === Response::HTTP_OK) { | |
$body = json_decode($response->body()); | |
if ($body) { | |
return $body[0]->id; // each application can have only 1 subscription | |
} else { | |
return null; // no subscription found | |
} | |
} | |
Log::channel('strava')->error(json_encode($response->body()), [$response->status()]); | |
return null; | |
} | |
// GET https://mycallbackurl.com?hub.verify_token=STRAVA&hub.challenge=15f7d1a91c1f40f8a748fd134752feb3&hub.mode=subscribe | |
public function validate(string $mode, string $token, string $challenge): Response|JsonResponse | |
{ | |
// Checks if a token and mode is in the query string of the request | |
if ($mode && $token) { | |
// Verifies that the mode and token sent are valid | |
if ($mode === 'subscribe' && $token === $this->verify_token) { | |
// Responds with the challenge token from the request | |
return response()->json(['hub.challenge' => $challenge]); | |
} else { | |
// Responds with '403 Forbidden' if verify tokens do not match | |
return response('', Response::HTTP_FORBIDDEN); | |
} | |
} | |
return response('', Response::HTTP_FORBIDDEN); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// app/Console/Commands/SubscribeToStravaWebhookCommand.php | |
namespace App\Console\Commands; | |
use App\Services\StravaWebhookService; | |
use Illuminate\Console\Command; | |
class SubscribeToStravaWebhookCommand extends Command | |
{ | |
protected $signature = 'strava:subscribe'; | |
protected $description = 'Subscribes to a Strava webhook'; | |
public function __construct() | |
{ | |
parent::__construct(); | |
} | |
public function handle() | |
{ | |
$id = app(StravaWebhookService::class)->subscribe(); | |
if ($id) { | |
$this->info("Successfully subscribed ID: {$id}"); | |
} else { | |
$this->warn('Unable to subscribe'); | |
} | |
return 0; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// app/Console/Commands/UnsubscribeStravaWebhookCommand.php | |
namespace App\Console\Commands; | |
use App\Services\StravaWebhookService; | |
use Illuminate\Console\Command; | |
class UnsubscribeStravaWebhookCommand extends Command | |
{ | |
protected $signature = 'strava:unsubscribe'; | |
protected $description = 'Deletes a Strava webhook subscription'; | |
public function __construct() | |
{ | |
parent::__construct(); | |
} | |
public function handle() | |
{ | |
if (app(StravaWebhookService::class)->unsubscribe()) { | |
$this->info("Successfully unsubscribed"); | |
} else { | |
$this->warn('Error or no subscription found'); | |
} | |
return 0; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// app/Console/Commands/ViewStravaWebhookCommand.php | |
namespace App\Console\Commands; | |
use App\Services\StravaWebhookService; | |
use Illuminate\Console\Command; | |
class ViewStravaWebhookCommand extends Command | |
{ | |
protected $signature = 'strava:view-subscription'; | |
protected $description = 'Views a Strava webhook subscription'; | |
public function __construct() | |
{ | |
parent::__construct(); | |
} | |
public function handle() | |
{ | |
$id = app(StravaWebhookService::class)->view(); | |
if ($id) { | |
$this->info("Subscription ID: $id"); | |
} else { | |
$this->warn('Error or no subscription found'); | |
} | |
return 0; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// routes/web.php | |
Route::get('/webhook', function (Request $request) { | |
$mode = $request->query('hub_mode'); // hub.mode | |
$token = $request->query('hub_verify_token'); // hub.verify_token | |
$challenge = $request->query('hub_challenge'); // hub.challenge | |
return app(StravaWebhookService::class)->validate($mode, $token, $challenge); | |
}); | |
Route::post('/webhook', function (Request $request) { | |
$aspect_type = $request['aspect_type']; // "create" | "update" | "delete" | |
$event_time = $request['event_time']; // time the event occurred | |
$object_id = $request['object_id']; // activity ID | athlete ID | |
$object_type = $request['object_type']; // "activity" | "athlete" | |
$owner_id = $request['owner_id']; // athlete ID | |
$subscription_id = $request['subscription_id']; // push subscription ID receiving the event | |
$updates = $request['updates']; // activity update: {"title" | "type" | "private": true/false} ; app deauthorization: {"authorized": false} | |
Log::channel('strava')->info(json_encode($request->all())); | |
return response('EVENT_RECEIVED', Response::HTTP_OK); | |
})->withoutMiddleware(VerifyCsrfToken::class); |
Not at all. The webhook payload is same as a manual sync, which I can do in dev. So I just pushed it to prod (where it's just me so I don't care if it break), signed in, and waited for a new activity to be dispatched, while keeping an eye on the logs for any errors. It worked first try for me. Somehow the webhook workflow was trouble-free from the beginning.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This will help me a lot.
One question, to test in development, did you use that Ngrok in Laravel?