-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add stage2 customer gateway types #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
54491da
3864e90
e9d0c8c
2e20fb9
a420c35
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,13 +2,179 @@ | |||||
|
|
||||||
| from __future__ import annotations | ||||||
|
|
||||||
| from typing import Any | ||||||
|
|
||||||
| from wasila.core.contracts import CustomerEvent | ||||||
| from wasila.core.ports import CustomerGateway, OwnerGateway | ||||||
|
|
||||||
| from wasila.gateways.webhook import WebhookCustomerGateway, WebhookOwnerGateway | ||||||
|
|
||||||
|
|
||||||
| class TelegramCustomerGateway(WebhookCustomerGateway): | ||||||
| """Customer gateway adapter for Telegram payloads. | ||||||
|
|
||||||
| Stage 2 keeps Telegram transport compatible with webhook payload ingestion. | ||||||
| """ | ||||||
|
|
||||||
| def __init__(self, metadata: dict[str, str] | None = None) -> None: | ||||||
| super().__init__(metadata=metadata) | ||||||
| self.name = "telegram" | ||||||
|
|
||||||
| def normalize(self, payload: dict) -> CustomerEvent: | ||||||
| message = payload.get("message") | ||||||
| raw_text = message if isinstance(message, str) else ( | ||||||
| message.get("text") | ||||||
| if isinstance(message, dict) | ||||||
| else "" | ||||||
| ) | ||||||
| if raw_text == "" and isinstance(message, dict): | ||||||
| raw_text = message.get("body") or message.get("caption") or "" | ||||||
| if raw_text == "": | ||||||
| raw_text = payload.get("text") or payload.get("body") or "" | ||||||
|
|
||||||
| if not isinstance(raw_text, str): | ||||||
| raw_text = "" | ||||||
|
|
||||||
| sender = payload.get("from") | ||||||
| if not isinstance(sender, dict): | ||||||
| sender = payload.get("sender") | ||||||
| if not isinstance(sender, dict) and isinstance(message, dict): | ||||||
| sender = message.get("from") | ||||||
|
|
||||||
| event_id = payload.get("event_id") | ||||||
| if event_id is not None and not isinstance(event_id, str): | ||||||
| event_id = str(event_id) | ||||||
|
Comment on lines
+42
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This normalization only reads Useful? React with 👍 / 👎. |
||||||
| sender_id = "" | ||||||
| sender_name = "" | ||||||
| if isinstance(sender, dict): | ||||||
| sender_id = str( | ||||||
| sender.get("id") | ||||||
| or sender.get("username") | ||||||
| or sender.get("first_name") | ||||||
| or "" | ||||||
| ) | ||||||
| sender_name = str(sender.get("first_name") or sender.get("username") or "") | ||||||
|
|
||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential
Suggested change
|
||||||
| return CustomerEvent( | ||||||
| gateway=payload.get("gateway", self.metadata.get("id", "telegram")), | ||||||
| gateway_role=payload.get("gateway_role", "customer"), | ||||||
| external_conversation_id=str( | ||||||
| payload.get("external_conversation_id") | ||||||
| or (payload.get("chat") or {}).get("id") | ||||||
| or payload.get("conversation_id") | ||||||
|
Comment on lines
+60
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||||||
| or "" | ||||||
| ), | ||||||
| external_customer_id=str(payload.get("external_customer_id") or payload.get("customer_id") or sender_id or ""), | ||||||
| message_text=raw_text, | ||||||
| message_timestamp=payload.get("message_timestamp") or "", | ||||||
| id=event_id, | ||||||
| customer_id=payload.get("customer_id"), | ||||||
| metadata_json={ | ||||||
| "name": sender_name or payload.get("name") or payload.get("display_name"), | ||||||
| "source": "telegram", | ||||||
| "raw": payload, | ||||||
| }, | ||||||
| ) | ||||||
|
|
||||||
|
|
||||||
| class WhatsAppCustomerGateway(WebhookCustomerGateway): | ||||||
| """Customer gateway adapter for WhatsApp payloads. | ||||||
|
|
||||||
| Stage 2 keeps WhatsApp transport compatible with webhook payload ingestion. | ||||||
| """ | ||||||
|
|
||||||
| def __init__(self, metadata: dict[str, str] | None = None) -> None: | ||||||
| super().__init__(metadata=metadata) | ||||||
| self.name = "whatsapp" | ||||||
|
|
||||||
| def normalize(self, payload: dict) -> CustomerEvent: | ||||||
| raw_text = payload.get("message") or payload.get("body") or "" | ||||||
| raw_msg: dict | None = None | ||||||
| if isinstance(payload.get("entry"), list) and payload["entry"]: | ||||||
| first_entry = payload["entry"][0] | ||||||
| if isinstance(first_entry, dict): | ||||||
| changes = first_entry.get("changes") | ||||||
| if isinstance(changes, list) and changes: | ||||||
| raw_msg = changes[0] | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential messages = value.get("messages")
if isinstance(messages, list) and messages:
raw_msg = messages[0] |
||||||
| if isinstance(raw_msg, dict): | ||||||
| value = raw_msg.get("value") | ||||||
| if isinstance(value, dict): | ||||||
| messages = value.get("messages") | ||||||
| if isinstance(messages, list) and messages: | ||||||
| raw_msg = messages[0] | ||||||
| if isinstance(raw_msg, dict): | ||||||
| text_candidate = ( | ||||||
| raw_msg.get("text") | ||||||
| if isinstance(raw_msg.get("text"), str) | ||||||
| else ( | ||||||
| raw_msg.get("text", {}).get("body") | ||||||
| if isinstance(raw_msg.get("text"), dict) | ||||||
| else None | ||||||
| ) | ||||||
| ) | ||||||
| if isinstance(text_candidate, str): | ||||||
| raw_text = text_candidate | ||||||
| else: | ||||||
| raw_text = raw_msg.get("message") or raw_msg.get("body") or raw_text | ||||||
|
|
||||||
| payload["from"] = raw_msg.get("from", first_entry.get("from")) | ||||||
|
|
||||||
| if isinstance(raw_text, dict): | ||||||
| raw_text = "" | ||||||
| if not isinstance(raw_text, str): | ||||||
| raw_text = str(raw_text) | ||||||
|
|
||||||
| event_id = payload.get("event_id") | ||||||
| if event_id is not None and not isinstance(event_id, str): | ||||||
| event_id = str(event_id) | ||||||
|
Comment on lines
+125
to
+127
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
After extracting Useful? React with 👍 / 👎. |
||||||
|
|
||||||
| sender = payload.get("from") | ||||||
| if isinstance(raw_msg, dict) and isinstance(raw_msg.get("from"), dict): | ||||||
| sender = raw_msg.get("from") | ||||||
|
|
||||||
| sender_id = "" | ||||||
| sender_name = "" | ||||||
| if isinstance(sender, dict): | ||||||
| sender_id = str( | ||||||
| sender.get("id") | ||||||
| or sender.get("wa_id") | ||||||
| or sender.get("phone") | ||||||
| or sender.get("username") | ||||||
| or "" | ||||||
| ) | ||||||
| sender_name = str(sender.get("name") or sender.get("profile_name") or sender_id) | ||||||
| else: | ||||||
| sender_id = str(sender or "") | ||||||
|
|
||||||
| if event_id is None and isinstance(raw_msg, dict): | ||||||
| msg_event_id = raw_msg.get("id") | ||||||
| if msg_event_id is not None and not isinstance(msg_event_id, str): | ||||||
| msg_event_id = str(msg_event_id) | ||||||
| if msg_event_id is not None: | ||||||
| event_id = msg_event_id | ||||||
|
|
||||||
| return CustomerEvent( | ||||||
| gateway=payload.get("gateway", self.metadata.get("id", "whatsapp")), | ||||||
| gateway_role=payload.get("gateway_role", "customer"), | ||||||
| external_conversation_id=str( | ||||||
| payload.get("external_conversation_id") | ||||||
| or (raw_msg or {}).get("conversation") | ||||||
| or payload.get("conversation_id") | ||||||
| or payload.get("wa_id") | ||||||
| or sender_id | ||||||
| or "" | ||||||
| ), | ||||||
| external_customer_id=str(payload.get("external_customer_id") or payload.get("customer_id") or sender_id or ""), | ||||||
| message_text=raw_text, | ||||||
| message_timestamp=payload.get("message_timestamp") or "", | ||||||
| id=event_id, | ||||||
| customer_id=payload.get("customer_id"), | ||||||
| metadata_json={ | ||||||
| "name": sender_name or payload.get("name") or payload.get("display_name"), | ||||||
| "source": "whatsapp", | ||||||
| "raw": payload, | ||||||
| }, | ||||||
| ) | ||||||
|
|
||||||
|
|
||||||
| class OpenClawOwnerGateway(WebhookOwnerGateway): | ||||||
| """Owner gateway adapter for OpenClaw. | ||||||
|
|
||||||
|
|
@@ -35,6 +201,10 @@ def __init__(self, metadata: dict[str, str] | None = None) -> None: | |||||
| def build_customer_gateway(gateway_type: str, metadata: dict[str, str] | None = None) -> CustomerGateway: | ||||||
| if gateway_type == "webhook": | ||||||
| return WebhookCustomerGateway(metadata=metadata) | ||||||
| if gateway_type == "telegram": | ||||||
| return TelegramCustomerGateway(metadata=metadata) | ||||||
| if gateway_type == "whatsapp": | ||||||
| return WhatsAppCustomerGateway(metadata=metadata) | ||||||
| raise ValueError(f"unsupported customer gateway type: {gateway_type}") | ||||||
|
|
||||||
|
|
||||||
|
|
@@ -52,6 +222,8 @@ def build_owner_gateway(gateway_type: str, metadata: dict[str, str] | None = Non | |||||
| __all__ = [ | ||||||
| "WebhookCustomerGateway", | ||||||
| "WebhookOwnerGateway", | ||||||
| "TelegramCustomerGateway", | ||||||
| "WhatsAppCustomerGateway", | ||||||
| "OpenClawOwnerGateway", | ||||||
| "HermesOwnerGateway", | ||||||
| "build_customer_gateway", | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Telegram webhook updates carry sender info under
message.from, but this normalization only looks for top-levelfrom/sender, sosender_idstays empty for standard Telegram payloads unless callers prefillexternal_customer_id. In that caseWorkflow.runpasses an empty external customer id intoensure_customer, which creates a new random customer record instead of reusing the prior one, breaking conversation continuity and customer history for repeated messages from the same Telegram user.Useful? React with 👍 / 👎.