NON-PRODUCTION

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ụcGiá trị
Gatewayadmin-api-gateway · REST /api/v1/chat/*
AuthCMS JWT, header Authorization: Bearer <token>
ActorLuôn lấy từ JWT. Super admin khi RoleGroupID <= 0 hoặc role ChatSupportSuper
Scope socialCMS = admin + bot. Không tạo P2P admin↔user
Realtimeadmin-api-gateway GET /api/v1/chat/ws?access_token= — xem chat-ws
Hai loại bot khác nhau: /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

RoleIDDùng cho
ChatSupportView81000GET support queues + history
ChatSupportEdit81001Claim / add-agent / close / send
ChatSupportSuper81002Đóng ticket người khác; list closed tất cả admin
ChatBotView81003GET FAQ + social bots
ChatBotEdit81004CRUD FAQ + social bots
ChatConversationView81005List / get conversation + members + messages
ChatConversationEdit81006Tạ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": "..." }
List conversation / support trả wrapper PascalCase: 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.

MethodPathBody / querymsg
GET/api/v1/chat/support/pendinglimit, cursor_unixlist chưa claim
GET/api/v1/chat/support/claimedidemticket của admin đang login
GET/api/v1/chat/support/closedidemclosed 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/sendxem dướimessage
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": []
}
History: 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.

MethodPathGhi chú
GET/api/v1/chat/botsquery enabled_only=true để lọc
POST/api/v1/chat/bots{"name","enabled"} — enabled mặc định true
GET/api/v1/chat/bots/:idid số
PUT/api/v1/chat/bots/:idname 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.

MethodPathBody / querymsg
GET/api/v1/chat/conversationslimit, cursor_unixđã accepted của admin
GET/api/v1/chat/conversations/pendingideminvite 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/groupname bắt buộc, description, avatar, member_idsconversation
POST/api/v1/chat/conversations/channelnhư group; only_admin_send=trueconversation
POST/api/v1/chat/conversations/selfrỗngsaved messages, idempotent
GET/api/v1/chat/conversations/:idconversation
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/closenull
POST/api/v1/chat/conversations/:id/joinconversation
POST/api/v1/chat/conversations/:id/rejectnull
POST/api/v1/chat/conversations/:id/leavenull
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

MethodPathBody / 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/:memberIdquery 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.

MethodPathBody / querymsg
GET/api/v1/chat/conversations/:id/messagesquery before_seq, limitarray Message
POST/api/v1/chat/conversations/:id/messages/states{"message_ids":["..."]}array MessageState
POST/api/v1/chat/conversations/:id/messagestype, text, attachments, reply_to_idmessage
POST/api/v1/chat/conversations/:id/read{"seq"}null
DELETE/api/v1/chat/messages/:id{"conversation_id"} — chỉ xóa tin của mìnhmessage (deleted)
POST/api/v1/chat/messages/:id/react{"conversation_id","emoji"} — emoji rỗng = gỡmessage
GET/api/v1/chat/messages/:id/reactions/:reactionquery limit, cursorReactionUserPage
GET/api/v1/chat/messages/:id/seenquery limit, cursorSeenUserPage
P2P/self: 1 actor 1 emoji trong 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>
ParamNơiMô tả
idpathConversation ID
before_seqquery0 / bỏ trống = trang mới nhất. Trang sau = seq nhỏ nhất trang hiện tại
limitqueryMặ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"] }
ParamNơiMô tả
idpathConversation ID — tin không thuộc conversation này trả state rỗng
message_idsJSONObjectId 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>
ParamNơiMô tả
idpathMessage ObjectId hex
reactionpathEmoji, URL-encode (encodeURIComponent("👍") → %F0%9F%91%8D). Fallback query reaction nếu path rỗng
limitqueryMặc định 25, tối đa 50
cursorquerynext_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>
ParamNơiMô tả
idpathMessage ObjectId hex
limitqueryMặc định 25, tối đa 50
cursorquerynext_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ốngHành vi
JWT hết hạn / thiếu / sai rolegateway auth — không vào handler
Body không parseHTTP 400, popup
already claimed / closed / blocked / pending / not participantHTTP 500, popup 1000
Chỉ owner/super đóng supportcannot close
Không tìm thấydata not found
message id / reaction rỗng, ObjectId saiinvalid input
Admin không đọc được conversationnot a conversation participant / member is pending

12. Checklist