# WXM Realtime — دليل تيم الموبايل (Socket.IO)

> توثيق ريكويستات وأحداث السوكت اللي الموبايل بيتعامل معاها.
> السيرفر ده **Socket.IO** (مش WebSocket خام ومش Laravel Reverb/Pusher) — لازم SDK بتاع socket.io.

---

## 0. الفكرة باختصار

- **الإرسال (Send)** بيتم عن طريق **REST API** العادي (`POST .../messages`).
- **الاستقبال اللحظي (Receive)** بيتم عن طريق **Socket.IO**.
- يعني الموبايل **مايبعتش رسائل على السوكت أبدًا** — السوكت **للاستقبال** + **presence** بس.

```
بعتي رسالة:   App ──POST /api/v1/app/conversations/{id}/messages──▶ Laravel
                                                                      │
                                                                      ▼ Redis publish
وصول لحظي:   Laravel ──Redis──▶ Socket.IO server ──emit('message')──▶ كل الأجهزة المعنية
```

---

## 1. الاتصال (Connection)

| | القيمة |
|---|---|
| Protocol | Socket.IO v4 (`socket.io-client` ^4.x) |
| URL (dev) | `http://<server-host>:6001` |
| URL (prod) | `https://<socket-domain>` (خلف Nginx/TLS — مش بورت 6001 مباشر) |
| Transport | افتراضي (`polling` → upgrade لـ `websocket`) |
| Auth | JWT بتاع تسجيل الدخول (نفس توكن الـ API) |

### مكان التوكن في الـ handshake
السيرفر بيقبل التوكن من **3 أماكن** (اختاري الأنسب للـ SDK بتاعك):

1. **`auth.token`** ← المفضّل
2. هيدر `Authorization: Bearer <token>`
3. query string `?token=<token>`

### مثال — JavaScript / React Native
```js
import { io } from 'socket.io-client';

const socket = io('https://socket.wxm.app', {
  transports: ['websocket'],
  auth: { token: accessToken },   // نفس JWT بتاع الـ API
});

socket.on('connect', () => console.log('connected', socket.id));

// فشل المصادقة بيوصل هنا
socket.on('connect_error', (err) => {
  // err.message = النص، والكود في err.data.code (شوف جدول الأخطاء تحت)
  console.warn('connect_error', err.message, err.data?.code);
});
```

### مثال — iOS (Swift, socket.io-client-swift)
```swift
let manager = SocketManager(
  socketURL: URL(string: "https://socket.wxm.app")!,
  config: [.connectParams(["token": accessToken]), .forceWebsockets(true)]
)
let socket = manager.defaultSocket
```

### مثال — Android (Kotlin, socket.io-client java)
```kotlin
val opts = IO.Options().apply { auth = mapOf("token" to accessToken) }
val socket = IO.socket("https://socket.wxm.app", opts)
socket.connect()
```

### اللي بيحصل أوتوماتيك أول ما تتصلي
1. السيرفر بيتحقق من التوكن (signature + expiry + بيسأل Laravel إنه لسه صالح).
2. الجهاز بيدخل **الـ inbox الشخصي** بتاعه تلقائيًا → بيستقبل أي رسالة موجّهة ليك من غير أي خطوة زيادة.
3. حالتك بتبقى **online** ويتبعت إشعار presence لأي حد بيراقبك.

> **مهم:** عشان تستقبلي الرسائل (DMs والـ support) **مش محتاجة تعملي أي join** — الـ inbox الشخصي بيكفّي. الـ `conversation.join` اختياري (شوف القسم 3).

---

## 2. أكواد أخطاء الـ Handshake

لو الاتصال اترفض، الكود بيوصل في `err.data.code`:

| code | المعنى | تصرّف الموبايل |
|---|---|---|
| `AUTH_MISSING` | مفيش توكن في الـ handshake | ابعتي التوكن |
| `AUTH_EXPIRED` | التوكن منتهي | اعملي refresh للتوكن وأعيدي الاتصال |
| `AUTH_INVALID` | توكن غير صالح (توقيع غلط) | اعملي logout / login |
| `AUTH_INVALID_SUB` | الـ `sub` مش user id صحيح | login من جديد |
| `AUTH_REVOKED` | Laravel رفض التوكن (logout / ban / قديم) | اعتبري المستخدم خارج → login |
| `AUTH_FAILED` | فشل عام في التحقق | retry ثم login |

---

## 3. أحداث Client ▶ Server (اللي الموبايل بيبعتها)

كل الأحداث دي بترجع **ack callback** فيها `{ ok: true, ... }` أو `{ ok: false, code, message }`.

### `conversation.join` — (اختياري) دخول غرفة محادثة لحظية
بيستخدم لما تفتحي شاشة شات معيّنة وعايزة تأكيد إنك جوه الغرفة (وعشان منع التكرار — شوف القسم 4).
```js
socket.emit('conversation.join', { conversation_id: 42 }, (ack) => {
  if (!ack.ok) console.warn(ack.code, ack.message);
});
```
- **Payload:** `{ conversation_id: number }`
- **أخطاء ممكنة:** `BAD_PAYLOAD` (id ناقص/غلط)، `AUTH_MISSING`، `FORBIDDEN` (مش مشارك في المحادثة).

### `conversation.leave` — الخروج من غرفة المحادثة
```js
socket.emit('conversation.leave', { conversation_id: 42 }, (ack) => {});
```
- **Payload:** `{ conversation_id: number }`
- **أخطاء:** `BAD_PAYLOAD`.

### `presence.watch` — متابعة حالة (online/offline) لمجموعة مستخدمين
بيرجّع snapshot فوري بالحالة الحالية في الـ ack، وبعدها أي تغيير بيوصل على حدث `presence` (القسم 5).
```js
socket.emit('presence.watch', { user_ids: [7, 12, 30] }, (ack) => {
  if (ack.ok) console.log(ack.states); // { "7": true, "12": false, "30": true }
});
```
- **Payload:** `{ user_ids: number[] }` — مصفوفة أرقام موجبة، **بحد أقصى 500**، مش فاضية.
- **ack نجاح:** `{ ok: true, states: { [userId]: boolean } }`
- **أخطاء:** `BAD_PAYLOAD`، `INTERNAL`.

### `presence.unwatch` — إيقاف المتابعة
```js
socket.emit('presence.unwatch', { user_ids: [7, 12] }, (ack) => {});
```
- **Payload:** `{ user_ids: number[] }` — **أخطاء:** `BAD_PAYLOAD`.

### `presence.query` — استعلام لمرة واحدة (من غير اشتراك)
زي `watch` بس بيرجّع الحالة الحالية بس ومش بيشترك في التحديثات.
```js
socket.emit('presence.query', { user_ids: [7, 12] }, (ack) => {
  if (ack.ok) console.log(ack.states);
});
```
- **ack نجاح:** `{ ok: true, states: { [userId]: boolean } }` — **أخطاء:** `BAD_PAYLOAD`، `INTERNAL`.

---

## 4. أحداث Server ▶ Client (اللي الموبايل بيستقبلها)

### `message` — رسالة DM جديدة
```js
socket.on('message', (payload) => { /* ... */ });
```
```jsonc
{
  "event": "message.created",
  "conversation_id": 42,
  "message": {
    "id": 9001,
    "sender_id": 5,          // ابعت/قارن مع الـ id بتاعك عشان تعرفي وارد/صادر
    "body": "salam",         // ممكن يكون null لو الرسالة ميديا بس
    "media_id": null,        // id المرفق إن وُجد
    "created_at": "2026-05-16T12:34:56+00:00"  // ISO-8601 UTC، ممكن null
  }
}
```

### `support.message` — رسالة شات الدعم
```js
socket.on('support.message', (payload) => { /* ... */ });
```
```jsonc
{
  "event": "support.message.created",
  "thread_id": 3,
  "user_id": 5,
  "message": {
    "id": 201,
    "sender_id": 5,
    "sender_role": "user",   // "user" أو "admin" — عشان تفرّقي شكل الفقاعة
    "body": "Can you help?",
    "media_id": null,
    "created_at": "2026-05-28T10:00:00+00:00"
  }
}
```

### `presence` — تغيّر حالة مستخدم بتراقبه
بيوصل بس للمستخدمين اللي عملتلهم `presence.watch`.
```js
socket.on('presence', (payload) => { /* ... */ });
```
```jsonc
{ "event": "user.online", "user_id": 42 }   // أو "user.offline"
```

### ⚠️ منع التكرار (Deduplication)
الرسالة الواحدة بتتبعت لمكانين: غرفة المحادثة `conversation.{id}` + الـ inbox الشخصي.
السيرفر بيمنع وصولها مرتين لو إنتي **عاملة `conversation.join`** للمحادثة دي.
**التوصية:** اعملي de-dup برضه على مستوى الموبايل بالاعتماد على `message.id` (تجاهلي أي id اتعرض قبل كده) كأمان إضافي.

---

## 5. الفصل الإجباري (Forced disconnect)

لما المستخدم يعمل **logout** (أو يتباظر/يتمسح)، Laravel بيبعت إشارة والسيرفر بيقطع السوكت بتاعه فورًا.
بعد القطع، أي محاولة إعادة اتصال بنفس التوكن هتترفض بـ `AUTH_REVOKED`.

**تصرّف الموبايل:** لو وصلك `disconnect` وبعدها `connect_error` بكود `AUTH_REVOKED` → نضّفي السيشن وروّحي المستخدم لشاشة الـ login.

```js
socket.on('disconnect', (reason) => { /* أعيدي الاتصال إلا لو السبب logout */ });
```

---

## 6. إعادة الاتصال (Reconnection)
- `socket.io-client` بيعمل reconnect أوتوماتيك.
- **بعد كل reconnect ناجح**، أعيدي إرسال أي `presence.watch`/`conversation.join` كنتي عاملاهم — الاشتراكات مش بتتحفظ بعد القطع.
- اعملي pull للرسائل الفايتة من الـ REST (`GET .../messages`) لو كنتي offline فترة، لأن السوكت بيوصّل **اللحظي** بس مش التاريخ.

```js
socket.on('connect', () => {
  socket.emit('presence.watch', { user_ids: watchedIds }, () => {});
  // + اعملي sync للرسائل من REST
});
```

---

## 7. راوتات REST المرتبطة (الإرسال + التاريخ)

كلها تحت `auth:api` + هيدر `Accept-Language: ar|en`.

### Direct Messages — `/api/v1/app`
| Method | Path | الوظيفة |
|---|---|---|
| `GET`  | `/conversations` | قائمة المحادثات |
| `POST` | `/conversations/start` | بدء محادثة مع مستخدم |
| `GET`  | `/conversations/{id}/messages` | رسائل المحادثة (history) |
| `POST` | `/conversations/{id}/messages` | **إرسال رسالة** (ده اللي بيولّد حدث `message`) |
| `POST` | `/conversations/{id}/read` | تعليم كمقروء |
| `POST` | `/conversations/{id}/mute` | كتم المحادثة |

### Support Chat — `/api/v1/app`
| Method | Path | الوظيفة |
|---|---|---|
| `GET`  | `/support/thread` | ثريد الدعم بتاع المستخدم |
| `GET`  | `/support/messages` | رسائل الثريد (history) |
| `POST` | `/support/messages` | **إرسال رسالة دعم** (بيولّد `support.message`) |
| `POST` | `/support/read` | تعليم كمقروء |

---

## 8. السيناريو الكامل (End-to-End)

```js
// 1) اتصلي
const socket = io(SOCKET_URL, { auth: { token }, transports: ['websocket'] });

// 2) استقبلي الرسائل (الـ inbox الشخصي شغّال تلقائيًا)
socket.on('message', renderIncomingDm);
socket.on('support.message', renderSupportMessage);

// 3) (اختياري) في شاشة شات مفتوحة: ادخلي الغرفة وراقبي presence الطرف التاني
socket.emit('conversation.join', { conversation_id: 42 }, () => {});
socket.emit('presence.watch', { user_ids: [otherUserId] }, (a) => setOnline(a.states));
socket.on('presence', (p) => setOnline({ [p.user_id]: p.event === 'user.online' }));

// 4) الإرسال عبر REST (مش عبر السوكت)
await api.post('/api/v1/app/conversations/42/messages', { body: 'hi' });

// 5) عند الخروج من الشاشة
socket.emit('conversation.leave', { conversation_id: 42 }, () => {});
socket.emit('presence.unwatch', { user_ids: [otherUserId] }, () => {});
```

---

## 9. مرجع أكواد الأخطاء (acks + handshake)

| code | بيظهر في | المعنى |
|---|---|---|
| `BAD_PAYLOAD` | ack | بيانات الريكوست ناقصة/غلط (id أو user_ids) |
| `FORBIDDEN` | ack (`conversation.join`) | مش مشارك في المحادثة |
| `AUTH_MISSING` | ack / handshake | مفيش توكن |
| `INTERNAL` | ack (presence) | خطأ مؤقت في السيرفر — اعملي retry |
| `AUTH_EXPIRED` / `AUTH_INVALID` / `AUTH_REVOKED` / `AUTH_FAILED` / `AUTH_INVALID_SUB` | handshake | مشاكل توكن — شوف القسم 2 |

---

## 10. ملاحظات سريعة
- **التوكن** = نفس JWT بتاع الـ API بالظبط. لو اتعمله refresh، اقطعي وأعيدي الاتصال بالتوكن الجديد.
- **التواريخ** ISO-8601 بتوقيت UTC.
- **presence** بحد أقصى **500** user_id في الريكوست الواحد.
- السوكت **مايرجّعش history** — استخدمي REST للرسائل القديمة، والسوكت لِلّي بييجي لحظيًا.
- في الإنتاج البورت 6001 داخلي بس؛ اتصلي على دومين/ساب-دومين السوكت عبر HTTPS.
