"""WhatsApp service via Baileys Bridge (self-hosted, free). Simple REST bridge that wraps WhatsApp Web via Baileys. Supports per-tenant configuration via bridge_url parameter. """ import requests from config import WHATSAPP_BRIDGE_URL HEADERS = {'Content-Type': 'application/json'} def _get_url(bridge_url=None): return bridge_url or WHATSAPP_BRIDGE_URL def get_status(bridge_url=None): url = _get_url(bridge_url) try: return requests.get(f'{url}/status', timeout=5).json() except Exception as e: return {'state': 'error', 'error': str(e)} def get_qr(bridge_url=None): url = _get_url(bridge_url) try: return requests.get(f'{url}/qr', timeout=5).json() except Exception as e: return {'state': 'error', 'error': str(e)} def connect(bridge_url=None): url = _get_url(bridge_url) try: return requests.post(f'{url}/connect', headers=HEADERS, timeout=5).json() except Exception as e: return {'state': 'error', 'error': str(e)} def send_message(phone, text, bridge_url=None): url = _get_url(bridge_url) try: return requests.post(f'{url}/send', headers=HEADERS, json={'phone': phone, 'message': text}, timeout=15).json() except Exception as e: return {'error': str(e)} def send_quote(phone, quote_data, bridge_url=None): lines = [f"*Cotizacion #{quote_data.get('id', '')}*", ""] for item in quote_data.get('items', []): lines.append(f"- {item.get('quantity', 1)}x {item.get('name', '')} ${item.get('subtotal', 0):,.2f}") lines.append(f"\nSubtotal: ${quote_data.get('subtotal', 0):,.2f}") lines.append(f"IVA: ${quote_data.get('tax_total', 0):,.2f}") lines.append(f"*Total: ${quote_data.get('total', 0):,.2f}*") return send_message(phone, "\n".join(lines), bridge_url=bridge_url) def logout(bridge_url=None): url = _get_url(bridge_url) try: return requests.post(f'{url}/logout', headers=HEADERS, timeout=5).json() except Exception as e: return {'error': str(e)} def process_incoming(webhook_data): """Extract a normalized dict from a Baileys webhook payload. Supports text messages, image messages, audio (voice notes), and video. Media content comes pre-downloaded as base64 from the bridge so Python doesn't have to re-authenticate with WhatsApp servers. Returns: dict with keys: phone — numeric phone (no JID suffix) jid — full remote JID (may be @s.whatsapp.net or @lid) text — text content (plain text or media caption) from_me — bool, True if we sent the message message_id — WhatsApp message ID media_kind — 'text' | 'image' | 'audio' | 'video' media_base64 — base64 string if media, else None media_mimetype — e.g. 'image/jpeg', 'audio/ogg' is_voice_note — True for WhatsApp voice notes (audioMessage ptt) push_name — display name from WhatsApp """ data = webhook_data.get('data', {}) key = data.get('key', {}) message = data.get('message', {}) # remoteJid can be phone@s.whatsapp.net or LID:instance@lid remote_jid = key.get('remoteJid', '') # Strip JID suffixes and LID instance suffix (:12) phone = remote_jid.split('@')[0].split(':')[0] if remote_jid else '' # DEBUG import json print(f"[WA-DEBUG] key fields: {json.dumps({k: v for k, v in key.items() if k in ('remoteJid', 'senderPn', 'fromMe', 'id')})}") # senderPn contains the real phone number when remoteJid is a privacy LID sender_pn = key.get('senderPn', '') if sender_pn: sender_pn = sender_pn.replace('@s.whatsapp.net', '') # The bridge now classifies and passes these extra fields. Fall back to # the old parsing if they're missing (older bridge version). media_kind = data.get('media_kind', 'text') media_base64 = data.get('media_base64') media_mimetype = data.get('media_mimetype') media_caption = data.get('media_caption') or '' is_voice_note = bool(data.get('media_ptt')) push_name = data.get('push_name') or '' # Text content: # - For 'text' messages → conversation or extendedTextMessage # - For 'image'/'video' → the caption (may be empty) # - For 'audio' → empty (filled in later by Whisper transcription) # - For 'location' → synthetic text with coordinates if media_kind == 'text': text = ( message.get('conversation', '') or message.get('extendedTextMessage', {}).get('text', '') or '' ) else: text = media_caption # Location fields (from bridge classification) latitude = data.get('latitude') longitude = data.get('longitude') return { 'phone': phone, 'jid': remote_jid, 'sender_pn': sender_pn, 'text': text, 'from_me': key.get('fromMe', False), 'message_id': key.get('id', ''), 'media_kind': media_kind, 'media_base64': media_base64, 'media_mimetype': media_mimetype, 'is_voice_note': is_voice_note, 'push_name': push_name, 'latitude': latitude, 'longitude': longitude, } def send_image(phone, caption, base64_image, bridge_url=None): """Send an image message via the Baileys bridge.""" url = _get_url(bridge_url) try: return requests.post( f'{url}/send-image', headers=HEADERS, json={'phone': phone, 'caption': caption, 'base64': base64_image}, timeout=15 ).json() except Exception as e: return {'error': str(e)}