Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support messaging over WhatsApp (foundation only) #1611

Draft
wants to merge 34 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
aa05f7a
added whatsapp transport
djamg Jan 31, 2023
4d8732d
Added webhook for WhatsApp
djamg Feb 7, 2023
088d600
Merge branch 'main' into whatsapp-integration
jace Feb 7, 2023
9cd55a0
Move WhatsApp transport into its own folder
jace Feb 7, 2023
be9dbe4
Merge branch 'main' into whatsapp-integration
jace Feb 12, 2023
daf3ab2
Merge branch 'main' into whatsapp-integration
jace Mar 7, 2023
d41b5b6
Merge branch 'main' into whatsapp-integration
jace Mar 13, 2023
e6f95f1
Merge branch 'main' into whatsapp-integration
jace May 11, 2023
b026451
Merge branch 'main' into whatsapp-integration
jace Oct 30, 2023
247e54f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 30, 2023
689dc0a
Merge branch 'main' into whatsapp-integration
djamg Nov 9, 2023
6f23b08
Modified webhook
djamg Nov 10, 2023
133a947
Updated webhook
djamg Nov 10, 2023
9d533d7
Modified WhatsApp Transport
djamg Nov 10, 2023
c7bcdf3
Updated key for the secret
djamg Nov 10, 2023
e52c7cc
Added whatsapp as a medium to send OTPs
djamg Nov 10, 2023
632c8b2
Merge branch 'main' into whatsapp-integration
jace Nov 15, 2023
098e16b
Fix typing
jace Nov 15, 2023
57fbdc5
Fix spelling
jace Nov 15, 2023
c732b0f
More typing, remove callback param doc
jace Nov 15, 2023
d3c909f
Typing fixes
jace Nov 15, 2023
22115ab
Env config
jace Nov 15, 2023
356ffbb
Rename function; remove dupe method
jace Nov 15, 2023
b07333b
Fix WhatsApp notification delivery worker, add error handling to SMS …
jace Nov 15, 2023
38bef42
Use walrus operator to batch notification deliveries
jace Nov 15, 2023
d568eba
Update API event handler
jace Nov 15, 2023
22d9eb4
Additional event handler fixes, and FIXME remarks
jace Nov 15, 2023
3433782
Only mark WA availability on message delivery
jace Nov 15, 2023
9bf7daf
Merge branch 'main' into whatsapp-integration
jace Nov 20, 2023
9da920f
Updated callback handler
djamg Nov 20, 2023
37516e8
Merge branch 'main' into whatsapp-integration
jace Nov 20, 2023
0400176
Merge branch 'whatsapp-integration' of https://github.com/hasgeek/fun…
djamg Nov 20, 2023
c30a9c8
Merge branch 'main' into whatsapp-integration
jace Dec 13, 2023
f58932d
Restore unresolved FIXME markers
jace Dec 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion funnel/models/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions funnel/transports/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,7 @@ def init():
platform_transports['email'] = True
if sms_init():
platform_transports['sms'] = True
if app.config.get('WHATSAPP_TOKEN'):
jace marked this conversation as resolved.
Show resolved Hide resolved
platform_transports['whatsapp'] = True

# Other transports are not supported yet
110 changes: 109 additions & 1 deletion funnel/transports/whatsapp.py
Original file line number Diff line number Diff line change
@@ -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']
jace marked this conversation as resolved.
Show resolved Hide resolved


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:
jace marked this conversation as resolved.
Show resolved Hide resolved
raise TransportRecipientError(_("Whatsapp is disabled for this phone number"))
jace marked this conversation as resolved.
Show resolved Hide resolved
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:
jace marked this conversation as resolved.
Show resolved Hide resolved
"""
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,
jace marked this conversation as resolved.
Show resolved Hide resolved
}
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:
jace marked this conversation as resolved.
Show resolved Hide resolved
"""
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
28 changes: 27 additions & 1 deletion funnel/views/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
jace marked this conversation as resolved.
Show resolved Hide resolved
str(view.transport_for('sms')), view.sms_with_unsubscribe()
jace marked this conversation as resolved.
Show resolved Hide resolved
)
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 --------------------------------------------------

Expand Down