Skip to content

Commit

Permalink
Merge branch 'main' into monkeytype-auto-annotate
Browse files Browse the repository at this point in the history
  • Loading branch information
whabanks authored May 17, 2024
2 parents 70e907d + 86fb27b commit 3c5872f
Show file tree
Hide file tree
Showing 49 changed files with 1,409 additions and 177 deletions.
4 changes: 2 additions & 2 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ services:
- db

db:
image: postgres:11.20-bullseye@sha256:98fac4e8dc6fb58a75f2be563e876842f53db5baadb0d98abdd3205a20f6e6eb
image: postgres:11.22-bullseye@sha256:c886a3236b3d11abc302e64309186c90a69b49e53ccff23fd8c8b057b5b4bce9
volumes:
- ./initdb:/docker-entrypoint-initdb.d
restart: always
Expand All @@ -38,7 +38,7 @@ services:
- "5432:5432"

redis:
image: redis:6.2@sha256:9e75c88539241ad7f61bc9c39ea4913b354064b8a75ca5fc40e1cef41b645bc0
image: redis:6.2@sha256:d4948d011cc38e94f0aafb8f9a60309bd93034e07d10e0767af534512cf012a9
restart: always
command: redis-server --port 6380
ports:
Expand Down
1 change: 1 addition & 0 deletions .devcontainer/scripts/notify-dev-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ echo -e "alias poe='poetry run poe'" >> ~/.zshrc

# Smoke test
# requires adding files .env_staging and .env_prod to the root of the project
echo -e "alias smoke-local='cd /workspace && cp .env_smoke_local tests_smoke/.env && poetry run make smoke-test-local'" >> ~/.zshrc
echo -e "alias smoke-staging='cd /workspace && cp .env_smoke_staging tests_smoke/.env && poetry run make smoke-test'" >> ~/.zshrc
echo -e "alias smoke-prod='cd /workspace && cp .env_smoke_prod tests_smoke/.env && poetry run make smoke-test'" >> ~/.zshrc

Expand Down
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ AWS_PINPOINT_REGION=us-west-2
AWS_EMF_ENVIRONMENT=local

CONTACT_FORM_EMAIL_ADDRESS = ""

AWS_PINPOINT_SC_POOL_ID=
AWS_PINPOINT_SC_TEMPLATE_IDS=
6 changes: 3 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

- name: Initialize CodeQL
uses: github/codeql-action/init@2f93e4319b2f04a2efc38fa7f78bd681bc3f7b2f # v2.23.2
uses: github/codeql-action/init@ffd3158cb9024ebd018dbf20756f28befbd168c7 # v2.24.10
with:
languages: ${{ matrix.language }}
queries: +security-and-quality

- name: Autobuild
uses: github/codeql-action/autobuild@2f93e4319b2f04a2efc38fa7f78bd681bc3f7b2f # v2.23.2
uses: github/codeql-action/autobuild@ffd3158cb9024ebd018dbf20756f28befbd168c7 # v2.24.10

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@2f93e4319b2f04a2efc38fa7f78bd681bc3f7b2f # v2.23.2
uses: github/codeql-action/analyze@ffd3158cb9024ebd018dbf20756f28befbd168c7 # v2.24.10
with:
category: "/language:${{ matrix.language }}"
4 changes: 2 additions & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ jobs:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:11.20-bullseye@sha256:4e4b23580ada59c9ec5a712bdff9f91b0e6a7898d9ea954306b953c426727cef
image: postgres:11.22-bullseye@sha256:c886a3236b3d11abc302e64309186c90a69b49e53ccff23fd8c8b057b5b4bce9
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
Expand Down Expand Up @@ -67,7 +67,7 @@ jobs:
run: |
cp -f .env.example .env
- name: Checks for new endpoints against AWS WAF rules
uses: cds-snc/notification-utils/.github/actions/waffles@06a40db6286f525fe3551e029418458d33342592 # 52.1.0
uses: cds-snc/notification-utils/.github/actions/waffles@52.2.2
with:
app-loc: '/github/workspace'
app-libs: '/github/workspace/env/site-packages'
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ format:
smoke-test:
cd tests_smoke && poetry run python smoke_test.py

.PHONY: smoke-test-local
smoke-test-local:
cd tests_smoke && poetry run python smoke_test.py --local --nofiles

.PHONY: run
run: ## Run the web app
poetry run flask run -p 6011 --host=0.0.0.0
Expand Down
5 changes: 4 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
PerformancePlatformClient,
)
from app.clients.salesforce.salesforce_client import SalesforceClient
from app.clients.sms.aws_pinpoint import AwsPinpointClient
from app.clients.sms.aws_sns import AwsSnsClient
from app.dbsetup import RoutingSQLAlchemy
from app.encryption import CryptoSigner
Expand All @@ -45,6 +46,7 @@
notify_celery = NotifyCelery()
aws_ses_client = AwsSesClient()
aws_sns_client = AwsSnsClient()
aws_pinpoint_client = AwsPinpointClient()
signer_notification = CryptoSigner()
signer_personalisation = CryptoSigner()
signer_complaint = CryptoSigner()
Expand Down Expand Up @@ -107,6 +109,7 @@ def create_app(application, config=None):
statsd_client.init_app(application)
logging.init_app(application, statsd_client)
aws_sns_client.init_app(application, statsd_client=statsd_client)
aws_pinpoint_client.init_app(application, statsd_client=statsd_client)
aws_ses_client.init_app(application.config["AWS_REGION"], statsd_client=statsd_client)
notify_celery.init_app(application)

Expand All @@ -120,7 +123,7 @@ def create_app(application, config=None):

performance_platform_client.init_app(application)
document_download_client.init_app(application)
clients.init_app(sms_clients=[aws_sns_client], email_clients=[aws_ses_client])
clients.init_app(sms_clients=[aws_sns_client, aws_pinpoint_client], email_clients=[aws_ses_client])

if application.config["FF_SALESFORCE_CONTACT"]:
salesforce_client.init_app(application)
Expand Down
93 changes: 93 additions & 0 deletions app/aws/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,83 @@ def sns_failed_callback(provider_response, reference=None, timestamp="2016-06-28
return _sns_callback(body)


# Note that 1467074434 = 2016-06-28 00:40:34.558 UTC
def pinpoint_successful_callback(reference=None, timestamp=1467074434, destination="+1XXX5550100"):
body = {
"eventType": "TEXT_SUCCESSFUL",
"eventVersion": "1.0",
"eventTimestamp": timestamp,
"isFinal": False,
"originationPhoneNumber": "+18078061258",
"destinationPhoneNumber": destination,
"isoCountryCode": "CA",
"mcc": "302",
"mnc": "610",
"carrierName": "Bell Cellular Inc. / Aliant Telecom",
"messageId": reference,
"messageRequestTimestamp": timestamp,
"messageEncoding": "GSM",
"messageType": "TRANSACTIONAL",
"messageStatus": "SUCCESSFUL",
"messageStatusDescription": "Message has been accepted by phone carrier",
"totalMessageParts": 1,
"totalMessagePrice": 0.00581,
"totalCarrierFee": 0.00767,
}

return _pinpoint_callback(body)


def pinpoint_delivered_callback(reference=None, timestamp=1467074434, destination="+1XXX5550100"):
body = {
"eventType": "TEXT_DELIVERED",
"eventVersion": "1.0",
"eventTimestamp": timestamp,
"isFinal": True,
"originationPhoneNumber": "+13655550100",
"destinationPhoneNumber": destination,
"isoCountryCode": "CA",
"mcc": "302",
"mnc": "610",
"carrierName": "Bell Cellular Inc. / Aliant Telecom",
"messageId": reference,
"messageRequestTimestamp": timestamp,
"messageEncoding": "GSM",
"messageType": "TRANSACTIONAL",
"messageStatus": "DELIVERED",
"messageStatusDescription": "Message has been accepted by phone",
"totalMessageParts": 1,
"totalMessagePrice": 0.00581,
"totalCarrierFee": 0.006,
}

return _pinpoint_callback(body)


# Note that 1467074434 = 2016-06-28 00:40:34.558 UTC
def pinpoint_failed_callback(provider_response, reference=None, timestamp=1467074434, destination="+1XXX5550100"):
body = {
"eventType": "TEXT_CARRIER_UNREACHABLE",
"eventVersion": "1.0",
"eventTimestamp": timestamp,
"isFinal": True,
"originationPhoneNumber": "+13655550100",
"destinationPhoneNumber": destination,
"isoCountryCode": "CA",
"messageId": reference,
"messageRequestTimestamp": timestamp,
"messageEncoding": "GSM",
"messageType": "TRANSACTIONAL",
"messageStatus": "CARRIER_UNREACHABLE",
"messageStatusDescription": provider_response,
"totalMessageParts": 1,
"totalMessagePrice": 0.00581,
"totalCarrierFee": 0.006,
}

return _pinpoint_callback(body)


def _ses_bounce_callback(reference, bounce_type, bounce_subtype=None):
ses_message_body = {
"bounce": {
Expand Down Expand Up @@ -267,3 +344,19 @@ def _sns_callback(body):
"UnsubscribeUrl": "https://sns.ca-central-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=[REACTED]",
"MessageAttributes": {},
}


def _pinpoint_callback(body):
return {
"Type": "Notification",
"MessageId": "8e83c020-1234-1234-1234-92a8ee9baa0a",
"TopicArn": "arn:aws:sns:ca-central-1:12341234:ses_notifications",
"Subject": None,
"Message": json.dumps(body),
"Timestamp": "2017-11-17T12:14:03.710Z",
"SignatureVersion": "1",
"Signature": "[REDACTED]",
"SigningCertUrl": "https://sns.ca-central-1.amazonaws.com/SimpleNotificationService-[REDACTED].pem",
"UnsubscribeUrl": "https://sns.ca-central-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=[REACTED]",
"MessageAttributes": {},
}
152 changes: 152 additions & 0 deletions app/celery/process_pinpoint_receipts_tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
from datetime import datetime
from typing import Union

from flask import current_app, json
from notifications_utils.statsd_decorators import statsd
from sqlalchemy.orm.exc import NoResultFound

from app import notify_celery, statsd_client
from app.config import QueueNames
from app.dao import notifications_dao
from app.models import (
NOTIFICATION_DELIVERED,
NOTIFICATION_PERMANENT_FAILURE,
NOTIFICATION_SENT,
NOTIFICATION_TECHNICAL_FAILURE,
NOTIFICATION_TEMPORARY_FAILURE,
PINPOINT_PROVIDER,
)
from app.notifications.callbacks import _check_and_queue_callback_task
from celery.exceptions import Retry

# Pinpoint receipts are of the form:
# {
# "eventType": "TEXT_DELIVERED",
# "eventVersion": "1.0",
# "eventTimestamp": 1712944268877,
# "isFinal": true,
# "originationPhoneNumber": "+13655550100",
# "destinationPhoneNumber": "+16135550123",
# "isoCountryCode": "CA",
# "mcc": "302",
# "mnc": "610",
# "carrierName": "Bell Cellular Inc. / Aliant Telecom",
# "messageId": "221bc70c-7ee6-4987-b1ba-9684ba25be20",
# "messageRequestTimestamp": 1712944267685,
# "messageEncoding": "GSM",
# "messageType": "TRANSACTIONAL",
# "messageStatus": "DELIVERED",
# "messageStatusDescription": "Message has been accepted by phone",
# "totalMessageParts": 1,
# "totalMessagePrice": 0.00581,
# "totalCarrierFee": 0.006
# }


@notify_celery.task(bind=True, name="process-pinpoint-result", max_retries=5, default_retry_delay=300)
@statsd(namespace="tasks")
def process_pinpoint_results(self, response):
try:
receipt = json.loads(response["Message"])
reference = receipt["messageId"]
status = receipt["messageStatus"]
provider_response = receipt["messageStatusDescription"]

notification_status = determine_pinpoint_status(status, provider_response)

if notification_status == NOTIFICATION_SENT:
return # we don't want to update the status to sent if it's already sent

if not notification_status:
current_app.logger.warning(f"unhandled provider response for reference {reference}, received '{provider_response}'")
notification_status = NOTIFICATION_TECHNICAL_FAILURE # revert to tech failure by default

try:
notification = notifications_dao.dao_get_notification_by_reference(reference)
except NoResultFound:
try:
current_app.logger.warning(
f"RETRY {self.request.retries}: notification not found for Pinpoint reference {reference} (update to {notification_status}). "
f"Callback may have arrived before notification was persisted to the DB. Adding task to retry queue"
)
self.retry(queue=QueueNames.RETRY)
except self.MaxRetriesExceededError:
current_app.logger.warning(
f"notification not found for Pinpoint reference: {reference} (update to {notification_status}). Giving up."
)
return
if notification.sent_by != PINPOINT_PROVIDER:
current_app.logger.exception(f"Pinpoint callback handled notification {notification.id} not sent by Pinpoint")
return

if notification.status != NOTIFICATION_SENT:
notifications_dao._duplicate_update_warning(notification, notification_status)
return

notifications_dao._update_notification_status(
notification=notification,
status=notification_status,
provider_response=provider_response,
)

if notification_status != NOTIFICATION_DELIVERED:
current_app.logger.info(
(
f"Pinpoint delivery failed: notification id {notification.id} and reference {reference} has error found. "
f"Provider response: {provider_response}"
)
)
else:
current_app.logger.info(
f"Pinpoint callback return status of {notification_status} for notification: {notification.id}"
)

statsd_client.incr(f"callback.pinpoint.{notification_status}")

if notification.sent_at:
statsd_client.timing_with_dates("callback.pinpoint.elapsed-time", datetime.utcnow(), notification.sent_at)

_check_and_queue_callback_task(notification)

except Retry:
raise

except Exception as e:
current_app.logger.exception(f"Error processing Pinpoint results: {str(e)}")
self.retry(queue=QueueNames.RETRY)


def determine_pinpoint_status(status: str, provider_response: str) -> Union[str, None]:
"""Determine the notification status based on the SMS status and provider response.
Args:
status (str): message status from AWS
provider_response (str): detailed status from the SMS provider
Returns:
Union[str, None]: the notification status or None if the status is not handled
"""

if status == "DELIVERED":
return NOTIFICATION_DELIVERED
elif status == "SUCCESSFUL": # carrier has accepted the message but it hasn't gone to the phone yet
return NOTIFICATION_SENT

response_lower = provider_response.lower()

if "blocked" in response_lower:
return NOTIFICATION_TECHNICAL_FAILURE
elif "invalid" in response_lower:
return NOTIFICATION_TECHNICAL_FAILURE
elif "is opted out" in response_lower:
return NOTIFICATION_PERMANENT_FAILURE
elif "unknown error" in response_lower:
return NOTIFICATION_TECHNICAL_FAILURE
elif "exceed max price" in response_lower:
return NOTIFICATION_TECHNICAL_FAILURE
elif "phone carrier is currently unreachable/unavailable" in response_lower:
return NOTIFICATION_TEMPORARY_FAILURE
elif "phone is currently unreachable/unavailable" in response_lower:
return NOTIFICATION_PERMANENT_FAILURE
else:
return None
Loading

0 comments on commit 3c5872f

Please sign in to comment.