From 14b8840247bf6933239fce7e72a2ddb5df6e6567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joseba=20Echevarr=C3=ADa=20Garc=C3=ADa?= Date: Tue, 4 Feb 2025 00:14:10 +0100 Subject: [PATCH 1/4] Added the source code for the End User Messaging REST frontend --- .../.gitignore | 155 +++++++++++++ .../README.md | 184 ++++++++++++++++ .../end_user_messaging_rest_frontend/app.py | 11 + .../end_user_messaging_rest_frontend/cdk.json | 78 +++++++ .../cdk/__init__.py | 0 .../cdk/message_api.py | 86 ++++++++ .../cdk/message_router.py | 204 ++++++++++++++++++ .../cdk/message_tracker.py | 150 +++++++++++++ .../cdk/rest_api.py | 203 +++++++++++++++++ .../docs/architecture.png | Bin 0 -> 121146 bytes .../lambda/send_sms/main.py | 69 ++++++ .../lambda/send_whatsapp/.dockerignore | 2 + .../lambda/send_whatsapp/Dockerfile | 15 ++ .../lambda/send_whatsapp/main.py | 126 +++++++++++ .../lambda/send_whatsapp/requirements.txt | 2 + .../lambda/sms_status_handler/main.py | 85 ++++++++ .../lambda/wa_status_handler/main.py | 140 ++++++++++++ .../requirements.txt | 4 + 18 files changed, 1514 insertions(+) create mode 100644 python/end_user_messaging_rest_frontend/.gitignore create mode 100644 python/end_user_messaging_rest_frontend/README.md create mode 100644 python/end_user_messaging_rest_frontend/app.py create mode 100644 python/end_user_messaging_rest_frontend/cdk.json create mode 100644 python/end_user_messaging_rest_frontend/cdk/__init__.py create mode 100644 python/end_user_messaging_rest_frontend/cdk/message_api.py create mode 100644 python/end_user_messaging_rest_frontend/cdk/message_router.py create mode 100644 python/end_user_messaging_rest_frontend/cdk/message_tracker.py create mode 100644 python/end_user_messaging_rest_frontend/cdk/rest_api.py create mode 100644 python/end_user_messaging_rest_frontend/docs/architecture.png create mode 100644 python/end_user_messaging_rest_frontend/lambda/send_sms/main.py create mode 100644 python/end_user_messaging_rest_frontend/lambda/send_whatsapp/.dockerignore create mode 100644 python/end_user_messaging_rest_frontend/lambda/send_whatsapp/Dockerfile create mode 100644 python/end_user_messaging_rest_frontend/lambda/send_whatsapp/main.py create mode 100644 python/end_user_messaging_rest_frontend/lambda/send_whatsapp/requirements.txt create mode 100644 python/end_user_messaging_rest_frontend/lambda/sms_status_handler/main.py create mode 100644 python/end_user_messaging_rest_frontend/lambda/wa_status_handler/main.py create mode 100644 python/end_user_messaging_rest_frontend/requirements.txt diff --git a/python/end_user_messaging_rest_frontend/.gitignore b/python/end_user_messaging_rest_frontend/.gitignore new file mode 100644 index 0000000000..8800910958 --- /dev/null +++ b/python/end_user_messaging_rest_frontend/.gitignore @@ -0,0 +1,155 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +.DS_Store + +# CDK +*.swp +package-lock.json +.pytest_cache +*.egg-info + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/python/end_user_messaging_rest_frontend/README.md b/python/end_user_messaging_rest_frontend/README.md new file mode 100644 index 0000000000..06be3b0fee --- /dev/null +++ b/python/end_user_messaging_rest_frontend/README.md @@ -0,0 +1,184 @@ +# Introduction + +This repository implements an Infrastructure as Code (IaC), serverless stack that exposes a REST API for sending +SMS & WhatsApp messages to your customers while handling conversation windows with WhatsApp destinations +(more on this below). + +It uses AWS End User Messaging as its communications platform and logs message history and handles +WhatsApp user consent automatically in Amazon DynamoDB. + +[TOC] + +# Requirements + +The code has been tested with Python 3.12 in macOS. You will also need: +* [AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) +* Python 3.12 +* Docker or Podman for compiling the Lambda images +* The requirements defined in [`requirements.txt`](requirements.txt) +* SMS-related requirements in AWS End User Messaging SMS: + - [Configuration set](https://docs.aws.amazon.com/sms-voice/latest/userguide/configuration-sets.html) + - [Phone number or sender ID](https://docs.aws.amazon.com/sms-voice/latest/userguide/phone-number-types.html). This is + referred to as the "originating entity" later in this document. +* WhatsApp-related requirements: + - A [WhatsApp Business Account](https://docs.aws.amazon.com/social-messaging/latest/userguide/managing-waba.html) + [configured in AWS End User Messaging Social](https://docs.aws.amazon.com/social-messaging/latest/userguide/managing-phone-numbers-add.html). + + If you only have configured a single WhatsApp phone number, the solution will use that for sending messages. + For other use cases and for efficiency purposes, you can specify the phone number to use when deploying the + solution. + + The Business Account in End User Messaging Social must be configured with a + [message and event destination](https://docs.aws.amazon.com/social-messaging/latest/userguide/managing-event-destinations.html) + pointing to an SNS topic that this solution will use for tracking SMS message delivery. + - A default WhatsApp template in English requesting that your users to connect with you. When you try to send a + WhatsApp message to a number that has not communicated with you in the last 24h, the solution will send this + template to the user and keep the message in a queue for 2h. If the customer replies to your template in this 2h + window your original message will be automatically sent to the user automatically. + +# Architecture + +The diagram below ilustrates the main components of the architecture of the solution and their dependencies. +There are two main flows of information: + +* The flow which sends the SMS/WhatsApp messages to the end users, automatically tracking user consent in a + specific DynamoDB table. +* The flow which receives the message notifications from either SNS or EventBridge and tracks their status in a + separate DynamoDB table. This flow also handles the case where the end user writes a WhatsApp message to the + phone associated with the WhatsApp Business Account and registers their consent receive free-text messages. + +![Application architecture](docs/architecture.png) + +# Sending messages + +## SMS + +Sending SMS messages is handled with AWS End User Messaging SMS and in order to be able to send SMS you will need to +register a Configuration set and a phone number or sender ID, as described above. + +The tracking of SMS delivery is performed by monitoring +[EventBridge events](https://docs.aws.amazon.com/sms-voice/latest/userguide/monitor-event-bridge.html) and follows +the principles described [below](#observability). + +## WhatsApp + +In WhatsApp you generally cannot send free-form messages to users unless they have contacted you in the previous 24 +hours. In order to contact new users, you must either have them send you a message or send them +[a Meta-approved template](https://developers.facebook.com/docs/whatsapp/message-templates/guidelines/) asking the +destination user to write back to you. When they do, you are allowed to send free-text messages to your users for the +next 24 hours. + +The communication flow for talking to your clients in WhatsApp is as follows: + +1. You send a pre-approved message template to your customer asking them to write back to you. This message should + explicitly ask the user to not answer anything if they do not want to be contacted. +2. If the user answers with any text you can start sending free-form messages for the next 24 hours. +3. After 24 hours the communication window closes and you have to send a new template to the customer in order to be + able to send free-form messages. + +This solution tracks user communications with your number by automatically sending a template as needed and only trying +to send free-form messages if the user has responded to the template. If the user does not respond 2 hours after the +template is sent to them, the initiating message is discarded. + +The following flow is executed when you send a request to the REST API endpoint to send a free-form message: + +```mermaid +flowchart TD + Client([Original free-form message]) -->|POST /v1/sendWhatsApp| API[API Gateway] + API -->|Validates & authorizes| Q["`WhatsApp Queue + (2h TTL)`"] + Q --> E{{User wrote to us in the last 24h?}} + E -->|Yes|L([Send free-form message]) + E -->|No|T{{Template sent?}} + T -->|Yes|W[Return message to queue] + T -->|No|ST[Send template] + ST -->W + W -->Q + + style E fill:#009,color:#ddd + style T fill:#009,color:#ddd +``` + +This handling is transparent to you, and you are only responsible for sending the initial request to send a free-form +message. + +Once you make the initial request to the API to send the message, you can track its status as described +[below](#observability). + +# Observability + +Metadata about the messages and whether they were delivered or not (but not the messages themselves) is stored in +a DynamoDB table for observability purposes. This data does not, however, contain destination phone numbers and has a +TTL of 1 year. These messages are identified by their AWS End User Messaging message ID and (sometimes) by their +WahtsApp message ID, but contain no other perrsonally identifiable information. + +An entry in the message tracking table will typically contain the following fields: + +* `type`: Message type (either `sms` or `whatsapp`) +* `eum_msg_id`: AWS End User Messaging message ID. This is a random unique id. +* `wa_msg_id`: Meta-provided WhatsApp Message ID. Only available for WhatsApp messages and only once + Meta server have processed the message send request. Contains `__UNKOWN__` for SMS messages or WhatsApp messages + that have not yet been processed by Meta. +* `delivery_history`: Map with the history of the ISO-formatted instants when the message transitioned states. +* `expiration_date`: The UTC timestamp when the memssage will expire. +* `latest_status`: The most recent delivery status for the message. +* `latest_update`: The UTC timestamp when the message delivery information was last updated. +* `registration_date`: The ISO-formatted instant when the message was registered. + +The status a message transverses through its lifecyle are: + +* `unknown`: Message status is unknown. Unused. +* `failed`: Message delivery has failed. Unused. +* `sent_for_delivery`: Message has been processed by this stack and sent to AWS End User Messaging for delivery. +* `sent`: Message has been sent to the user. Does not gguarantee that the user has received it. +* `delivered`: Message has been delivered to the user's terminal. Does not guarantee that the user has read it. Also, + SMS carriers might not provide us with this information so correctly delivered SMS messages might not be marked as + `delivered` in the table. +* `read`: [WhatsApp specific] The message has been shown to the user in the WhatsApp application. + +# Deployment sample + +```bash +# Run this only if using Podman instead of Docker +export CDK_DOCKER=podman +# Deploy the solution +cdk deploy \ + --parameters ConfigurationSetArn='${CONFIGURATION_SET_ARN}' \ + --parameters OriginatingEntity='${ORIGINATING_PHONE_ARN}' \ + --parameters WhatsAppNotificationTopicARN='${SNS_TOPIC_ARN}' \ + --parameters MessageType='TRANSACTIONAL' \ + --parameters WATemplate='${WHATSAPP_TEMPLATE_NAME}' \ + --parameters WAPhoneNumberARN='${WHATSAPP_PHONE_NUMBER_ARN}' +``` + +You will get several outputs if everything is correct, they're referenced in the step below as the following fields: +* `RestAPIAPIKey`: the ID of the Rest API key +* `RestAPISMSApiGateway`: the URL of the SMS send endpoint in API Gateway +* `RestAPIWhatsAppApiGateway`: the URL of the WhatsApp send endpoint in API Gateway + +# Message sending sample + +```bash +# Send a SMS message +curl -X POST -H "x-api-key: $(aws apigateway get-api-key --api-key ${RestAPIAPIKey} --include-value | jq -r .value)" -H "Content-Type: application/json" -d '{"destination_number": "${DESTINATION_NUMBER}", "message_body": "${MESSAGE_BODY}"}' ${RestAPISMSApiGateway} +# Send a WhatsApp message +curl -X POST -H "x-api-key: $(aws apigateway get-api-key --api-key ${RestAPIAPIKey} --include-value | jq -r .value)" -H "Content-Type: application/json" -d '{"destination_number": "${DESTINATION_NUMBER}", "message_body": "${MESSAGE_BODY}"}' ${RestAPIWhatsAppApiGateway} +``` + +# Future work + +More work is required to turn this code into a production sample. Some ideas for future improvement: + +* WhatsApp delivery error handling in particular should be improved. While the solution should handle 24h + WhatsApp communication windows automatically and re-sends the default template if needed, it does not + handle the case where delivery to WhatsApp phone numbers fails for whatever reason. + The logic for handling these failures can be found in the [`wa_status_handler`](lambda/wa_status_handler/main.py) + lambda code. +* Also, the WhatsApp sending logic only sends English templates. WhatsApp templates can be configured per-language, so + you will most likely want to make the template sending logic configurable per-language. + The handling code is located in the [`send_whatsapp`](lambda/send_whatsapp/main.py) lambda. +* In the WhatsApp flow, if the user answers to the template message more than 2h after the template has been sent (and + therefore the initiating free-form message has already been automatically discarded) no extra communication is sent, + which can be confusing for users. Extra work should be done to improve the UX for these cases (maybe by sending a + specific message explaining that the original message has expired?). +* The solution only supports sending basic message types. WhatsApp supports a + [wide variety of rich messages](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages). The + solution could be extended to support these different message types. diff --git a/python/end_user_messaging_rest_frontend/app.py b/python/end_user_messaging_rest_frontend/app.py new file mode 100644 index 0000000000..5ea39fc568 --- /dev/null +++ b/python/end_user_messaging_rest_frontend/app.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 + +import cdk_nag +import aws_cdk as cdk +from cdk.message_api import MessageAPI + + +app = cdk.App() +MessageAPI(app, 'MessagingRESTAPI') +cdk.Aspects.of(app).add(cdk_nag.AwsSolutionsChecks(verbose=True)) +app.synth() diff --git a/python/end_user_messaging_rest_frontend/cdk.json b/python/end_user_messaging_rest_frontend/cdk.json new file mode 100644 index 0000000000..fc14e6687a --- /dev/null +++ b/python/end_user_messaging_rest_frontend/cdk.json @@ -0,0 +1,78 @@ +{ + "app": "python app.py", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "source.bat", + "**/__init__.py", + "**/__pycache__", + "tests" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true + } +} diff --git a/python/end_user_messaging_rest_frontend/cdk/__init__.py b/python/end_user_messaging_rest_frontend/cdk/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/end_user_messaging_rest_frontend/cdk/message_api.py b/python/end_user_messaging_rest_frontend/cdk/message_api.py new file mode 100644 index 0000000000..92a27f2aa6 --- /dev/null +++ b/python/end_user_messaging_rest_frontend/cdk/message_api.py @@ -0,0 +1,86 @@ +from aws_cdk import (aws_iam as iam, + aws_sqs as sqs, + CfnParameter, + Duration, + RemovalPolicy, + Stack) +from constructs import Construct +from cdk.rest_api import RestAPI +from cdk.message_router import MessageRouter +from cdk.message_tracker import MessageTracker + + +class MessageAPI(Stack): + def __init__(self, scope: Construct, construct_id: str) -> None: + super().__init__(scope, construct_id) + + whatsapp_notification_topic_arn = CfnParameter(scope=self, + id='WhatsAppNotificationTopicARN', + type='String', + description='The name of the topic used as an event destination ' + 'in AWS End User Compute Social', + no_echo=True) + configuration_set_arn = CfnParameter(scope=self, + id='ConfigurationSetArn', + type='String', + description='ARN of the SES Configuration Set') + message_type = CfnParameter(scope=self, + id='MessageType', + type='String', + description='Message category to send', + allowed_values=['PROMOTIONAL', 'TRANSACTIONAL'], + default='TRANSACTIONAL') + origination_entity = CfnParameter(scope=self, + id='OriginatingEntity', + type='String', + description='Sender ID. Can be the phone number ID/ARN, ' + 'Sender ID/ARN or Pool ID/ARN') + default_whatsapp_phone_arn = CfnParameter(scope=self, + id='WAPhoneNumberARN', + type='String', + description='The ARN for the phone number configured in AWS End User ' + 'Messaging Social that will be used for sending WhatsApp ' + 'Messages. Leave empty to try to automatically detect ' + 'the phone number.', + default=MessageRouter.get_waba_phone_number_arn()) + default_whatsapp_template = CfnParameter(scope=self, + id='WATemplate', + type='String', + description='The name of the WhatsApp template to send if the ' + 'WhatsApp message recipient has not written to our ' + 'number in the last 24h') + + # Dead Letter Queue to be used by all other queues + dlq = sqs.Queue(scope=self, + id='DLQ', + removal_policy=RemovalPolicy.DESTROY, + retention_period=Duration.days(2)) + dlq.add_to_resource_policy(iam.PolicyStatement(effect=iam.Effect.DENY, + principals=[iam.AnyPrincipal()], + actions=['sqs:*'], + resources=[dlq.queue_arn], + conditions={'Bool': { + 'aws:SecureTransport': 'false'}})) + + # Message tracking queue, in charge of tracking message delivery status & user consent + message_tracker = MessageTracker(scope=self, + construct_id='MessageTracker', + notifications_topic_arn=whatsapp_notification_topic_arn.value_as_string, + dlq=dlq) + # Create the infrastructure used for sending the messages + message_router = MessageRouter(scope=self, + construct_id='MessageRouter', + wa_notification_handler_lambda=message_tracker.wa_status_handler_lambda, + consent_table=message_tracker.consent_table, + message_tracking_table=message_tracker.message_tracking_table, + configuration_set_arn=configuration_set_arn, + message_type=message_type, + origination_entity=origination_entity, + default_whatsapp_template=default_whatsapp_template, + default_whatsapp_phone_arn=default_whatsapp_phone_arn, + dlq=dlq) + # Rest API + rest_api = RestAPI(scope=self, + construct_id='RestAPI', + sms_queue=message_router.sms_queue, + whatsapp_queue=message_router.wa_queue) diff --git a/python/end_user_messaging_rest_frontend/cdk/message_router.py b/python/end_user_messaging_rest_frontend/cdk/message_router.py new file mode 100644 index 0000000000..be75664b45 --- /dev/null +++ b/python/end_user_messaging_rest_frontend/cdk/message_router.py @@ -0,0 +1,204 @@ +import boto3 +import platform +import cdk_nag +from aws_cdk import ( + aws_dynamodb as ddb, + aws_ecr_assets, + aws_iam as iam, + aws_lambda as lambda_, + aws_lambda_event_sources as event_sources, + aws_logs as logs, + aws_sqs as sqs, + CfnOutput, + CfnParameter, + Duration, + RemovalPolicy, + Stack +) +from constructs import Construct + + +class MessageRouter(Construct): + def __init__(self, + scope: Stack, + construct_id: str, + wa_notification_handler_lambda: lambda_.Function, + consent_table: ddb.TableV2, + message_tracking_table: ddb.TableV2, + configuration_set_arn: CfnParameter, + message_type: CfnParameter, + origination_entity: CfnParameter, + default_whatsapp_template: CfnParameter, + default_whatsapp_phone_arn: CfnParameter, + dlq: sqs.Queue) -> None: + super().__init__(scope, construct_id) + + # Create the queues where the REST API will put the requests for sending the messages + self.sms_queue = sqs.Queue(scope=self, + id='SMSMessagingQueue', + removal_policy=RemovalPolicy.DESTROY, + retention_period=Duration.hours(2), + dead_letter_queue=sqs.DeadLetterQueue(max_receive_count=2 * 3600 // 30, + queue=dlq)) + self.sms_queue.add_to_resource_policy(iam.PolicyStatement(effect=iam.Effect.DENY, + principals=[iam.AnyPrincipal()], + actions=['sqs:*'], + resources=[self.sms_queue.queue_arn], + conditions={'Bool': { + 'aws:SecureTransport': 'false'}})) + + self.wa_queue = sqs.Queue(scope=self, + id='WhatsAppMessagingQueue', + removal_policy=RemovalPolicy.DESTROY, + retention_period=Duration.hours(2), + dead_letter_queue=sqs.DeadLetterQueue(max_receive_count=2 * 3600 // 30, + queue=dlq)) + self.wa_queue.add_to_resource_policy(iam.PolicyStatement(effect=iam.Effect.DENY, + principals=[iam.AnyPrincipal()], + actions=['sqs:*'], + resources=[self.wa_queue.queue_arn], + conditions={'Bool': { + 'aws:SecureTransport': 'false'}})) + + # DynamoDB table to keep track of open communication windows + consent_table.grant_read_write_data(wa_notification_handler_lambda) + + # Create a lambda for sending the SMS & WhatsApp messages + # Determine the lambda platform architecture + if platform.machine() == 'arm64': + lambda_architecture = lambda_.Architecture.ARM_64 + lambda_platform = aws_ecr_assets.Platform.LINUX_ARM64 + else: + lambda_architecture = lambda_.Architecture.X86_64 + lambda_platform = aws_ecr_assets.Platform.LINUX_AMD64 + + base_lambda_policy = iam.ManagedPolicy.from_aws_managed_policy_name( + managed_policy_name='service-role/AWSLambdaBasicExecutionRole') + sms_sender_lambda_role = iam.Role(scope=self, + id='SQS2SMSLambdaRole', + assumed_by=iam.ServicePrincipal('lambda.amazonaws.com'), + managed_policies=[base_lambda_policy]) + sms_sender_lambda_role.add_to_policy(iam.PolicyStatement(sid='SMSMessagingStatement', + effect=iam.Effect.ALLOW, + resources=[origination_entity.value_as_string], + actions=['sms-voice:SendTextMessage'])) + self.sms_sender_lambda = lambda_.Function(scope=self, + id='SMSSender', + code=lambda_.Code.from_asset('lambda/send_sms'), + runtime=lambda_.Runtime.PYTHON_3_13, + handler='main.handler', + environment={'CONFIGURATION_SET': + configuration_set_arn.value_as_string, + 'MESSAGE_TYPE': message_type.value_as_string, + 'ORIGINATION_ENTITY': origination_entity.value_as_string, + 'MESSAGE_TRACKING_TABLE_NAME': + message_tracking_table.table_name}, + timeout=Duration.seconds(5), + memory_size=256, + role=sms_sender_lambda_role, + architecture=lambda_architecture, + log_group=logs.LogGroup(scope=self, + id='SMSSenderLogGroup', + retention=logs.RetentionDays.THREE_DAYS, + removal_policy=RemovalPolicy.DESTROY)) + self.sms_sender_lambda.add_event_source(event_sources.SqsEventSource(self.sms_queue)) + message_tracking_table.grant_read_write_data(self.sms_sender_lambda) + + # Lambda that sends the SQS messagges to WhatsApp + default_whatsapp_phone_arn = default_whatsapp_phone_arn.value_as_string + if len(default_whatsapp_phone_arn) == 0: + default_whatsapp_phone_arn = self.get_waba_phone_number_arn() + wa_sender_lambda_role = iam.Role(scope=self, + id='SQS2WhatsAppLambdaRole', + assumed_by=iam.ServicePrincipal('lambda.amazonaws.com'), + managed_policies=[base_lambda_policy]) + wa_sender_lambda_role.add_to_policy(iam.PolicyStatement(sid='SocialMessagingStatement', + effect=iam.Effect.ALLOW, + resources=[default_whatsapp_phone_arn], + actions=['social-messaging:SendWhatsAppMessage'])) + image = lambda_.DockerImageCode.from_image_asset('lambda/send_whatsapp', + platform=lambda_platform) + self.wa_sender_lambda = lambda_.DockerImageFunction(scope=self, + id='WASender', + code=image, + environment={'CONSENT_TABLE_NAME': + consent_table.table_name, + 'MESSAGE_TRACKING_TABLE_NAME': + message_tracking_table.table_name, + 'WA_TEMPLATE_NAME': + default_whatsapp_template.value_as_string, + 'WHATSAPP_PHONE_ID': + default_whatsapp_phone_arn}, + timeout=Duration.seconds(5), + memory_size=256, + architecture=lambda_architecture, + role=wa_sender_lambda_role, + log_group=logs.LogGroup(scope=self, + id='WASenderLogGroup', + retention=logs.RetentionDays.THREE_DAYS, + removal_policy=RemovalPolicy.DESTROY)) + self.wa_sender_lambda.add_environment(key='WHATSAPP_PHONE_ID', value=default_whatsapp_phone_arn) + self.wa_sender_lambda.add_event_source(event_sources.SqsEventSource(self.wa_queue)) + message_tracking_table.grant_read_write_data(self.wa_sender_lambda) + consent_table.grant_read_write_data(self.wa_sender_lambda) + + # Stack outputs + CfnOutput(self, "WABAPhoneARN", + description="WhatsApp Business Applications configured phone number ARN", + value=default_whatsapp_phone_arn) + + # Add cdk-nag suppresions for lambdas using the base lambda policy + for path in ('/MessagingRESTAPI/MessageRouter/SQS2SMSLambdaRole/Resource', + '/MessagingRESTAPI/MessageRouter/SQS2WhatsAppLambdaRole/Resource'): + cdk_nag.NagSuppressions.add_resource_suppressions_by_path(stack=scope, + path=path, + suppressions=[ + { + "id": "AwsSolutions-IAM4", + "reason": 'Using the AWS Lambda base ' + 'policy as starting point for the ' + 'Lambda roles', + } + ]) + for path in ('/MessagingRESTAPI/MessageRouter/SQS2SMSLambdaRole/DefaultPolicy/Resource', + '/MessagingRESTAPI/MessageRouter/SQS2WhatsAppLambdaRole/DefaultPolicy/Resource'): + cdk_nag.NagSuppressions.add_resource_suppressions_by_path(stack=scope, + path=path, + suppressions=[ + { + "id": "AwsSolutions-IAM5", + "reason": 'Using the AWS Lambda base ' + 'policy as starting point for the ' + 'Lambda roles, as well as policies ' + 'created automatically by CDK when ' + 'granting read-write permissions ' + 'to the DynamoDB table', + } + ]) + + @staticmethod + def get_waba_phone_number_arn() -> str: + """ + Try to automatically determine the sender phone number ARN + + The method will query the social messaging service. If there is only one account + linked with a single phone number, the code will use that. + """ + # If no phone id was provided, try to find it + whatsapp = boto3.client('socialmessaging') + try: + response = whatsapp.list_linked_whatsapp_business_accounts(maxResults=1) + if len(response['linkedAccounts']) != 1: + raise RuntimeError('Could not automatically determine the WhatsApp phone number and none was defined') + + waba_id = response['linkedAccounts'][0]['id'] + response = whatsapp.get_linked_whatsapp_business_account(id=waba_id) + if response['account']['registrationStatus'] != 'COMPLETE': + raise RuntimeError('Business account is not fully registered') + if len(response['account']['phoneNumbers']) != 1: + raise RuntimeError('Cannot determine automatically what WhatsApp phone number to use') + phone_arn = response['account']['phoneNumbers'][0]['arn'] + except whatsapp.exceptions.ClientError: + phone_arn = '' + + return phone_arn diff --git a/python/end_user_messaging_rest_frontend/cdk/message_tracker.py b/python/end_user_messaging_rest_frontend/cdk/message_tracker.py new file mode 100644 index 0000000000..489a7197ca --- /dev/null +++ b/python/end_user_messaging_rest_frontend/cdk/message_tracker.py @@ -0,0 +1,150 @@ +import platform +import cdk_nag +from aws_cdk import (aws_dynamodb as ddb, + aws_events as events, + aws_events_targets as event_targets, + aws_iam as iam, + aws_lambda as lambda_, + aws_lambda_event_sources as event_sources, + aws_sns as sns, + aws_sns_subscriptions as sns_subscriptions, + aws_logs as logs, + aws_sqs as sqs, + Duration, + RemovalPolicy, + Stack) +from constructs import Construct + + +class MessageTracker(Construct): + def __init__(self, scope: Stack, construct_id: str, notifications_topic_arn: str, dlq: sqs.Queue) -> None: + super().__init__(scope, construct_id) + + # Determine the lambda platform architecture + if platform.machine() == 'arm64': + lambda_architecture = lambda_.Architecture.ARM_64 + else: + lambda_architecture = lambda_.Architecture.X86_64 + + # WhatsApp messaging components, also create a suscription for SNS -> SQS routing + notifications_topic = sns.Topic.from_topic_arn(scope=self, + id='WhatsAppNotificationsTopic', + topic_arn=notifications_topic_arn) + wa_notifications_queue = sqs.Queue(scope=self, + id='WhatsAppNotificationQueue', + retention_period=Duration.hours(2), + dead_letter_queue=sqs.DeadLetterQueue(max_receive_count=10, + queue=dlq)) + wa_notifications_queue.add_to_resource_policy(iam.PolicyStatement(effect=iam.Effect.DENY, + principals=[iam.AnyPrincipal()], + actions=['sqs:*'], + resources=[wa_notifications_queue.queue_arn], + conditions={'Bool': { + 'aws:SecureTransport': 'false'}})) + notifications_topic.add_subscription(sns_subscriptions.SqsSubscription(queue=wa_notifications_queue, + raw_message_delivery=True)) + + # SMS messaging components, send EventBridge events to SQS + event_rule = events.Rule(scope=self, + id='SMSNotificationsRule', + enabled=True, + event_pattern={'source': ['aws.sms-voice'], + 'detail_type': ['Text Message Delivery Status Updated']}) + sms_notifications_queue = sqs.Queue(scope=self, + id='SMSNotificationQueue', + retention_period=Duration.hours(2), + dead_letter_queue=sqs.DeadLetterQueue(max_receive_count=10, + queue=dlq)) + sms_notifications_queue.add_to_resource_policy(iam.PolicyStatement(effect=iam.Effect.DENY, + principals=[iam.AnyPrincipal()], + actions=['sqs:*'], + resources=[ + sms_notifications_queue.queue_arn], + conditions={'Bool': { + 'aws:SecureTransport': 'false'}})) + event_rule.add_target(event_targets.SqsQueue(sms_notifications_queue)) + + # DynamoDB table to keep track of user consent to message them + self.consent_table = ddb.TableV2(scope=self, + id='WAConsentTable', + removal_policy=RemovalPolicy.DESTROY, + partition_key=ddb.Attribute(name='phone_id', + type=ddb.AttributeType.STRING), + time_to_live_attribute='expiration_date') + # DynamoDB table to keep track of WhatsApp mesage status + self.message_tracking_table = ddb.TableV2(scope=self, + id='MessageTrackingTable', + removal_policy=RemovalPolicy.DESTROY, + partition_key=ddb.Attribute(name='eum_msg_id', + type=ddb.AttributeType.STRING), + time_to_live_attribute='expiration_date') + self.message_tracking_table.add_global_secondary_index(index_name='WhatsAppMessageId', + partition_key=ddb.Attribute(name='wa_msg_id', + type=ddb.AttributeType.STRING), + projection_type=ddb.ProjectionType.KEYS_ONLY) + + # WhatsApp status change handling Lambda + self.wa_status_handler_lambda = lambda_.Function(scope=self, + id='WAMessageHandler', + code=lambda_.Code.from_asset('lambda/wa_status_handler', + exclude=['samples/']), + runtime=lambda_.Runtime.PYTHON_3_13, + handler='main.handler', + environment={'CONSENT_TABLE_NAME': + self.consent_table.table_name, + 'TRACKING_TABLE_NAME': + self.message_tracking_table.table_name}, + timeout=Duration.seconds(10), + memory_size=256, + architecture=lambda_architecture, + log_group=logs.LogGroup(scope=self, + id='WAMessageHandlerLogGroup', + retention=logs.RetentionDays.THREE_DAYS, + removal_policy=RemovalPolicy.DESTROY)) + self.wa_status_handler_lambda.add_event_source(event_sources.SqsEventSource(wa_notifications_queue)) + self.consent_table.grant_read_write_data(self.wa_status_handler_lambda) + self.message_tracking_table.grant_read_write_data(self.wa_status_handler_lambda) + + # SMS status change handling Lambda + self.sms_status_handler_lambda = lambda_.Function(scope=self, + id='SMSMessageHandler', + code=lambda_.Code.from_asset('lambda/sms_status_handler'), + runtime=lambda_.Runtime.PYTHON_3_13, + handler='main.handler', + environment={'TRACKING_TABLE_NAME': + self.message_tracking_table.table_name}, + timeout=Duration.seconds(10), + memory_size=256, + architecture=lambda_architecture, + log_group=logs.LogGroup(scope=self, + id='SMSMessageHandlerLogGroup', + retention=logs.RetentionDays.THREE_DAYS, + removal_policy=RemovalPolicy.DESTROY)) + self.sms_status_handler_lambda.add_event_source(event_sources.SqsEventSource(sms_notifications_queue)) + self.message_tracking_table.grant_read_write_data(self.sms_status_handler_lambda) + + # Add cdk-nag suppresions for lambdas using the base lambda policy + for path in ('/MessagingRESTAPI/MessageTracker/WAMessageHandler/ServiceRole/Resource', + '/MessagingRESTAPI/MessageTracker/SMSMessageHandler/ServiceRole/Resource'): + cdk_nag.NagSuppressions.add_resource_suppressions_by_path(stack=scope, + path=path, + suppressions=[ + { + "id": "AwsSolutions-IAM4", + "reason": 'Using the AWS Lambda base ' + 'policy as starting point for the ' + 'Lambda roles', + } + ]) + for path in ('/MessagingRESTAPI/MessageTracker/WAMessageHandler/ServiceRole/DefaultPolicy/Resource', + '/MessagingRESTAPI/MessageTracker/SMSMessageHandler/ServiceRole/DefaultPolicy/Resource'): + cdk_nag.NagSuppressions.add_resource_suppressions_by_path(stack=scope, + path=path, + suppressions=[ + { + "id": "AwsSolutions-IAM5", + "reason": 'Using the AWS Lambda base ' + 'policy as starting point for the ' + 'Lambda roles', + } + ]) diff --git a/python/end_user_messaging_rest_frontend/cdk/rest_api.py b/python/end_user_messaging_rest_frontend/cdk/rest_api.py new file mode 100644 index 0000000000..5cacd38d88 --- /dev/null +++ b/python/end_user_messaging_rest_frontend/cdk/rest_api.py @@ -0,0 +1,203 @@ +import cdk_nag +from aws_cdk import ( + aws_apigateway as apigateway, + aws_iam as iam, + aws_logs as logs, + aws_sqs as sqs, + CfnOutput, + RemovalPolicy, + Stack +) +from constructs import Construct + + +class RestAPI(Construct): + def __init__(self, + scope: Stack, + construct_id: str, + sms_queue: sqs.Queue, + whatsapp_queue: sqs.Queue) -> None: + super().__init__(scope, construct_id) + + # Create API Gateway + api_logs = logs.LogGroup(scope=self, + id='ApiGatewayLogs', + retention=logs.RetentionDays.ONE_WEEK, + removal_policy=RemovalPolicy.DESTROY) + api = apigateway.RestApi(scope=self, + id='ApiGateway', + rest_api_name="Messaging API", + deploy_options=apigateway.StageOptions( + access_log_destination=apigateway.LogGroupLogDestination(api_logs), + access_log_format=apigateway.AccessLogFormat.clf(), + logging_level=apigateway.MethodLoggingLevel.INFO), + default_cors_preflight_options=apigateway.CorsOptions( + allow_origins=['*'], + allow_methods=['POST', 'OPTIONS'], + allow_headers=['Content-Type', 'X-Amz-Date', 'Authorization', 'X-Api-Key', + 'X-Amz-Security-Token'] + ) + ) + + # Create API Gateway Role for SQS access + api_role = iam.Role(scope=self, + id='APIGatewayWhatsAppRole', + assumed_by=iam.ServicePrincipal("apigateway.amazonaws.com")) + api_role.add_to_policy(iam.PolicyStatement(actions=['sqs:SendMessage'], + resources=[sms_queue.queue_arn, + whatsapp_queue.queue_arn])) + + # Create v1 resource + v1_resource = api.root.add_resource('v1') + sms_resource = v1_resource.add_resource("sendSMS") + whatsapp_resource = v1_resource.add_resource("sendWhatsApp") + + # Create request validator + validator = api.add_request_validator(id='MessageRequestValidator', + validate_request_body=True, + validate_request_parameters=True) + + # Create model for message requests + message_model = api.add_model(id="MessageRequestModel", + content_type="application/json", + model_name="MessageRequest", + schema=apigateway.JsonSchema( + type=apigateway.JsonSchemaType.OBJECT, + required=["message_body", "destination_number"], + properties={ + "message_body": apigateway.JsonSchema( + type=apigateway.JsonSchemaType.STRING), + "destination_number": apigateway.JsonSchema( + type=apigateway.JsonSchemaType.STRING) + } + ) + ) + + # Add SMS POST method + sms_integration = apigateway.AwsIntegration( + service='sqs', + integration_http_method='POST', + path=f"{scope.account}/{sms_queue.queue_name}", + options=apigateway.IntegrationOptions( + credentials_role=api_role, + request_templates={ + "application/json": "Action=SendMessage&MessageBody=$util.urlEncode($input.body)" + }, + request_parameters={ + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'" + }, + integration_responses=[{ + "statusCode": "200", + "responseTemplates": { + "application/json": "" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'", + "method.response.header.Access-Control-Allow-Methods": "'POST,OPTIONS'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + }], + passthrough_behavior=apigateway.PassthroughBehavior.NEVER + ) + ) + + sms_resource.add_method( + "POST", + sms_integration, + api_key_required=True, + request_validator=validator, + request_models={"application/json": message_model}, + method_responses=[ + apigateway.MethodResponse( + status_code="200", + response_parameters={ + "method.response.header.Access-Control-Allow-Headers": True, + "method.response.header.Access-Control-Allow-Methods": True, + "method.response.header.Access-Control-Allow-Origin": True + } + ) + ] + ) + # Add WhatsApp POST method with similar configuration + whatsapp_integration = apigateway.AwsIntegration( + service="sqs", + integration_http_method="POST", + path=f"{scope.account}/{whatsapp_queue.queue_name}", + options=apigateway.IntegrationOptions( + credentials_role=api_role, + request_templates={ + "application/json": 'Action=SendMessage&MessageBody=$util.urlEncode($input.body)' + }, + request_parameters={ + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'" + }, + integration_responses=[{ + "statusCode": "200", + "responseTemplates": { + "application/json": "" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'", + "method.response.header.Access-Control-Allow-Methods": "'POST,OPTIONS'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + }], + passthrough_behavior=apigateway.PassthroughBehavior.NEVER + ) + ) + + whatsapp_resource.add_method( + "POST", + whatsapp_integration, + api_key_required=True, + request_validator=validator, + request_models={"application/json": message_model}, + method_responses=[ + apigateway.MethodResponse( + status_code="200", + response_parameters={ + "method.response.header.Access-Control-Allow-Headers": True, + "method.response.header.Access-Control-Allow-Methods": True, + "method.response.header.Access-Control-Allow-Origin": True + } + ) + ] + ) + + # Create Usage Plan & API Key + api_key = api.add_api_key(id='APIKey', + api_key_name='Message API Key V1', + description='CloudFormation API Key V1') + plan = api.add_usage_plan(id='APIUsagePlan', + name='SMS_WhatsApp_Plan', + description='Send SMS & Whatsapp Messages usage plan', + api_stages=[apigateway.UsagePlanPerApiStage(api=api, + stage=api.deployment_stage)]) + plan.add_api_key(api_key) + + # Add outputs + CfnOutput(self, "SMSApiGateway", + description="SMS End Point in API Gateway (POST)", + value=f"{api.url}v1/sendSMS") + CfnOutput(self, "WhatsAppApiGateway", + description="WhatsApp End Point in API Gateway (POST)", + value=f"{api.url}v1/sendWhatsApp") + CfnOutput(self, "APIKey", + description="API Key for the API Gateway", + value=api_key.key_id) + + # Finally, add cdk-nag suppresions for the POST endpoints in the API + for suppression in ('AwsSolutions-APIG4', 'AwsSolutions-COG4'): + for path in ('/MessagingRESTAPI/RestAPI/ApiGateway/Default/v1/sendSMS/POST/Resource', + '/MessagingRESTAPI/RestAPI/ApiGateway/Default/v1/sendWhatsApp/POST/Resource'): + cdk_nag.NagSuppressions.add_resource_suppressions_by_path(stack=scope, + path=path, + suppressions=[ + { + "id": suppression, + "reason": 'API authorization is used ' + 'for this backend method ' + 'where we do not leverage ' + 'the concept of users', + } + ]) diff --git a/python/end_user_messaging_rest_frontend/docs/architecture.png b/python/end_user_messaging_rest_frontend/docs/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..f8f7799cbe09fa3d868739bad118fda00420f650 GIT binary patch literal 121146 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

DjZk>_EdHPr){D2vLYi`{m*8>a*8WOPBai##`Gx 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

fDlNc!GfdMoS6PW;(ZS7=3 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 str | None: + """ + Function used for actually sending the WhatsApp mesasge with Social Messaging + """ + try: + # Convert the message to a JSON string and then to bytes (no Base64 encoding needed) + message_json = json.dumps(whatsapp_message).encode() + + # Send the WhatsApp message + response = whatsapp.send_whatsapp_message(originationPhoneNumberId=sender_id, + message=message_json, + metaApiVersion='v20.0') + return response.get('messageId') + + except botocore.exceptions.ClientError as e: + logging.exception(e) + return None + + +def generate_template(recipient_id: str, template_name: str) -> dict | None: + """ + Generate a dict payload representing a default template WhatsApp Text message + + If there is no need to send the template (because the communication window is still open) + `None` will be returned. + """ + # Send the template & update the window open time + return {"messaging_product": "whatsapp", + "to": f'{recipient_id}', + "type": "template", + "template": {"name": template_name, + "language": {"code": "es_ES"}}} + + +def generate_text(recipient_id: str, message: str) -> dict: + """ + Generate a dict payload representing a regular WhatsApp Text message + """ + whatsapp_message = {"messaging_product": "whatsapp", + "type": "text", + "preview_url": True, + "to": f"{recipient_id}", + "text": {"body": message}} + + return whatsapp_message + + +def with_recipient_consent(recipient_id: str) -> bool: + """ + Determine if we have the user's consent to send free-form messages + + We will only have this if the user has written to us in the last 24h, which triggers + a registration in the corresponding DynamoDB table + """ + response = consent_table.get_item(Key={'phone_id': recipient_id}) + return response.get('Item', {}).get('user_consents', False) + + +def consent_request_sent(recipient_id: str) -> bool: + """ + Determine if we have already sent the consent request + """ + response = consent_table.get_item(Key={'phone_id': recipient_id}) + return response.get('Item', {}).get('consent_requested', False) + + +def handler(event, _): + failed_msgs = [] + # Handle messages one by one (by the construct of the application we should only be getting one, though) + for record in event.get('Records', []): + payload = json.loads(record.get('body')) + recipient_id = payload.get('destination_number') + message = payload.get('message_body') + if recipient_id is None or message is None: + logging.warning(f'Skipping empty message in event.') + continue + + if not recipient_id.startswith('+'): + recipient_id = f'+{recipient_id}' + + # If the user has provided consent to be messaged, just send the message + now = datetime.now(tz=UTC) + if with_recipient_consent(recipient_id): + msg_id = send_message(sender_id=whatsapp_phone_id, + whatsapp_message=generate_text(recipient_id, message)) + status = 'sent_for_delivery' + ttl = int((now + timedelta(days=365)).timestamp()) + msg_tracking_table.put_item(Item={'type': 'whatsapp', + 'eum_msg_id': msg_id, + 'wa_msg_id': '__UNKNOWN__', + 'latest_status': status, + 'latest_update': int(now.timestamp()), + 'delivery_history': {now.isoformat(): 'sent_for_delivery'}, + 'expiration_date': ttl, + 'registration_date': now.isoformat()}) + else: + if not consent_request_sent(recipient_id): + send_message(sender_id=whatsapp_phone_id, + whatsapp_message=generate_template(recipient_id, template_name)) + ttl = int((now + timedelta(days=7)).timestamp()) + consent_table.put_item(Item={'phone_id': recipient_id, + 'consent_requested': True, + 'user_consents': False, + 'request_date': now.isoformat(), + 'expiration_date': ttl}) + else: + # Register the message processing as failed, so that the message + # is sent back to the queue + failed_msgs.append({'itemIdentifier': record['messageId']}) + + return {'batchItemFailures': failed_msgs} diff --git a/python/end_user_messaging_rest_frontend/lambda/send_whatsapp/requirements.txt b/python/end_user_messaging_rest_frontend/lambda/send_whatsapp/requirements.txt new file mode 100644 index 0000000000..6f1972d8d4 --- /dev/null +++ b/python/end_user_messaging_rest_frontend/lambda/send_whatsapp/requirements.txt @@ -0,0 +1,2 @@ +boto3~=1.35.54 +botocore~=1.35.75 diff --git a/python/end_user_messaging_rest_frontend/lambda/sms_status_handler/main.py b/python/end_user_messaging_rest_frontend/lambda/sms_status_handler/main.py new file mode 100644 index 0000000000..f897432a70 --- /dev/null +++ b/python/end_user_messaging_rest_frontend/lambda/sms_status_handler/main.py @@ -0,0 +1,85 @@ +import os +import json +import boto3 +import logging +import botocore.exceptions +from datetime import datetime, UTC +from boto3.dynamodb.conditions import Attr + +dynamodb = boto3.resource('dynamodb') +tracking_table = dynamodb.Table(os.environ['TRACKING_TABLE_NAME']) +msg_status_translation = {'TEXT_SUCCESSFUL': 'sent', + 'TEXT_DELIVERED': 'delivered'} +msg_status = {'unknown': -999, 'failed': -1, 'sent_for_delivery': 0, + 'sent': 1, 'delivered': 2} + + +def get_message_status(msg_id: str | None) -> dict | None: + response = tracking_table.get_item(Key={'eum_msg_id': msg_id}) + if 'Item' in response: + return response['Item'] + + return None + + +def update_message_status(msg_id: str | None, new_status: str, timestamp: datetime) -> None: + """ + Update the message history and status in the Message tracking DynamoDB table + """ + details = get_message_status(msg_id) + if details is None: + raise RuntimeError(f'Cannot find message with id {msg_id}, failing') + + # Update the record to the new status, checking consistency + for i in range(5): + latest_update = details['latest_update'] + if msg_status.get(new_status, -999) > msg_status.get(details['latest_status'], -1): + details['latest_status'] = new_status + details['latest_update'] = int(datetime.now(tz=UTC).timestamp()) + + if new_status not in details['delivery_history'].values(): + details['delivery_history'][timestamp.isoformat()] = new_status + + try: + # We can just update the existing element since we're not altering the key + tracking_table.put_item(Item=details, ConditionExpression=Attr('latest_update').eq(latest_update)) + return + except botocore.exceptions.ClientError as e: + logging.error(f'Failed to update the status of the message ({e}). This probably means that the message ' + f'status was updated separately. Retrying...') + details = get_message_status(msg_id) + + raise RuntimeError('Could not update the message status') + + +def handler(event, _): + """ + Handle a SMS status update notifications. + + Message delivery notifications are stored for traceability. We record the full history of the message for + several months. We only store metadata about the interactions, not the content of the communications or + phone numbers. + """ + batch_item_failures = [] + for record in event.get('Records', []): + # Fetch the End User Messaging-specific message ID + # We should only get it if this message is for a `accepted` status + notification = json.loads(record['body']) + details = notification.get('detail', {}) + msg_id = details.get('messageId') + if msg_id is None: + logging.error(f'Failing to parse message without message ID: {notification}') + continue + status = msg_status_translation[details['eventType']] + try: + timestamp = datetime.fromtimestamp(details['eventTimestamp'] / 1000) + except (ValueError, TypeError) as e: + logging.warning(f'Could not parse msg timestamp ({details["eventTimestamp"]}), using current time') + timestamp = datetime.now(tz=UTC) + + try: + update_message_status(msg_id, status, timestamp) + except RuntimeError: + batch_item_failures.append({"itemIdentifier": msg_id}) + + return {'batchItemFailures': batch_item_failures} diff --git a/python/end_user_messaging_rest_frontend/lambda/wa_status_handler/main.py b/python/end_user_messaging_rest_frontend/lambda/wa_status_handler/main.py new file mode 100644 index 0000000000..34bbc48006 --- /dev/null +++ b/python/end_user_messaging_rest_frontend/lambda/wa_status_handler/main.py @@ -0,0 +1,140 @@ +import os +import json +import boto3 +import logging +import botocore.exceptions +from datetime import datetime, timedelta, UTC +from boto3.dynamodb.conditions import Attr, Key + +dynamodb = boto3.resource('dynamodb') +consent_table = dynamodb.Table(os.environ['CONSENT_TABLE_NAME']) +tracking_table = dynamodb.Table(os.environ['TRACKING_TABLE_NAME']) +msg_status = {'unknown': -999, 'failed': -1, 'created': 0, 'sent_for_delivery': 1, + 'accepted': 2, 'sent': 3, 'delivered': 4, 'read': 5} + + +def get_message_status(msg_id: str | None, whatsapp_msg_id: str) -> dict | None: + # Try to get the current message ID status, in the table we + # might either the WhatsApp or the AWS msg id + response = tracking_table.query(IndexName='WhatsAppMessageId', + KeyConditionExpression=Key('wa_msg_id').eq(whatsapp_msg_id)) + if response.get('Count', 0) > 0: + msg_id = response['Items'][0]['eum_msg_id'] + + response = tracking_table.get_item(Key={'eum_msg_id': msg_id}) + if 'Item' in response: + return response['Item'] + + return None + + +def update_message_status(msg_id: str | None, whatsapp_msg_id: str, new_status: str, timestamp: datetime) -> None: + """ + Update the message history and status in the Message tracking DynamoDB table + """ + details = get_message_status(msg_id, whatsapp_msg_id) + if details is None: + raise RuntimeError(f'Cannot find message with id {whatsapp_msg_id}, failing') + if details['wa_msg_id'] == '__UNKNOWN__': + details['wa_msg_id'] = whatsapp_msg_id + + # Update the record to the new status, checking consistency + for i in range(5): + latest_update = details['latest_update'] + if msg_status.get(new_status, -999) > msg_status.get(details['latest_status'], -1): + details['latest_status'] = new_status + details['latest_update'] = int(datetime.now(tz=UTC).timestamp()) + + if new_status not in details['delivery_history'].values(): + details['delivery_history'][timestamp.isoformat()] = new_status + + try: + # We can just update the existing element since we're not altering the key + tracking_table.put_item(Item=details, ConditionExpression=Attr('latest_update').eq(latest_update)) + return + except botocore.exceptions.ClientError as e: + logging.error(f'Failed to update the status of the message ({e}). This probably means that the message ' + f'status was updated separately. Retrying...') + details = get_message_status(msg_id, whatsapp_msg_id) + + raise RuntimeError('Could not update the message status') + + +def register_consent(sender_id: str, start_time: datetime): + """ + Register the fact that the user messaged us, opening a 24h communications + window where we can send free-form messages + """ + ttl = int((start_time + timedelta(hours=23, minutes=50)).timestamp()) + consent_table.put_item(Item={'phone_id': sender_id, + 'user_consents': True, + 'expiration_date': ttl}) + + +def handler(event, _): + """ + Handle a WhatsApp WebHook event. + + WhatsApp sends these when the conversations we're part of have been updated in any way, such as: + - User has replied/reacted to a message + - Message status updates (message has been delivered/read/failed to deliver...) + + More info on how WhatsApp webhooks work at + https://developers.facebook.com/docs/whatsapp/cloud-api/guides/set-up-webhooks + + We want to react to these notifications in two ways: + - If the user has written to us, we have 24h through which we can message them with free text messages. + We track these events in the CONSENT_TABLE_NAME table, so that we know in other parts of the stack + that we can message the user normally. These entries are short-lived in the consent table and are removed + after ~24h), but do contain phone numbers. + - Message delivery notifications are stored for traceability. We record the full history of the message for + several months. We only store metadata about the interactions, not the content of the communications or + phone numbers. + - Other message types are simply ignored. + """ + batch_item_failures = [] + for record in event.get('Records', []): + # Fetch the End User Messaging-specific message ID + # We should only get it if this message is for a `accepted` status + notification = json.loads(record['body']) + msg_id = notification.get('messageId') + entry = json.loads(notification['whatsAppWebhookEntry']) + for change in entry.get('changes', []): + if 'value' not in change: + logging.debug(f'Skipping malformed message as there is no `values` key. Existing keys: {change.keys()}') + continue + if 'statuses' in change['value']: + # A message has changed its status, register it in the table + for update in change['value']['statuses']: + if 'id' not in update: + filtered_info = {k: v for k, v in update.items() if k != 'recipient_id'} + logging.error(f'Failed to parse message WhatsApp response, skipping: {filtered_info}') + continue + whatsapp_msg_id = update['id'] + status = update['status'] + try: + timestamp = datetime.fromtimestamp(int(update['timestamp'])) + except (ValueError, TypeError) as e: + logging.warning(f'Could not parse msg timestamp ({update["timestamp"]}), using current time') + timestamp = datetime.now(tz=UTC) + + try: + update_message_status(msg_id, whatsapp_msg_id, status, timestamp) + except RuntimeError: + batch_item_failures.append({"itemIdentifier": record['messageId']}) + elif 'messages' in change['value']: + # The user has written to us, thus opening a new 24h communication window + for msg in change['value']['messages']: + sender_id = msg['from'] + if not sender_id.startswith('+'): + sender_id = f'+{sender_id}' + try: + send_time = datetime.fromtimestamp(int(msg['timestamp'])) + except (ValueError, TypeError): + send_time = datetime.now(tz=UTC) + + register_consent(sender_id=sender_id, start_time=send_time) + else: + logging.info(f'Skipping unsupported message type: {change["value"].keys()}') + + return {'batchItemFailures': batch_item_failures} diff --git a/python/end_user_messaging_rest_frontend/requirements.txt b/python/end_user_messaging_rest_frontend/requirements.txt new file mode 100644 index 0000000000..f9f962098e --- /dev/null +++ b/python/end_user_messaging_rest_frontend/requirements.txt @@ -0,0 +1,4 @@ +aws-cdk-lib>=2.171.1 +boto3>=1.35.78 +cdk-nag>=2.34.21 +constructs>=10.0.0,<11.0.0 From c559b854e3406ca50c523e2cd5d8cb4b36cc0265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joseba=20Echevarr=C3=ADa=20Garc=C3=ADa?= Date: Tue, 4 Feb 2025 00:15:19 +0100 Subject: [PATCH 2/4] Renamed the sample folder --- .../.gitignore | 0 .../README.md | 0 .../app.py | 0 .../cdk.json | 0 .../cdk/__init__.py | 0 .../cdk/message_api.py | 0 .../cdk/message_router.py | 0 .../cdk/message_tracker.py | 0 .../cdk/rest_api.py | 0 .../docs/architecture.png | Bin .../lambda/send_sms/main.py | 0 .../lambda/send_whatsapp/.dockerignore | 0 .../lambda/send_whatsapp/Dockerfile | 0 .../lambda/send_whatsapp/main.py | 0 .../lambda/send_whatsapp/requirements.txt | 0 .../lambda/sms_status_handler/main.py | 0 .../lambda/wa_status_handler/main.py | 0 .../requirements.txt | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/.gitignore (100%) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/README.md (100%) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/app.py (100%) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/cdk.json (100%) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/cdk/__init__.py (100%) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/cdk/message_api.py (100%) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/cdk/message_router.py (100%) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/cdk/message_tracker.py (100%) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/cdk/rest_api.py (100%) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/docs/architecture.png (100%) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/lambda/send_sms/main.py (100%) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/lambda/send_whatsapp/.dockerignore (100%) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/lambda/send_whatsapp/Dockerfile (100%) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/lambda/send_whatsapp/main.py (100%) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/lambda/send_whatsapp/requirements.txt (100%) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/lambda/sms_status_handler/main.py (100%) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/lambda/wa_status_handler/main.py (100%) rename python/{end_user_messaging_rest_frontend => end-user-messaging-rest-frontend}/requirements.txt (100%) diff --git a/python/end_user_messaging_rest_frontend/.gitignore b/python/end-user-messaging-rest-frontend/.gitignore similarity index 100% rename from python/end_user_messaging_rest_frontend/.gitignore rename to python/end-user-messaging-rest-frontend/.gitignore diff --git a/python/end_user_messaging_rest_frontend/README.md b/python/end-user-messaging-rest-frontend/README.md similarity index 100% rename from python/end_user_messaging_rest_frontend/README.md rename to python/end-user-messaging-rest-frontend/README.md diff --git a/python/end_user_messaging_rest_frontend/app.py b/python/end-user-messaging-rest-frontend/app.py similarity index 100% rename from python/end_user_messaging_rest_frontend/app.py rename to python/end-user-messaging-rest-frontend/app.py diff --git a/python/end_user_messaging_rest_frontend/cdk.json b/python/end-user-messaging-rest-frontend/cdk.json similarity index 100% rename from python/end_user_messaging_rest_frontend/cdk.json rename to python/end-user-messaging-rest-frontend/cdk.json diff --git a/python/end_user_messaging_rest_frontend/cdk/__init__.py b/python/end-user-messaging-rest-frontend/cdk/__init__.py similarity index 100% rename from python/end_user_messaging_rest_frontend/cdk/__init__.py rename to python/end-user-messaging-rest-frontend/cdk/__init__.py diff --git a/python/end_user_messaging_rest_frontend/cdk/message_api.py b/python/end-user-messaging-rest-frontend/cdk/message_api.py similarity index 100% rename from python/end_user_messaging_rest_frontend/cdk/message_api.py rename to python/end-user-messaging-rest-frontend/cdk/message_api.py diff --git a/python/end_user_messaging_rest_frontend/cdk/message_router.py b/python/end-user-messaging-rest-frontend/cdk/message_router.py similarity index 100% rename from python/end_user_messaging_rest_frontend/cdk/message_router.py rename to python/end-user-messaging-rest-frontend/cdk/message_router.py diff --git a/python/end_user_messaging_rest_frontend/cdk/message_tracker.py b/python/end-user-messaging-rest-frontend/cdk/message_tracker.py similarity index 100% rename from python/end_user_messaging_rest_frontend/cdk/message_tracker.py rename to python/end-user-messaging-rest-frontend/cdk/message_tracker.py diff --git a/python/end_user_messaging_rest_frontend/cdk/rest_api.py b/python/end-user-messaging-rest-frontend/cdk/rest_api.py similarity index 100% rename from python/end_user_messaging_rest_frontend/cdk/rest_api.py rename to python/end-user-messaging-rest-frontend/cdk/rest_api.py diff --git a/python/end_user_messaging_rest_frontend/docs/architecture.png b/python/end-user-messaging-rest-frontend/docs/architecture.png similarity index 100% rename from python/end_user_messaging_rest_frontend/docs/architecture.png rename to python/end-user-messaging-rest-frontend/docs/architecture.png diff --git a/python/end_user_messaging_rest_frontend/lambda/send_sms/main.py b/python/end-user-messaging-rest-frontend/lambda/send_sms/main.py similarity index 100% rename from python/end_user_messaging_rest_frontend/lambda/send_sms/main.py rename to python/end-user-messaging-rest-frontend/lambda/send_sms/main.py diff --git a/python/end_user_messaging_rest_frontend/lambda/send_whatsapp/.dockerignore b/python/end-user-messaging-rest-frontend/lambda/send_whatsapp/.dockerignore similarity index 100% rename from python/end_user_messaging_rest_frontend/lambda/send_whatsapp/.dockerignore rename to python/end-user-messaging-rest-frontend/lambda/send_whatsapp/.dockerignore diff --git a/python/end_user_messaging_rest_frontend/lambda/send_whatsapp/Dockerfile b/python/end-user-messaging-rest-frontend/lambda/send_whatsapp/Dockerfile similarity index 100% rename from python/end_user_messaging_rest_frontend/lambda/send_whatsapp/Dockerfile rename to python/end-user-messaging-rest-frontend/lambda/send_whatsapp/Dockerfile diff --git a/python/end_user_messaging_rest_frontend/lambda/send_whatsapp/main.py b/python/end-user-messaging-rest-frontend/lambda/send_whatsapp/main.py similarity index 100% rename from python/end_user_messaging_rest_frontend/lambda/send_whatsapp/main.py rename to python/end-user-messaging-rest-frontend/lambda/send_whatsapp/main.py diff --git a/python/end_user_messaging_rest_frontend/lambda/send_whatsapp/requirements.txt b/python/end-user-messaging-rest-frontend/lambda/send_whatsapp/requirements.txt similarity index 100% rename from python/end_user_messaging_rest_frontend/lambda/send_whatsapp/requirements.txt rename to python/end-user-messaging-rest-frontend/lambda/send_whatsapp/requirements.txt diff --git a/python/end_user_messaging_rest_frontend/lambda/sms_status_handler/main.py b/python/end-user-messaging-rest-frontend/lambda/sms_status_handler/main.py similarity index 100% rename from python/end_user_messaging_rest_frontend/lambda/sms_status_handler/main.py rename to python/end-user-messaging-rest-frontend/lambda/sms_status_handler/main.py diff --git a/python/end_user_messaging_rest_frontend/lambda/wa_status_handler/main.py b/python/end-user-messaging-rest-frontend/lambda/wa_status_handler/main.py similarity index 100% rename from python/end_user_messaging_rest_frontend/lambda/wa_status_handler/main.py rename to python/end-user-messaging-rest-frontend/lambda/wa_status_handler/main.py diff --git a/python/end_user_messaging_rest_frontend/requirements.txt b/python/end-user-messaging-rest-frontend/requirements.txt similarity index 100% rename from python/end_user_messaging_rest_frontend/requirements.txt rename to python/end-user-messaging-rest-frontend/requirements.txt From 938ae50d6c0446d05623a7948d7c32760000724b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joseba=20Echevarr=C3=ADa=20Garc=C3=ADa?= Date: Tue, 4 Feb 2025 00:16:38 +0100 Subject: [PATCH 3/4] Minor markdown fixes here and there --- .../README.md | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/python/end-user-messaging-rest-frontend/README.md b/python/end-user-messaging-rest-frontend/README.md index 06be3b0fee..b21a2e7688 100644 --- a/python/end-user-messaging-rest-frontend/README.md +++ b/python/end-user-messaging-rest-frontend/README.md @@ -1,14 +1,12 @@ # Introduction This repository implements an Infrastructure as Code (IaC), serverless stack that exposes a REST API for sending -SMS & WhatsApp messages to your customers while handling conversation windows with WhatsApp destinations +SMS & WhatsApp messages to your customers while handling conversation windows with WhatsApp destinations (more on this below). -It uses AWS End User Messaging as its communications platform and logs message history and handles +It uses AWS End User Messaging as its communications platform and logs message history and handles WhatsApp user consent automatically in Amazon DynamoDB. -[TOC] - # Requirements The code has been tested with Python 3.12 in macOS. You will also need: @@ -20,18 +18,18 @@ The code has been tested with Python 3.12 in macOS. You will also need: - [Configuration set](https://docs.aws.amazon.com/sms-voice/latest/userguide/configuration-sets.html) - [Phone number or sender ID](https://docs.aws.amazon.com/sms-voice/latest/userguide/phone-number-types.html). This is referred to as the "originating entity" later in this document. -* WhatsApp-related requirements: - - A [WhatsApp Business Account](https://docs.aws.amazon.com/social-messaging/latest/userguide/managing-waba.html) +* WhatsApp-related requirements: + - A [WhatsApp Business Account](https://docs.aws.amazon.com/social-messaging/latest/userguide/managing-waba.html) [configured in AWS End User Messaging Social](https://docs.aws.amazon.com/social-messaging/latest/userguide/managing-phone-numbers-add.html). - + If you only have configured a single WhatsApp phone number, the solution will use that for sending messages. - For other use cases and for efficiency purposes, you can specify the phone number to use when deploying the + + If you only have configured a single WhatsApp phone number, the solution will use that for sending messages. + For other use cases and for efficiency purposes, you can specify the phone number to use when deploying the solution. - + The Business Account in End User Messaging Social must be configured with a + + The Business Account in End User Messaging Social must be configured with a [message and event destination](https://docs.aws.amazon.com/social-messaging/latest/userguide/managing-event-destinations.html) pointing to an SNS topic that this solution will use for tracking SMS message delivery. - - A default WhatsApp template in English requesting that your users to connect with you. When you try to send a - WhatsApp message to a number that has not communicated with you in the last 24h, the solution will send this - template to the user and keep the message in a queue for 2h. If the customer replies to your template in this 2h + - A default WhatsApp template in English requesting that your users to connect with you. When you try to send a + WhatsApp message to a number that has not communicated with you in the last 24h, the solution will send this + template to the user and keep the message in a queue for 2h. If the customer replies to your template in this 2h window your original message will be automatically sent to the user automatically. # Architecture @@ -60,9 +58,9 @@ the principles described [below](#observability). ## WhatsApp -In WhatsApp you generally cannot send free-form messages to users unless they have contacted you in the previous 24 -hours. In order to contact new users, you must either have them send you a message or send them -[a Meta-approved template](https://developers.facebook.com/docs/whatsapp/message-templates/guidelines/) asking the +In WhatsApp you generally cannot send free-form messages to users unless they have contacted you in the previous 24 +hours. In order to contact new users, you must either have them send you a message or send them +[a Meta-approved template](https://developers.facebook.com/docs/whatsapp/message-templates/guidelines/) asking the destination user to write back to you. When they do, you are allowed to send free-text messages to your users for the next 24 hours. @@ -72,7 +70,7 @@ The communication flow for talking to your clients in WhatsApp is as follows: explicitly ask the user to not answer anything if they do not want to be contacted. 2. If the user answers with any text you can start sending free-form messages for the next 24 hours. 3. After 24 hours the communication window closes and you have to send a new template to the customer in order to be - able to send free-form messages. + able to send free-form messages. This solution tracks user communications with your number by automatically sending a template as needed and only trying to send free-form messages if the user has responded to the template. If the user does not respond 2 hours after the @@ -92,7 +90,7 @@ flowchart TD T -->|No|ST[Send template] ST -->W W -->Q - + style E fill:#009,color:#ddd style T fill:#009,color:#ddd ``` @@ -100,7 +98,7 @@ flowchart TD This handling is transparent to you, and you are only responsible for sending the initial request to send a free-form message. -Once you make the initial request to the API to send the message, you can track its status as described +Once you make the initial request to the API to send the message, you can track its status as described [below](#observability). # Observability @@ -114,10 +112,10 @@ An entry in the message tracking table will typically contain the following fiel * `type`: Message type (either `sms` or `whatsapp`) * `eum_msg_id`: AWS End User Messaging message ID. This is a random unique id. -* `wa_msg_id`: Meta-provided WhatsApp Message ID. Only available for WhatsApp messages and only once +* `wa_msg_id`: Meta-provided WhatsApp Message ID. Only available for WhatsApp messages and only once Meta server have processed the message send request. Contains `__UNKOWN__` for SMS messages or WhatsApp messages that have not yet been processed by Meta. -* `delivery_history`: Map with the history of the ISO-formatted instants when the message transitioned states. +* `delivery_history`: Map with the history of the ISO-formatted instants when the message transitioned states. * `expiration_date`: The UTC timestamp when the memssage will expire. * `latest_status`: The most recent delivery status for the message. * `latest_update`: The UTC timestamp when the message delivery information was last updated. @@ -132,7 +130,7 @@ The status a message transverses through its lifecyle are: * `delivered`: Message has been delivered to the user's terminal. Does not guarantee that the user has read it. Also, SMS carriers might not provide us with this information so correctly delivered SMS messages might not be marked as `delivered` in the table. -* `read`: [WhatsApp specific] The message has been shown to the user in the WhatsApp application. +* `read`: [WhatsApp specific] The message has been shown to the user in the WhatsApp application. # Deployment sample @@ -167,18 +165,18 @@ curl -X POST -H "x-api-key: $(aws apigateway get-api-key --api-key ${RestAPIAPIK More work is required to turn this code into a production sample. Some ideas for future improvement: -* WhatsApp delivery error handling in particular should be improved. While the solution should handle 24h - WhatsApp communication windows automatically and re-sends the default template if needed, it does not - handle the case where delivery to WhatsApp phone numbers fails for whatever reason. - The logic for handling these failures can be found in the [`wa_status_handler`](lambda/wa_status_handler/main.py) +* WhatsApp delivery error handling in particular should be improved. While the solution should handle 24h + WhatsApp communication windows automatically and re-sends the default template if needed, it does not + handle the case where delivery to WhatsApp phone numbers fails for whatever reason. + The logic for handling these failures can be found in the [`wa_status_handler`](lambda/wa_status_handler/main.py) lambda code. -* Also, the WhatsApp sending logic only sends English templates. WhatsApp templates can be configured per-language, so +* Also, the WhatsApp sending logic only sends English templates. WhatsApp templates can be configured per-language, so you will most likely want to make the template sending logic configurable per-language. The handling code is located in the [`send_whatsapp`](lambda/send_whatsapp/main.py) lambda. -* In the WhatsApp flow, if the user answers to the template message more than 2h after the template has been sent (and - therefore the initiating free-form message has already been automatically discarded) no extra communication is sent, - which can be confusing for users. Extra work should be done to improve the UX for these cases (maybe by sending a +* In the WhatsApp flow, if the user answers to the template message more than 2h after the template has been sent (and + therefore the initiating free-form message has already been automatically discarded) no extra communication is sent, + which can be confusing for users. Extra work should be done to improve the UX for these cases (maybe by sending a specific message explaining that the original message has expired?). -* The solution only supports sending basic message types. WhatsApp supports a +* The solution only supports sending basic message types. WhatsApp supports a [wide variety of rich messages](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages). The solution could be extended to support these different message types. From cba65e869d2d2bb26569771555ea18344ad4f822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joseba=20Echevarr=C3=ADa=20Garc=C3=ADa?= Date: Tue, 4 Feb 2025 00:20:52 +0100 Subject: [PATCH 4/4] Added the reference to the EUM sample in the README --- python/README.md | 51 ++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/python/README.md b/python/README.md index 4b70780d47..9ee386f415 100644 --- a/python/README.md +++ b/python/README.md @@ -49,28 +49,29 @@ $ cdk destroy ## Table of Contents -| Example | Description | -|---------|-------------| -| [api-cors-lambda](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/api-cors-lambda/) | Shows creation of Rest API (GW) with an /example GET endpoint, with CORS enabled | -| [application-load-balancer](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/application-load-balancer/) | Using an AutoScalingGroup with an Application Load Balancer | -| [appsync-graphql-dynamodb](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/appsync-graphql-dynamodb/) | Creating a single GraphQL API with an API Key, and four Resolvers doing CRUD operations over a single DynamoDB | -| [classic-load-balancer](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/classic-load-balancer/) | Using an AutoScalingGroup with a Classic Load Balancer | -| [custom-resource](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/custom-resource/) | Shows adding a Custom Resource to your CDK app | -| [dockerized-app](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/docker-app-with-asg-alb/) | Deploys a containerized app into 3 tiers with userdata in an autoscaling group | -| [ecs-cluster](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/ecs/cluster/) | Provision an ECS Cluster with custom Autoscaling Group configuration | -| [ecs-load-balanced-service](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/ecs/ecs-load-balanced-service/) | Starting a container fronted by a load balancer on ECS | -| [ecs-service-with-task-placement](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/ecs/ecs-service-with-task-placement/) | Starting a container ECS with task placement specifications | -| [ecs-service-with-advanced-alb-config](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/ecs/ecs-service-with-advanced-alb-config/) | Starting a container fronted by a load balancer on ECS with added load balancer configuration | -| [ecs-service-with-task-networking](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/ecs/ecs-service-with-task-networking/) | Starting an ECS service with task networking, allowing ingress traffic to the task but blocking for the instance | -| [fargate-load-balanced-service](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/ecs/fargate-load-balanced-service/) | Starting a container fronted by a load balancer on Fargate | -| [fargate-service-with-autoscaling](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/ecs/fargate-service-with-autoscaling/) | Starting an ECS service of FARGATE launch type that auto scales based on average CPU Utilization | -| [lambda-cron](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/lambda-cron/) | Running a Lambda on a schedule | -| [lambda-layer](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/lambda-layer/) | Running a Lambda with a lambda layer | -| [lambda-s3-trigger](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/lambda-s3-trigger/) | S3 trigger for Lambda | -| [rds](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/rds/) | Creating a MySQL RDS database inside its dedicated VPC | -| [s3-object-lambda](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/s3-object-lambda/) | Creating an S3 Object Lambda and access point | -| [stepfunctions](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/stepfunctions/) | A simple StepFunctions workflow | -| [url-shortener](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/url-shortener) | Demo from the [Infrastructure ***is*** Code with the AWS CDK](https://youtu.be/ZWCvNFUN-sU) AWS Online Tech Talk | -| [ec2-instance](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/ec2/instance/) | Create EC2 Instance in new VPC with Systems Manager enabled | -| [serverless-backend](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/serverless-backend/) | Create a serverless backend with API Gateway, Lambda, S3, DynamoDB, and Cognito | -| [vpc-ec2-local-zones](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/vpc-ec2-local-zones/) | Create a VPC with public and private subnets in AWS Local Zones | \ No newline at end of file +| Example | Description | +|------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------| +| [api-cors-lambda](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/api-cors-lambda/) | Shows creation of Rest API (GW) with an /example GET endpoint, with CORS enabled | +| [application-load-balancer](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/application-load-balancer/) | Using an AutoScalingGroup with an Application Load Balancer | +| [appsync-graphql-dynamodb](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/appsync-graphql-dynamodb/) | Creating a single GraphQL API with an API Key, and four Resolvers doing CRUD operations over a single DynamoDB | +| [classic-load-balancer](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/classic-load-balancer/) | Using an AutoScalingGroup with a Classic Load Balancer | +| [custom-resource](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/custom-resource/) | Shows adding a Custom Resource to your CDK app | +| [dockerized-app](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/docker-app-with-asg-alb/) | Deploys a containerized app into 3 tiers with userdata in an autoscaling group | +| [ecs-cluster](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/ecs/cluster/) | Provision an ECS Cluster with custom Autoscaling Group configuration | +| [ecs-load-balanced-service](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/ecs/ecs-load-balanced-service/) | Starting a container fronted by a load balancer on ECS | +| [ecs-service-with-task-placement](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/ecs/ecs-service-with-task-placement/) | Starting a container ECS with task placement specifications | +| [ecs-service-with-advanced-alb-config](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/ecs/ecs-service-with-advanced-alb-config/) | Starting a container fronted by a load balancer on ECS with added load balancer configuration | +| [ecs-service-with-task-networking](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/ecs/ecs-service-with-task-networking/) | Starting an ECS service with task networking, allowing ingress traffic to the task but blocking for the instance | +| [end-user-messaging-rest-frontend](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/ecs/end-user-messaging-rest-frontend/) | REST Frontend to AWS End User Messaging with automatic WhatsApp message retries | +| [fargate-load-balanced-service](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/ecs/fargate-load-balanced-service/) | Starting a container fronted by a load balancer on Fargate | +| [fargate-service-with-autoscaling](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/ecs/fargate-service-with-autoscaling/) | Starting an ECS service of FARGATE launch type that auto scales based on average CPU Utilization | +| [lambda-cron](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/lambda-cron/) | Running a Lambda on a schedule | +| [lambda-layer](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/lambda-layer/) | Running a Lambda with a lambda layer | +| [lambda-s3-trigger](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/lambda-s3-trigger/) | S3 trigger for Lambda | +| [rds](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/rds/) | Creating a MySQL RDS database inside its dedicated VPC | +| [s3-object-lambda](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/s3-object-lambda/) | Creating an S3 Object Lambda and access point | +| [stepfunctions](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/stepfunctions/) | A simple StepFunctions workflow | +| [url-shortener](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/url-shortener) | Demo from the [Infrastructure ***is*** Code with the AWS CDK](https://youtu.be/ZWCvNFUN-sU) AWS Online Tech Talk | +| [ec2-instance](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/ec2/instance/) | Create EC2 Instance in new VPC with Systems Manager enabled | +| [serverless-backend](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/serverless-backend/) | Create a serverless backend with API Gateway, Lambda, S3, DynamoDB, and Cognito | +| [vpc-ec2-local-zones](https://github.com/aws-samples/aws-cdk-examples/tree/master/python/vpc-ec2-local-zones/) | Create a VPC with public and private subnets in AWS Local Zones |