NON-PRODUCTION

Chat Game — tài liệu ghép client

Chat bạn bè, group, CS support, FAQ bot. Client gọi POST /api/v2/rpc trên game-api-gateway (AES giống mission). Tin realtime qua WebSocket chat-api — không đi gateway.

1. Tổng quan

MụcGiá trị
Gatewaygame-api-gateway · POST /api/v2/rpc
AuthJWT game, middleware GameJWTVerify. Không gửi user_id
Prefix method/chat
ScopeGame = user + bot. Không chat trực tiếp với admin trừ ticket support
Realtimechat-api GET /ws?access_token= — chat-ws
Ba kênh: category=social (P2P/group/self), support (1 ticket active), game_room (chat bàn — node game tạo, client chỉ nhận WS + gọi history/send).

2. Auth & envelope

Giống mọi RPC lobby: decrypt data → JSON {"method","data"} → encrypt {"code","msg"}.

POST /api/v2/rpc
Authorization: Bearer <access_token>
Content-Type: application/json

decrypt → {
  "method": "/chat/social/list",
  "data": {}
}

success → HTTP 200, encrypted { "code": 0, "msg": { ... } }
error   → HTTP 500, encrypted { "code": ..., "msg": ... }
List social / pending trả wrapper items, next_cursor, has_next. Conversation / message / member / file bên trong cũng snake_case. cursor_unix = unix nanoseconds. Limit mặc định / tối đa 50. History game RPC cố định 25 tin — client không gửi limit.

3. Models

Conversation

{
  "id": "p2p:social:user_1001_user_1002",
  "type": "p2p",                 // p2p | group | channel | self
  "category": "social",          // social | support | game_room
  "scope": "game",
  "name": "",
  "avatar": "",
  "description": "",
  "owner_id": 1001,
  "participants": [
    { "id": 1001, "type": "user", "role": "owner" },
    { "id": 1002, "type": "user", "role": "owner" }
  ],
  "settings": { "only_admin_send": false, "expired_at": null },
  "reference": { "type": "game_room", "id": "tienlen/room-9" },
  "metadata": { "game_id": "tienlen", "room_id": "room-9" },
  "last_seq": 4,
  "last_message_at": "2026-09-14T03:20:00Z",
  "last_message_preview": "hi",
  "claimed_by": null,
  "status": "active",
  "created_at": "2026-09-14T03:00:00Z",
  "updated_at": "2026-09-14T03:20:00Z"
}

Message

{
  "id": "66e1ab0c2f1a4b0012345678",
  "conversation_id": "p2p:social:user_1001_user_1002",
  "seq": 4,
  "sender_id": 1001,
  "sender_type": "user",
  "type": "text",                // text | image | video | file | audio
  "content": {
    "text": "hi",
    "attachments": [],
    "p2p_reactions": { "1002": "❤️" },
    "reaction_count": {}
  },
  "reply_to": null,
  "deleted": false,
  "created_at": "2026-09-14T03:20:00Z"
}

Text tối đa 4000. Ảnh/video/file/audio cần attachments[0].url. Game room cho phép audio.

List wrapper

{ "items": [ /* Conversation */ ], "next_cursor": 1757812345678901234, "has_next": false }

ConversationMember

{
  "id": "...",
  "conversation_id": "group:...",
  "user_id": 1002,
  "type": "user",                 // user | bot
  "role": "member",               // member | admin | owner
  "state": "accepted",            // pending | accepted | blocked
  "joined_at": "2026-09-14T03:00:00Z",
  "last_read_seq": 4
}

ConversationSettings

{
  "only_admin_send": false,
  "auto_delete_duration": 86400,
  "expired_after_inactive": null,
  "expired_at": null
}

auto_delete_duration là số giây — tin mới sẽ có expired_at = created + duration. only_admin_send=true thì chỉ owner/admin gửi được.

ConversationFile

{
  "id": "66e1ab0c2f1a4b0012345678",
  "message_id": "66e1ab0c2f1a4b0012345679",
  "conversation_id": "group:...",
  "owner_id": 1001,
  "type": "image",
  "filename": "photo.jpg",
  "mime_type": "image/jpeg",
  "size": 128000,
  "width": 800,
  "height": 600,
  "duration_ms": 0,
  "url": "https://cdn/...",
  "thumbnail": "https://cdn/...",
  "created_at": "2026-09-14T03:20:00Z"
}

Attachment

{ "url": "https://cdn/...", "mime": "audio/mpeg", "name": "v.m4a",
  "size": 32000, "duration_ms": 1500, "width": 0, "height": 0, "thumbnail": "" }

4. Social

Tạo P2P: caller accepted, peer (user) pending cho tới /social/join. Bot (peer_type=bot) auto-accept. Block 2 chiều chặn send. Tạo lại P2P cùng peer là idempotent (trả conversation cũ).

methoddatamsg
/chat/social/listlimit, cursor_unixlist accepted
/chat/social/pendingideminvite chờ join/reject
/chat/social/counts{}{pending_count,unread_count}
/chat/social/openalias /social/createconversation
/chat/social/createpeer_id, peer_type (user|bot)conversation
/chat/social/create-groupname bắt buộc, description, avatar, member_idsconversation
/chat/social/create-self{}saved messages, idempotent
/chat/social/getconversation_idconversation
/chat/social/joinconversation_idconversation
/chat/social/rejectconversation_idnull
/chat/social/leaveconversation_idnull
/chat/social/membersconversation_id, limit, cursortrang member
/chat/social/members/addconversation_id, members:[{member_id,member_type}]null
/chat/social/members/removeconversation_id, member_id, member_typenull
/chat/social/members/roleconversation_id, member_id, member_type, role (member|admin)null
/chat/social/transfer-ownerconversation_id, member_id, member_typenull
/chat/social/updateconversation_id, name, avatar, descriptionnull
/chat/social/settingsconversation_id, only_admin_send, auto_delete_duration, expired_after_inactive, expired_atnull
/chat/social/blockpeer_idnull
/chat/social/unblockpeer_idnull
/chat/social/blocked{}array {blocker_id,blocked_id,created_at}
data /chat/social/create:
{ "peer_id": 1002, "peer_type": "user" }

data /chat/social/create-group:
{ "name": "Bàn tối", "description": "", "avatar": "", "member_ids": [1002, 1003] }

data /chat/social/list:
{ "limit": 50, "cursor_unix": 0 }

msg /chat/social/list:
{ "items": [ /* Conversation accepted */ ], "next_cursor": 0, "has_next": false }

Quản lý group

Add / remove / role / update info chỉ cho conversation type=group (hoặc channel). P2P/self dùng participants — các API này trả no permission / invalid input. Actor phải là owner hoặc admin, trừ transfer-owner (chỉ owner).

data /chat/social/members/add:
{
  "conversation_id": "group:...",
  "members": [
    { "member_id": 1004, "member_type": "user" },
    { "member_id": 3, "member_type": "bot" }
  ]
}

data /chat/social/members/remove:
{ "conversation_id": "group:...", "member_id": 1004, "member_type": "user" }

data /chat/social/members/role:
{ "conversation_id": "group:...", "member_id": 1004, "member_type": "user", "role": "admin" }

data /chat/social/transfer-owner:
{ "conversation_id": "group:...", "member_id": 1004, "member_type": "user" }

data /chat/social/update:
{ "conversation_id": "group:...", "name": "Bàn tối", "avatar": "", "description": "" }

data /chat/social/settings:
{ "conversation_id": "group:...", "only_admin_send": true, "auto_delete_duration": 86400 }
User được add vào group ở trạng thái pending — nhận WS conversation.invited, phải /social/join mới chat. Bot auto-accept. Member đã có trong group bị bỏ qua (idempotent). Không tự remove mình (dùng /social/leave). Không remove / đổi role owner. role chỉ member hoặc admin — không set owner bằng role API. Transfer owner: owner cũ thành admin. Settings đổi đẩy WS conversation.settings.updated (system message). Remove member đẩy conversation.user.left.
Chưa join thì send trả member is pending. Channel (nếu có) chỉ owner/admin gửi. Không tạo P2P với peer_type=admin. member_type trống = user.

5. Messages

Dùng cho social, support, và game room (khi đã là member bàn).

methoddatamsg
/chat/messages/sendconversation_id, type, text, attachments, reply_to_idmessage
/chat/messages/historyconversation_id, before_seqarray (25 tin)
/chat/messages/deleteconversation_id, message_id — chỉ tin của mìnhmessage deleted
/chat/messages/editconversation_id, message_id, textmessage
/chat/messages/reactconversation_id, message_id, emoji (rỗng = gỡ)message
/chat/messages/statesconversation_id, messages:[{message_id,version}] hoặc message_idsarray MessageState
/chat/messages/reactionsmessage_id, reaction, limit, cursortrang user đã react
/chat/messages/seenmessage_id, limit, cursortrang user đã seen
/chat/messages/readconversation_id, seqnull
/chat/messages/typingconversation_idnull; WS message.typing.created
data /chat/messages/send:
{
  "conversation_id": "p2p:social:user_1001_user_1002",
  "type": "text",
  "text": "hello",
  "reply_to_id": ""
}

data /chat/messages/history:
{ "conversation_id": "...", "before_seq": 0 }

data /chat/messages/typing:
{ "conversation_id": "p2p:social:user_1001_user_1002" }

Typing không lưu DB. Member đọc được conversation thì gọi được. Peer nhận WS message.typing.created (actor_id, conversation_id) — UI tự ẩn sau vài giây.

before_seq=0 = trang mới nhất. Trang sau: before_seq = seq nhỏ nhất trang hiện tại. P2P: 1 user 1 emoji trong p2p_reactions. Group: reaction_count.

6. Attachments

Upload theo hai bước: tạo session upload, client PUT file theo URL/headers trả về, rồi complete trước khi đưa attachment_id vào /chat/messages/send.

methoddatamsg
/chat/attachments/createfilename, mime_type, size, type, width, height, duration_ms, idempotency_keyupload session
/chat/attachments/completeattachment_idattachment
/chat/attachments/urlattachment_iddownload URL
/chat/attachments/deleteattachment_idnull
/chat/attachments/listconversation_id, cursortrang file trong conversation
data /chat/attachments/list:
{ "conversation_id": "group:...", "cursor": "" }

msg /chat/attachments/list:
{
  "files": [ /* ConversationFile */ ],
  "next_cursor": "1757812345678901234:66e1ab0c2f1a4b0012345678"
}

Cần đọc được conversation (member accepted hoặc pending). Mỗi trang 20 file, mới → cũ. cursor rỗng = trang đầu; trang sau gửi next_cursor. Hết trang thì next_cursor rỗng. url / thumbnail là presigned download.

7. Support

User mở 1 ticket active. Gọi start khi đang có ticket → trả ticket cũ, không tạo mới. FAQ bot tách khỏi live chat.

methoddatamsg
/chat/support/start{}conversation
/chat/support/conversation{}{conversation} ticket active hoặc null
/chat/support/bot{}array FAQ config (enabled, cho user)
msg /chat/support/bot:
[
  {
    "id": "66e1...",
    "title": "Nạp tiền",
    "description": "",
    "locale": "vi",
    "items": [
      { "id": "q1", "question": "...", "answer": "...", "enabled": true, "sort_order": 1 }
    ],
    "enabled": true,
    "sort_order": 10
  }
]
Sau start: gửi tin bằng /chat/messages/send với id ticket. Admin claim/close đẩy event WS support.claimed / support.closed. Ticket closed reject mọi send.

8. Game room (client)

Node game (không phải client) gọi chat-client CreateGameRoom / JoinGameRoom / LeaveGameRoom / SnapshotGameRoom theo lifecycle bàn. Client không có RPC create/join room.

9. Gợi ý UI

  1. Login xong: connect WS chat-api + gọi /chat/social/list và /chat/social/pending.
  2. Inbox: card theo last_message_preview / last_message_at. Badge unread = last_seq local vs server (sau read).
  3. Mở thread: history before_seq=0, scroll lên gọi tiếp với seq nhỏ nhất.
  4. P2P mới: /social/create → nếu pending, hiện “chờ chấp nhận”. Peer: pending list → join/reject.
  5. Group: owner/admin /social/members/add; peer pending nhận conversation.invited → join/reject. Media tab: /attachments/list.
  6. Composer: debounce /messages/typing (~2–3s). WS message.typing.created để hiện “đang gõ”.
  7. CS: màn FAQ từ /support/bot; nút “Chat CS” → /support/start.
  8. Trong bàn: không invent conversation id — lấy từ event WS hoặc snapshot node đẩy xuống gameplay.
  9. Block: ẩn composer, không retry send.

10. Error codes

Lỗi domain map popup i18n code=1000 (HTTP 500 encrypted). Bind sai: -2.

Tình huốngHành vi client
JWT hết hạncode -3 — login lại; WS handshake code=4
Thiếu tokencode -4
Body không parse / thiếu conversation_id / text rỗngcode -2 hoặc popup invalid input
Chưa join / member pendingpopup — hiện accept invite
Blockedẩn send
Conversation closed / expiredkhóa composer, list lại
Không phải participant / no permissionđóng thread. Owner/admin mới add/remove/role/update/settings
Xóa tin người khácno permission
Not foundrefresh list
Kháccode -1 unknown

11. Checklist