Gateway › Webhook
Webhook
Luồng
Lệnh bắt đầu bằng / trong trường text; xem Lệnh trong chat.
Hai hướng riêng biệt:
- Inbound: chatbot của bạn
POST /webhooktới agent-hub (127.0.0.1:8788). - Outbound: agent-hub
POSTcallback — callback URL của bạn (đặt trên integration).
| Payload | Vai trò |
|---|---|
không có group_id | DM |
group_id | Group |
group_id + topic_id | Topic |
+ thread_id | Thread trong topic |
reply_to | Reply một tin |
Cấu hình
Trên trang Integrations của web app, New integration: Id, Name, Mode (pull, callback hoặc connector), Callback URL cho mode callback, Owner user id (optional); rồi trên thẻ Gateways bấm Enable trên dòng webhook. Credentials (secret, callback URL, owner) được agent-hub lưu lại, không phải .env.
Mọi id trong payload là chuỗi khác rỗng, không có ”:” hay khoảng trắng, và group_id không bắt đầu bằng “dm.”.
Ký số
X-Hub-Timestamp: <unix seconds>
X-Hub-Signature-256: sha256=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>
import hashlib, hmac
signature = "sha256=" + hmac.new(
secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256
).hexdigest()
| chuỗi được ký | timestamp + ”.” + đúng các byte của body. |
| 300 s | Request lệch đồng hồ quá 300 s bị từ chối → chống replay. |
| so sánh | Dùng compare_digest / timingSafeEqual. |
Inbound
SECRET="<secret của integration>"
BODY='{"event":"message","message_id":"m1","user_id":"u42","user_name":"an","group_id":"g1","topic_id":"t1","text":"/agent dev status"}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -sS -X POST http://127.0.0.1:8788/webhook \
-H "Content-Type: application/json" -H "X-Hub-Timestamp: $TS" \
-H "X-Hub-Signature-256: sha256=$SIG" -d "$BODY"
message: { event:"message", message_id, user_id, user_name, display_name?, is_bot?,
group_id?, topic_id?, thread_id?, text,
reply_to?: { text, user_id, user_name? },
attachments?: [{ id, name, mime, url }] }
button: { event:"button", callback_id, message_id, action, user_id, user_name,
group_id?, topic_id?, thread_id? }
topic_created: { event:"topic_created", group_id, topic_id, name }
member_left: { event:"member_left", group_id, user_id }
| thành viên | Tin đầu tiên của một người trong group tự đăng ký họ; member_left xoá họ. |
is_bot | Đánh dấu tin của bot — tốt nhất đừng chuyển tin bot khác. |
topic_created | Hiện scope picker một lần trong topic đó (cần owner đã gắn). |
attachments | Máy agent tự GET url (ký trên body rỗng), tối đa 20 MB. |
| text thường | Không phải lệnh và không phải reply tin agent → bị bỏ qua. |
| Status | Khi |
|---|---|
200 {"ok":true} | Đã nhận và chuyển cho lõi. |
400 | JSON sai, event lạ, trường thiếu/sai. |
401 | Chữ ký sai hoặc lệch giờ — kiểm tra trước khi parse. |
404 · 405 | Sai path · sai method. |
408 · 413 | Body quá 10 s · quá 1 MB. |
500 | Lõi lỗi; có thể đã làm một phần. |
Outbound
common: { id: uuid, type, ... }
chat: { user_id } | { group_id, topic_id?, thread_id? }
message: { type:"message", chat, text, buttons?: [{label, action}],
reply_markup: "markdown" | "plain" } → { message_id }
edit: { type:"edit", chat, message_id, text, buttons? }
ack: { type:"ack", chat, message_id, state }
typing: { type:"typing", chat }
callback_answer: { type:"callback_answer", callback_id, text?, alert }
message → message_id | Trả lời bằng id của bạn; edit và ack sau này dùng id đó. |
id | Giống nhau ở mọi lần retry: bỏ qua id đã xử lý. |
| retry | message: lỗi kết nối / 5xx được thử lại với backoff tăng dần; 4xx không retry; timeout sau khi gửi = uncertain, không retry. |
| best-effort | edit, ack, typing, callback_answer gửi một lần. |
import hashlib, hmac, http.server, json, os, time, uuid
SECRET = os.environ["HUB_SECRET"].encode()
class Receiver(http.server.BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers.get("Content-Length") or 0))
ts = self.headers.get("X-Hub-Timestamp") or ""
sent = self.headers.get("X-Hub-Signature-256") or ""
good = "sha256=" + hmac.new(SECRET, ts.encode() + b"." + body,
hashlib.sha256).hexdigest()
if (not ts.isdigit() or abs(time.time() - int(ts)) > 300
or not hmac.compare_digest(good, sent)):
self.send_response(401); self.end_headers(); return
event = json.loads(body)
reply = {"message_id": uuid.uuid4().hex} if event["type"] == "message" else {}
raw = json.dumps(reply).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(raw)))
self.end_headers(); self.wfile.write(raw)
http.server.ThreadingHTTPServer(("127.0.0.1", 9000), Receiver).serve_forever()
Receiver phải xử lý đồng thời (ThreadingHTTPServer). POST /webhook chỉ trả lời sau khi lõi xử lý xong, mà lõi có thể POST callback về receiver trong lúc đó.
Đưa ra ngoài
Giữ bind ở 127.0.0.1. Chatbot ở máy khác dùng URL tunnel (/agent web): <tunnel_url>/webhook/<intg>, hoặc reverse proxy có TLS:
agent-hub.example.com {
reverse_proxy /webhook 127.0.0.1:8788
}
Đồng bộ đồng hồ hai máy bằng NTP — lệch quá 300 s làm mọi request thành 401.