Skip to content

Commit 14b8840

Browse files
author
Joseba Echevarría García
committed
Added the source code for the End User Messaging REST frontend
1 parent c37e3ad commit 14b8840

File tree

18 files changed

+1514
-0
lines changed

18 files changed

+1514
-0
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
build/
12+
develop-eggs/
13+
dist/
14+
downloads/
15+
eggs/
16+
.eggs/
17+
lib/
18+
lib64/
19+
parts/
20+
sdist/
21+
var/
22+
wheels/
23+
share/python-wheels/
24+
*.egg-info/
25+
.installed.cfg
26+
*.egg
27+
MANIFEST
28+
29+
# PyInstaller
30+
# Usually these files are written by a python script from a template
31+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
32+
*.manifest
33+
*.spec
34+
35+
# Installer logs
36+
pip-log.txt
37+
pip-delete-this-directory.txt
38+
39+
# Unit test / coverage reports
40+
htmlcov/
41+
.tox/
42+
.nox/
43+
.coverage
44+
.coverage.*
45+
.cache
46+
nosetests.xml
47+
coverage.xml
48+
*.cover
49+
*.py,cover
50+
.hypothesis/
51+
.pytest_cache/
52+
cover/
53+
54+
# Translations
55+
*.mo
56+
*.pot
57+
58+
# Django stuff:
59+
*.log
60+
local_settings.py
61+
db.sqlite3
62+
db.sqlite3-journal
63+
64+
# Flask stuff:
65+
instance/
66+
.webassets-cache
67+
68+
# Scrapy stuff:
69+
.scrapy
70+
71+
# Sphinx documentation
72+
docs/_build/
73+
74+
# PyBuilder
75+
.pybuilder/
76+
target/
77+
78+
# Jupyter Notebook
79+
.ipynb_checkpoints
80+
81+
# IPython
82+
profile_default/
83+
ipython_config.py
84+
85+
# pdm
86+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
87+
#pdm.lock
88+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
89+
# in version control.
90+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
91+
.pdm.toml
92+
.pdm-python
93+
.pdm-build/
94+
95+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
96+
__pypackages__/
97+
98+
# Celery stuff
99+
celerybeat-schedule
100+
celerybeat.pid
101+
102+
# SageMath parsed files
103+
*.sage.py
104+
105+
# Environments
106+
.env
107+
.venv
108+
env/
109+
venv/
110+
ENV/
111+
env.bak/
112+
venv.bak/
113+
114+
# Spyder project settings
115+
.spyderproject
116+
.spyproject
117+
118+
# Rope project settings
119+
.ropeproject
120+
121+
# mkdocs documentation
122+
/site
123+
124+
# mypy
125+
.mypy_cache/
126+
.dmypy.json
127+
dmypy.json
128+
129+
# Pyre type checker
130+
.pyre/
131+
132+
# pytype static type analyzer
133+
.pytype/
134+
135+
# Cython debug symbols
136+
cython_debug/
137+
138+
# PyCharm
139+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
140+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
141+
# and can be added to the global gitignore or merged into this file. For a more nuclear
142+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
143+
.idea/
144+
145+
.DS_Store
146+
147+
# CDK
148+
*.swp
149+
package-lock.json
150+
.pytest_cache
151+
*.egg-info
152+
153+
# CDK asset staging directory
154+
.cdk.staging
155+
cdk.out
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Introduction
2+
3+
This repository implements an Infrastructure as Code (IaC), serverless stack that exposes a REST API for sending
4+
SMS & WhatsApp messages to your customers while handling conversation windows with WhatsApp destinations
5+
(more on this below).
6+
7+
It uses AWS End User Messaging as its communications platform and logs message history and handles
8+
WhatsApp user consent automatically in Amazon DynamoDB.
9+
10+
[TOC]
11+
12+
# Requirements
13+
14+
The code has been tested with Python 3.12 in macOS. You will also need:
15+
* [AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html)
16+
* Python 3.12
17+
* Docker or Podman for compiling the Lambda images
18+
* The requirements defined in [`requirements.txt`](requirements.txt)
19+
* SMS-related requirements in AWS End User Messaging SMS:
20+
- [Configuration set](https://docs.aws.amazon.com/sms-voice/latest/userguide/configuration-sets.html)
21+
- [Phone number or sender ID](https://docs.aws.amazon.com/sms-voice/latest/userguide/phone-number-types.html). This is
22+
referred to as the "originating entity" later in this document.
23+
* WhatsApp-related requirements:
24+
- A [WhatsApp Business Account](https://docs.aws.amazon.com/social-messaging/latest/userguide/managing-waba.html)
25+
[configured in AWS End User Messaging Social](https://docs.aws.amazon.com/social-messaging/latest/userguide/managing-phone-numbers-add.html).
26+
+ If you only have configured a single WhatsApp phone number, the solution will use that for sending messages.
27+
For other use cases and for efficiency purposes, you can specify the phone number to use when deploying the
28+
solution.
29+
+ The Business Account in End User Messaging Social must be configured with a
30+
[message and event destination](https://docs.aws.amazon.com/social-messaging/latest/userguide/managing-event-destinations.html)
31+
pointing to an SNS topic that this solution will use for tracking SMS message delivery.
32+
- A default WhatsApp template in English requesting that your users to connect with you. When you try to send a
33+
WhatsApp message to a number that has not communicated with you in the last 24h, the solution will send this
34+
template to the user and keep the message in a queue for 2h. If the customer replies to your template in this 2h
35+
window your original message will be automatically sent to the user automatically.
36+
37+
# Architecture
38+
39+
The diagram below ilustrates the main components of the architecture of the solution and their dependencies.
40+
There are two main flows of information:
41+
42+
* The flow which sends the SMS/WhatsApp messages to the end users, automatically tracking user consent in a
43+
specific DynamoDB table.
44+
* The flow which receives the message notifications from either SNS or EventBridge and tracks their status in a
45+
separate DynamoDB table. This flow also handles the case where the end user writes a WhatsApp message to the
46+
phone associated with the WhatsApp Business Account and registers their consent receive free-text messages.
47+
48+
![Application architecture](docs/architecture.png)
49+
50+
# Sending messages
51+
52+
## SMS
53+
54+
Sending SMS messages is handled with AWS End User Messaging SMS and in order to be able to send SMS you will need to
55+
register a Configuration set and a phone number or sender ID, as described above.
56+
57+
The tracking of SMS delivery is performed by monitoring
58+
[EventBridge events](https://docs.aws.amazon.com/sms-voice/latest/userguide/monitor-event-bridge.html) and follows
59+
the principles described [below](#observability).
60+
61+
## WhatsApp
62+
63+
In WhatsApp you generally cannot send free-form messages to users unless they have contacted you in the previous 24
64+
hours. In order to contact new users, you must either have them send you a message or send them
65+
[a Meta-approved template](https://developers.facebook.com/docs/whatsapp/message-templates/guidelines/) asking the
66+
destination user to write back to you. When they do, you are allowed to send free-text messages to your users for the
67+
next 24 hours.
68+
69+
The communication flow for talking to your clients in WhatsApp is as follows:
70+
71+
1. You send a pre-approved message template to your customer asking them to write back to you. This message should
72+
explicitly ask the user to not answer anything if they do not want to be contacted.
73+
2. If the user answers with any text you can start sending free-form messages for the next 24 hours.
74+
3. After 24 hours the communication window closes and you have to send a new template to the customer in order to be
75+
able to send free-form messages.
76+
77+
This solution tracks user communications with your number by automatically sending a template as needed and only trying
78+
to send free-form messages if the user has responded to the template. If the user does not respond 2 hours after the
79+
template is sent to them, the initiating message is discarded.
80+
81+
The following flow is executed when you send a request to the REST API endpoint to send a free-form message:
82+
83+
```mermaid
84+
flowchart TD
85+
Client([Original free-form message]) -->|POST /v1/sendWhatsApp| API[API Gateway]
86+
API -->|Validates & authorizes| Q["`WhatsApp Queue
87+
(2h TTL)`"]
88+
Q --> E{{User wrote to us in the last 24h?}}
89+
E -->|Yes|L([Send free-form message])
90+
E -->|No|T{{Template sent?}}
91+
T -->|Yes|W[Return message to queue]
92+
T -->|No|ST[Send template]
93+
ST -->W
94+
W -->Q
95+
96+
style E fill:#009,color:#ddd
97+
style T fill:#009,color:#ddd
98+
```
99+
100+
This handling is transparent to you, and you are only responsible for sending the initial request to send a free-form
101+
message.
102+
103+
Once you make the initial request to the API to send the message, you can track its status as described
104+
[below](#observability).
105+
106+
# Observability
107+
108+
Metadata about the messages and whether they were delivered or not (but not the messages themselves) is stored in
109+
a DynamoDB table for observability purposes. This data does not, however, contain destination phone numbers and has a
110+
TTL of 1 year. These messages are identified by their AWS End User Messaging message ID and (sometimes) by their
111+
WahtsApp message ID, but contain no other perrsonally identifiable information.
112+
113+
An entry in the message tracking table will typically contain the following fields:
114+
115+
* `type`: Message type (either `sms` or `whatsapp`)
116+
* `eum_msg_id`: AWS End User Messaging message ID. This is a random unique id.
117+
* `wa_msg_id`: Meta-provided WhatsApp Message ID. Only available for WhatsApp messages and only once
118+
Meta server have processed the message send request. Contains `__UNKOWN__` for SMS messages or WhatsApp messages
119+
that have not yet been processed by Meta.
120+
* `delivery_history`: Map with the history of the ISO-formatted instants when the message transitioned states.
121+
* `expiration_date`: The UTC timestamp when the memssage will expire.
122+
* `latest_status`: The most recent delivery status for the message.
123+
* `latest_update`: The UTC timestamp when the message delivery information was last updated.
124+
* `registration_date`: The ISO-formatted instant when the message was registered.
125+
126+
The status a message transverses through its lifecyle are:
127+
128+
* `unknown`: Message status is unknown. Unused.
129+
* `failed`: Message delivery has failed. Unused.
130+
* `sent_for_delivery`: Message has been processed by this stack and sent to AWS End User Messaging for delivery.
131+
* `sent`: Message has been sent to the user. Does not gguarantee that the user has received it.
132+
* `delivered`: Message has been delivered to the user's terminal. Does not guarantee that the user has read it. Also,
133+
SMS carriers might not provide us with this information so correctly delivered SMS messages might not be marked as
134+
`delivered` in the table.
135+
* `read`: [WhatsApp specific] The message has been shown to the user in the WhatsApp application.
136+
137+
# Deployment sample
138+
139+
```bash
140+
# Run this only if using Podman instead of Docker
141+
export CDK_DOCKER=podman
142+
# Deploy the solution
143+
cdk deploy \
144+
--parameters ConfigurationSetArn='${CONFIGURATION_SET_ARN}' \
145+
--parameters OriginatingEntity='${ORIGINATING_PHONE_ARN}' \
146+
--parameters WhatsAppNotificationTopicARN='${SNS_TOPIC_ARN}' \
147+
--parameters MessageType='TRANSACTIONAL' \
148+
--parameters WATemplate='${WHATSAPP_TEMPLATE_NAME}' \
149+
--parameters WAPhoneNumberARN='${WHATSAPP_PHONE_NUMBER_ARN}'
150+
```
151+
152+
You will get several outputs if everything is correct, they're referenced in the step below as the following fields:
153+
* `RestAPIAPIKey`: the ID of the Rest API key
154+
* `RestAPISMSApiGateway`: the URL of the SMS send endpoint in API Gateway
155+
* `RestAPIWhatsAppApiGateway`: the URL of the WhatsApp send endpoint in API Gateway
156+
157+
# Message sending sample
158+
159+
```bash
160+
# Send a SMS message
161+
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}
162+
# Send a WhatsApp message
163+
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}
164+
```
165+
166+
# Future work
167+
168+
More work is required to turn this code into a production sample. Some ideas for future improvement:
169+
170+
* WhatsApp delivery error handling in particular should be improved. While the solution should handle 24h
171+
WhatsApp communication windows automatically and re-sends the default template if needed, it does not
172+
handle the case where delivery to WhatsApp phone numbers fails for whatever reason.
173+
The logic for handling these failures can be found in the [`wa_status_handler`](lambda/wa_status_handler/main.py)
174+
lambda code.
175+
* Also, the WhatsApp sending logic only sends English templates. WhatsApp templates can be configured per-language, so
176+
you will most likely want to make the template sending logic configurable per-language.
177+
The handling code is located in the [`send_whatsapp`](lambda/send_whatsapp/main.py) lambda.
178+
* In the WhatsApp flow, if the user answers to the template message more than 2h after the template has been sent (and
179+
therefore the initiating free-form message has already been automatically discarded) no extra communication is sent,
180+
which can be confusing for users. Extra work should be done to improve the UX for these cases (maybe by sending a
181+
specific message explaining that the original message has expired?).
182+
* The solution only supports sending basic message types. WhatsApp supports a
183+
[wide variety of rich messages](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages). The
184+
solution could be extended to support these different message types.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env python3
2+
3+
import cdk_nag
4+
import aws_cdk as cdk
5+
from cdk.message_api import MessageAPI
6+
7+
8+
app = cdk.App()
9+
MessageAPI(app, 'MessagingRESTAPI')
10+
cdk.Aspects.of(app).add(cdk_nag.AwsSolutionsChecks(verbose=True))
11+
app.synth()

0 commit comments

Comments
 (0)