From aa05f7a37a2af5bd5ca104c7227761966fc1a3b5 Mon Sep 17 00:00:00 2001 From: Amogh M Aradhya Date: Tue, 31 Jan 2023 13:33:33 +0530 Subject: [PATCH] added whatsapp transport --- funnel/models/notification.py | 2 +- funnel/transports/base.py | 2 + funnel/transports/whatsapp.py | 110 +++++++++++++++++++++++++++++++++- funnel/views/notification.py | 28 ++++++++- 4 files changed, 139 insertions(+), 3 deletions(-) diff --git a/funnel/models/notification.py b/funnel/models/notification.py index 55984e113..3352492a7 100644 --- a/funnel/models/notification.py +++ b/funnel/models/notification.py @@ -1303,7 +1303,7 @@ def main_notification_preferences(self) -> NotificationPreferences: by_sms=True, by_webpush=False, by_telegram=False, - by_whatsapp=False, + by_whatsapp=True, ) db.session.add(main) return main diff --git a/funnel/transports/base.py b/funnel/transports/base.py index 335c271be..fc933b493 100644 --- a/funnel/transports/base.py +++ b/funnel/transports/base.py @@ -24,5 +24,7 @@ def init(): platform_transports['email'] = True if sms_init(): platform_transports['sms'] = True + if app.config.get('WHATSAPP_TOKEN'): + platform_transports['whatsapp'] = True # Other transports are not supported yet diff --git a/funnel/transports/whatsapp.py b/funnel/transports/whatsapp.py index 03f963f21..2113f4397 100644 --- a/funnel/transports/whatsapp.py +++ b/funnel/transports/whatsapp.py @@ -1,3 +1,111 @@ -"""Support functions for sending a WhatsApp message. Forthcoming.""" +"""Support functions for sending an Whatsapp messages.""" from __future__ import annotations + +from typing import Union + +from models import PhoneNumber, PhoneNumberBlockedError +import phonenumbers +import requests + +from baseframe import _ + +from .. import app +from .exc import ( + TransportConnectionError, + TransportRecipientError, + TransportTransactionError, +) + +__all__ = ['send_wa_via_meta', 'send_wa_via_on_premise'] + + +def get_phone_number( + phone: Union[str, phonenumbers.PhoneNumber, PhoneNumber] +) -> PhoneNumber: + if isinstance(phone, PhoneNumber): + if not phone.number: + raise TransportRecipientError(_("This phone number is not available")) + return phone + try: + phone_number = PhoneNumber.add(phone) + except PhoneNumberBlockedError as exc: + raise TransportRecipientError(_("This phone number has been blocked")) from exc + if not phone_number.allow_whatsapp: + raise TransportRecipientError(_("Whatsapp is disabled for this phone number")) + if not phone_number.number: + # This should never happen as :meth:`PhoneNumber.add` will restore the number + raise TransportRecipientError(_("This phone number is not available")) + return phone_number + + +def send_wa_via_meta(phone: str, message, callback: bool = True) -> str: + """ + Send the Whatsapp message using Meta Cloud API. + + :param phone: Phone number + :param message: Message to deliver to phone number + :param callback: Whether to request a status callback + :return: Transaction id + """ + phone_number = get_phone_number(phone) + sid = app.config['WHATSAPP_PHONE_ID'] + token = app.config['WHATSAPP_TOKEN'] + payload = { + "messaging_product": "whatsapp", + "recipient_type": "individual", + 'to': phone_number.number, + "type": "template", + 'body': str(message), + 'DltEntityId': message.registered_entityid, + } + try: + r = requests.post( + f'https://graph.facebook.com/v15.0/{sid}/messages', + timeout=30, + auth=(token), + data=payload, + ) + if r.status_code == 200: + jsonresponse = r.json() + transactionid = jsonresponse['messages'].get('id') + return transactionid + raise TransportTransactionError(_("Whatsapp API error"), r.status_code, r.text) + except requests.ConnectionError as exc: + raise TransportConnectionError(_("Whatsapp not reachable")) from exc + + +def send_wa_via_on_premise(phone: str, message, callback: bool = True) -> str: + """ + Send the Whatsapp message using Meta Cloud API. + + :param phone: Phone number + :param message: Message to deliver to phone number + :param callback: Whether to request a status callback + :return: Transaction id + """ + phone_number = get_phone_number(phone) + sid = app.config['WHATSAPP_PHONE_ID'] + token = app.config['WHATSAPP_TOKEN'] + payload = { + "messaging_product": "whatsapp", + "recipient_type": "individual", + 'to': phone_number.number, + "type": "template", + 'body': str(message), + 'DltEntityId': message.registered_entityid, + } + try: + r = requests.post( + f'https://graph.facebook.com/v15.0/{sid}/messages', + timeout=30, + auth=(token), + data=payload, + ) + if r.status_code == 200: + jsonresponse = r.json() + transactionid = jsonresponse['messages'].get('id') + return transactionid + raise TransportTransactionError(_("Whatsapp API error"), r.status_code, r.text) + except requests.ConnectionError as exc: + raise TransportConnectionError(_("Whatsapp not reachable")) from exc diff --git a/funnel/views/notification.py b/funnel/views/notification.py index 4e7b4dfdb..c8e1047ea 100644 --- a/funnel/views/notification.py +++ b/funnel/views/notification.py @@ -546,8 +546,34 @@ def dispatch_transport_sms(user_notification, view): ) +@rqjob +@transport_worker_wrapper +def dispatch_transport_whatsapp(user_notification, view): + if not user_notification.user.main_notification_preferences.by_transport( + 'whatsapp' + ): + # Cancel delivery if user's main switch is off. This was already checked, but + # the worker may be delayed and the user may have changed their preference. + user_notification.messageid_whatsapp = 'cancelled' + return + user_notification.messageid_whatsapp = sms.send( + str(view.transport_for('sms')), view.sms_with_unsubscribe() + ) + statsd.incr( + 'notification.transport', + tags={ + 'notification_type': user_notification.notification_type, + 'transport': 'whatsapp', + }, + ) + + # Add transport workers here as their worker methods are written -transport_workers = {'email': dispatch_transport_email, 'sms': dispatch_transport_sms} +transport_workers = { + 'email': dispatch_transport_email, + 'sms': dispatch_transport_sms, + 'whatsapp': dispatch_transport_whatsapp, +} # --- Notification background workers --------------------------------------------------