This is a fork of frappe/helpdesk with a WhatsApp integration added. See AGENTS.md for upstream coding conventions (semantic Tailwind tokens, Vue 3 Composition API, etc.) — follow those.
- Bench root:
/home/kushal/frappe-bench - Site (local):
dev.localhost - Site (remote):
dev.cecypo.tech - Web server: port 8002 · SocketIO: port 9002
- After Python changes: restart gunicorn (
pkill -f "frappe.app" && bench serve --port 8002 &) - After Vue/TS changes:
bench build --app helpdeskthen hard-refresh
helpdesk/integrations/wa.py— all WhatsApp API endpoints and doc_event hooks (both WABA and WA Line paths live here)helpdesk/helpdesk/doctype/whatsapp_helpdesk_settings/— singleton settings for the WABA pathhelpdesk/hooks.py—WhatsApp Messagedoc_events pointing towa.pyintegration handlers
desk/src/components/whatsapp/WhatsAppChatTab.vue— WABA tab component (rendered when the ticket has nobaileys_jid)desk/src/components/whatsapp/BaileysGroupChatTab.vue— Evolution API (WA Line) tab component (rendered when the ticket has abaileys_jid); handles both 1:1 and group chats despite the namedesk/src/components/whatsapp/WhatsAppBubble.vue— message bubble shared by both tabs (outgoing = green, incoming = surface-white)desk/src/components/whatsapp/WhatsAppReplyBox.vue— WABA reply input (file attach, paste, drag-drop)desk/src/components/whatsapp/BaileysReplyBox.vue— Evolution API reply input (file attach, paste, drag-drop, @mentions, saved replies)desk/src/components/icons/WhatsAppIcon.vue— SVG icondesk/src/components/ticket-agent/TicketActivityPanel.vue— modified: adds WhatsApp tabdesk/src/components/ticket-agent/TicketContactTab.vue— modified: shows company, designation, mobiledesk/src/components/notifications/Notifications.vue— modified: WhatsApp notification renderingdesk/src/stores/notification.ts— modified: bell reload + sound on incoming WhatsApp
All WhatsApp APIs: helpdesk.integrations.wa.<function>
git fetch upstream
git merge upstream/develop
# resolve conflicts in the modified files above
bench build --app helpdeskThis fork supports two completely separate WhatsApp integrations that share the same message bubble and backend module (wa.py) but render in separate tabs. They are distinguished by whether an HD Ticket has the custom field baileys_jid set.
| Item | Detail |
|---|---|
| Provider | Meta's official WhatsApp Business API |
| App dependency | frappe_whatsapp (must be installed) |
| Message DocType | WhatsApp Message (from frappe_whatsapp) |
| Ticket link | WhatsApp Message.reference_name → HD Ticket |
| Routing key | HD Ticket has no baileys_jid value |
| Reply function | _send_fw_reply() |
| Media function | _send_fw_reply() with media_url → sets attach field on WhatsApp Message |
| 24-hr window | Enforced — after 24 h only templates can be sent |
| Settings | WhatsApp Helpdesk Settings singleton |
| Realtime events | helpdesk:whatsapp-message, helpdesk:whatsapp-status-update |
The frappe_whatsapp controller (WhatsAppMessage.before_insert → send_outgoing) handles the actual Meta API call. When sending media, attach must be set on the WhatsApp Message doc — if it is empty for a non-text content_type, Meta rejects the request.
Naming: the provider/integration is Evolution API. "Baileys" is only the underlying protocol/library Evolution wraps — keep it out of prose, but do not rename the code identifiers that bake it in (
baileys_jid,baileys_line, thehelpdesk:baileys-*realtime events,BaileysGroupChatTab.vue,BaileysReplyBox.vue).
| Item | Detail |
|---|---|
| Provider | Self-hosted Evolution API (wraps the Baileys protocol) |
| App dependency | None — uses WA API Settings + WA Line DocTypes in helpdesk |
| Message DocType | WA Message (custom DocType in this app) |
| Ticket link | HD Ticket.baileys_jid custom field + WA Message.reference_name |
| Routing key | HD Ticket has a baileys_jid value |
| Reply function | send_wa_reply() Evolution API path |
| Media function | send_wa_media() → send_wa_reply() with media_url → WA Message.media_url |
| 24-hr window | Not enforced (no window restriction) |
| Settings | WA API Settings singleton + WA Line per-instance docs |
| Realtime events | helpdesk:baileys-message, helpdesk:baileys-status-update |
Custom fields on HD Ticket (added via fixtures):
baileys_jid— the WhatsApp JID (e.g.2547XXXXXXXX@s.whatsapp.net)baileys_line— theWA Linename that owns this conversation
Important: baileys_jid and baileys_line are custom fields. On sites that haven't run bench migrate after installing WA Line fixtures, filtering HD Ticket by baileys_jid raises an OperationalError. All such queries must be wrapped in try/except.
send_wa_reply(ticket, jid=None, ...)
├── ticket + no jid → look up HD Ticket.baileys_jid
│ ├── has baileys_jid → Evolution API (WA Line) path
│ └── no baileys_jid → _send_fw_reply() [WABA path]
└── jid provided → Evolution API (WA Line) path directly
Both integrations share the WhatsAppBubble component and the get_whatsapp_ticket_info() API (which returns via_frappe_whatsapp: True for WABA tickets so the frontend can show the 24-hr window UI), but they render in different tabs — see the component list above.
frappe-ui's preset already defines [data-theme='dark'] CSS variable overrides for all semantic tokens (--ink-gray-*, --surface-*, --outline-*). Since the app uses semantic classes almost exclusively, dark mode is ~90% free — just needs:
- A toggle that sets
document.documentElement.setAttribute("data-theme", "dark")(and removes it for light) - Persistence via
localStorage - Fix
WhatsAppBubble.vue:bg-green-100 text-green-700→ dark-aware equivalents
EmailContent.vue already reads data-theme from document.documentElement and propagates it to its iframe — the pattern is established.
| Event | Direction | Integration | Purpose |
|---|---|---|---|
helpdesk:whatsapp-message |
server → all | WABA | New incoming/outgoing frappe_whatsapp message |
helpdesk:whatsapp-status-update |
server → all | WABA | Delivery status change (sent/delivered/read) |
helpdesk:baileys-message |
server → all | WA Line | New incoming/outgoing Evolution API (WA Line) message |
helpdesk:baileys-status-update |
server → all | WA Line | WA Line delivery status change |
helpdesk:whatsapp-message-edit |
server → all | WA Line | Message text edited |
helpdesk:comment-reaction-update |
server → all | both | Bell reload |
All events are broadcast to the "all" room. Room-based routing was avoided because socket.io clients lose room membership on reconnect.
match_phone_to_contact() checks:
Contact.mobile_no/Contact.phone(normalized, digits-only comparison)Contact Phonechild table (fallback for multi-number contacts)
When creating contacts from WhatsApp, always use the phone_nos child table with is_primary_mobile_no: 1 — setting mobile_no directly is overwritten by Contact.validate().