Tài liệu kỹ thuật chi tiết cho nhà phát triển, tích hợp API, và mở rộng hệ thống.
- 1. Tổng quan kiến trúc
- 2. Matrix Protocol — Nền tảng
- 3. Auth Service API
- 4. Matrix Client-Server API
- 5. Frontend Architecture
- 6. WebRTC Call System
- 7. Bot Integration Guide
- 8. API Integration cho Website/Platform
- 9. Deployment & Scaling
- 10. Security Analysis
- 11. Widget Messages & Smart Features
┌────────────────────────────────────────────────────────────────┐
│ CLIENTS │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Web/PWA │ │ Android │ │ Desktop │ │ External Bot │ │
│ │(Next.js) │ │(Capacitor│ │ (Tauri) │ │ (HTTP/SDK) │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │
│ │ │ │ │ │
└───────┼──────────────┼──────────────┼───────────────┼───────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌────────────────────────────────────────────────────────────────┐
│ NGINX REVERSE PROXY │
│ (SSL termination, routing) │
│ │
│ /chat/* → Frontend:3000 /_matrix/* → Dendrite:8008 │
│ /auth/* → Auth:4000 /media/* → Dendrite:8008 │
└────────┬──────────────┬────────────────┬───────────────────────┘
│ │ │
┌────▼────┐ ┌─────▼─────┐ ┌─────▼─────┐
│Frontend │ │ Auth │ │ Dendrite │
│Next.js │ │ Service │ │ Matrix │
│ SSR+SPA │ │Express.js │ │Homeserver │
│Port 3000│ │Port 4000 │ │Port 8008 │
└─────────┘ └───────────┘ └─────┬─────┘
│
┌─────▼─────┐
│ SQLite/ │
│ PostgreSQL │
└───────────┘
User Action → Frontend (Zustand Store) → Matrix API → Dendrite → Database
↑
│ Polling (2s interval)
│
Dendrite → /sync → Frontend → UI Update
| Quyết định | Lý do |
|---|---|
| Dendrite thay vì Synapse | Nhẹ hơn 10x, single binary, Go thay vì Python |
| Polling thay vì WebSocket | Dendrite /sync API chỉ hỗ trợ long-polling, đơn giản hóa deploy |
| Zustand thay vì Redux | Nhẹ, không boilerplate, perfect cho real-time state |
| Next.js App Router | SSR cho SEO, nhưng chủ yếu dùng CSR cho chat |
| Auth Service riêng | Matrix không hỗ trợ OTP/phone natively, cần middleware |
| WebRTC qua Matrix signaling | Tận dụng Matrix events cho signaling, không cần TURN/STUN server riêng cho LAN |
Matrix là giao thức nhắn tin phi tập trung (decentralized), mã nguồn mở, federation-ready. PieChat sử dụng Dendrite — implementation nhẹ nhất của Matrix bằng Go.
┌─────────────────────────────────────────────────┐
│ Matrix Concepts │
│ │
│ User: @alice:piechart.site │
│ Room: !abcd1234:piechart.site │
│ Event: $eventId (tin nhắn, reaction, call...) │
│ Server: piechart.site (homeserver) │
│ │
│ Room Types (PieChat mapping): │
│ ├── DM → Direct Message (1-1) │
│ ├── Group → Nhóm chat │
│ ├── Channel → Kênh (chứa nhiều groups) │
│ └── Assistant → Bot AI conversation │
└─────────────────────────────────────────────────┘
Base URL: https://piechart.site/_matrix/client/v3/
| Endpoint | Method | Mô tả |
|---|---|---|
/login |
POST | Đăng nhập, nhận access_token |
/register |
POST | Đăng ký user mới |
/sync |
GET | Long-polling: nhận events mới |
/rooms/{roomId}/send/{eventType}/{txnId} |
PUT | Gửi event (tin nhắn, reaction...) |
/rooms/{roomId}/messages |
GET | Lấy lịch sử tin nhắn |
/rooms/{roomId}/state/{eventType} |
GET/PUT | Room state (tên, topic, avatar...) |
/createRoom |
POST | Tạo room mới |
/joined_rooms |
GET | Danh sách rooms đã join |
/user_directory/search |
POST | Tìm kiếm user |
/account/whoami |
GET | Xác minh token |
/profile/{userId} |
GET/PUT | Profile (display name, avatar) |
/rooms/{roomId}/invite |
POST | Mời user vào room |
/rooms/{roomId}/join |
POST | Join room |
/rooms/{roomId}/leave |
POST | Rời room |
/rooms/{roomId}/read_markers |
POST | Đánh dấu đã đọc |
/upload |
POST | Upload media file |
/download/{serverName}/{mediaId} |
GET | Download media |
┌──────┐ ┌────────────┐ ┌──────────┐ ┌──────────┐
│Client│ │Auth Service│ │ Dendrite │ │SMS Gateway│
└──┬───┘ └─────┬──────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ POST /auth/request-otp │ │
│ {phone, password, deviceId} │ │
├─────────────────▶│ │ │
│ │ POST /login │ │
│ │ (verify password) │ │
│ ├───────────────────▶│ │
│ │◀───────────────────┤ │
│ │ │ │
│ │ trusted device? │ │
│ │ YES → return {requiresOtp: false, matrixUsername}
│ │ NO → generate OTP │ │
│ │ │ send SMS │
│ ├────────────────────┼───────────────────▶│
│◀─────────────────┤ │ │
│ {requiresOtp: true, otpToken, maskedPhone} │
│ │ │ │
│ POST /auth/verify-otp │ │
│ {otpToken, otpCode} │ │
├─────────────────▶│ │ │
│ │ verify & trust │ │
│◀─────────────────┤ │ │
│ {success: true, matrixUsername} │ │
│ │ │ │
│ POST /_matrix/client/v3/login │ │
├──────────────────────────────────────▶│ │
│◀──────────────────────────────────────┤ │
│ {access_token, user_id, device_id} │ │
Base URL: https://piechart.site/auth/
Bước 1 của đăng nhập — xác minh mật khẩu, gửi OTP nếu thiết bị mới.
Request:
{
"phone": "0901234567",
"password": "MyPassword123",
"deviceId": "browser_abc123"
}Response (thiết bị tin cậy — bỏ qua OTP):
{
"requiresOtp": false,
"matrixUsername": "u0901234567"
}Response (thiết bị mới — cần OTP):
{
"requiresOtp": true,
"otpToken": "otp_1234567890_abcdef",
"maskedPhone": "****4567",
"matrixUsername": "u0901234567",
"devOtp": "123456" // chỉ có ở development
}Error responses:
| Status | Error |
|---|---|
| 400 | Dữ liệu đăng nhập không hợp lệ |
| 401 | Sai số điện thoại hoặc mật khẩu |
| 429 | Bạn nhập sai quá nhiều lần (rate limited) |
| 502 | Không gửi được OTP (SMS gateway error) |
Bước 2 — xác minh mã OTP.
Request:
{
"otpToken": "otp_1234567890_abcdef",
"otpCode": "123456"
}Success:
{
"success": true,
"matrixUsername": "u0901234567"
}Errors: 400 (missing), 401 (wrong code), 410 (expired), 429 (blocked)
Cho phép đăng nhập web bằng cách quét QR code từ mobile app.
POST /auth/qr/generate → { sessionId, qrData }
GET /auth/qr/status/:sessionId → { status: 'pending' | 'approved' | 'expired' }
POST /auth/qr/approve → { ok: true, userId }
Flow:
- Web gọi
/qr/generate→ hiển thị QR code chứasessionId - Web polling
/qr/status/:sessionIdmỗi 2 giây - Mobile quét QR → gửi
POST /qr/approvevới{sessionId, accessToken} - Web nhận
status: approvedvớiaccessToken→ đăng nhập thành công
Liệt kê thiết bị tin cậy.
GET /auth/devices?phone=0901234567
→ { devices: [{ deviceId, name, lastUsed, ip }] }
Thu hồi thiết bị tin cậy (buộc OTP lần đăng nhập kế).
{ "phone": "0901234567", "deviceId": "browser_abc123" }Lịch sử đăng nhập & hoạt động bảo mật.
GET /auth/login-events?phone=0901234567&suspiciousOnly=1&sinceMs=1700000000000
→ { events: [{ type, success, suspicious, timestamp, ip, userAgent, message }] }
Health check endpoint.
GET /auth/health → { status: 'ok', service: 'piechat-auth', timestamp: ... }
PieChat sử dụng Matrix CS API chuẩn qua matrix-service.ts. Dưới đây là các API chính:
POST /_matrix/client/v3/login
{
"type": "m.login.password",
"identifier": { "type": "m.id.user", "user": "u0901234567" },
"password": "MyPassword123",
"device_id": "PIECHAT_abc",
"initial_device_display_name": "PieChat Web"
}
→ { "access_token": "syt_...", "user_id": "@u0901234567:piechart.site" }GET /_matrix/client/v3/sync?since=s123&timeout=30000
// Headers: Authorization: Bearer syt_...
→ {
"rooms": {
"join": {
"!roomId:server": {
"timeline": { "events": [...] },
"state": { "events": [...] },
"unread_notifications": { "notification_count": 2 }
}
}
},
"next_batch": "s124"
}PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message/{txnId}
// Headers: Authorization: Bearer syt_...
{
"msgtype": "m.text",
"body": "Xin chào!"
}PieChat sử dụng state events tuỳ chỉnh để lưu metadata:
| Event Type | Mô tả | Ví dụ |
|---|---|---|
com.piechat.room_type |
Loại room (dm, group, channel) | { "type": "group", "channelId": "!abc" } |
com.piechat.friendship |
Trạng thái kết bạn | { "status": "accepted", "requester": "@alice:..." } |
com.piechat.channel_roles |
Phân quyền kênh | { "@alice:...": "admin" } |
com.piechat.group_roles |
Phân quyền nhóm | { "@bob:...": "member" } |
com.piechat.group_priority |
Độ ưu tiên sắp xếp | { "priority": 1 } |
com.piechat.restrict_speaking |
Hạn chế phát biểu | { "restricted": true } |
m.call.invite |
WebRTC call invite | { "offer": { "sdp": "..." } } |
m.call.answer |
WebRTC call accept | { "answer": { "sdp": "..." } } |
m.call.hangup |
WebRTC call end | { "reason": "user" } |
m.call.candidates |
ICE candidates | { "candidates": [...] } |
┌─────────────────────────────────────────────────────┐
│ Zustand Stores │
│ │
│ matrix-store │ Auth, rooms, messages, CRUD │
│ call-store │ Call status, streams, remote user │
│ theme-store │ Accent color, custom color │
│ ui-store │ Sidebar, modals, UI state │
│ assistant-store │ Bot config, AI model settings │
│ sticker-store │ Sticker packs, favorites │
└─────────────────────────────────────────────────────┘
app/layout.tsx
├── AccentProvider (dynamic CSS variables)
├── PwaRegister (service worker)
└── app/chat/layout.tsx
├── Sidebar (desktop)
│ ├── Tab: Hội thoại (conversations)
│ ├── Tab: Nhóm (channels + groups)
│ ├── Tab: Danh bạ (contacts + message requests)
│ └── Tab: Bot (AI assistants)
├── MobileBottomBar
├── CallOverlay
│ ├── AudioVisualizer (Web Audio API)
│ ├── DeviceSelector (mic/speaker)
│ └── IncomingCallScreen (voice/video differentiated)
└── app/chat/[roomId]/page.tsx
├── ChatHeader
├── MessageList
│ ├── TextMessage
│ ├── ImageMessage
│ ├── StickerMessage
│ ├── FileMessage
│ └── EmojiReactions
└── MessageInput
├── StickerPicker
├── FileUpload
└── TypingIndicator
// frontend/lib/config.ts
// Hỗ trợ 4 platforms qua runtime detection:
// 1. Web (default) — same-origin inference
// 2. Desktop (Tauri) — window.__TAURI__
// 3. Mobile (Capacitor) — window.Capacitor
// 4. PWA — display-mode: standalone
getConfig() → {
matrixBaseUrl: string, // Dendrite URL
authBaseUrl: string, // Auth service URL
platform: 'web' | 'desktop' | 'mobile' | 'pwa',
isDev: boolean
}┌──────────┐ ┌──────────┐
│ Caller A │ │ Callee B │
│ │ │ │
│ 1. getUserMedia() │ │
│ 2. createPeerConnection() │ │
│ 3. createOffer() │ │
│ 4. setLocalDescription() │ │
│ 5. Send m.call.invite ──────────────▶ │ │
│ │ 6. ◀─ setRemoteDescription()
│ │ 7. ◀─ getUserMedia()
│ │ 8. ◀─ createAnswer()
│ │ ◀─────────────── 9. Send m.call.answer
│ 10. setRemoteDescription() │ │
│ │ │ │
│ 11. Exchange m.call.candidates ◀──────▶ │ │
│ │ │ │
│ 12. ICE Connected ═══════════════════ │ │
│ Audio/Video P2P Stream │ │
└──────────┘ └──────────┘
Vấn đề phổ biến: audio không phát dù stream có data. PieChat giải quyết bằng dual approach:
// Method 1: <audio> element (standard)
remoteAudioRef.current.srcObject = remoteStream;
remoteAudioRef.current.play();
// Method 2: AudioContext → destination (fallback)
const ctx = new AudioContext();
const source = ctx.createMediaStreamSource(remoteStream);
source.connect(ctx.destination); // Direct to speakersSử dụng Web Audio API AnalyserNode để đo volume realtime:
const analyser = audioCtx.createAnalyser();
analyser.fftSize = 256;
source.connect(analyser); // don't connect to destination (no echo)
// Read frequency data → calculate RMS → render bars
const dataArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(dataArray);
const rms = Math.sqrt(sum / dataArray.length);
const level = Math.min(rms / 100, 1); // 0-1Bot giao tiếp trực tiếp với Dendrite qua Matrix API chuẩn — giống hệt user thông thường.
┌─────────┐ ┌──────────────┐ ┌──────────┐
│ PieChat │────▶│ Dendrite │◀────│ Your Bot │
│ Users │ │ (Matrix API) │ │ (SDK) │
└─────────┘ └──────────────┘ └──────────┘
Bước 1: Tạo bot user
# Trên server Dendrite
create-account -config dendrite.yaml -username mybot -password BotPass123Bước 2: Login bot
curl -X POST https://piechart.site/_matrix/client/v3/login \
-H 'Content-Type: application/json' \
-d '{
"type": "m.login.password",
"identifier": {"type": "m.id.user", "user": "mybot"},
"password": "BotPass123"
}'
# → { "access_token": "syt_xxx", "user_id": "@mybot:piechart.site" }Bước 3: Bot code (Node.js)
import { MatrixClient, SimpleFsStorageProvider, AutojoinRoomsMixin } from "matrix-bot-sdk";
const client = new MatrixClient(
"https://piechart.site",
"syt_xxx" // access_token from login
);
AutojoinRoomsMixin.setupOnClient(client);
client.on("room.message", (roomId, event) => {
if (event.sender === client.getUserId()) return; // ignore own messages
const body = event.content?.body;
if (body?.startsWith("!help")) {
client.sendText(roomId, "Available commands: !help, !weather, !translate");
}
});
await client.start();
console.log("Bot started!");Bước 4: Bot code (Python)
import asyncio
from nio import AsyncClient, MatrixRoom, RoomMessageText
client = AsyncClient("https://piechart.site", "@mybot:piechart.site")
async def message_callback(room: MatrixRoom, event: RoomMessageText):
if event.sender == client.user_id:
return
if event.body.startswith("!ping"):
await client.room_send(
room.room_id,
"m.room.message",
{"msgtype": "m.text", "body": "pong! 🏓"}
)
client.add_event_callback(message_callback, RoomMessageText)
async def main():
await client.login("BotPass123")
await client.sync_forever(timeout=30000)
asyncio.run(main())Tích hợp LLM agent (OpenAI, Groq, Ollama) với khả năng sử dụng tools.
┌─────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐
│ PieChat │────▶│ Dendrite │◀────│ Bot │────▶│ LLM API │
│ User │ │ │ │ Gateway │ │ (GPT/ │
└─────────┘ └──────────┘ │ │ │ Groq/ │
│ Tools: │ │ Ollama)│
│ ┌──────┐ │ └─────────┘
│ │Search│ │
│ │Web │ │
│ │Code │ │
│ │DB │ │
│ └──────┘ │
└──────────┘
Architecture:
// bot-gateway.ts
class AgentBot {
private matrixClient: MatrixClient;
private llm: LLMProvider; // OpenAI, Groq, etc.
private tools: Tool[]; // Functions the bot can call
async handleMessage(roomId: string, message: string) {
// 1. Send typing indicator
await this.matrixClient.sendTyping(roomId, true);
// 2. Build context from room history
const history = await this.matrixClient.getRoomMessages(roomId, 20);
// 3. Call LLM with tools
const response = await this.llm.chat({
messages: [...history, { role: 'user', content: message }],
tools: this.tools,
});
// 4. Execute tool calls if any
if (response.tool_calls) {
for (const call of response.tool_calls) {
const result = await this.executeTool(call);
// Continue conversation with tool result
}
}
// 5. Send response
await this.matrixClient.sendTyping(roomId, false);
await this.matrixClient.sendText(roomId, response.content);
}
}Frontend đã có Assistant system sẵn:
// frontend/lib/store/assistant-store.ts
interface AssistantConfig {
id: string;
name: string; // "Code Helper"
description: string; // "Hỗ trợ viết code"
model: string; // "gpt-4o"
provider: string; // "openai" | "groq" | "ollama"
apiKey: string;
baseUrl?: string; // Custom endpoint
systemPrompt: string; // Bot personality
temperature: number;
}Tích hợp chat widget vào website khác:
<!-- Embed PieChat Widget -->
<script>
window.__PIECHAT_CONFIG__ = {
matrixBaseUrl: 'https://piechart.site',
authBaseUrl: 'https://piechart.site',
};
</script>
<script src="https://piechart.site/widget.js"></script>
<div id="piechat-widget" data-room="!roomId:piechart.site"></div>Gửi tin nhắn từ hệ thống bên ngoài:
import requests
MATRIX_URL = "https://piechart.site"
BOT_TOKEN = "syt_xxx" # Bot access token
# Send message to room
def send_message(room_id: str, message: str):
txn_id = f"txn_{int(time.time() * 1000)}"
response = requests.put(
f"{MATRIX_URL}/_matrix/client/v3/rooms/{room_id}/send/m.room.message/{txn_id}",
headers={"Authorization": f"Bearer {BOT_TOKEN}"},
json={"msgtype": "m.text", "body": message}
)
return response.json()
# Create notification channel
def create_notification_room(user_id: str, room_name: str):
response = requests.post(
f"{MATRIX_URL}/_matrix/client/v3/createRoom",
headers={"Authorization": f"Bearer {BOT_TOKEN}"},
json={
"name": room_name,
"invite": [user_id],
"is_direct": True,
"preset": "trusted_private_chat"
}
)
return response.json()["room_id"]
# Listen for messages (webhook-style)
def poll_messages(since_token: str = ""):
response = requests.get(
f"{MATRIX_URL}/_matrix/client/v3/sync",
headers={"Authorization": f"Bearer {BOT_TOKEN}"},
params={"since": since_token, "timeout": 30000}
)
data = response.json()
for room_id, room_data in data.get("rooms", {}).get("join", {}).items():
for event in room_data.get("timeline", {}).get("events", []):
if event["type"] == "m.room.message":
yield {
"room_id": room_id,
"sender": event["sender"],
"body": event["content"].get("body", ""),
"timestamp": event["origin_server_ts"]
}
return data.get("next_batch", "")Gửi webhook khi có tin nhắn mới:
// Bot lắng nghe tin nhắn → forward tới webhook URL
client.on("room.message", async (roomId, event) => {
await fetch("https://your-app.com/webhook/piechat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
event: "message",
room_id: roomId,
sender: event.sender,
content: event.content.body,
timestamp: event.origin_server_ts,
})
});
});┌─────────────────────────────────────┐
│ @piechat/sdk │
│ │
│ class PieChatClient { │
│ login(phone, password) │
│ sendMessage(roomId, text) │
│ onMessage(callback) │
│ getRooms() │
│ createRoom(name, members) │
│ uploadFile(roomId, file) │
│ startCall(roomId, type) │
│ } │
│ │
│ // Usage: │
│ const chat = new PieChatClient({ │
│ baseUrl: 'https://piechart.site' │
│ }); │
│ await chat.login('0901234567', │
│ 'password'); │
│ chat.onMessage((msg) => { │
│ console.log(msg.body); │
│ }); │
└─────────────────────────────────────┘
Oracle Cloud Free Tier:
├── 4 ARM cores, 24GB RAM
├── Docker Compose (all services)
├── Nginx + Let's Encrypt SSL
└── SQLite (built-in Dendrite)
Phase 1: Vertical Scaling
├── Upgrade to 8 cores, 32GB RAM
├── Switch SQLite → PostgreSQL
└── Add PgBouncer (connection pooling)
Phase 2: Service Separation
├── Dendrite on dedicated server
├── Frontend + Auth on CDN edge
├── PostgreSQL on managed DB (RDS)
└── Media storage → S3/MinIO
Phase 3: Horizontal Scaling (50k+)
├── Multiple Dendrite instances (federation)
├── Redis for session/cache
├── CDN for media delivery
└── Load balancer (HAProxy/Nginx)
# Dendrite config optimizations
global:
max_open_conns: 100
cache:
max_size_estimated: 1gb
max_age: 1h
jetstream:
max_outstanding_messages: 8192
sync_api:
search:
enabled: true
index_path: /data/searchindex| Threat | Mitigation |
|---|---|
| Brute-force password | Rate limiting (5 fails → 15min block) |
| OTP spam | Rate limiting (3 OTP/5min) |
| Token theft | Short-lived sessions, device management |
| XSS | React auto-escaping, CSP headers |
| CSRF | SameSite cookies, token-based auth |
| Man-in-the-middle | TLS 1.3 everywhere |
| Unauthorized room access | Matrix power levels, server ACLs |
- HTTPS everywhere (Let's Encrypt)
- OTP 2FA for new devices
- Rate limiting on auth endpoints
- Login audit trail
- Device trust management
- Content Security Policy
- End-to-end encryption (E2EE) — planned
- Message retention policies — planned
- IP-based geo blocking — planned
Client ─── HTTPS/TLS 1.3 ──▶ Nginx ─── HTTP ──▶ Dendrite
│
└──── HTTP ──▶ Auth Service
Lưu ý: Giao tiếp giữa Nginx ↔ Dendrite/Auth là HTTP nội bộ Docker network (không expose ra internet). Chỉ Nginx expose port 80/443.
| Variable | Required | Default | Mô tả |
|---|---|---|---|
DOMAIN |
✅ | - | Domain name |
MATRIX_SERVER_NAME |
✅ | - | Matrix server identifier |
SSL_EMAIL |
✅ | - | Let's Encrypt email |
REGISTRATION_SHARED_SECRET |
✅ | - | Dendrite admin secret |
CORS_ORIGINS |
❌ | https://${DOMAIN} |
Allowed CORS origins |
DEV_MATRIX_PASSWORD |
❌ | Pass@12345 |
Dev password bypass |
SMS_PROVIDER |
❌ | webhook |
SMS provider type |
SMS_WEBHOOK_URL |
❌ | - | SMS gateway endpoint |
SMS_BRANDNAME |
❌ | PieChat |
SMS sender name |
# Docker
docker compose up -d --build # Start all
docker compose logs -f frontend # Tail frontend logs
docker compose restart dendrite # Restart Dendrite
# Dendrite admin
docker exec -it piechat-dendrite /usr/bin/create-account \
-config /etc/dendrite/dendrite.yaml \
-username newuser -password MyPass123
# SSL renewal
docker compose exec certbot certbot renew
# Database backup
docker exec piechat-dendrite tar czf /tmp/backup.tar.gz /data/
docker cp piechat-dendrite:/tmp/backup.tar.gz ./backup-$(date +%Y%m%d).tar.gzPieChat Technical Documentation v1.1 — Last updated: March 2026
Widget messages cho phép gửi giao diện tương tác (chart, table, code, form...) dưới dạng tin nhắn chat. Widget chạy trong một iframe sandbox cô lập hoàn toàn — không truy cập được DOM cha, cookie, hay localStorage.
Dán vào ô nhập chat với tiền tố //widget: theo sau là JSON:
//widget:{"type":"chart","title":"Doanh số Q1","chartType":"bar","labels":["T1","T2","T3","T4"],"datasets":[{"label":"Revenue","data":[12,18,15,22]}]}
Hệ thống sẽ:
- Nhận ra tiền tố → xóa text, hiện banner tím "🧩 Widget: [tên]"
- Khi nhấn ⚡ hoặc Enter → gọi
onSendWidget→matrixService.sendWidgetMessage() - Widget được serialize thành JSON string và lưu vào Matrix event field
io.piechat.widget - Khi render, parse string →
buildWidgetSrcdoc()→<iframe srcdoc=...>
| Type | Trường bắt buộc | Trường tùy chọn |
|---|---|---|
chart |
labels[], datasets[{label,data[]}] |
chartType (bar/line/area/pie/doughnut), title, showLegend |
table |
columns[], rows[][] |
title, sortable, striped |
code |
code |
language, title |
progress |
value |
max, label, color, showPercent, title |
custom |
html |
css, script, title, height, interactive |
Trường chung (áp dụng cho mọi type):
| Trường | Kiểu | Mô tả | Ví dụ |
|---|---|---|---|
width |
number hoặc string |
Chiều rộng widget. Số → px, chuỗi → CSS value | 500, "80%", "50%" |
height |
number |
Chiều cao widget (px) | 200 |
title |
string |
Tiêu đề hiển thị trên header | "Doanh thu Q1" |
Lưu ý: Nếu payload chứa field
htmlhoặcscriptthì hệ thống dùng trực tiếp (complete payload). Nếu không, SDK tự build từ shorthand.
Bar Chart:
//widget:{"type":"chart","title":"Doanh thu","chartType":"bar","labels":["T1","T2","T3","T4"],"datasets":[{"label":"2025","data":[12,18,15,22]}]}
Table:
//widget:{"type":"table","title":"Đơn hàng","columns":["Mã","Khách hàng","Tổng"],"rows":[["#001","Nguyễn A","5,000,000đ"],["#002","Trần B","3,200,000đ"]]}
Code:
//widget:{"type":"code","title":"Hello Python","language":"python","code":"print('Hello, PieChat!')"}
Progress:
//widget:{"type":"progress","title":"Tiến độ dự án","value":72,"max":100}
Chart với width 50%:
//widget:{"type":"chart","title":"50% Wide","chartType":"bar","width":"50%","labels":["A","B","C"],"datasets":[{"label":"Q1","data":[10,20,15]}]}
Progress với width 300px:
//widget:{"type":"progress","title":"Sprint 7","width":300,"value":87,"max":100}
Custom (Complete Payload — đã có html/script):
//widget:{"type":"custom","title":"Dashboard","height":180,"html":"<div style='padding:16px'><h2>Hello!</h2></div>","css":"body{font-family:sans-serif}","script":"setTimeout(()=>window.parent.postMessage({type:'piechat-widget-resize',height:document.body.scrollHeight},'*'),100)"}
Bot hoặc backend có thể gửi widget dùng sendWidgetMessage:
import { matrixService } from '@/lib/services/matrix-service';
import { createChartWidget } from '@/lib/widget-sdk';
const widget = createChartWidget({
type: 'bar',
title: 'Sales Q1',
labels: ['Jan', 'Feb', 'Mar'],
datasets: [{ label: 'Revenue', data: [100, 150, 120] }],
});
await matrixService.sendWidgetMessage(roomId, '📊 Sales Q1', widget);Hoặc dùng Matrix API trực tiếp:
curl -X PUT \
'https://piechart.site/_matrix/client/v3/rooms/{roomId}/send/m.room.message/{txnId}' \
-H 'Authorization: Bearer {access_token}' \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "m.text",
"body": "📊 My Chart",
"io.piechat.widget": "{\"type\":\"chart\",\"title\":\"Sales\",\"html\":\"...\",\"script\":\"...\"}"
}'
⚠️ Quan trọng:io.piechat.widgetphải là JSON string (không phải nested object) để tránh lỗiM_BAD_JSONtừ Dendrite/gomatrixserverlib.
Widget trong iframe giao tiếp với app cha qua window.parent.postMessage:
| Message type | Hướng | Payload | Tác dụng |
|---|---|---|---|
piechat-widget-resize |
iframe → parent | { height: number } |
Điều chỉnh chiều cao iframe |
piechat-widget-action |
iframe → parent | { action, data } |
Hành động (vd: copy code) |
// Trong script của widget:
window.parent.postMessage({
type: 'piechat-widget-resize',
height: document.body.scrollHeight
}, '*');
// Copy button:
window.parent.postMessage({
type: 'piechat-widget-action',
action: 'copy',
data: 'text to copy'
}, '*');default-src 'none';
script-src 'unsafe-inline';
style-src 'unsafe-inline';
img-src data: blob:;
- Không truy cập được external URLs, APIs, hoặc network
- Không truy cập được localStorage, cookies, parent DOM
- Chỉ có thể dùng
postMessageđể giao tiếp
Menu ⋮ bên cạnh tin nhắn (hover desktop) hoặc long-press sheet (mobile) hiển thị tùy chọn sao chép theo ngữ cảnh:
// Số điện thoại VN
/(?:\+84|0)[3-9]\d{8}\b/g
// Số tài khoản ngân hàng (9-19 chữ số, khi không có SĐT)
/\b\d{9,19}\b/g
// OTP code (4-8 chữ số, không trùng trong SĐT)
/\b\d{4,8}\b/g
// URL
/https?:\/\/[^\s,'\'"<>]+/g| Điều kiện | Label | Icon |
|---|---|---|
| Text bình thường (m.text) | Sao chép văn bản | Copy |
| Có SĐT VN | Sao chép số điện thoại | Phone (xanh lá) |
| Có OTP 4-8 digit | Sao chép mã OTP | KeyRound (cam) |
| Có số TK 9-19 digit (không có SĐT) | Sao chép số tài khoản | CreditCard (tím) |
| Có URL | Sao chép liên kết | Link2 (xanh dương) |
| Widget message | Sao chép mã script | Code2 (vàng) |
- Detection:
frontend/components/chat/message-bubble.tsx—extractPhones(),extractBankAccounts(),extractOtps(),extractUrls() - UI: Desktop 3-dot menu + mobile long-press bottom sheet
- Feedback: Toast hiển thị
✓ Đã sao chép [loại]trong 1.8 giây
Leader/Deputy của Channel có thể gửi thông báo đến tất cả nhóm trong kênh cùng lúc:
await matrixService.broadcastToChannel(channelId, message, groupRoomIds);
// Returns: number of groups successfully sent toUI: Nút 📣 trong header chat → Modal nhập nội dung → Gửi đến N nhóm.