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 /webhook tới agent-hub (127.0.0.1:8788).
  • Outbound: agent-hub POST callback — callback URL của bạn (đặt trên integration).
PayloadVai trò
không có group_idDM
group_idGroup
group_id + topic_idTopic
+ thread_idThread trong topic
reply_toReply 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 sRequest lệch đồng hồ quá 300 s bị từ chối → chống replay.
so sánhDù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ênTin đầ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_createdHiện scope picker một lần trong topic đó (cần owner đã gắn).
attachmentsMáy agent tự GET url (ký trên body rỗng), tối đa 20 MB.
text thườngKhông phải lệnh và không phải reply tin agent → bị bỏ qua.
StatusKhi
200 {"ok":true}Đã nhận và chuyển cho lõi.
400JSON sai, event lạ, trường thiếu/sai.
401Chữ ký sai hoặc lệch giờ — kiểm tra trước khi parse.
404 · 405Sai path · sai method.
408 · 413Body quá 10 s · quá 1 MB.
500Lõ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_idTrả lời bằng id của bạn; edit và ack sau này dùng id đó.
idGiống nhau ở mọi lần retry: bỏ qua id đã xử lý.
retrymessage: 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-effortedit, 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.