Skip to content

feat: add stage2 customer gateway types#35

Open
wauputr4 wants to merge 5 commits into
mainfrom
codex/stage2-gateway-expansion
Open

feat: add stage2 customer gateway types#35
wauputr4 wants to merge 5 commits into
mainfrom
codex/stage2-gateway-expansion

Conversation

@wauputr4
Copy link
Copy Markdown
Contributor

Stage 2 foundation: add Telegram and WhatsApp as supported customer gateway types with runtime builder support, broaden gateway CLI validation, and add gateway type tests. Owner gateway types remain webhook/openclaw/hermes. This keeps behavior in webhook transport compatibility for bootstrap.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for Telegram and WhatsApp customer gateways, updating the CLI, documentation, and internal gateway factory. New adapter classes were added to handle the specific payload normalization for these platforms. Review feedback identified potential runtime issues in the normalization logic, including a possible AttributeError when accessing nested Telegram chat data and an IndexError when processing WhatsApp message arrays.

or ""
)
sender_name = str(sender.get("first_name") or sender.get("username") or "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Potential AttributeError if the chat key exists in the payload but its value is explicitly None. In such cases, payload.get("chat", {}) returns None, and calling .get("id") on it will fail. Using (payload.get("chat") or {}) is a safer pattern to handle both missing keys and explicit None values.

Suggested change
external_conversation_id=str((payload.get("chat") or {}).get("id") or payload.get("conversation_id") or ""),

if isinstance(first_entry, dict):
changes = first_entry.get("changes")
if isinstance(changes, list) and changes:
raw_msg = changes[0]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Potential IndexError if value.get("messages") returns an empty list. The isinstance(..., list) check passes for empty lists, but accessing index [0] will crash. It is safer to check if the list is non-empty before accessing its elements.

                        messages = value.get("messages")
                        if isinstance(messages, list) and messages:
                            raw_msg = messages[0]

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3864e90a3b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/wasila/gateways/__init__.py Outdated
return CustomerEvent(
gateway=payload.get("gateway", self.metadata.get("id", "telegram")),
gateway_role=payload.get("gateway_role", "customer"),
external_conversation_id=str(payload.get("chat", {}).get("id") or payload.get("conversation_id") or ""),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve external_conversation_id in Telegram normalization

TelegramCustomerGateway.normalize does not read the canonical external_conversation_id field, so payloads that already follow the documented webhook event contract lose their conversation key here and often become "". In Workflow.run, an empty conversation id is replaced with external_customer_id, which collapses distinct threads for the same customer into one conversation and can corrupt ticket/message grouping for Telegram integrations that send pre-normalized webhook events.

Useful? React with 👍 / 👎.

Comment thread src/wasila/gateways/__init__.py Outdated
return CustomerEvent(
gateway=payload.get("gateway", self.metadata.get("id", "whatsapp")),
gateway_role=payload.get("gateway_role", "customer"),
external_conversation_id=str(payload.get("conversation_id") or payload.get("wa_id") or sender_id or ""),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve external_conversation_id in WhatsApp normalization

WhatsAppCustomerGateway.normalize also ignores external_conversation_id and only derives the conversation from conversation_id/wa_id/sender id. For webhook clients that already send the standard external_conversation_id, this drops the provided thread identifier and causes Workflow.run to fall back to customer id, merging separate conversations for the same WhatsApp user.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e20fb96f9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +36 to +38
sender = payload.get("from")
if not isinstance(sender, dict):
sender = payload.get("sender")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Parse Telegram sender from nested message payload

Telegram webhook updates carry sender info under message.from, but this normalization only looks for top-level from/sender, so sender_id stays empty for standard Telegram payloads unless callers prefill external_customer_id. In that case Workflow.run passes an empty external customer id into ensure_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 👍 / 👎.

Comment on lines +118 to +120
event_id = payload.get("event_id")
if event_id is not None and not isinstance(event_id, str):
event_id = str(event_id)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Populate WhatsApp event_id from nested message data

After extracting entry[].changes[].value.messages[0], the code still sets event_id only from top-level payload["event_id"] and ignores the native WhatsApp message id in the nested message object. Because _build_dedup_event_id depends on event.id, repeated inbound texts from the same sender can collapse into the same dedup key and be treated as duplicates, dropping legitimate later messages when integrations forward raw WhatsApp webhook payloads.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a420c351c1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +60 to +62
payload.get("external_conversation_id")
or (payload.get("chat") or {}).get("id")
or payload.get("conversation_id")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Extract Telegram chat ID from nested message.chat

TelegramCustomerGateway.normalize derives external_conversation_id from top-level chat/conversation_id, but Telegram webhook updates place chat identity under message.chat.id. For standard Telegram payloads this leaves external_conversation_id empty, and Workflow.run then falls back to external_customer_id, which merges different chats (e.g., separate groups/threads for the same sender) into one conversation and corrupts message/ticket grouping.

Useful? React with 👍 / 👎.

Comment on lines +42 to +44
event_id = payload.get("event_id")
if event_id is not None and not isinstance(event_id, str):
event_id = str(event_id)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use Telegram update_id when normalizing event IDs

This normalization only reads payload["event_id"], but native Telegram updates provide uniqueness via fields like update_id (and message date under message.date). As written, typical Telegram payloads produce event.id == None and empty timestamp, so repeated identical texts from the same sender collapse to the same _build_dedup_event_id and later messages can be dropped as duplicates by find_message_by_event.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant