Skip to content

Instantly share code, notes, and snippets.

@thecompez
Created October 21, 2025 19:31
Show Gist options
  • Save thecompez/6d9f147ca2f6f046a412e941a1f15ba4 to your computer and use it in GitHub Desktop.
Save thecompez/6d9f147ca2f6f046a412e941a1f15ba4 to your computer and use it in GitHub Desktop.
webhook php
<?php
declare(strict_types=1);
require '../../lib/vendor/autoload.php';
require "../../lib/core.php";
header('Content-Type: application/json');
class FarcasterWebhookHandler {
private array $webhookData = [];
private string $rawInput;
private string $logFile = 'logs/farcaster_webhook.log';
// Supported webhook events from Base docs
private const SUPPORTED_EVENTS = [
'miniapp_added',
'notifications_enabled',
'miniapp_removed',
'notifications_disabled'
];
public function __construct() {
$this->setupEnvironment();
$this->validateRequest();
$this->readAndValidateInput();
}
/**
* Set up the runtime environment
*/
private function setupEnvironment(): void {
// Error logging
ini_set('log_errors', '1');
ini_set('error_log', $this->logFile);
// Security headers
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');
header('Content-Type: application/json');
// Start session for token persistence
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
/**
* Validate the request method and content type
*/
private function validateRequest(): void {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$this->fail('Method not allowed', 405);
}
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
if (stripos($contentType, 'application/json') === false) {
$this->fail('Unsupported Media Type: Expected application/json', 415);
}
}
/**
* Read and validate input data
*/
private function readAndValidateInput(): void {
$this->rawInput = file_get_contents("php://input");
if (empty($this->rawInput)) {
$this->fail('Empty request body', 400);
}
$this->log("Raw webhook data received: " . $this->rawInput);
$decoded = json_decode($this->rawInput, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->log("JSON decode error: " . json_last_error_msg());
$this->fail('Invalid JSON input: ' . json_last_error_msg(), 400);
}
if (!is_array($decoded)) {
$this->fail('Invalid webhook data structure', 400);
}
$this->webhookData = $decoded;
}
/**
* Parse webhook event according to Base/Farcaster format
*/
private function parseWebhookEvent(): array {
// According to Base docs, the webhook data contains:
// - fid: The user's FID
// - appFid: The client's FID (Base app is 309857)
// - event: The event payload with type and notification details
$requiredFields = ['fid', 'appFid', 'event'];
foreach ($requiredFields as $field) {
if (!isset($this->webhookData[$field])) {
throw new Exception("Missing required field: {$field}");
}
}
$fid = $this->webhookData['fid'];
$appFid = $this->webhookData['appFid'];
$eventData = $this->webhookData['event'];
if (!isset($eventData['event'])) {
throw new Exception("Missing event type in event data");
}
$this->log("Webhook received - FID: {$fid}, App FID: {$appFid}, Event: {$eventData['event']}");
return [
'fid' => $fid,
'appFid' => $appFid,
'event' => $eventData['event'],
'notificationDetails' => $eventData['notificationDetails'] ?? null
];
}
/**
* Handles token storage from webhook events
*/
private function handleTokenStorage(array $parsedData): void {
$fid = $parsedData['fid'];
$appFid = $parsedData['appFid'];
$eventType = $parsedData['event'];
$tokenEvents = [
'miniapp_added' => 'store',
'notifications_enabled' => 'store',
'miniapp_removed' => 'clear',
'notifications_disabled' => 'clear'
];
if (!isset($tokenEvents[$eventType])) {
$this->log("Event {$eventType} does not require token handling");
return;
}
$userKey = "notification_token_{$fid}_{$appFid}";
if ($tokenEvents[$eventType] === 'store') {
if (!$parsedData['notificationDetails']) {
$this->log("{$eventType} event received but no notificationDetails provided");
return;
}
$details = $parsedData['notificationDetails'];
if (!isset($details['url'], $details['token'])) {
$this->log("{$eventType} event received but incomplete notificationDetails");
return;
}
// Store in session (you might want to use a database instead for production)
$_SESSION[$userKey] = [
"url" => $details['url'],
"token" => $details['token'],
"fid" => $fid,
"appFid" => $appFid
];
$this->log("Stored token from {$eventType} event for FID: {$fid}, App FID: {$appFid}");
// Send welcome notification for miniapp_added
if ($eventType === 'miniapp_added') {
$this->sendWelcomeNotification($fid, $appFid, $details);
}
} else {
unset($_SESSION[$userKey]);
$this->log("Cleared token due to {$eventType} event for FID: {$fid}, App FID: {$appFid}");
}
}
/**
* Send welcome notification when mini app is added
*/
private function sendWelcomeNotification(int $fid, int $appFid, array $notificationDetails): void {
// You'll need to implement this based on the sendNotification example
$this->log("Would send welcome notification to FID: {$fid}, App FID: {$appFid}");
// Example implementation:
/*
$notificationData = [
'notificationId' => uniqid(),
'title' => 'Welcome to Our App',
'body' => 'Thank you for adding our mini app!',
'targetUrl' => 'https://yourdomain.com', // Must be your domain
'tokens' => [$notificationDetails['token']]
];
// Make POST request to notification URL
$this->sendNotificationRequest($notificationDetails['url'], $notificationData);
*/
}
/**
* Send notification to Farcaster API
*/
private function sendNotificationRequest(string $url, array $notificationData): void {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($notificationData)
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$this->log("Notification sent - HTTP Code: {$httpCode}, Response: {$response}");
}
/**
* Processes the webhook event
*/
public function processWebhook(): void {
try {
$parsedData = $this->parseWebhookEvent();
$event = $parsedData['event'];
// Validate event type
if (!in_array($event, self::SUPPORTED_EVENTS)) {
$this->log("Unhandled webhook event: {$event}");
$this->fail("Unsupported event type: {$event}", 400);
}
$this->log("Processing webhook event: {$event}");
$this->handleTokenStorage($parsedData);
$this->success();
} catch (Exception $e) {
$this->log("Error processing webhook: " . $e->getMessage());
$this->fail($e->getMessage(), 400);
}
}
/**
* Send success response
*/
private function success(): void {
http_response_code(200);
echo json_encode([
"status" => "success",
"timestamp" => time()
]);
exit;
}
/**
* Send error response and exit
*/
private function fail(string $message, int $statusCode = 400): void {
http_response_code($statusCode);
echo json_encode([
"error" => $message,
"timestamp" => time()
]);
exit;
}
/**
* Log messages with timestamp
*/
private function log(string $message): void {
$timestamp = date('Y-m-d H:i:s');
error_log("[{$timestamp}] {$message}");
}
}
// Execute the webhook handler
try {
$handler = new FarcasterWebhookHandler();
$handler->processWebhook();
} catch (Throwable $e) {
// Global fallback error handler
http_response_code(500);
header('Content-Type: application/json');
echo json_encode([
"error" => "Internal server error",
"timestamp" => time()
]);
exit;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment