Chat CMS — tài liệu ghép admin
REST JSON trên admin-api-gateway. Admin quản ticket CS, FAQ chatbot, social bot, và hội thoại CMS (P2P / group / channel giữa admin + bot). Realtime: WebSocket events.
1. Tổng quan
| Mục | Giá trị |
|---|---|
| Gateway | admin-api-gateway · REST /api/v1/chat/* |
| Auth | CMS JWT, header Authorization: Bearer <token> |
| Actor | Luôn lấy từ JWT. Super admin khi RoleGroupID <= 0 hoặc role ChatSupportSuper |
| Scope social | CMS = admin + bot. Không tạo P2P admin↔user |
| Realtime | admin-api-gateway GET /api/v1/chat/ws?access_token= — xem chat-ws |
/chat/bot là FAQ Q&A (user đọc trên game),
/chat/bots là actor chat (admin mở P2P/group với bot).
2. Auth & role
| Role | ID | Dùng cho |
|---|---|---|
ChatSupportView | 81000 | GET support queues + history |
ChatSupportEdit | 81001 | Claim / add-agent / close / send |
ChatSupportSuper | 81002 | Đóng ticket người khác; list closed tất cả admin |
ChatBotView | 81003 | GET FAQ + social bots |
ChatBotEdit | 81004 | CRUD FAQ + social bots |
ChatConversationView | 81005 | List / get conversation + members + messages |
ChatConversationEdit | 81006 | Tạo / sửa / join / send / react / delete |
3. Envelope
Authorization: Bearer <cms_jwt>
Content-Type: application/json
success → HTTP 200 { "code": 0, "msg": ... }
bind fail → HTTP 400 { "code": 1000, "msg": "..." }
domain / internal → HTTP 500 { "code": 1000, "msg": "..." }
Items, NextCursor, HasNext.
Object bên trong (conversation, message) vẫn snake_case.
NextCursor / query cursor_unix là unix nanoseconds của updated_at.
Limit mặc định 50, tối đa 50.
4. Models
Conversation
{
"id": "support:1001:1757812345678901234",
"type": "p2p", // p2p | group | channel | self
"category": "support", // social | support | game_room
"scope": "cms", // cms | game (social)
"name": "Support",
"avatar": "",
"description": "",
"owner_id": 42,
"participants": [
{ "id": 1001, "type": "user", "role": "member" },
{ "id": 42, "type": "admin", "role": "owner" }
],
"settings": {
"only_admin_send": false,
"auto_delete_duration": null,
"expired_at": null
},
"reference": null,
"metadata": null,
"last_seq": 12,
"last_message_at": "2026-09-14T03:20:00Z",
"last_message_preview": "hello",
"claimed_by": 42,
"claimed_at": "2026-09-14T03:10:00Z",
"closed_by": null,
"closed_at": null,
"status": "active", // active | closed
"created_at": "2026-09-14T03:00:00Z",
"updated_at": "2026-09-14T03:20:00Z"
}
Timestamp trên REST là ISO-8601. type participant: user / admin / bot. Role: member / admin / owner / support.
Message
{
"id": "66e1ab0c2f1a4b0012345678",
"conversation_id": "p2p:social:admin_42_bot_3",
"seq": 7,
"sender_id": 42,
"sender_type": "admin",
"type": "text", // text | image | video | file | audio
"content": {
"text": "hello",
"attachments": [
{ "url": "https://cdn/.../a.png", "mime": "image/png", "name": "a.png",
"size": 1200, "duration_ms": 0, "width": 100, "height": 80, "thumbnail": "" }
],
"p2p_reactions": { "42": "👍" },
"reaction_count": { "👍": 1 }
},
"reply_to": { "message_id": "...", "sender_id": 3, "preview": "...", "type": "text" },
"deleted": false,
"created_at": "2026-09-14T03:20:00Z",
"reactions": [
{ "reaction": "👍", "count": 1, "users": [{ "user_id": 42 }] }
],
"my_reaction": "👍",
"seen_count": 1
}
Text tối đa 4000 ký tự. Media / audio bắt buộc attachments[0].url. Không gửi type=system.
reactions / my_reaction / seen_count là state gắn kèm lúc đọc (không lưu trên document message).
users trong summary tối đa 4 người react gần nhất; count là tổng thật.
my_reaction là emoji hiện tại của admin đang login (null nếu chưa react).
MessageState
{
"message_id": "66e1ab0c2f1a4b0012345678",
"reactions": [
{ "reaction": "👍", "count": 12, "users": [{ "user_id": 42 }, { "user_id": 7 }] }
],
"my_reaction": "👍",
"seen_count": 8
}
Reaction / seen page
{
"users": [{ "user_id": 42 }, { "user_id": 7 }],
"next_cursor": "1757812345678901234:42"
}
Chỉ user_id — CMS tự resolve tên/avatar. next_cursor rỗng = hết trang.
Cursor là opaque string; đừng parse. Reactions: {created_at_unix_nano}:{user_id}.
Seen: {last_read_seq}:{user_id}.
List wrapper
{
"Items": [ /* Conversation */ ],
"NextCursor": 1757812345678901234,
"HasNext": true
}
Member
{
"id": "...",
"conversation_id": "...",
"user_id": 42,
"type": "admin",
"role": "owner",
"state": "accepted", // pending | accepted | blocked
"joined_at": "2026-09-14T03:00:00Z",
"last_read_seq": 7,
"last_received_seq": 7
}
5. Support
User mở tối đa 1 ticket active. Claim gán admin làm owner. Chỉ owner hoặc super admin đóng được. Ticket closed không nhận thêm tin.
| Method | Path | Body / query | msg |
|---|---|---|---|
| GET | /api/v1/chat/support/pending | limit, cursor_unix | list chưa claim |
| GET | /api/v1/chat/support/claimed | idem | ticket của admin đang login |
| GET | /api/v1/chat/support/closed | idem | closed của admin; super → tất cả |
| POST | /api/v1/chat/support/claim | {"conversation_id"} | conversation |
| POST | /api/v1/chat/support/add-agent | {"conversation_id","support_admin_id"} | conversation |
| POST | /api/v1/chat/support/close | {"conversation_id"} | conversation |
| POST | /api/v1/chat/support/send | xem dưới | message |
| POST | /api/v1/chat/support/history | {"conversation_id","before_seq","limit"} | array message |
POST /api/v1/chat/support/send
{
"conversation_id": "support:1001:...",
"type": "text",
"text": "Xin chào",
"attachments": []
}
before_seq=0 = trang mới nhất. Limit mặc định 50, max 100.
Claim lần 2 cùng admin là no-op. Claim khi đã có người khác → popup already claimed.
6. FAQ bot
Q&A CMS. User game gọi /chat/support/bot (xem Game APIs) để đọc bản enabled.
GET /api/v1/chat/bot POST /api/v1/chat/bot PUT /api/v1/chat/bot DELETE /api/v1/chat/bot/:id
{
"id": "66e1ab0c2f1a4b0012345678",
"title": "Nạp tiền",
"description": "Câu hỏi thường gặp",
"locale": "vi",
"items": [
{ "id": "q1", "question": "Nạp thế nào?", "answer": "...", "enabled": true, "sort_order": 1 }
],
"enabled": true,
"sort_order": 10,
"updated_by": "admin.user",
"created_at": "2026-09-14T03:00:00Z",
"updated_at": "2026-09-14T03:00:00Z"
}
updated_by server ghi từ username JWT. PUT cần id trong body.
7. Social bots
Actor chat. Admin tạo P2P với peer_type=bot. Bot auto-accept invite.
| Method | Path | Ghi chú |
|---|---|---|
| GET | /api/v1/chat/bots | query enabled_only=true để lọc |
| POST | /api/v1/chat/bots | {"name","enabled"} — enabled mặc định true |
| GET | /api/v1/chat/bots/:id | id số |
| PUT | /api/v1/chat/bots/:id | name và/hoặc enabled |
| DELETE | /api/v1/chat/bots/:id |
{ "id": 3, "name": "CS Bot", "enabled": true, "created_by": "admin.user",
"created_at": "2026-09-14T03:00:00Z", "updated_at": "2026-09-14T03:00:00Z" }
8. Conversations
Social CMS: P2P admin↔admin hoặc admin↔bot; group/channel do admin tạo. Peer P2P (không phải bot) bắt đầu pending — phải join mới chat được.
| Method | Path | Body / query | msg |
|---|---|---|---|
| GET | /api/v1/chat/conversations | limit, cursor_unix | đã accepted của admin |
| GET | /api/v1/chat/conversations/pending | idem | invite chờ join |
| GET | /api/v1/chat/conversations/all | + scope (mặc định cms) | toàn bộ scope |
| POST | /api/v1/chat/conversations/p2p | {"peer_id","peer_type"} | conversation (idempotent) |
| POST | /api/v1/chat/conversations/group | name bắt buộc, description, avatar, member_ids | conversation |
| POST | /api/v1/chat/conversations/channel | như group; only_admin_send=true | conversation |
| POST | /api/v1/chat/conversations/self | rỗng | saved messages, idempotent |
| GET | /api/v1/chat/conversations/:id | conversation | |
| PUT | /api/v1/chat/conversations/:id | {"name","avatar"} | null |
| PUT | /api/v1/chat/conversations/:id/settings | {"only_admin_send","auto_delete_duration","expired_at"} | null |
| POST | /api/v1/chat/conversations/:id/close | null | |
| POST | /api/v1/chat/conversations/:id/join | conversation | |
| POST | /api/v1/chat/conversations/:id/reject | null | |
| POST | /api/v1/chat/conversations/:id/leave | null |
POST /api/v1/chat/conversations/p2p
{ "peer_id": 3, "peer_type": "bot" } // admin | bot (không dùng user)
peer_type trống được hiểu là user — CMS không được tạo P2P với user
(lỗi invalid participants). Channel chỉ owner/admin gửi tin.
9. Members
| Method | Path | Body / query |
|---|---|---|
| GET | /api/v1/chat/conversations/:id/members | |
| POST | /api/v1/chat/conversations/:id/members | {"members":[{"peer_id","peer_type"}]} |
| DELETE | /api/v1/chat/conversations/:id/members/:memberId | query type = admin|bot |
| PUT | /api/v1/chat/conversations/:id/members/:memberId/role | {"member_type","role"} |
| POST | /api/v1/chat/conversations/:id/transfer-owner | {"member_id","member_type"} |
10. Messages
Role ChatConversationView cho đọc; ChatConversationEdit cho send / read / react / delete. Actor luôn lấy từ JWT.
| Method | Path | Body / query | msg |
|---|---|---|---|
| GET | /api/v1/chat/conversations/:id/messages | query before_seq, limit | array Message |
| POST | /api/v1/chat/conversations/:id/messages/states | {"message_ids":["..."]} | array MessageState |
| POST | /api/v1/chat/conversations/:id/messages | type, text, attachments, reply_to_id | message |
| POST | /api/v1/chat/conversations/:id/read | {"seq"} | null |
| DELETE | /api/v1/chat/messages/:id | {"conversation_id"} — chỉ xóa tin của mình | message (deleted) |
| POST | /api/v1/chat/messages/:id/react | {"conversation_id","emoji"} — emoji rỗng = gỡ | message |
| GET | /api/v1/chat/messages/:id/reactions/:reaction | query limit, cursor | ReactionUserPage |
| GET | /api/v1/chat/messages/:id/seen | query limit, cursor | SeenUserPage |
content.p2p_reactions.
Group/channel: collection reaction riêng; content.reaction_count là map emoji → count.
Cả hai kiểu đều có reactions (summary) + my_reaction + seen_count trên Message / MessageState.
ListMessages
Lịch sử tin của conversation :id. Phải đọc được hội thoại (member accepted, hoặc admin observer: super admin CMS / mọi admin trên scope game / mọi admin trên support & game_room).
GET /api/v1/chat/conversations/{conversation_id}/messages?before_seq=0&limit=50
Authorization: Bearer <cms_jwt>
| Param | Nơi | Mô tả |
|---|---|---|
id | path | Conversation ID |
before_seq | query | 0 / bỏ trống = trang mới nhất. Trang sau = seq nhỏ nhất trang hiện tại |
limit | query | Mặc định 50, tối đa 100 |
HTTP 200
{
"code": 0,
"msg": [
{ "id": "...", "seq": 5, "type": "text", "reactions": [], "my_reaction": null, "seen_count": 2 },
{ "id": "...", "seq": 6, "type": "text", "reactions": [...], "my_reaction": "👍", "seen_count": 1 }
]
}
Mảng theo thứ tự thời gian (seq tăng). Mỗi tin đã hydrate state.
Dùng GetMessageStates để refresh reaction/seen của tin đang hiển thị mà không reload history.
GetMessageStates
Batch state (reactions + seen) cho tin đã có trên UI. Cùng quyền đọc như ListMessages.
POST /api/v1/chat/conversations/{conversation_id}/messages/states
Authorization: Bearer <cms_jwt>
Content-Type: application/json
{ "message_ids": ["66e1ab0c2f1a4b0012345678", "66e1ab0c2f1a4b0012345679"] }
| Param | Nơi | Mô tả |
|---|---|---|
id | path | Conversation ID — tin không thuộc conversation này trả state rỗng |
message_ids | JSON | ObjectId hex. Tối đa 100 (cắt phần dư). ID sai format / trùng bị bỏ |
HTTP 200
{
"code": 0,
"msg": [
{
"message_id": "66e1ab0c2f1a4b0012345678",
"reactions": [
{ "reaction": "👍", "count": 12, "users": [{ "user_id": 42 }, { "user_id": 7 }] }
],
"my_reaction": "👍",
"seen_count": 8
}
]
}
Thứ tự msg theo message_ids hợp lệ còn lại (sau khi lọc).
Summary users tối đa 4; bấm vào emoji → ListMessageReactions.
Bấm seen → ListMessageSeen.
ListMessageReactions
Danh sách người đã react một emoji trên một tin. Actor phải đọc được conversation chứa tin đó.
GET /api/v1/chat/messages/{message_id}/reactions/{reaction}?limit=25&cursor=
Authorization: Bearer <cms_jwt>
| Param | Nơi | Mô tả |
|---|---|---|
id | path | Message ObjectId hex |
reaction | path | Emoji, URL-encode (encodeURIComponent("👍") → %F0%9F%91%8D). Fallback query reaction nếu path rỗng |
limit | query | Mặc định 25, tối đa 50 |
cursor | query | next_cursor trang trước. Bỏ trống = trang đầu (mới nhất) |
HTTP 200
{
"code": 0,
"msg": {
"users": [{ "user_id": 42 }, { "user_id": 7 }],
"next_cursor": "1757812345678901234:7"
}
}
Sắp xếp mới → cũ. reaction rỗng hoặc message id không phải ObjectId → popup invalid input. 1 user 1 emoji (đổi emoji = chuyển group).
ListMessageSeen
Member accepted còn trong hội thoại, last_read_seq >= seq của tin. Không gồm người đã leave.
GET /api/v1/chat/messages/{message_id}/seen?limit=25&cursor=
Authorization: Bearer <cms_jwt>
| Param | Nơi | Mô tả |
|---|---|---|
id | path | Message ObjectId hex |
limit | query | Mặc định 25, tối đa 50 |
cursor | query | next_cursor trang trước. Bỏ trống = trang đầu |
HTTP 200
{
"code": 0,
"msg": {
"users": [{ "user_id": 42 }, { "user_id": 1001 }],
"next_cursor": "7:42"
}
}
Sắp xếp last_read_seq giảm dần (đọc gần đây / đọc xa hơn trước).
seen_count trên Message/MessageState là tổng; API này chỉ khi cần danh sách.
Cập nhật read: POST /conversations/:id/read với {"seq"}.
11. Errors
CMS luôn popup code=1000. msg là chuỗi domain (i18n key phía gateway khi parse được).
| Tình huống | Hành vi |
|---|---|
| JWT hết hạn / thiếu / sai role | gateway auth — không vào handler |
| Body không parse | HTTP 400, popup |
| already claimed / closed / blocked / pending / not participant | HTTP 500, popup 1000 |
| Chỉ owner/super đóng support | cannot close |
| Không tìm thấy | data not found |
| message id / reaction rỗng, ObjectId sai | invalid input |
| Admin không đọc được conversation | not a conversation participant / member is pending |
12. Checklist
- Queue CS: poll pending + mở WS
support_started/support_claimed. - Sau claim: subscribe conversation (WS auto-sub khi connect lại) rồi history + send.
- Không gửi tin khi
status=closed. - Phân biệt FAQ
/botvà actor/bots. - P2P CMS: đợi peer
join(trừ bot) trước khi send. - List conversation: đọc
Items/HasNext/NextCursor(nanoseconds). - History:
before_seq=0rồi lùi bằng seq nhỏ nhất. Reaction/seen list: lặp khinext_cursorkhác rỗng. - Poll
POST .../messages/statescho tin đang mở; đừng gọi list users mỗi tin. - Realtime payload: xem WebSocket events.