Skip to content

Instantly share code, notes, and snippets.

@thecompez
Last active October 21, 2025 23:17
Show Gist options
  • Save thecompez/8a8a6835833315140e5bb722eab30772 to your computer and use it in GitHub Desktop.
Save thecompez/8a8a6835833315140e5bb722eab30772 to your computer and use it in GitHub Desktop.
send-notification
<?php
declare(strict_types=1);
// Set up error logging.
ini_set('log_errors', 1);
ini_set('error_log', 'logs/error_log_file.log');
// Disable caching.
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');
header('Content-Type: application/json');
/**
* Interface NotificationStrategy
*
* Defines the structure for notification messages.
*/
interface NotificationStrategy {
public function getTitle(): string;
public function getBody(): string;
public function getTargetUrl(): string;
}
/**
* Welcome Notification
*/
class WelcomeNotification implements NotificationStrategy {
public function getTitle(): string {
return "🎉 Welcome to GenyTask!"; // 22 chars
}
public function getBody(): string {
return "Thank you for adding our mini app! You will receive notifications for new tasks and updates.";
}
public function getTargetUrl(): string {
return "https://task.genyapps.xyz";
}
}
/**
* Update Notification
*/
class UpdateNotification implements NotificationStrategy {
public function getTitle(): string {
return "🚀 GenyTask Updated!";
}
public function getBody(): string {
return "New features have been added to the miniapp. Let's check it out.";
}
public function getTargetUrl(): string {
return "https://task.genyapps.xyz";
}
}
/**
* Security Alert Notification
*/
class SecurityAlertNotification implements NotificationStrategy {
public function getTitle(): string {
return "⚠️ Security Alert!";
}
public function getBody(): string {
return "Unusual activity detected on your account. Take action!";
}
public function getTargetUrl(): string {
return "https://task.genyapps.xyz";
}
}
/**
* New Task Available Notification
*/
class NewTaskNotification implements NotificationStrategy {
private string $taskType;
private string $taskPoints;
private string $taskPlatform;
public function __construct(string $taskType = "regular", string $taskPoints = "", string $taskPlatform = "") {
$this->taskType = $taskType;
$this->taskPoints = $taskPoints;
$this->taskPlatform = $taskPlatform;
}
public function getTitle(): string {
return "🎯 New Task Available!"; // 22 chars
}
public function getBody(): string {
$body = "A new ";
// Add task type
switch ($this->taskType) {
case 'premium':
$body .= "⭐ PREMIUM task";
break;
case 'holding':
$body .= "💎 HOLDING task";
break;
case 'activities':
$body .= "🎯 ACTIVITY task";
break;
default:
$body .= "🔄 task";
}
$body .= " is waiting for you";
// Add platform if available
if (!empty($this->taskPlatform)) {
$body .= " on " . $this->taskPlatform;
}
// Add points if available
if (!empty($this->taskPoints)) {
$body .= " - Earn " . $this->taskPoints . " points!";
} else {
$body .= " - Complete it now to earn rewards!";
}
return $body;
}
public function getTargetUrl(): string {
return "https://task.genyapps.xyz";
}
}
/**
* Task Completion Notification
*/
class TaskCompletionNotification implements NotificationStrategy {
private string $pointsEarned;
private string $taskName;
public function __construct(string $pointsEarned = "", string $taskName = "") {
$this->pointsEarned = $pointsEarned;
$this->taskName = $taskName;
}
public function getTitle(): string {
return "✅ Task Completed!"; // 18 chars
}
public function getBody(): string {
if (!empty($this->taskName) && !empty($this->pointsEarned)) {
return "You completed '{$this->taskName}' and earned {$this->pointsEarned} points! Great work!";
} elseif (!empty($this->pointsEarned)) {
return "You earned {$this->pointsEarned} points! Your GENY balance has increased!";
} else {
return "Task completed successfully! Your progress is updated.";
}
}
public function getTargetUrl(): string {
return "https://task.genyapps.xyz";
}
}
/**
* Leaderboard Achievement Notification
*/
class LeaderboardNotification implements NotificationStrategy {
private string $rank;
private string $achievement;
public function __construct(string $rank = "", string $achievement = "") {
$this->rank = $rank;
$this->achievement = $achievement;
}
public function getTitle(): string {
return "🏆 Leaderboard Update!"; // 22 chars
}
public function getBody(): string {
if (!empty($this->rank)) {
return "You're now ranked #{$this->rank} on the leaderboard! Keep climbing!";
} elseif (!empty($this->achievement)) {
return "Achievement unlocked: {$this->achievement}! Check your leaderboard position.";
} else {
return "Your leaderboard position has changed! See where you stand in the community.";
}
}
public function getTargetUrl(): string {
return "https://task.genyapps.xyz";
}
}
/**
* Factory to create notifications dynamically.
*/
class NotificationFactory {
public static function createNotification(string $type, array $params = []): NotificationStrategy {
return match ($type) {
'update' => new UpdateNotification(),
'alert' => new SecurityAlertNotification(),
'new_task' => new NewTaskNotification(
$params['task_type'] ?? 'regular',
$params['task_points'] ?? '',
$params['task_platform'] ?? ''
),
'task_complete' => new TaskCompletionNotification(
$params['points_earned'] ?? '',
$params['task_name'] ?? ''
),
'leaderboard' => new LeaderboardNotification(
$params['rank'] ?? '',
$params['achievement'] ?? ''
),
default => new WelcomeNotification(), // Default to 'welcome'
};
}
}
/**
* Class NotificationSender
*
* Handles storing and sending Farcaster notification details from database.
*/
class NotificationSender {
/**
* Gets database connection
*/
private function getDatabaseConnection() {
require_once '../lib/db.php';
$conn = getDatabaseConnection('geny');
if (!$conn || $conn->connect_error) {
throw new Exception("Database connection failed: " . ($conn ? $conn->connect_error : 'Connection failed'));
}
return $conn;
}
/**
* Helper method to ensure title length compliance
*/
private function ensureTitleLength(string $title): string {
$maxLength = 32;
if (strlen($title) <= $maxLength) {
return $title;
}
// Truncate and add ellipsis if needed
return substr($title, 0, $maxLength - 3) . '...';
}
/**
* Gets all active notification tokens from database
*/
public function getNotificationTokens(): array {
$conn = $this->getDatabaseConnection();
$sql = "SELECT fid, app_fid, notification_url, notification_token
FROM user_notification_tokens
WHERE notification_url IS NOT NULL
AND notification_token IS NOT NULL
AND notification_url != ''
AND notification_token != ''";
$result = $conn->query($sql);
$tokens = [];
if ($result && $result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$tokens[] = [
'fid' => $row['fid'],
'app_fid' => $row['app_fid'],
'url' => $row['notification_url'],
'token' => $row['notification_token']
];
}
}
$conn->close();
return $tokens;
}
/**
* Gets notification tokens for a specific user
*/
public function getUserNotificationTokens(int $fid, int $appFid): ?array {
$conn = $this->getDatabaseConnection();
$sql = "SELECT notification_url, notification_token
FROM user_notification_tokens
WHERE fid = ? AND app_fid = ?
AND notification_url IS NOT NULL
AND notification_token IS NOT NULL
AND notification_url != ''
AND notification_token != ''";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ii", $fid, $appFid);
$stmt->execute();
$result = $stmt->get_result();
$tokenData = null;
if ($row = $result->fetch_assoc()) {
$tokenData = [
'url' => $row['notification_url'],
'token' => $row['notification_token']
];
}
$stmt->close();
$conn->close();
return $tokenData;
}
/**
* Sends notification to all users with proper success handling
*/
public function sendBroadcastNotification(string $type, array $params = []): array {
$tokens = $this->getNotificationTokens();
if (empty($tokens)) {
throw new Exception("No active notification tokens found in database");
}
$notification = NotificationFactory::createNotification($type, $params);
$results = [
'success_count' => 0,
'fail_count' => 0,
'total' => count($tokens),
'details' => []
];
foreach ($tokens as $tokenData) {
try {
$this->sendSingleNotification($tokenData, $notification, $type);
$results['success_count']++;
$results['details'][] = [
'fid' => $tokenData['fid'],
'app_fid' => $tokenData['app_fid'],
'status' => 'success'
];
} catch (Exception $e) {
$results['fail_count']++;
$results['details'][] = [
'fid' => $tokenData['fid'],
'app_fid' => $tokenData['app_fid'],
'status' => 'failed',
'error' => $e->getMessage()
];
error_log("Failed to send notification to FID: {$tokenData['fid']} - {$e->getMessage()}");
}
}
error_log("Broadcast completed: {$results['success_count']} successful, {$results['fail_count']} failed");
return $results;
}
/**
* Sends notification to specific user
*/
public function sendUserNotification(int $fid, int $appFid, string $type, array $params = []): void {
$tokenData = $this->getUserNotificationTokens($fid, $appFid);
if (!$tokenData) {
throw new Exception("No active notification tokens found for user FID: {$fid}");
}
$notification = NotificationFactory::createNotification($type, $params);
$this->sendSingleNotification(array_merge(['fid' => $fid, 'app_fid' => $appFid], $tokenData), $notification, $type);
}
/**
* Sends a single notification with proper Farcaster API response handling
*/
private function sendSingleNotification(array $tokenData, NotificationStrategy $notification, string $type): void {
$payload = [
'notificationId' => uniqid('notif_', true),
'title' => $this->ensureTitleLength($notification->getTitle()), // Use the helper here
'body' => $notification->getBody(),
'targetUrl' => $notification->getTargetUrl(),
'tokens' => [$tokenData['token']],
];
$ch = curl_init($tokenData['url']);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_TIMEOUT => 10
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) {
$error = curl_error($ch);
curl_close($ch);
throw new Exception("cURL error: $error");
}
curl_close($ch);
$responseData = json_decode($response, true);
// Handle Farcaster API response
if ($httpCode === 200) {
if (isset($responseData['result'])) {
$result = $responseData['result'];
// SUCCESS: If we have successfulTokens, the notification was delivered
if (isset($result['successfulTokens']) && count($result['successfulTokens']) > 0) {
// This is a SUCCESS - notification was delivered to at least one token
return;
}
// Check for other scenarios
if (isset($result['invalidTokens']) && count($result['invalidTokens']) > 0) {
throw new Exception("Invalid tokens: " . implode(', ', $result['invalidTokens']));
}
if (isset($result['rateLimitedTokens']) && count($result['rateLimitedTokens']) > 0) {
throw new Exception("Rate limited tokens: " . implode(', ', $result['rateLimitedTokens']));
}
// If we get here but no errors, it's still a success
return;
} else {
// Response doesn't have the expected structure, but HTTP 200 means success
return;
}
} elseif ($httpCode === 400) {
// Bad request - usually validation errors
if (isset($responseData['message'])) {
throw new Exception("Validation error: " . $responseData['message']);
} else {
throw new Exception("Bad request - HTTP: {$httpCode}");
}
} else {
throw new Exception("Notification failed - HTTP: {$httpCode}, Response: " . substr($response, 0, 200));
}
}
/**
* Gets notification parameters from request
*/
private function getNotificationParams(string $type): array {
$params = [];
switch ($type) {
case 'new_task':
$params = [
'task_type' => $_GET['task_type'] ?? 'regular',
'task_points' => $_GET['task_points'] ?? '',
'task_platform' => $_GET['task_platform'] ?? ''
];
break;
case 'task_complete':
$params = [
'points_earned' => $_GET['points_earned'] ?? '',
'task_name' => $_GET['task_name'] ?? ''
];
break;
case 'leaderboard':
$params = [
'rank' => $_GET['rank'] ?? '',
'achievement' => $_GET['achievement'] ?? ''
];
break;
}
return $params;
}
/**
* Processes the incoming request
*/
public function processRequest(): void {
try {
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$type = $_GET['type'] ?? 'welcome';
$fid = isset($_GET['fid']) ? (int)$_GET['fid'] : null;
$appFid = isset($_GET['app_fid']) ? (int)$_GET['app_fid'] : null;
$params = $this->getNotificationParams($type);
// If specific user is requested, send only to that user
if ($fid !== null) {
if ($appFid === null) {
// If no app_fid provided, get all tokens for the FID
$tokens = $this->getNotificationTokens();
$userTokens = array_filter($tokens, function($token) use ($fid) {
return $token['fid'] == $fid;
});
if (empty($userTokens)) {
throw new Exception("No active notification tokens found for user FID: {$fid}");
}
$results = [
'success_count' => 0,
'fail_count' => 0,
'total' => count($userTokens),
'details' => []
];
$notification = NotificationFactory::createNotification($type, $params);
foreach ($userTokens as $tokenData) {
try {
$this->sendSingleNotification($tokenData, $notification, $type);
$results['success_count']++;
$results['details'][] = [
'fid' => $tokenData['fid'],
'app_fid' => $tokenData['app_fid'],
'status' => 'success'
];
} catch (Exception $e) {
$results['fail_count']++;
$results['details'][] = [
'fid' => $tokenData['fid'],
'app_fid' => $tokenData['app_fid'],
'status' => 'failed',
'error' => $e->getMessage()
];
}
}
echo json_encode([
'status' => 'success',
'message' => "Notification sent to user FID: {$fid}",
'type' => $type,
'target' => 'user',
'fid' => $fid,
'results' => $results
]);
} else {
// Send to specific user with app_fid
$this->sendUserNotification($fid, $appFid, $type, $params);
echo json_encode([
'status' => 'success',
'message' => "Notification sent to user FID: {$fid}",
'type' => $type,
'target' => 'user',
'fid' => $fid,
'app_fid' => $appFid
]);
}
} else {
// Broadcast to all users (existing behavior)
$results = $this->sendBroadcastNotification($type, $params);
echo json_encode([
'status' => 'success',
'message' => "Broadcast notification sent to {$results['success_count']} users",
'type' => $type,
'target' => 'all',
'results' => $results
]);
}
} else {
echo json_encode([
'status' => 'error',
'message' => 'Only GET requests allowed for sending notifications'
]);
}
} catch (Exception $e) {
echo json_encode([
'status' => 'error',
'message' => $e->getMessage()
]);
}
}
}
// Create an instance and process the request.
$notificationSender = new NotificationSender();
$notificationSender->processRequest();
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment