"""Lambda push-notification-events function module.""" import json import boto3 from botocore.exceptions import ClientError from segment import analytics import sentry_sdk from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration from owsrequest import request from lambdacommon.common_config import logger import config if config.secrets_manager_client: sentry_sdk.init( config.secrets_manager_client.get_cred('SENTRY_DSN'), integrations=[AwsLambdaIntegration()] ) logger.info(f'Initializing with Sentry {config.secrets_manager_client is not None}') analytics.write_key = config.SEGMENT_WRITE_KEY def handler(event, context): """Lambda entry point. Process events from SNS Topic which contain DeliveryFailure messages from SNS Endpoint sending push notifications. Cleanup by removing device connection via ows-users. Args: event (dict): messages from SNS Topic context (object): AWS Lambda Context object https://docs.aws.amazon.com/lambda/latest/dg/python-context.html Returns: None """ try: sns_client = boto3.client('sns', region_name=config.AWS_REGION) logger.info(event) messages = extract_messages(event) logger.info(f'Processing: {len(messages)}') for message in messages: logger.info(f'Message body: {message}') # fetch SNS Endpoint to get extra attributes try: endpoint_attributes = sns_client.get_endpoint_attributes( EndpointArn=message['endpoint_arn']) logger.info(f'Endpoint Attributes: {endpoint_attributes}') except ClientError as e: logger.info(str(e)) if e.response['Error']['Code'] != 'NotFound': raise e else: continue device_id, identity_id, brand = extract_endpoint_attributes( endpoint_attributes) logger.info(f'Device ID: {device_id}') logger.info(f'Identity ID: {identity_id}') logger.info(f'Brand: {brand}') if not device_id or not identity_id: continue analytics.track( identity_id, 'Push Notification Bounced', { 'device_id': device_id, 'sns_endpoint_arn': message['endpoint_arn'], 'brand': brand, 'campaign': { 'medium': 'Push', 'source': 'orchard-notifications', } }) # call ows-users to remove device # soft deletes edge and removes SNS Endpoint param = f'?brand={brand}' if brand else '' response = request.process( 'lambda-notifications-push-delivery-failure', config.ENVIRONMENT, 'DELETE', 'ows-users', f'/users/identity/{identity_id}/device/{device_id}{param}' ) logger.info(f'Response HTTP code: {response.status_code}') if response.status_code not in [204, 404]: raise Exception( f'unexpected HTTP response status {response.status_code}') except Exception as e: logger.exception(str(e)) raise e def extract_endpoint_attributes(endpoint_attributes): """Pull out custom user data from SNS Endpoint attributes. Args: endpoint_attributes (object): boto3 response for https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sns.html#SNS.Client.set_endpoint_attributes Returns: tuple: (device_id, identity_id) """ device_id = None identity_id = None brand = None try: custom_attributes_json =\ endpoint_attributes['Attributes']['CustomUserData'] custom_attributes = json.loads(custom_attributes_json) device_id = custom_attributes.get('device_id') identity_id = custom_attributes.get('identity_id') brand = custom_attributes.get('brand') except (KeyError, json.decoder.JSONDecodeError): pass return device_id, identity_id, brand def extract_messages(event): """Filter for only DeliveryFailure events. Args: event (dict): messages from SNS Topic as list of 'Records' Returns: list: SNS Endpoint DeliveryFailure messages as dicts """ # pull out JSON message that contains events from push endpoint raw_messages = [ json.loads(x['Sns']['Message']) for x in event.get('Records', []) ] # return back event type we actually care about in consistent format return [ { 'id': x['MessageId'], 'event_type': x['EventType'], 'endpoint_arn': x['EndpointArn'] } for x in raw_messages if x['EventType'] == 'DeliveryFailure' ]