Update codebase
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
/**
|
||||
* sms/outbox_pull.php — نقطة سحب الرسائل الصادرة
|
||||
* ─────────────────────────────────────────────────────────────
|
||||
* تطبيق الأندرويد على الهاتف الحامل لرقمنا يستدعي هذه النقطة كل بضع
|
||||
* ثوانٍ: يسحب ما ينتظر الإرسال، يرسله عبر شريحته، ثم يبلّغ بالنتيجة.
|
||||
*
|
||||
* لماذا سحب لا دفع: الهاتف خلف شبكة الجوّال بلا عنوان ثابت ولا منفذ
|
||||
* مفتوح، فلا سبيل للسيرفر أن يبادره. والسحب يجعل انقطاع الهاتف حالة
|
||||
* طبيعية — تتراكم الرسائل في الطابور وتُرسل حين يعود، بلا فقدان.
|
||||
*
|
||||
* العمليتان:
|
||||
* GET/POST action=pull → يحجز رسائل وينقلها إلى 'sending'
|
||||
* POST action=ack → يبلّغ بنجاح أو فشل ما أُرسل
|
||||
*
|
||||
* المصادقة: نفس مفتاح webhook_sms — الهاتف واحد والمفتاح واحد.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../core/bootstrap.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// ── المصادقة ────────────────────────────────────────────────
|
||||
$secretKeyPath = getenv('WEBHOOK_SECRET_KEY_PATH');
|
||||
$secretKey = '';
|
||||
if ($secretKeyPath && file_exists($secretKeyPath)) {
|
||||
$secretKey = trim(file_get_contents($secretKeyPath));
|
||||
}
|
||||
if ($secretKey === '') {
|
||||
$secretKey = getenv('WEBHOOK_SECRET_KEY') ?: '';
|
||||
}
|
||||
|
||||
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||||
if ($secretKey === '' || !hash_equals('Bearer ' . $secretKey, $authHeader)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'DB unavailable']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$payload = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||
$action = $_GET['action'] ?? ($payload['action'] ?? 'pull');
|
||||
$device = mb_substr((string) ($payload['device'] ?? $_GET['device'] ?? 'phone-1'), 0, 64);
|
||||
|
||||
// عدد الرسائل في السحبة الواحدة. صغير عمداً: الهاتف يرسل بالتسلسل
|
||||
// عبر شريحة واحدة، وحجز خمسين رسالة يعني بقاء آخرها محجوزة دقائق.
|
||||
const SMS_PULL_BATCH = 10;
|
||||
|
||||
// رسالة بقيت 'sending' أطول من هذا يُفترض أن الهاتف مات قبل تبليغنا،
|
||||
// فنعيدها للطابور. الرقم أكبر بكثير من زمن إرسال دفعة كاملة.
|
||||
const SMS_CLAIM_TIMEOUT_MINUTES = 10;
|
||||
|
||||
// الحد الأقصى لمحاولات الرسالة الواحدة — رقم معطّل لن يستقبل مهما
|
||||
// كررنا، وإبقاؤه في الطابور يعطّل ما خلفه.
|
||||
const SMS_MAX_ATTEMPTS = 3;
|
||||
|
||||
if ($action === 'ack') {
|
||||
// ═══ التبليغ بالنتيجة ═══════════════════════════════════
|
||||
$results = $payload['results'] ?? [];
|
||||
if (!is_array($results)) $results = [];
|
||||
|
||||
$ok = 0; $bad = 0;
|
||||
foreach ($results as $r) {
|
||||
$id = (int) ($r['id'] ?? 0);
|
||||
if ($id <= 0) continue;
|
||||
|
||||
if (!empty($r['sent'])) {
|
||||
$con->prepare("
|
||||
UPDATE sms_outbox SET status='sent', sent_at=NOW()
|
||||
WHERE id = ? AND status = 'sending'
|
||||
")->execute([$id]);
|
||||
$ok++;
|
||||
} else {
|
||||
// فشل: نعيدها للطابور ما لم تستنفد محاولاتها.
|
||||
$con->prepare("
|
||||
UPDATE sms_outbox
|
||||
SET status = IF(attempts >= " . SMS_MAX_ATTEMPTS . ", 'failed', 'pending'),
|
||||
last_error = ?, claimed_by = NULL, claimed_at = NULL
|
||||
WHERE id = ? AND status = 'sending'
|
||||
")->execute([mb_substr((string) ($r['error'] ?? 'unknown'), 0, 255), $id]);
|
||||
$bad++;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['status' => 'success', 'acked' => $ok, 'failed' => $bad]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ═══ السحب ══════════════════════════════════════════════════
|
||||
|
||||
// أولاً: تحرير ما عَلِق. هاتف انطفأ بعد الحجز وقبل الإرسال كان
|
||||
// سيحبس رسائله إلى الأبد.
|
||||
try {
|
||||
$con->prepare("
|
||||
UPDATE sms_outbox
|
||||
SET status='pending', claimed_by=NULL, claimed_at=NULL
|
||||
WHERE status='sending'
|
||||
AND claimed_at < NOW() - INTERVAL " . SMS_CLAIM_TIMEOUT_MINUTES . " MINUTE
|
||||
")->execute();
|
||||
} catch (PDOException $e) {
|
||||
error_log('[sms/outbox] تعذّر تحرير العالق: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// الحجز ثم القراءة، لا العكس: قراءة أولاً ثم تحديث تسمح لهاتفين
|
||||
// (أو لسحبتين متتاليتين) بأخذ نفس الرسالة وإرسالها مرتين.
|
||||
// وسم فريد لكل سحبة، لا اسم الجهاز وحده: لو سحب الهاتف دفعةً ولم
|
||||
// يبلّغ عنها بعد، ثم سحب ثانيةً، لأعاد الاستعلام الدفعة الأولى معها
|
||||
// وأُرسلت رسائلها مرتين.
|
||||
$claim = mb_substr($device . ':' . bin2hex(random_bytes(6)), 0, 64);
|
||||
|
||||
try {
|
||||
$con->prepare("
|
||||
UPDATE sms_outbox
|
||||
SET status='sending', claimed_by=?, claimed_at=NOW(), attempts=attempts+1
|
||||
WHERE status='pending'
|
||||
ORDER BY id ASC
|
||||
LIMIT " . SMS_PULL_BATCH
|
||||
)->execute([$claim]);
|
||||
|
||||
$stmt = $con->prepare("
|
||||
SELECT id, recipient, body, context, ref_id
|
||||
FROM sms_outbox
|
||||
WHERE status='sending' AND claimed_by=?
|
||||
ORDER BY id ASC
|
||||
LIMIT " . SMS_PULL_BATCH
|
||||
);
|
||||
$stmt->execute([$claim]);
|
||||
$messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log('[sms/outbox] فشل السحب: ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Query failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'count' => count($messages),
|
||||
'messages' => $messages,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
Reference in New Issue
Block a user