feat(whatsapp): per-tenant WhatsApp configuration
- Refactor whatsapp_service.py to accept bridge_url parameter - whatsapp_bp.py: remove hardcoded tenant_id=11, use g.tenant_id - whatsapp_bp.py: webhook now accepts ?tenant_id param with fallback - config_bp.py: add GET/PUT /config/whatsapp endpoints - Each tenant can now have its own Baileys bridge URL and settings
This commit is contained in:
@@ -451,3 +451,129 @@ def update_vehicle_compat_source():
|
|||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'message': 'Vehicle compatibility source updated', 'source': source})
|
return jsonify({'message': 'Vehicle compatibility source updated', 'source': source})
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Allowed Part Brands ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Whitelist of part manufacturers shown in the allowed-brands selector
|
||||||
|
_ALLOWED_PART_BRANDS = [
|
||||||
|
'Luk', 'Motocraft', 'Euzcadi', 'Gates', 'Injetech', 'Bilstein',
|
||||||
|
'Monroe', 'Yokomitzu', 'Ecom', 'Lth', 'Dynamik', 'Wagner',
|
||||||
|
'Bosch', 'Brembo', 'Champion', 'Dorman', 'Kyb', 'Handkook',
|
||||||
|
'Tomco', 'Mann Filter', 'Total Parts', 'Kanadian', 'Pirelli',
|
||||||
|
'NGK', 'Moresa', 'Fritec', 'Acdelco', 'Dash4', 'Moog', 'SYD',
|
||||||
|
'FRAM', 'AUTOLITE'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@config_bp.route('/available-brands', methods=['GET'])
|
||||||
|
@require_auth()
|
||||||
|
def get_available_brands():
|
||||||
|
"""Return whitelisted aftermarket manufacturer names from master DB."""
|
||||||
|
from tenant_db import get_master_conn
|
||||||
|
conn = get_master_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
SELECT DISTINCT m.name_manufacture
|
||||||
|
FROM manufacturers m
|
||||||
|
JOIN aftermarket_parts ap ON ap.manufacturer_id = m.id_manufacture
|
||||||
|
WHERE m.name_manufacture IS NOT NULL AND m.name_manufacture != ''
|
||||||
|
AND LOWER(m.name_manufacture) = ANY(%s)
|
||||||
|
ORDER BY m.name_manufacture ASC
|
||||||
|
""", ([b.lower() for b in _ALLOWED_PART_BRANDS],))
|
||||||
|
brands = [r[0] for r in cur.fetchall()]
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'brands': brands})
|
||||||
|
|
||||||
|
|
||||||
|
@config_bp.route('/allowed-brands', methods=['GET'])
|
||||||
|
@require_auth()
|
||||||
|
def get_allowed_brands():
|
||||||
|
"""Return the tenant's allowed part brands from tenant_config."""
|
||||||
|
import json
|
||||||
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT value FROM tenant_config WHERE key = 'allowed_part_brands'")
|
||||||
|
row = cur.fetchone()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
if row and row[0]:
|
||||||
|
try:
|
||||||
|
brands = json.loads(row[0])
|
||||||
|
if isinstance(brands, list):
|
||||||
|
return jsonify({'brands': brands})
|
||||||
|
except (json.JSONDecodeError, ValueError):
|
||||||
|
pass
|
||||||
|
return jsonify({'brands': []})
|
||||||
|
|
||||||
|
|
||||||
|
@config_bp.route('/allowed-brands', methods=['PUT'])
|
||||||
|
@require_auth('config.edit')
|
||||||
|
def update_allowed_brands():
|
||||||
|
"""Save the tenant's allowed part brands to tenant_config."""
|
||||||
|
import json
|
||||||
|
data = request.get_json() or {}
|
||||||
|
brands = data.get('brands', [])
|
||||||
|
if not isinstance(brands, list):
|
||||||
|
return jsonify({'error': 'brands must be an array'}), 400
|
||||||
|
|
||||||
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO tenant_config (key, value) VALUES ('allowed_part_brands', %s)
|
||||||
|
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
|
||||||
|
""", (json.dumps(brands),))
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'message': 'Allowed brands updated', 'brands': brands})
|
||||||
|
|
||||||
|
|
||||||
|
# ─── WhatsApp Configuration ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@config_bp.route('/whatsapp', methods=['GET'])
|
||||||
|
@require_auth('config.view')
|
||||||
|
def get_whatsapp_config():
|
||||||
|
"""Get WhatsApp bridge configuration for this tenant."""
|
||||||
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT key, value FROM tenant_config WHERE key LIKE 'whatsapp_%'")
|
||||||
|
rows = {row[0]: row[1] for row in cur.fetchall()}
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'bridge_url': rows.get('whatsapp_bridge_url', ''),
|
||||||
|
'bridge_key': rows.get('whatsapp_bridge_key', ''),
|
||||||
|
'enabled': rows.get('whatsapp_enabled', 'false').lower() == 'true',
|
||||||
|
'phone_number': rows.get('whatsapp_phone_number', ''),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@config_bp.route('/whatsapp', methods=['PUT'])
|
||||||
|
@require_auth('config.edit')
|
||||||
|
def update_whatsapp_config():
|
||||||
|
"""Update WhatsApp bridge configuration for this tenant."""
|
||||||
|
data = request.get_json() or {}
|
||||||
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
'whatsapp_bridge_url': data.get('bridge_url', ''),
|
||||||
|
'whatsapp_bridge_key': data.get('bridge_key', ''),
|
||||||
|
'whatsapp_enabled': 'true' if data.get('enabled') else 'false',
|
||||||
|
'whatsapp_phone_number': data.get('phone_number', ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in settings.items():
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO tenant_config (key, value) VALUES (%s, %s)
|
||||||
|
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
|
||||||
|
""", (key, value))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify({'message': 'WhatsApp configuration updated'})
|
||||||
|
|||||||
@@ -19,6 +19,21 @@ from services import whatsapp_service
|
|||||||
whatsapp_bp = Blueprint('whatsapp', __name__, url_prefix='/pos/api/whatsapp')
|
whatsapp_bp = Blueprint('whatsapp', __name__, url_prefix='/pos/api/whatsapp')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_whatsapp_config(conn):
|
||||||
|
"""Read WhatsApp bridge configuration from tenant_config.
|
||||||
|
Returns dict with bridge_url, enabled, etc."""
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT key, value FROM tenant_config WHERE key LIKE 'whatsapp_%'")
|
||||||
|
config = {row[0]: row[1] for row in cur.fetchall()}
|
||||||
|
cur.close()
|
||||||
|
return {
|
||||||
|
'bridge_url': config.get('whatsapp_bridge_url', ''),
|
||||||
|
'bridge_key': config.get('whatsapp_bridge_key', ''),
|
||||||
|
'enabled': config.get('whatsapp_enabled', 'false').lower() == 'true',
|
||||||
|
'phone_number': config.get('whatsapp_phone_number', ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _resolve_mye_ids(vehicle, master_conn):
|
def _resolve_mye_ids(vehicle, master_conn):
|
||||||
"""Return list of MYE ids matching vehicle brand/model/year text."""
|
"""Return list of MYE ids matching vehicle brand/model/year text."""
|
||||||
if not master_conn or not vehicle:
|
if not master_conn or not vehicle:
|
||||||
@@ -271,25 +286,45 @@ def _enrich_wa_reply_with_part(search_query, vehicle, tenant_conn, master_conn=N
|
|||||||
@whatsapp_bp.route('/status', methods=['GET'])
|
@whatsapp_bp.route('/status', methods=['GET'])
|
||||||
@require_auth()
|
@require_auth()
|
||||||
def status():
|
def status():
|
||||||
return jsonify(whatsapp_service.get_status())
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
|
cfg = _get_whatsapp_config(conn)
|
||||||
|
conn.close()
|
||||||
|
if not cfg['enabled'] or not cfg['bridge_url']:
|
||||||
|
return jsonify({'state': 'disabled', 'message': 'WhatsApp not configured for this tenant'})
|
||||||
|
return jsonify(whatsapp_service.get_status(bridge_url=cfg['bridge_url']))
|
||||||
|
|
||||||
|
|
||||||
@whatsapp_bp.route('/qr', methods=['GET'])
|
@whatsapp_bp.route('/qr', methods=['GET'])
|
||||||
@require_auth()
|
@require_auth()
|
||||||
def qr():
|
def qr():
|
||||||
return jsonify(whatsapp_service.get_qr())
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
|
cfg = _get_whatsapp_config(conn)
|
||||||
|
conn.close()
|
||||||
|
if not cfg['enabled'] or not cfg['bridge_url']:
|
||||||
|
return jsonify({'state': 'disabled', 'message': 'WhatsApp not configured for this tenant'})
|
||||||
|
return jsonify(whatsapp_service.get_qr(bridge_url=cfg['bridge_url']))
|
||||||
|
|
||||||
|
|
||||||
@whatsapp_bp.route('/connect', methods=['POST'])
|
@whatsapp_bp.route('/connect', methods=['POST'])
|
||||||
@require_auth()
|
@require_auth()
|
||||||
def connect():
|
def connect():
|
||||||
return jsonify(whatsapp_service.connect())
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
|
cfg = _get_whatsapp_config(conn)
|
||||||
|
conn.close()
|
||||||
|
if not cfg['enabled'] or not cfg['bridge_url']:
|
||||||
|
return jsonify({'state': 'error', 'error': 'WhatsApp not configured for this tenant'}), 400
|
||||||
|
return jsonify(whatsapp_service.connect(bridge_url=cfg['bridge_url']))
|
||||||
|
|
||||||
|
|
||||||
@whatsapp_bp.route('/logout', methods=['POST'])
|
@whatsapp_bp.route('/logout', methods=['POST'])
|
||||||
@require_auth()
|
@require_auth()
|
||||||
def logout():
|
def logout():
|
||||||
return jsonify(whatsapp_service.logout())
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
|
cfg = _get_whatsapp_config(conn)
|
||||||
|
conn.close()
|
||||||
|
if not cfg['enabled'] or not cfg['bridge_url']:
|
||||||
|
return jsonify({'state': 'error', 'error': 'WhatsApp not configured for this tenant'}), 400
|
||||||
|
return jsonify(whatsapp_service.logout(bridge_url=cfg['bridge_url']))
|
||||||
|
|
||||||
|
|
||||||
@whatsapp_bp.route('/webhook', methods=['POST'])
|
@whatsapp_bp.route('/webhook', methods=['POST'])
|
||||||
@@ -311,16 +346,34 @@ def webhook():
|
|||||||
if not msg.get('phone') or msg.get('from_me'):
|
if not msg.get('phone') or msg.get('from_me'):
|
||||||
return jsonify({'ok': True})
|
return jsonify({'ok': True})
|
||||||
|
|
||||||
# Reuse one tenant connection for the whole webhook path — we need it
|
# Resolve tenant: try query param first, then fallback to first enabled tenant
|
||||||
# for persistence AND for the inventory-context lookup.
|
tenant_id = request.args.get('tenant_id', type=int)
|
||||||
# TODO: resolve tenant from phone number when multi-tenant WhatsApp arrives.
|
if not tenant_id:
|
||||||
tenant_id = 11
|
# Fallback: find first tenant with whatsapp enabled
|
||||||
|
try:
|
||||||
|
mconn = get_master_conn()
|
||||||
|
mcur = mconn.cursor()
|
||||||
|
mcur.execute("""
|
||||||
|
SELECT t.id FROM tenants t
|
||||||
|
JOIN tenant_config c ON c.key = 'whatsapp_enabled' AND c.value = 'true'
|
||||||
|
WHERE t.is_active = true
|
||||||
|
ORDER BY t.id LIMIT 1
|
||||||
|
""")
|
||||||
|
row = mcur.fetchone()
|
||||||
|
mcur.close()
|
||||||
|
mconn.close()
|
||||||
|
tenant_id = row[0] if row else None
|
||||||
|
except Exception:
|
||||||
|
tenant_id = None
|
||||||
|
|
||||||
tenant_conn = None
|
tenant_conn = None
|
||||||
master_conn = None
|
master_conn = None
|
||||||
inventory_context = None
|
inventory_context = None
|
||||||
|
wa_config = {}
|
||||||
try:
|
try:
|
||||||
tenant_conn = get_tenant_conn(tenant_id)
|
tenant_conn = get_tenant_conn(tenant_id)
|
||||||
master_conn = get_master_conn()
|
master_conn = get_master_conn()
|
||||||
|
wa_config = _get_whatsapp_config(tenant_conn)
|
||||||
|
|
||||||
# 1. Log the incoming message (with contact display name)
|
# 1. Log the incoming message (with contact display name)
|
||||||
cur = tenant_conn.cursor()
|
cur = tenant_conn.cursor()
|
||||||
@@ -434,7 +487,7 @@ def webhook():
|
|||||||
except Exception as del_err:
|
except Exception as del_err:
|
||||||
print(f"[WA-AI] Failed to clear conversation history: {del_err}")
|
print(f"[WA-AI] Failed to clear conversation history: {del_err}")
|
||||||
reply = '🗑️ *Conversación reiniciada.*\n\n¡Hola de nuevo! ¿En qué puedo ayudarte?'
|
reply = '🗑️ *Conversación reiniciada.*\n\n¡Hola de nuevo! ¿En qué puedo ayudarte?'
|
||||||
result = whatsapp_service.send_message(reply_to, reply)
|
result = whatsapp_service.send_message(reply_to, reply, bridge_url=wa_config.get('bridge_url'))
|
||||||
if tenant_conn:
|
if tenant_conn:
|
||||||
try:
|
try:
|
||||||
cur_save = tenant_conn.cursor()
|
cur_save = tenant_conn.cursor()
|
||||||
@@ -451,7 +504,7 @@ def webhook():
|
|||||||
if intent is not None:
|
if intent is not None:
|
||||||
# It was a quote command — send reply and skip the AI
|
# It was a quote command — send reply and skip the AI
|
||||||
if reply:
|
if reply:
|
||||||
result = whatsapp_service.send_message(reply_to, reply)
|
result = whatsapp_service.send_message(reply_to, reply, bridge_url=wa_config.get('bridge_url'))
|
||||||
if tenant_conn:
|
if tenant_conn:
|
||||||
try:
|
try:
|
||||||
cur_save = tenant_conn.cursor()
|
cur_save = tenant_conn.cursor()
|
||||||
@@ -549,7 +602,7 @@ def webhook():
|
|||||||
|
|
||||||
# Send reply if we produced one
|
# Send reply if we produced one
|
||||||
if reply:
|
if reply:
|
||||||
result = whatsapp_service.send_message(reply_to, reply)
|
result = whatsapp_service.send_message(reply_to, reply, bridge_url=wa_config.get('bridge_url'))
|
||||||
print(f"[WA-AI] Replied to {reply_to} ({media_kind}): {reply[:80]}... result={result}")
|
print(f"[WA-AI] Replied to {reply_to} ({media_kind}): {reply[:80]}... result={result}")
|
||||||
|
|
||||||
# Save the bot's reply to DB so it shows in the WhatsApp UI
|
# Save the bot's reply to DB so it shows in the WhatsApp UI
|
||||||
@@ -592,11 +645,17 @@ def send():
|
|||||||
if not phone or not message:
|
if not phone or not message:
|
||||||
return jsonify({'error': 'phone and message required'}), 400
|
return jsonify({'error': 'phone and message required'}), 400
|
||||||
|
|
||||||
result = whatsapp_service.send_message(phone, message)
|
# Load tenant WhatsApp config
|
||||||
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
|
cfg = _get_whatsapp_config(conn)
|
||||||
|
if not cfg['enabled'] or not cfg['bridge_url']:
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'error': 'WhatsApp not configured for this tenant'}), 400
|
||||||
|
|
||||||
|
result = whatsapp_service.send_message(phone, message, bridge_url=cfg['bridge_url'])
|
||||||
|
|
||||||
# Save outgoing message
|
# Save outgoing message
|
||||||
try:
|
try:
|
||||||
conn = get_tenant_conn(g.tenant_id)
|
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO whatsapp_messages (phone, direction, message_text)
|
INSERT INTO whatsapp_messages (phone, direction, message_text)
|
||||||
@@ -604,9 +663,10 @@ def send():
|
|||||||
""", (phone, message))
|
""", (phone, message))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""WhatsApp service via Baileys Bridge (self-hosted, free).
|
"""WhatsApp service via Baileys Bridge (self-hosted, free).
|
||||||
|
|
||||||
Simple REST bridge at localhost:21465 that wraps WhatsApp Web via Baileys.
|
Simple REST bridge that wraps WhatsApp Web via Baileys.
|
||||||
|
Supports per-tenant configuration via bridge_url parameter.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -9,47 +10,56 @@ from config import WHATSAPP_BRIDGE_URL
|
|||||||
HEADERS = {'Content-Type': 'application/json'}
|
HEADERS = {'Content-Type': 'application/json'}
|
||||||
|
|
||||||
|
|
||||||
def get_status():
|
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:
|
try:
|
||||||
return requests.get(f'{WHATSAPP_BRIDGE_URL}/status', timeout=5).json()
|
return requests.get(f'{url}/status', timeout=5).json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {'state': 'error', 'error': str(e)}
|
return {'state': 'error', 'error': str(e)}
|
||||||
|
|
||||||
|
|
||||||
def get_qr():
|
def get_qr(bridge_url=None):
|
||||||
|
url = _get_url(bridge_url)
|
||||||
try:
|
try:
|
||||||
return requests.get(f'{WHATSAPP_BRIDGE_URL}/qr', timeout=5).json()
|
return requests.get(f'{url}/qr', timeout=5).json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {'state': 'error', 'error': str(e)}
|
return {'state': 'error', 'error': str(e)}
|
||||||
|
|
||||||
|
|
||||||
def connect():
|
def connect(bridge_url=None):
|
||||||
|
url = _get_url(bridge_url)
|
||||||
try:
|
try:
|
||||||
return requests.post(f'{WHATSAPP_BRIDGE_URL}/connect', headers=HEADERS, timeout=5).json()
|
return requests.post(f'{url}/connect', headers=HEADERS, timeout=5).json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {'state': 'error', 'error': str(e)}
|
return {'state': 'error', 'error': str(e)}
|
||||||
|
|
||||||
|
|
||||||
def send_message(phone, text):
|
def send_message(phone, text, bridge_url=None):
|
||||||
|
url = _get_url(bridge_url)
|
||||||
try:
|
try:
|
||||||
return requests.post(f'{WHATSAPP_BRIDGE_URL}/send', headers=HEADERS, json={'phone': phone, 'message': text}, timeout=15).json()
|
return requests.post(f'{url}/send', headers=HEADERS, json={'phone': phone, 'message': text}, timeout=15).json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {'error': str(e)}
|
return {'error': str(e)}
|
||||||
|
|
||||||
|
|
||||||
def send_quote(phone, quote_data):
|
def send_quote(phone, quote_data, bridge_url=None):
|
||||||
lines = [f"*Cotizacion #{quote_data.get('id', '')}*", ""]
|
lines = [f"*Cotizacion #{quote_data.get('id', '')}*", ""]
|
||||||
for item in quote_data.get('items', []):
|
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"- {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"\nSubtotal: ${quote_data.get('subtotal', 0):,.2f}")
|
||||||
lines.append(f"IVA: ${quote_data.get('tax_total', 0):,.2f}")
|
lines.append(f"IVA: ${quote_data.get('tax_total', 0):,.2f}")
|
||||||
lines.append(f"*Total: ${quote_data.get('total', 0):,.2f}*")
|
lines.append(f"*Total: ${quote_data.get('total', 0):,.2f}*")
|
||||||
return send_message(phone, "\n".join(lines))
|
return send_message(phone, "\n".join(lines), bridge_url=bridge_url)
|
||||||
|
|
||||||
|
|
||||||
def logout():
|
def logout(bridge_url=None):
|
||||||
|
url = _get_url(bridge_url)
|
||||||
try:
|
try:
|
||||||
return requests.post(f'{WHATSAPP_BRIDGE_URL}/logout', headers=HEADERS, timeout=5).json()
|
return requests.post(f'{url}/logout', headers=HEADERS, timeout=5).json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {'error': str(e)}
|
return {'error': str(e)}
|
||||||
|
|
||||||
@@ -72,6 +82,7 @@ def process_incoming(webhook_data):
|
|||||||
media_base64 — base64 string if media, else None
|
media_base64 — base64 string if media, else None
|
||||||
media_mimetype — e.g. 'image/jpeg', 'audio/ogg'
|
media_mimetype — e.g. 'image/jpeg', 'audio/ogg'
|
||||||
is_voice_note — True for WhatsApp voice notes (audioMessage ptt)
|
is_voice_note — True for WhatsApp voice notes (audioMessage ptt)
|
||||||
|
push_name — display name from WhatsApp
|
||||||
"""
|
"""
|
||||||
data = webhook_data.get('data', {})
|
data = webhook_data.get('data', {})
|
||||||
key = data.get('key', {})
|
key = data.get('key', {})
|
||||||
|
|||||||
Reference in New Issue
Block a user