C-WTS برنامج سي للواتس

دليل الربط مع Laravel

خطوات تكامل C-WTS مع تطبيق Laravel، مع الأمثلة وأكواد الأخطاء

١. نظرة عامة

C-WTS يوفّر REST API بسيط لإرسال رسائل واتساب نيابةً عن أي عميل مرتبط من لوحة التحكم. كل عميل لديه instance_id ثابت وaccess_token فريد، يستخدمهما تطبيقك للمصادقة.

Base URL الحالي: http://c-wts.com

خطوات التهيئة (للمبرمج)

  1. أنشئ العميل من لوحة التحكم: العملاء › إضافة حساب
  2. افتح صفحة العميل وامسح QR من واتساب → ستصبح الجلسة نشطة
  3. انسخ instance_id وaccess_token من جدول العملاء
  4. ضع البيانات في .env الخاص بـ Laravel كما في القسم 4
  5. استخدم الـ Service Class لإرسال الرسائل من أي مكان في تطبيقك

٢. المصادقة

كل طلب يحتاج لمعاملين:

المعاملالمثالالمكان
instance_id0001query أو body
access_tokenconst0001query أو body

الأمان: لا تكشف access_token في الـ frontend أبداً. اجعل كل الطلبات من الـ backend (Laravel).

٣. الـ Endpoints

GET /api/status

يرجع حالة الجلسة الحالية ومعلومات الاشتراك.

cURL
curl "http://c-wts.com/api/status?instance_id=0001&access_token=const0001"
الرد عند النجاح
{
  "ok": true,
  "status": "connected",
  "phone": "966501234567",
  "avatar_url": "https://...",
  "platform": "android",
  "subscription": {
    "start": 1700000000,
    "end":   1702592000,
    "days_remaining": 25
  }
}
GET /api/qrcode

يرجع كود QR (base64 PNG) لربط واتساب جديد. يفشل لو كانت الجلسة متصلة بالفعل.

cURL
curl "http://c-wts.com/api/qrcode?instance_id=0001&access_token=const0001"
الرد عند النجاح
{ "ok": true, "qr": "data:image/png;base64,iVBORw0KGgo..." }
إذا الكود لم يُولّد بعد (202)
{ "ok": false, "code": "qr_not_ready", "error": "..." }
POST /api/send

إرسال رسالة نصية. الرقم بصيغة دولية بدون + أو 00 (مثل 966501234567).

الـ body المطلوب
instance_id0001
access_tokenconst0001
number966501234567
messageنص الرسالة
cURL
curl -X POST "http://c-wts.com/api/send" \
  -d "instance_id=0001" \
  -d "access_token=const0001" \
  -d "number=966501234567" \
  -d "message=مرحباً من Laravel"
الرد عند النجاح
{
  "ok": true,
  "message_id": "3EB0F5F0A8F5B26402B11D",
  "timestamp": 1700000000
}

٤. كلاس Service جاهز للاستخدام

انسخ الكود التالي في تطبيق Laravel — يوفّر دوال جاهزة لكل العمليات مع التعامل الصحيح مع الأخطاء.

أ. أضف القيم في .env

WA_GATEWAY_URL=http://c-wts.com
WA_GATEWAY_INSTANCE_ID=0001
WA_GATEWAY_ACCESS_TOKEN=const0001

ب. أضف في config/services.php

'wa_gateway' => [
    'base_url'     => env('WA_GATEWAY_URL', 'http://localhost:3001'),
    'instance_id'  => env('WA_GATEWAY_INSTANCE_ID'),
    'access_token' => env('WA_GATEWAY_ACCESS_TOKEN'),
],

ج. أنشئ الملف app/Services/WaGateway.php

<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class WaGateway
{
    protected string $baseUrl;
    protected string $instanceId;
    protected string $accessToken;

    public function __construct(?string $instanceId = null, ?string $accessToken = null)
    {
        $this->baseUrl     = rtrim(config('services.wa_gateway.base_url'), '/');
        $this->instanceId  = $instanceId  ?? config('services.wa_gateway.instance_id');
        $this->accessToken = $accessToken ?? config('services.wa_gateway.access_token');
    }

    /** حالة الجلسة */
    public function status(): array
    {
        return $this->get('status');
    }

    /** جلب QR لربط جلسة جديدة */
    public function qrcode(): array
    {
        return $this->get('qrcode');
    }

    /** إرسال رسالة نصية */
    public function send(string $number, string $message): array
    {
        return $this->post('send', [
            'number'  => $this->cleanNumber($number),
            'message' => $message,
        ]);
    }

    /** تنظيف الرقم: إزالة + و 00 والمسافات */
    protected function cleanNumber(string $n): string
    {
        $d = preg_replace('/\D+/', '', $n);
        if (str_starts_with($d, '00')) $d = substr($d, 2);
        return $d;
    }

    protected function get(string $endpoint): array
    {
        try {
            $r = Http::timeout(20)
                ->acceptJson()
                ->get("{$this->baseUrl}/api/{$endpoint}", $this->credentials());
            return $r->json() ?? ['ok' => false, 'error' => 'Empty response'];
        } catch (\Throwable $e) {
            Log::error('WaGateway GET failed', ['endpoint' => $endpoint, 'msg' => $e->getMessage()]);
            return ['ok' => false, 'code' => 'network_error', 'error' => $e->getMessage()];
        }
    }

    protected function post(string $endpoint, array $data): array
    {
        try {
            $r = Http::timeout(30)
                ->acceptJson()
                ->asForm()
                ->post("{$this->baseUrl}/api/{$endpoint}", array_merge($this->credentials(), $data));
            return $r->json() ?? ['ok' => false, 'error' => 'Empty response'];
        } catch (\Throwable $e) {
            Log::error('WaGateway POST failed', ['endpoint' => $endpoint, 'msg' => $e->getMessage()]);
            return ['ok' => false, 'code' => 'network_error', 'error' => $e->getMessage()];
        }
    }

    protected function credentials(): array
    {
        return [
            'instance_id'  => $this->instanceId,
            'access_token' => $this->accessToken,
        ];
    }
}

٥. أمثلة استخدام

إرسال رسالة بسيطة

use App\Services\WaGateway;

$wa = new WaGateway();
$res = $wa->send('966501234567', 'مرحباً من Laravel 👋');

if ($res['ok']) {
    return "تم — message_id: {$res['message_id']}";
}
return "فشل: {$res['error']}";

التحقق من حالة الاتصال قبل الإرسال

$wa = new WaGateway();
$status = $wa->status();

if (!$status['ok'] || $status['status'] !== 'connected') {
    return back()->with('error', 'الجلسة غير متصلة، اربط واتساب أولاً');
}

$wa->send($patient->phone, "موعدك في {$appointment->date}");

استخدام عميل مختلف لكل عيادة

// كل عيادة لها instance_id و access_token خاص بها (محفوظين في DB)
$wa = new WaGateway($clinic->instance_id, $clinic->access_token);
$wa->send($patient->phone, $message);

إرسال جماعي (Broadcast)

foreach ($patients as $p) {
    $res = $wa->send($p->phone, "تذكير بالموعد غداً");
    if (!$res['ok']) {
        Log::warning("فشل إرسال لـ {$p->phone}: {$res['error']}");
    }
    usleep(500_000); // نصف ثانية بين كل رسالة (لتفادي الحظر)
}

٦. صفحة الربط الجاهزة (Embed HTML)

قالب HTML كامل ذاتي الاحتواء (HTML + CSS + JS في ملف واحد) لعرض QR ومراقبة الاتصال — ضعه في موقعك ليربط عملاؤك واتسابهم بدون تصميم صفحة من الصفر.

تنزيل سريع: wa-connect-template.html

أ. الكود الجاهز

انسخ الكود التالي وضعه في صفحة بموقعك (مثل connect.html). استبدل INSTANCE_ID_HERE وACCESS_TOKEN_HERE ببيانات اعتماد عميلك، أو اجلبها ديناميكياً من backend.

<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>اربط واتسابك</title>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body { font-family: 'Cairo', sans-serif; background: linear-gradient(135deg,#f0fdf4,#ecfdf5); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; color: #1f2937; }
  .wa-emb { background: #fff; border-radius: 16px; box-shadow: 0 20px 50px rgba(0,0,0,.1); padding: 32px; max-width: 480px; width: 100%; text-align: center; }
  .wa-emb-icon { width: 64px; height: 64px; border-radius: 50%; background: linear-gradient(135deg,#25d366,#128c7e); color: #fff; display: inline-flex; align-items: center; justify-content: center; font-size: 32px; margin: 0 auto 16px; }
  .wa-emb h1 { font-size: 22px; font-weight: 800; margin-bottom: 6px; }
  .wa-emb .sub { color: #6b7280; font-size: 14px; margin-bottom: 24px; }
  .wa-emb-stage { min-height: 320px; display: flex; flex-direction: column; align-items: center; justify-content: center; }
  .wa-emb-spinner { width: 40px; height: 40px; border: 3px solid #e5e7eb; border-top-color: #16a34a; border-radius: 50%; animation: wa-spin 1s linear infinite; }
  @keyframes wa-spin { to { transform: rotate(360deg); } }
  .wa-emb-qr { padding: 14px; background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; box-shadow: 0 6px 20px rgba(0,0,0,.06); }
  .wa-emb-qr img { width: 240px; height: 240px; display: block; }
  .wa-emb-steps { text-align: right; margin: 16px 0; padding: 14px; background: #f5f3ff; border-radius: 10px; font-size: 13.5px; line-height: 2; color: #374151; list-style: none; counter-reset: s; }
  .wa-emb-steps li::before { content: counter(s) '. '; counter-increment: s; color: #6366f1; font-weight: 700; }
  .wa-emb-status { display: inline-flex; align-items: center; gap: 6px; padding: 5px 14px; border-radius: 999px; font-size: 12.5px; font-weight: 700; margin-bottom: 12px; }
  .wa-emb-status.ok { background: #d1fae5; color: #16a34a; }
  .wa-emb-status.wait { background: #fef3c7; color: #d97706; }
  .wa-emb-phone { font-size: 22px; font-weight: 800; direction: ltr; margin-top: 8px; }
  .wa-emb-success { color: #16a34a; }
  .wa-emb-success svg { width: 64px; height: 64px; margin-bottom: 12px; }
  .wa-emb-err { color: #dc2626; padding: 14px; background: #fef2f2; border-radius: 10px; font-size: 13px; }
  .wa-emb-foot { margin-top: 18px; font-size: 11.5px; color: #9ca3af; }
</style>
</head>
<body>

<div class="wa-emb">
  <div class="wa-emb-icon">📱</div>
  <h1>اربط حساب واتساب</h1>
  <p class="sub">امسح كود QR من هاتفك لتفعيل الإرسال الآلي</p>

  <div id="wa-stage" class="wa-emb-stage">
    <div class="wa-emb-spinner"></div>
    <p style="margin-top:14px;color:#6b7280;font-size:13px">جاري تحميل الكود...</p>
  </div>

  <p class="wa-emb-foot">مدعوم من C-WTS</p>
</div>

<script>
(function () {
  var API = "http://c-wts.com";
  var INSTANCE_ID  = "INSTANCE_ID_HERE";
  var ACCESS_TOKEN = "ACCESS_TOKEN_HERE";
  var stage = document.getElementById('wa-stage');
  var lastQr = null, lastView = null;

  function showSpinner(msg) {
    stage.innerHTML = '<div class="wa-emb-spinner"></div><p style="margin-top:14px;color:#6b7280;font-size:13px">' + msg + '</p>';
  }
  function showQR(qr) {
    if (qr === lastQr) return;
    lastQr = qr;
    stage.innerHTML =
      '<div class="wa-emb-status wait">⏳ في انتظار المسح</div>' +
      '<ol class="wa-emb-steps">' +
        '<li>افتح <strong>واتساب</strong> على هاتفك</li>' +
        '<li>اذهب للإعدادات → <strong>الأجهزة المرتبطة</strong></li>' +
        '<li>اضغط <strong>ربط جهاز</strong> ثم وجّه الكاميرا للكود</li>' +
      '</ol>' +
      '<div class="wa-emb-qr"><img src="' + qr + '" alt="QR"></div>';
  }
  function showConnected(phone) {
    stage.innerHTML =
      '<div class="wa-emb-success">' +
        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>' +
        '<h2 style="font-size:18px;margin-bottom:6px">تم الربط بنجاح!</h2>' +
        (phone ? '<div class="wa-emb-phone">+' + phone + '</div>' : '') +
        '<p style="margin-top:10px;color:#6b7280;font-size:13.5px">حسابك جاهز لاستقبال طلبات الإرسال.</p>' +
      '</div>';
  }
  function showError(msg) {
    stage.innerHTML = '<div class="wa-emb-err">⚠ ' + msg + '</div>';
  }

  function poll() {
    var url = API + '/api/qrcode?instance_id=' + encodeURIComponent(INSTANCE_ID) + '&access_token=' + encodeURIComponent(ACCESS_TOKEN);
    fetch(url, { cache: 'no-store' })
      .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, status: r.status, body: j }; }); })
      .then(function (res) {
        if (res.body.ok && res.body.qr) {
          if (lastView !== 'qr') lastView = 'qr';
          showQR(res.body.qr);
        } else if (res.status === 409) {
          if (lastView !== 'connected') {
            lastView = 'connected';
            fetch(API + '/api/status?instance_id=' + INSTANCE_ID + '&access_token=' + ACCESS_TOKEN)
              .then(function (r) { return r.json(); })
              .then(function (s) { showConnected(s.phone); });
          }
        } else if (res.status === 202) {
          if (lastView !== 'wait') { lastView = 'wait'; showSpinner('جاري توليد الكود... خلال ثوانٍ'); }
        } else if (res.status === 401 || res.status === 403) {
          showError(res.body.error || 'بيانات اعتماد غير صحيحة');
          return;
        } else {
          showError(res.body.error || 'حدث خطأ غير متوقع');
        }
        setTimeout(poll, 3000);
      })
      .catch(function (e) {
        showError('فشل الاتصال بالخادم. أعد المحاولة قريباً.');
        setTimeout(poll, 5000);
      });
  }
  poll();
})();
</script>

</body>
</html>

ب. مثال Laravel Blade (يحقن البيانات تلقائياً)

لو عندك multi-tenant: حقن قيم العميل من backend مباشرة في الـ HTML.

{{-- resources/views/wa-connect.blade.php --}}
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
    <meta charset="UTF-8">
    <title>اربط واتساب — {{ $clinic->name }}</title>
    {{-- ... باقي الـ CSS من القالب أعلاه ... --}}
</head>
<body>

<div class="wa-emb">
    <h1>اربط حساب واتساب لـ {{ $clinic->name }}</h1>
    <div id="wa-stage" class="wa-emb-stage">
        <div class="wa-emb-spinner"></div>
    </div>
</div>

<script>
(function () {
    var API          = @json(config('services.wa_gateway.base_url'));
    var INSTANCE_ID  = @json($clinic->wa_instance_id);
    var ACCESS_TOKEN = @json($clinic->wa_access_token);
    // ... باقي الـ JS من القالب أعلاه ...
})();
</script>

</body>
</html>

ج. مثال Controller في Laravel

// app/Http/Controllers/WaConnectController.php
public function show(Clinic $clinic)
{
    return view('wa-connect', compact('clinic'));
}

// routes/web.php
Route::get('/clinic/{clinic}/wa-connect', [WaConnectController::class, 'show'])
    ->name('wa.connect');

د. كيف يعمل القالب؟

  • عند تحميل الصفحة، يستدعي GET /api/qrcode كل 3 ثوانٍ.
  • إذا رجع QR → يعرضه مع خطوات المسح.
  • إذا رجع 409 (متصل بالفعل) → يستدعي /api/status ويعرض شاشة "تم الربط بنجاح" بالرقم.
  • إذا رجع 202 (لم يُولّد بعد) → يعرض spinner ويعيد المحاولة.
  • إذا رجع 401/403 → يعرض الخطأ ويتوقف.

٧. أكواد الأخطاء

HTTPcodeالمعنى
200نجاح
400invalid_inputالمعاملات ناقصة أو غير صالحة
400invalid_numberالرقم قصير جداً أو طويل جداً
400number_not_registeredالرقم غير مسجّل في WhatsApp
400session_not_connectedالجلسة ليست متصلة، اربط QR أولاً
400timeoutالإرسال استغرق وقتاً أكثر من اللازم
401missing_credentialsinstance_id أو access_token مفقود
401invalid_credentialsبيانات الاعتماد غير صحيحة
403subscription_expiredانتهى اشتراك العميل
409already_connectedالجلسة متصلة بالفعل (عند طلب QR)
202qr_not_readyكود QR لم يُولّد بعد، أعد المحاولة بعد 3-5 ثوانٍ

٨. أخطاء شائعة وحلولها

الرسالة لا تُرسل ولا يظهر خطأ

السبب: الرقم بصيغة خاطئة (يبدأ بـ + أو 00 أو فيه مسافات).

الحل: الـ Service Class يُنظّف الرقم تلقائياً، لكن تأكّد من تمرير رقم بصيغة دولية كاملة.

session_not_connected

السبب: العميل لم يمسح QR، أو فُصلت الجلسة.

الحل: ادخل لوحة التحكم → افتح صفحة العميل → امسح QR من جديد.

number_not_registered

السبب: الرقم غير مسجّل في WhatsApp.

الحل: تأكد من صحة الرقم وكود الدولة. الـ API يفحص أولاً قبل الإرسال.

subscription_expired

السبب: انتهت مدة اشتراك هذا العميل.

الحل: جدّد الاشتراك من لوحة التحكم → أيقونة 🔄 الخضراء بجانب اسم العميل.

أول رسالة لرقم جديد بطيئة (5-15 ثانية)

السبب: Baileys يبني encryption keys مع المستلم لأول مرة.

الحل: طبيعي. الرسائل التالية لنفس الرقم فورية. زد timeout في Http إلى 30 ثانية على الأقل.

الإرسال الجماعي يتم حظره

السبب: WhatsApp يكتشف نمط إرسال آلي.

الحل: ضع usleep(500_000) أو أطول بين كل رسالتين، وتجنّب إرسال نفس النص حرفياً لكل المستلمين (نوّع قليلاً).