$value) { if (strncmp($key, 'HTTP_', 5) === 0 && is_string($value)) { $out[str_replace('_', '-', substr($key, 5))] = $value; } } return $out; } function respond(int $code, string $text): void { http_response_code($code); header('Content-Type: text/plain; charset=utf-8'); echo $text; exit; } if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { header('Allow: POST'); respond(405, 'method not allowed'); } $secret = (string) getenv('TONPING_WEBHOOK_SECRET'); if ($secret === '') { error_log('TONPING_WEBHOOK_SECRET is not set'); respond(500, 'server not configured'); } $rawBody = (string) file_get_contents('php://input'); $event = tonping_verify($secret, $rawBody, request_headers(), time()); if ($event === null) { respond(401, 'invalid signature'); } $eventId = $event['event_id'] ?? null; if (!is_string($eventId) || $eventId === '') { respond(400, 'missing event_id'); } // Record the id only after the handler succeeds; a failure answers 500 and TonPing retries. try { $code = tonping_handle_once(tonping_store_path(), $event, 'handle_event'); } catch (Throwable $e) { // The dedupe store is unusable (path, permissions, disk). Never answer 200 without it. error_log('tonping dedupe store failed: ' . $e->getMessage()); respond(500, 'store failure'); } respond($code, $code === 200 ? 'ok' : 'handler failed');