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ục | Giá trị |
|---|---|
| Gateway | game-api-gateway · POST /api/v2/rpc |
| Auth | JWT game, middleware GameJWTVerify. Không gửi user_id |
| Prefix method | /chat |
| Scope | Game = user + bot. Không chat trực tiếp với admin trừ ticket support |
| Realtime | chat-api GET /ws?access_token= — chat-ws |
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": ... }
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ũ).
| method | data | msg |
|---|---|---|
/chat/social/list | limit, cursor_unix | list accepted |
/chat/social/pending | idem | invite chờ join/reject |
/chat/social/counts | {} | {pending_count,unread_count} |
/chat/social/open | alias /social/create | conversation |
/chat/social/create | peer_id, peer_type (user|bot) | conversation |
/chat/social/create-group | name bắt buộc, description, avatar, member_ids | conversation |
/chat/social/create-self | {} | saved messages, idempotent |
/chat/social/get | conversation_id | conversation |
/chat/social/join | conversation_id | conversation |
/chat/social/reject | conversation_id | null |
/chat/social/leave | conversation_id | null |
/chat/social/members | conversation_id, limit, cursor | trang member |
/chat/social/members/add | conversation_id, members:[{member_id,member_type}] | null |
/chat/social/members/remove | conversation_id, member_id, member_type | null |
/chat/social/members/role | conversation_id, member_id, member_type, role (member|admin) | null |
/chat/social/transfer-owner | conversation_id, member_id, member_type | null |
/chat/social/update | conversation_id, name, avatar, description | null |
/chat/social/settings | conversation_id, only_admin_send, auto_delete_duration, expired_after_inactive, expired_at | null |
/chat/social/block | peer_id | null |
/chat/social/unblock | peer_id | null |
/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 }
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.
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).
| method | data | msg |
|---|---|---|
/chat/messages/send | conversation_id, type, text, attachments, reply_to_id | message |
/chat/messages/history | conversation_id, before_seq | array (25 tin) |
/chat/messages/delete | conversation_id, message_id — chỉ tin của mình | message deleted |
/chat/messages/edit | conversation_id, message_id, text | message |
/chat/messages/react | conversation_id, message_id, emoji (rỗng = gỡ) | message |
/chat/messages/states | conversation_id, messages:[{message_id,version}] hoặc message_ids | array MessageState |
/chat/messages/reactions | message_id, reaction, limit, cursor | trang user đã react |
/chat/messages/seen | message_id, limit, cursor | trang user đã seen |
/chat/messages/read | conversation_id, seq | null |
/chat/messages/typing | conversation_id | null; 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.
| method | data | msg |
|---|---|---|
/chat/attachments/create | filename, mime_type, size, type, width, height, duration_ms, idempotency_key | upload session |
/chat/attachments/complete | attachment_id | attachment |
/chat/attachments/url | attachment_id | download URL |
/chat/attachments/delete | attachment_id | null |
/chat/attachments/list | conversation_id, cursor | trang 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.
| method | data | msg |
|---|---|---|
/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
}
]
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.
- Conversation
type=group,category=game_room, idgame_room:{gameID}:{roomID}. - TTL mặc định 30 phút kể từ create / join / leave / send. Hết hạn → closed, send lỗi expired.
- Tin chỉ cache Redis (~20 tin), không history Mongo dài hạn.
- Cho phép
type=audio. - Client: mở WS, nhận
game_room.created/game_room.joined, rồi/chat/messages/history+send.
9. Gợi ý UI
- Login xong: connect WS chat-api + gọi
/chat/social/listvà/chat/social/pending. - Inbox: card theo
last_message_preview/last_message_at. Badge unread = last_seq local vs server (sauread). - Mở thread: history
before_seq=0, scroll lên gọi tiếp với seq nhỏ nhất. - P2P mới:
/social/create→ nếu pending, hiện “chờ chấp nhận”. Peer: pending list → join/reject. - Group: owner/admin
/social/members/add; peer pending nhậnconversation.invited→ join/reject. Media tab:/attachments/list. - Composer: debounce
/messages/typing(~2–3s). WSmessage.typing.createdđể hiện “đang gõ”. - CS: màn FAQ từ
/support/bot; nút “Chat CS” →/support/start. - Trong bàn: không invent conversation id — lấy từ event WS hoặc snapshot node đẩy xuống gameplay.
- 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ống | Hành vi client |
|---|---|
| JWT hết hạn | code -3 — login lại; WS handshake code=4 |
| Thiếu token | code -4 |
| Body không parse / thiếu conversation_id / text rỗng | code -2 hoặc popup invalid input |
| Chưa join / member pending | popup — hiện accept invite |
| Blocked | ẩn send |
| Conversation closed / expired | khó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ác | no permission |
| Not found | refresh list |
| Khác | code -1 unknown |
11. Checklist
- Mọi method prefix
/chat/..., user_id chỉ từ JWT. - List đọc
items/next_cursor/has_next. - History RPC 25 tin/trang;
before_seqđể phân trang. - P2P: join trước khi send (trừ bot).
- Group: owner/admin mới add/remove/role/update/settings; transfer-owner chỉ owner.
- 1 support active; FAQ riêng
/support/bot. - Game room: WS + messages API, không RPC create room.
- Connect WS sau login — xem WebSocket events.