Skip to content

Instantly share code, notes, and snippets.

@thecompez
Created October 21, 2025 22:55
Show Gist options
  • Save thecompez/231aff5cc69571757041f9f898c2b4a8 to your computer and use it in GitHub Desktop.
Save thecompez/231aff5cc69571757041f9f898c2b4a8 to your computer and use it in GitHub Desktop.
WebHook
<?php
declare(strict_types=1);
// Headers - SET THESE FIRST
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
// Handle preflight requests
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit(0);
}
// Turn off HTML errors
ini_set('display_errors', '0');
ini_set('log_errors', '1');
error_reporting(E_ALL);
// Simple response function
function jsonResponse($success, $message, $data = []) {
$response = ['success' => $success];
if ($success) {
$response['message'] = $message;
if (!empty($data)) {
$response['data'] = $data;
}
} else {
$response['error'] = $message;
}
echo json_encode($response);
exit;
}
// Only allow POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
jsonResponse(false, 'Only POST requests allowed');
}
class FarcasterWebhookHandler {
private array $webhookData = [];
private string $logFile = 'logs/farcaster_webhook.log';
// Supported webhook events from Base docs
private const SUPPORTED_EVENTS = [
'miniapp_added',
'frame_added',
'notifications_enabled',
'miniapp_removed',
'frame_removed',
'notifications_disabled'
];
public function __construct() {
$this->readAndValidateInput();
}
/**
* Read and validate input data
*/
private function readAndValidateInput(): void {
$rawInput = file_get_contents('php://input');
if (empty($rawInput)) {
jsonResponse(false, 'Empty request body');
}
$this->log("Raw webhook data received: " . $rawInput);
$decoded = json_decode($rawInput, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->log("JSON decode error: " . json_last_error_msg());
jsonResponse(false, 'Invalid JSON input: ' . json_last_error_msg());
}
if (!is_array($decoded)) {
jsonResponse(false, 'Invalid webhook data structure');
}
$this->webhookData = $decoded;
}
/**
* Parse webhook event according to Base/Farcaster format
*/
private function parseWebhookEvent(): array {
$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
];
}
/**
* Store notification token in database using your existing method
*/
private function storeTokenInDatabase(int $fid, int $appFid, array $notificationDetails): bool {
try {
// Include database using your existing method
require_once '../../lib/db.php';
// Get database connection
$conn = getDatabaseConnection('geny');
if (!$conn || $conn->connect_error) {
$error = $conn ? $conn->connect_error : 'Connection failed';
$this->log("Database connection failed: " . $error);
return false;
}
// Escape inputs for safety
$escapedUrl = $conn->real_escape_string($notificationDetails['url']);
$escapedToken = $conn->real_escape_string($notificationDetails['token']);
// Check if record exists
$checkSql = "SELECT id FROM user_notification_tokens WHERE fid = $fid AND app_fid = $appFid";
$result = $conn->query($checkSql);
if ($result && $result->num_rows > 0) {
// Update existing record
$sql = "UPDATE user_notification_tokens
SET notification_url = '$escapedUrl',
notification_token = '$escapedToken',
updated_at = NOW()
WHERE fid = $fid AND app_fid = $appFid";
} else {
// Insert new record
$sql = "INSERT INTO user_notification_tokens
(fid, app_fid, notification_url, notification_token, created_at, updated_at)
VALUES ($fid, $appFid, '$escapedUrl', '$escapedToken', NOW(), NOW())";
}
$success = $conn->query($sql);
if (!$success) {
$this->log("Database error: " . $conn->error);
}
$conn->close();
return $success;
} catch (Exception $e) {
$this->log("Database error storing token: " . $e->getMessage());
return false;
}
}
/**
* Remove notification token from database
*/
private function removeTokenFromDatabase(int $fid, int $appFid): bool {
try {
require_once '../../lib/db.php';
$conn = getDatabaseConnection('geny');
if (!$conn || $conn->connect_error) {
$error = $conn ? $conn->connect_error : 'Connection failed';
$this->log("Database connection failed: " . $error);
return false;
}
$sql = "DELETE FROM user_notification_tokens WHERE fid = $fid AND app_fid = $appFid";
$success = $conn->query($sql);
if (!$success) {
$this->log("Database error: " . $conn->error);
}
$conn->close();
return $success;
} catch (Exception $e) {
$this->log("Database error removing token: " . $e->getMessage());
return false;
}
}
/**
* Handles token storage from webhook events
*/
private function handleTokenStorage(array $parsedData): void {
$fid = $parsedData['fid'];
$appFid = $parsedData['appFid'];
$eventType = $parsedData['event'];
$tokenEvents = [
'frame_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;
}
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 database
if ($this->storeTokenInDatabase($fid, $appFid, $details)) {
$this->log("Stored token from {$eventType} event for FID: {$fid}, App FID: {$appFid}");
// Send welcome notification for frame_added
if ($eventType === 'frame_added') {
$this->sendWelcomeNotification($fid, $appFid, $details);
}
} else {
$this->log("Failed to store token in database for FID: {$fid}");
}
} else {
if ($this->removeTokenFromDatabase($fid, $appFid)) {
$this->log("Cleared token due to {$eventType} event for FID: {$fid}, App FID: {$appFid}");
} else {
$this->log("Failed to clear token from database for FID: {$fid}");
}
}
}
/**
* Send welcome notification when mini app is added
*/
private function sendWelcomeNotification(int $fid, int $appFid, array $notificationDetails): void {
$notificationData = [
'notificationId' => 'welcome-' . $fid . '-' . time(),
'title' => 'Welcome to GenyTask! 🎉',
'body' => 'Thank you for adding our mini app! You will receive notifications for new tasks and updates.',
'targetUrl' => 'https://task.genyapps.xyz',
'tokens' => [$notificationDetails['token']]
];
$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),
CURLOPT_TIMEOUT => 10
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$this->log("Welcome 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}");
jsonResponse(false, "Unsupported event type: {$event}");
}
$this->log("Processing webhook event: {$event}");
$this->handleTokenStorage($parsedData);
jsonResponse(true, "Webhook processed successfully");
} catch (Exception $e) {
$this->log("Error processing webhook: " . $e->getMessage());
jsonResponse(false, $e->getMessage());
}
}
/**
* 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) {
jsonResponse(false, 'Internal server error: ' . $e->getMessage());
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment