-
Notifications
You must be signed in to change notification settings - Fork 27
Meta Cloud API Whatsapp Integration #2975
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
base: main
Are you sure you want to change the base?
Changes from 22 commits
226a18e
8808a3d
c2506b0
12ff881
2f6506f
45520d1
959106f
a3fb00a
9bec7c0
35f7005
58322c8
02f330e
05b60c2
ee17335
d822a95
c6f252d
86e698f
4d1aa14
2714d88
2e893fe
e09de8e
de26e8a
4a5ca39
694d11b
8fa0ace
cc7853b
75b1220
36331f2
aed1d25
33ba093
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -235,14 +235,33 @@ def clean_number(self): | |
| number_obj = phonenumbers.parse(self.cleaned_data["number"]) | ||
| number = phonenumbers.format_number(number_obj, phonenumbers.PhoneNumberFormat.E164) | ||
| service = self.messaging_provider.get_messaging_service() | ||
| if not service.is_valid_number(number): | ||
| if self.messaging_provider.type == MessagingProviderType.meta_cloud_api: | ||
| try: | ||
| phone_number_id = service.get_phone_number_id(number) | ||
| except Exception: | ||
| raise forms.ValidationError( | ||
| "Could not validate this number with Meta right now. Please try again." | ||
| ) from None | ||
| if not phone_number_id: | ||
| raise forms.ValidationError( | ||
| f"{number} was not found in the WhatsApp Business Account. " | ||
| "Please verify the number is registered with your business." | ||
| ) | ||
| self._phone_number_id = phone_number_id | ||
|
||
| elif not service.is_valid_number(number): | ||
| self.warning_message = ( | ||
| f"{number} was not found at the provider. Please make sure it is there before proceeding" | ||
| ) | ||
| return number | ||
| except phonenumbers.NumberParseException: | ||
| raise forms.ValidationError("Enter a valid phone number (e.g. +12125552368).") from None | ||
|
|
||
| def post_save(self, channel: ExperimentChannel): | ||
| super().post_save(channel) | ||
| if hasattr(self, "_phone_number_id"): | ||
| channel.extra_data["phone_number_id"] = self._phone_number_id | ||
| channel.save(update_fields=["extra_data"]) | ||
|
|
||
|
|
||
| class SureAdhereChannelForm(WebhookUrlFormBase): | ||
| sureadhere_tenant_id = forms.CharField( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import hashlib | ||
| import hmac | ||
|
|
||
| from django.http import HttpResponse, HttpResponseBadRequest | ||
| from django.utils.crypto import constant_time_compare | ||
|
|
||
| from apps.service_providers.models import MessagingProvider, MessagingProviderType | ||
|
|
||
|
|
||
| def extract_message_values(data: dict) -> list[dict]: | ||
| """Extract value dicts that contain messages from Meta webhook payload. | ||
|
|
||
| See https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/create-webhook-endpoint/ | ||
|
|
||
| See https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview for an | ||
| example of the payload | ||
| """ | ||
|
|
||
| values = [] | ||
| for entry in data.get("entry", []): | ||
| for change in entry.get("changes", []): | ||
| value = change.get("value", {}) | ||
| if value.get("messages") and value.get("metadata", {}).get("phone_number_id"): | ||
| values.append(value) | ||
| return values | ||
|
|
||
|
|
||
| def verify_webhook(request) -> HttpResponse: | ||
| """Handle the Meta webhook GET verification handshake.""" | ||
| mode = request.GET.get("hub.mode") | ||
| token = request.GET.get("hub.verify_token") | ||
| challenge = request.GET.get("hub.challenge") | ||
|
|
||
| if mode != "subscribe" or not token or not challenge: | ||
| return HttpResponseBadRequest("Verification failed.") | ||
|
|
||
| token_hash = hashlib.sha256(token.encode()).hexdigest() | ||
| exists = MessagingProvider.objects.filter( | ||
| type=MessagingProviderType.meta_cloud_api, | ||
| extra_data__verify_token_hash=token_hash, | ||
| ).exists() | ||
|
|
||
| if exists: | ||
| return HttpResponse(challenge, content_type="text/plain") | ||
|
|
||
| return HttpResponseBadRequest("Verification failed.") | ||
|
|
||
|
|
||
| def verify_signature(payload: bytes, signature_header: str, app_secret: str) -> bool: | ||
| """Verify the X-Hub-Signature-256 header from Meta webhooks.""" | ||
| if not signature_header.startswith("sha256=") or not app_secret: | ||
| return False | ||
|
|
||
| expected_signature = signature_header.removeprefix("sha256=") | ||
| computed = hmac.new( | ||
| app_secret.encode(), | ||
| payload, | ||
| hashlib.sha256, | ||
| ).hexdigest() | ||
| return constant_time_compare(computed, expected_signature) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,14 @@ | |
| from twilio.request_validator import RequestValidator | ||
|
|
||
| from apps.channels.clients.connect_client import CommCareConnectClient, Message | ||
| from apps.channels.datamodels import BaseMessage, SureAdhereMessage, TelegramMessage, TurnWhatsappMessage, TwilioMessage | ||
| from apps.channels.datamodels import ( | ||
| BaseMessage, | ||
| MetaCloudAPIMessage, | ||
| SureAdhereMessage, | ||
| TelegramMessage, | ||
| TurnWhatsappMessage, | ||
| TwilioMessage, | ||
| ) | ||
| from apps.channels.models import ChannelPlatform, ExperimentChannel | ||
| from apps.chat.channels import ( | ||
| ApiChannel, | ||
|
|
@@ -174,11 +181,27 @@ def handle_commcare_connect_message(self, experiment_id: int, participant_data_i | |
|
|
||
| def get_experiment_channel(platform, **query_kwargs): | ||
| query = get_experiment_channel_base_query(platform, **query_kwargs) | ||
| return query.select_related("experiment", "team").first() | ||
| return query.select_related("experiment", "team", "messaging_provider").first() | ||
|
|
||
|
|
||
| def get_experiment_channel_base_query(platform, **query_kwargs): | ||
| return ExperimentChannel.objects.filter( | ||
| platform=platform, | ||
| **query_kwargs, | ||
| ).filter(experiment__is_archived=False) | ||
|
|
||
|
|
||
| @shared_task(bind=True, base=TaskbadgerTask, ignore_result=True) | ||
| def handle_meta_cloud_api_message(self, phone_number_id: str, message_data: dict): | ||
| message = MetaCloudAPIMessage.parse(message_data) | ||
SmittieC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| experiment_channel = get_experiment_channel( | ||
| ChannelPlatform.WHATSAPP, | ||
| extra_data__phone_number_id=phone_number_id, | ||
| messaging_provider__type=MessagingProviderType.meta_cloud_api, | ||
| ) | ||
| if not experiment_channel: | ||
| log.info("No experiment channel found for incoming Meta Cloud API message") | ||
| return | ||
|
||
| channel = WhatsappChannel(experiment_channel.experiment.default_version, experiment_channel) | ||
| update_taskbadger_data(self, channel, message) | ||
| channel.new_user_message(message) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can't this be moved into
MessagingService.is_valid_numberThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 4a5ca39. Added
is_valid_numbertoMetaCloudAPIServicewhich wraps the phone number ID lookup. The form'scleanmethod now callsservice.is_valid_number(number)uniformly for all providers — no provider-type check needed for validation.