# เอกสารอ้างอิง Chat API: บทสนทนา ข้อความ สถานะอ่านแล้ว และอีเวนต์

> วิธีเรียก Hibira Chat API: คำขอที่ลงลายเซ็น บทสนทนาและสมาชิก ข้อความ การตามข้อความที่พลาด สถานะอ่านแล้วและจำนวนที่ยังไม่อ่าน โทเคนผู้ใช้สำหรับเบราว์เซอร์ ไฟล์แนบ พุช การกลั่นกรอง อีเวนต์ และขีดจำกัด

Source: https://hibira.io/th/chat-api

## ก่อนเริ่มต้น

Chat API เก็บบทสนทนาและข้อความ แล้วส่งผ่านการเชื่อมต่อเรียลไทม์ที่แอปของคุณมีอยู่แล้ว เซิร์ฟเวอร์ของคุณเป็นผู้กำหนดว่าใครอยู่ในบทสนทนาใดและเป็นผู้เรียก API ส่วนไคลเอนต์แค่เชื่อมต่อ ลงชื่อเข้าใช้ และรับฟัง

- **อยู่ในช่วงเบต้า** บัญชีใช้งานได้เมื่ออยู่ในเบต้าหรือมีแพ็กเสริม Chat ก่อนหน้านั้นทุกการเรียกจะได้ `403 the Chat API is not enabled for this app` [ลงชื่อในรายชื่อรอ](https://hibira.io/th/chat) เพื่อขอใช้งาน
- **Base URL:** `https://ws.hibira.io/apps/APP_ID/chat` (ใช้โฮสต์ที่แสดงในหน้าแอปบนพอร์ทัล)
- **เนื้อหา**เป็น JSON ข้อผิดพลาดตอบเป็น `{"error": "..."}` พร้อมสถานะ 4xx หรือ 5xx
- **ID ผู้ใช้**เป็นของคุณเอง: ใช้ ID เดียวกับที่ไคลเอนต์ส่งให้ `pusher.signin()`

## วิธีการทำงาน

1. เซิร์ฟเวอร์ของคุณสร้างบทสนทนาและสมาชิก (`PUT /conversations/{id}`)
2. แต่ละไคลเอนต์เชื่อมต่อด้วย pusher-js แล้วเรียก `pusher.signin()` ในนามผู้ใช้ของตน ซึ่งจะสมัครรับ `#server-to-user-<user_id>` สมาชิกจึงไม่ต้องขออนุญาตช่องสำหรับแต่ละบทสนทนา
3. เซิร์ฟเวอร์ของคุณส่งข้อความแทนผู้ใช้ (`POST /conversations/{id}/messages`) Hibira จะเก็บด้วยหมายเลขลำดับถัดไปของบทสนทนา (`seq`) แล้วส่ง `chat.message` ให้สมาชิกทุกคนและเรียกเว็บฮุก `chat_message` ของคุณ
4. ไคลเอนต์ที่เคยออฟไลน์อ่านข้อความที่พลาดด้วย `GET /conversations/{id}/messages?after=<seq ล่าสุดที่มี>`

ทุกการเปลี่ยนแปลงจะถูกเก็บก่อนส่งอีเวนต์ใด ๆ หากแอปเกินโควตาข้อความรายวัน การเปลี่ยนแปลงยังถูกเก็บแต่จะไม่มีอีเวนต์ส่งออกไป ไคลเอนต์จึงตามให้ทันได้ด้วยการอ่าน

## การลงลายเซ็นคำขอ

คำขอจากเซิร์ฟเวอร์ของคุณลงลายเซ็นแบบเดียวกับคำขออื่นของ Pusher HTTP API ด้วยคีย์และซีเคร็ตของแอป: query string มี `auth_key`, `auth_timestamp`, `auth_version=1.0`, `body_md5` (เมื่อมีเนื้อหา) และ `auth_signature` ซึ่งคือ HMAC-SHA256 ของ `METHOD\nPATH\nQUERY` โดยเรียงพารามิเตอร์ตามชื่อ โค้ดลงลายเซ็นของไลบรารีเซิร์ฟเวอร์ Pusher ใดก็ใช้ได้ นี่คือทั้งหมดใน Node.js:

```js
import crypto from 'node:crypto';

const { PUSHER_APP_ID: id, PUSHER_APP_KEY: key, PUSHER_APP_SECRET: secret } = process.env;

export async function chat(method, path, body) {
  const fullPath = `/apps/${id}/chat${path}`;
  const json = body === undefined ? '' : JSON.stringify(body);
  const params = { auth_key: key, auth_timestamp: Math.floor(Date.now() / 1000), auth_version: '1.0' };
  if (json) params.body_md5 = crypto.createHash('md5').update(json).digest('hex');
  const query = Object.keys(params).sort().map((k) => `${k}=${params[k]}`).join('&');
  const signature = crypto.createHmac('sha256', secret).update(`${method}\n${fullPath}\n${query}`).digest('hex');
  const res = await fetch(`https://ws.hibira.io${fullPath}?${query}&auth_signature=${signature}`, {
    method, body: json || undefined, headers: { 'Content-Type': 'application/json' },
  });
  if (!res.ok) throw new Error(`${res.status} ${(await res.json()).error}`);
  return res.json();
}

await chat('PUT', '/conversations/order-1042', { name: 'Order #1042', members: ['ann', 'support-bob'] });
await chat('POST', '/conversations/order-1042/messages', { user_id: 'ann', name: 'Ann', text: 'Where is my parcel?' });
```

พารามิเตอร์ของ `GET` (เช่น `after` หรือ `limit`) ก็เป็นส่วนหนึ่งของ query ที่ลงลายเซ็นด้วย

## บทสนทนาและสมาชิก

| เมธอดและพาธ | เนื้อหาหรือ query | ผลลัพธ์ |
|---|---|---|
| `PUT /conversations/{id}` | `name`, `metadata`, `members` (ID ผู้ใช้; เว้นไว้เพื่อคงสมาชิกเดิม), `roles` (`{user_id: role}`), `watchers` (ช่อง private หรือ presence สูงสุด 10 ช่อง) | `201` เมื่อสร้างใหม่ หรือ `200` เมื่อแทนที่, `{conversation}` |
| `GET /conversations/{id}` | | `{conversation}` |
| `DELETE /conversations/{id}` | | `{}`; ลบสมาชิก ข้อความ และไฟล์ด้วย |
| `POST /conversations/{id}/members` | `user_ids` | `{conversation}` |
| `PUT /conversations/{id}/members/{user_id}` | `role` (1–32 ตัวของ `a-z 0-9 _ -`; `""` คือไม่มี) | `{conversation}` |
| `DELETE /conversations/{id}/members/{user_id}` | | `{conversation}` |
| `GET /users/{user_id}/conversations` | `limit` (1–100), `before` (`next` ของหน้าก่อน) | `{conversations, next}` เรียงตามกิจกรรมล่าสุด แต่ละรายการมี `unread` |

บทสนทนา:

```json
{"id": "order-1042", "name": "Order #1042", "metadata": {"kind": "support"},
 "members": [{"user_id": "ann", "read_seq": 3}, {"user_id": "support-bob", "read_seq": 1, "role": "agent"}],
 "last_seq": 3, "created_at": "...", "updated_at": "..."}
```

**บทบาทและโน้ตภายใน** ข้อความที่มี `visible_to` (สูงสุด 5 บทบาท) จะถึงเฉพาะสมาชิกที่มีบทบาทนั้น คนอื่นจะไม่ได้รับ อ่านไม่ได้ และไม่นับเป็นยังไม่อ่าน **Watchers** คือช่อง private หรือ presence ที่ได้รับทุกอีเวนต์ของบทสนทนา ให้กล่องข้อความของทีมติดตามทุกบทสนทนาได้ด้วยการสมัครเดียว อนุญาตเฉพาะพนักงานของคุณเท่านั้น

## ข้อความ

| เมธอดและพาธ | เนื้อหาหรือ query | ผลลัพธ์ |
|---|---|---|
| `POST /conversations/{id}/messages` | `user_id` (สมาชิก), `name`, `text`, `metadata`, `push`, `push_title`, `push_url`, `visible_to`, `attachments` | `201 {message, push}` |
| `GET /conversations/{id}/messages` | `after` (seq) และ `limit` หรือ `last` (n ข้อความล่าสุด) | `{messages, last_seq}` เก่าสุดก่อน |
| `GET /conversations/{id}/search` | `q` (1–100 ตัวอักษร), `before`, `limit` (1–200) | `{messages, next}` ใหม่สุดก่อน |
| `PUT /conversations/{id}/messages/{seq}` | `user_id` (ผู้ส่ง), `text`, `metadata` | `{message}` พร้อม `edited_at` |
| `DELETE /conversations/{id}/messages/{seq}` | `user_id` (ผู้ส่ง) ใน query; เว้นไว้เพื่อลบในนามแอป | `{message}` พร้อม `deleted_at` |
| `POST /conversations/{id}/messages/{seq}/reactions` | `user_id`, `emoji` | `{message}` |
| `DELETE /conversations/{id}/messages/{seq}/reactions` | `user_id`, `emoji` ใน query | `{message}` |
| `POST /conversations/{id}/system` | `text`, `metadata` (เช่น `{"event": "transferred", "to": "bob"}`) | `201 {message}` ที่มี `kind: "system"` |
| `POST /conversations/{id}/typing` | `user_id`, `typing` (`true` หรือ `false`) | `{}`; ไม่มีการเก็บ |

ข้อความ:

```json
{"id": "...", "seq": 3, "sender": "ann", "name": "Ann", "kind": "text",
 "text": "hi", "metadata": {"reply_to": 2}, "reactions": {"👍": ["support-bob"]}, "created_at": "..."}
```

ข้อความที่แก้ไขจะคง `seq` เดิมและมี `edited_at` ข้อความที่ลบจะคงตำแหน่งไว้พร้อม `deleted_at` และไม่มีข้อความ แก้ไขหรือกดอีโมจิไม่ได้อีก (`409`) การอ่านแบบ `after` คืนเฉพาะข้อความใหม่กว่า ไคลเอนต์ที่กลับมาจึงอ่านข้อความที่แสดงอยู่ซ้ำ (`last=n`) เพื่อรับการแก้ไข การลบ และอีโมจิ การค้นหาจับคู่ส่วนใดก็ได้ของคำ ไม่สนตัวพิมพ์เล็กใหญ่และอักขระเต็มความกว้าง จึงใช้ได้กับภาษาจีน ญี่ปุ่น และเกาหลี ข้อความระบบบอกว่าเกิดอะไรขึ้น (เข้าร่วม โอนต่อ ปิด) มีเพียงเซิร์ฟเวอร์ของคุณที่ส่งได้ ไม่มีการพุชและไม่นับเป็นยังไม่อ่าน

## สถานะอ่านแล้วและจำนวนที่ยังไม่อ่าน

| เมธอดและพาธ | เนื้อหา | ผลลัพธ์ |
|---|---|---|
| `POST /conversations/{id}/read` | `user_id`, `seq` | `{read_seq}` |

ตำแหน่งที่สมาชิกอ่านถึงจะเดินหน้าเท่านั้นและไม่เกิน `last_seq` ผู้ส่งข้อความถือว่าอ่านแล้ว `unread` ในรายการบทสนทนาของผู้ใช้นับข้อความหลัง `read_seq` ที่ไม่ใช่ข้อความระบบ สมาชิกทุกคนได้รับ `chat.read` เมื่อ `read_seq` ของใครเดินหน้า

## เบราว์เซอร์และแอป: โทเคนผู้ใช้

ไคลเอนต์เรียก Chat API เองได้ที่ `/apps/APP_ID/chat/me` พร้อม `Authorization: Bearer <user token>` โดยไม่ต้องมีพร็อกซีบนเซิร์ฟเวอร์ของคุณ โทเคนคือ JWT ที่ลงลายเซ็นด้วยซีเคร็ตของแอป (HS256, `{sub, exp}`, นานสุด 30 วัน) เซิร์ฟเวอร์ของคุณสร้างได้ด้วยไลบรารี JWT ใดก็ได้หรือด้วย `POST /tokens`:

| เมธอดและพาธ | เนื้อหา | ผลลัพธ์ |
|---|---|---|
| `POST /tokens` | `user_id`, `conversation_id` (จำกัดโทเคนไว้ที่บทสนทนานั้น), `join` (เพิ่มผู้ใช้เข้าบทสนทนาเมื่อใช้ครั้งแรก), `ttl` (60–86400 วินาที) | `201 {token, expires_at}` |

เมื่อมีโทเคน ไคลเอนต์แสดงรายการบทสนทนาของตน (`GET /me/conversations`) อ่าน ค้นหา ส่ง แก้ไขและลบข้อความของตัวเอง กดอีโมจิ ทำเครื่องหมายอ่านแล้ว และส่งสถานะกำลังพิมพ์ได้ ผู้ใช้คือ `sub` ของโทเคนเสมอ ไคลเอนต์สร้างบทสนทนา เปลี่ยนสมาชิก ตั้งค่าพุช ปิดเสียง หรือแบนไม่ได้ สิ่งเหล่านั้นอยู่ที่เซิร์ฟเวอร์ของคุณ ผู้ใช้แต่ละคนเขียนได้สูงสุด 2 ครั้งต่อวินาที และสูงสุด 10 ครั้งแบบต่อเนื่อง โทเคนที่มี `conversation_id` และ `join` เหมาะกับผู้เยี่ยมชมนิรนามของแชตซัพพอร์ต: เซิร์ฟเวอร์ของคุณสร้างบทสนทนา ตรวจสอบผู้เยี่ยมชม แล้วออกโทเคนอายุสั้นให้ ID ที่สร้างขึ้นเอง

## ไฟล์แนบ

1. เซิร์ฟเวอร์ของคุณขออัปโหลด: `POST /conversations/{id}/attachments` พร้อม `user_id`, `name`, `content_type` และ `size` (สูงสุด 25 MB) คำตอบมี `attachment` และ `upload` (`url`, `method`, `headers`)
2. ไคลเอนต์อัปโหลดไฟล์ตรงไปที่ `upload.url` ภายใน 15 นาที ด้วยเฮดเดอร์เหล่านั้นทุกตัว
3. เซิร์ฟเวอร์ของคุณส่งข้อความพร้อม `attachments`: `id`, `name` และ `content_type` จากขั้นที่ 1 (สูงสุด 10 ไฟล์ต่อข้อความ และ `text` เว้นว่างได้)

ไฟล์แนบทุกไฟล์ที่อ่านจาก API หรืออีเวนต์มี `url` ที่ดาวน์โหลดได้ภายในหนึ่งชั่วโมง ไฟล์ที่อัปโหลดแต่ไม่ได้ส่งภายในหนึ่งวันจะถูกลบ และการลบข้อความหรือบทสนทนาจะลบไฟล์ของมันด้วย

## พุชถึงสมาชิกที่ออฟไลน์

ข้อความใหม่จะถูกพุชผ่าน Hibira Push ของแอปไปยังสมาชิกที่ไม่ได้เชื่อมต่อ (`push: "offline"` ค่าเริ่มต้น) ไปยังสมาชิกทุกคนยกเว้นผู้ส่ง (`"always"`) หรือไม่พุชเลย (`"never"`) หัวเรื่องคือชื่อผู้ส่งและชื่อบทสนทนา (หรือ `push_title`) เนื้อหาคือ 180 ตัวอักษรแรกของข้อความ และ `push_url` จะเปิดเมื่อแตะ อุปกรณ์ลงทะเบียนด้วย SDK พุชที่เข้ากันได้กับ Beams ด้วย ID ผู้ใช้เดียวกัน `push` ในคำตอบบอกว่าพุชถึงสมาชิกกี่คน หรือทำไมไม่มีการพุช

## การกลั่นกรอง

| เมธอดและพาธ | เนื้อหาหรือ query | ผลลัพธ์ |
|---|---|---|
| `PUT /moderation/filter` | `words` (สูงสุด 500), `action` (`mask` หรือ `reject`) | `{filter}` |
| `POST /users/{user_id}/ban` | `reason`, `until` (RFC 3339; เว้นไว้คือไม่มีกำหนด) | `{user_id, banned, until}` |
| `DELETE /users/{user_id}/ban` | | `{user_id, banned}` |
| `POST /conversations/{id}/members/{user_id}/mute` | `reason`, `until` | `{conversation_id, user_id, muted, until}` |
| `DELETE /conversations/{id}/members/{user_id}/mute` | | `{conversation_id, user_id, muted}` |
| `POST /conversations/{id}/messages/{seq}/reports` | `user_id`, `reason` | `201 {report}` |
| `GET /moderation/reports` | `limit` (1–200) | `{reports}` ใหม่สุดก่อน |

ตัวกรองคำตรวจทุกข้อความใหม่หรือที่แก้ไข จับคู่ได้แม้อยู่กลางคำ ไม่สนตัวพิมพ์และอักขระเต็มความกว้าง จึงจับภาษาจีนและญี่ปุ่นได้ด้วย `mask` แทนส่วนที่ตรงด้วย ＊ ส่วน `reject` ตอบ `422` และไม่เก็บอะไร ผู้ใช้ที่ถูกแบนได้ `403` เมื่อส่ง แก้ไข กดอีโมจิ และพิมพ์ในทุกบทสนทนาของแอป สมาชิกที่ถูกปิดเสียงได้ `403` เมื่อส่งและพิมพ์ในบทสนทนานั้นแต่ยังอ่านได้

## อีเวนต์และเว็บฮุก

อีเวนต์ถูกส่งไปที่ `#server-to-user-<user_id>` ของสมาชิกแต่ละคนที่เกี่ยวข้อง และไปที่ watchers ของบทสนทนา แต่ละอีเวนต์นับเป็นหนึ่งข้อความในโควตารายวัน

| อีเวนต์ | ถึง | ข้อมูล |
|---|---|---|
| `chat.conversation` | สมาชิกทุกคน เมื่อสร้างหรือเปลี่ยน หรือเพิ่มสมาชิก | `{conversation}` |
| `chat.message` | สมาชิกทุกคน | `{conversation_id, message}` |
| `chat.message_updated` | สมาชิกทุกคน หลังแก้ไข ลบ หรือกดอีโมจิ | `{conversation_id, message}` |
| `chat.typing` | สมาชิกทุกคนยกเว้นคนที่กำลังพิมพ์ | `{conversation_id, user_id, typing}` |
| `chat.read` | สมาชิกทุกคน | `{conversation_id, user_id, read_seq}` |
| `chat.left` | สมาชิกที่ถูกนำออก | `{conversation_id}` |
| `chat.deleted` | สมาชิกทุกคนของบทสนทนาที่ถูกลบ | `{conversation_id}` |

```js
const pusher = new Pusher('APP_KEY', { cluster: 'hkg', wsHost: 'ws.hibira.io', userAuthentication: { endpoint: '/pusher/user-auth' } });
pusher.signin();
pusher.user.bind('chat.message', ({ conversation_id, message }) => showMessage(conversation_id, message));
```

เซิร์ฟเวอร์ของคุณยังได้รับเว็บฮุก `chat_message` สำหรับทุกข้อความใหม่: `channel` คือ ID บทสนทนา `user_id` คือผู้ส่ง และ `data` คือ JSON `{"conversation_id", "message"}` เพิ่ม `chat_message` ในอีเวนต์ของเว็บฮุกหนึ่งของแอปเพื่อรับ

## ขีดจำกัด

| | |
|---|---|
| ID บทสนทนา | 1–128 ตัวของ `A-Z a-z 0-9 _ - = @ , . ;` |
| ID ผู้ใช้ | กฎเดียวกับ ID ผู้ใช้ของ `pusher.signin` |
| สมาชิกต่อบทสนทนา | 100 |
| ข้อความ | 4,000 ตัวอักษร ห้ามว่าง |
| ชื่อบทสนทนาหรือผู้ส่ง | 100 ตัวอักษร |
| `metadata` | ออบเจกต์ JSON ไม่เกิน 4,096 ไบต์ |
| ข้อความต่อการอ่าน | 200 |
| อีโมจิ | ตัวละ 1–32 ไบต์ ต่างกันได้ 20 แบบต่อข้อความ |
| ไฟล์แนบ | ไฟล์ละ 25 MB, 10 ไฟล์ต่อข้อความ |

การเขียนใช้ขีดจำกัดอัตรา HTTP API ของแอปร่วมกัน (เกินแล้วได้ `429`)

## ผู้ใช้งานรายเดือนและระยะเวลาเก็บ

ผู้ใช้นับเป็นผู้ใช้งานในเดือนนั้น (UTC) เมื่อส่งข้อความหรือทำเครื่องหมายอ่านแล้ว นับครั้งเดียวทุกแอปของบัญชี ทุกบัญชีมี 500 คนต่อเดือน แพ็กเสริม Chat เพิ่มเป็น 1,000, 5,000, 20,000 หรือ 60,000 คน ([แพ็กเกจ](https://hibira.io/th/chat)) เมื่อเดือนนั้นเต็ม ผู้ใช้ที่ยังไม่ถูกนับจะได้ `429` เมื่อส่งและอ่านจนถึงเดือนถัดไป ผู้ใช้ที่ถูกนับแล้วใช้ต่อได้ `GET /usage` ตอบ `{month, active_users, limit}`

ข้อความแก้ไขได้เต็มที่ภายใน 30 วัน หลังจากนั้น บัญชีที่มีแพ็กเกจแบบชำระเงินหรือแพ็กเสริม Chat จะเก็บไว้ในคลังที่ API ยังคืนให้ (อ่านอย่างเดียว) ส่วนแพ็กเกจ Free ที่ไม่มีแพ็กเสริมจะถูกลบ

## ใช้งานจาก AI agent

[MCP เซิร์ฟเวอร์ของ Hibira](https://hibira.io/th/mcp-server) มีเครื่องมือ Chat API ให้ AI agent สำหรับเขียนโค้ดตั้งค่าและทดสอบแชตแทนคุณได้: `create_conversation`, `send_message`, `list_conversations` และ `list_messages` เครื่องมือเหล่านี้ทำงานกับแอปของคุณเองด้วยการตรวจสอบเดียวกับคำขอที่ลงลายเซ็น และต้องให้บัญชีเปิดใช้ Chat API แล้ว [ปลั๊กอินสำหรับ agent](https://hibira.io/th/guides/agent-plugin) ของ Claude Code, Codex, Cursor และอื่น ๆ มีสกิลที่เชื่อม Chat API เข้ากับโปรเจกต์ตามขั้นตอนนี้ หากต้องการหน้าสนทนาสำเร็จรูปสำหรับ React หรือ JavaScript ล้วน ดู [@hibira-io/chat-ui](https://hibira.io/th/guides/chat-ui)
