def handleSubUpdate(client, updateContent):
    """
    Handles Strava webhook subscription update. This function is called by a valid Strava POST request to the webhook
    subscription callback URL.

    Parameters
    ----------
    client. Stravalib model client object. Contains access token to strava API for the user.
    updateContent. Dict. POST request JSON data formatted by Flask as a dict.

    Returns
    -------
    Nothing. Data are inserted into Postgres/PostGIS.
    """

    # Parse update information into a model using stravalib
    update = client.handle_subscription_update(updateContent)
    # Verify that the athlete(s) and subscription ID contained in the message are in Postgres
    if DBQueriesStrava.checkAthleteAndSub(update.owner_id,
                                          update.subscription_id):
        application.logger.debug("Sub update from Strava appears valid")
        # Insert subscription update message details into Postgres
        DBQueriesStrava.insertSubUpdate(update)
        # Verify that the update is a activity creation event
        if update.aspect_type == "create" and update.object_type == "activity":
            application.logger.debug(
                "This is a activity create event, creating thread to process activity"
            )
            try:
                # Create a thread to handle async processing of the activity and its derivatives
                # Threading allows the activity to long process with a quick 200 code to be sent to the Strava API
                Thread(target=APIFunctionsStrava.singleActivityProcessing,
                       args=(client, update.object_id)).start()
            except Exception as e:
                application.logger.error(
                    f"Creating a thread to process new activity failed with in the error: {e}"
                )
                errorEmail.sendErrorEmail(script="Webhook Activity Threading",
                                          exceptiontype=e.__class__.__name__,
                                          body=e)
        elif update.aspect_type == "update" and update.object_type == "activity":
            application.logger.debug(
                "This is a activity update event, updating existing record")
            # Update existing activity title
            DBQueriesStrava.updateExistingActivity(update)
        else:
            # Write logic to handle delete events
            application.logger.debug(
                "Sub update message contains an delete event, skipping request"
            )
            pass
    else:
        application.logger.debug(
            "POST request is invalid, user ID or subscription ID don't match those in database!"
        )
예제 #2
0
def removewebhooksub():
    """
    Removes activate webhook subscription from database and Strava API. SubID is disabled and will no longer be accepted
    by Flask.
    Called by Strava Activity admin page inputs.
    """
    # Get POST request info
    # athID = int(request.form['athID'])
    # subID = int(request.form['subID'])
    # if DBQueriesStrava.checkAthleteAndSub(athID, subID):
    # Get Strava API access credentials
    client = OAuthStrava.getAuth()
    # Send request to Strava to delete webhook subscription
    # Get webhook subID
    subID = DBQueriesStrava.getActiveSubID()
    application.logger.debug(f"Sub ID is: {subID}")
    if subID:
        try:
            application.logger.debug(
                f"Received request to remove the webhook subscription {subID}")
            client.delete_subscription(subID, os.getenv('STRAVA_CLIENT_ID'),
                                       os.getenv('STRAVA_CLIENT_SECRET'))
            # Set active webhook to inactive in database
            DBQueriesStrava.setWebhookInactive(subID)
            application.logger.debug(
                f"webhook subscription {subID} has been set to inactive")
            return Response(status=200)
        except Exception as e:
            application.logger.error(e)
            return Response(status=400)
    else:
        application.logger.debug(
            "No active webhook subscription to remove in DB, querying Strava API to check if one "
            "exists.")
        # Send request to Strava API webhook service to get details of existing subscription
        request = requests.get(stravaSubUrl,
                               data={
                                   "client_id":
                                   os.getenv("STRAVA_CLIENT_ID"),
                                   "client_secret":
                                   os.getenv("STRAVA_CLIENT_SECRET")
                               })
        # Parse to JSON
        r = request.json()[0]
        if r["id"]:
            application.logger.debug("Webhook exists, sending delete request")
            delR = client.delete_subscription(
                r["id"], os.getenv('STRAVA_CLIENT_ID'),
                os.getenv('STRAVA_CLIENT_SECRET'))
            application.logger.debug("Existing webhook has been removed!")
        return Response(status=200)
def singleActivityProcessing(client, actID):
    """
    Processes a single Strava Activity by placing the full activity in the database, making a simplified and masked public
    version, and by creating a privacy masked stream CSV which is added to a S3 Bucket. Finally a TopoJSON of the
    public activities is generated and uploaded to the S3 Bucket.

    @param client: stravalib client instance with valid access token
    @param actID: Int. ID of Strava Activity to be processed
    @return: Email. Message states if process was successful or failed
    """

    try:
        # Wait 45 minutes before processing update, this allows time for user to update any ride details before they
        #  are processed, in particular changing details uploaded from Wahoo
        # Check if in development mode, if not wait 45 minutes
        if application.config['ENV'] != "development":
            time.sleep(2700)
        application.logger.debug("Getting full activity details")
        # Get all activity details for newly created activity, including stream data
        activity = getFullDetails(client, actID)
        application.logger.debug("Inserting activity details")
        # Insert original, non-masked, coordinates and attribute details into Postgres/PostGIS
        DBQueriesStrava.insertOriginalAct(activity['act'])
        # Calculate masked, publicly sharable, activities and insert into Postgres masked table
        application.logger.debug("Processing and inserting masked geometries")
        DBQueriesStrava.processActivitiesPublic(activity["act"]["actId"])
        # Handle CSV stream processing
        generateAndUploadCSVStream(client, actID, activity)
        # Create topojson file
        topoJSON = DBQueriesStrava.createStravaPublicActTopoJSON()
        # Upload topoJSON to AWS S3
        StravaAWSS3.uploadToS3(topoJSON)
        # Send success email
        errorEmail.sendSuccessEmail(
            "Webhook Activity Update",
            f'The strava activity: {activity["act"]["actId"]}'
            f' has been processed, the activity can be'
            f' viewed on Strava at: '
            f'https://www.strava.com/activities/{activity["act"]["actId"]}')
        application.logger.debug("Strava activity has been processed!")
    except Exception as e:
        application.logger.error(
            f"Handling and inserting new webhook activity inside a thread failed with the error {e}"
        )
        errorEmail.sendErrorEmail(
            script="Webhook Activity Threaded Task Update",
            exceptiontype=e.__class__.__name__,
            body=e)
        # Raise another exception, this will signal the route function to return an error 500
        raise ()
def deleteSingleActivity(actID):
    """
    Deletes a single Strava activity from database and removes S3 file

    @param actID: Int. Strava Activity ID
    @return: Nothing
    """
    application.logger.debug(f"Deleting activity {actID}")
    # Delete activity from database
    DBQueriesStrava.removeActivityFromDB(actID)
    # Delete from S3
    # Get bucket details from environmental variable
    bucket = os.getenv("S3_TRIMMED_STREAM_BUCKET")
    application.logger.debug(f"Removing {actID} from S3 bucket")
    StravaAWSS3.deleteFromS3(bucket, "trimmedCSV", actID)
예제 #5
0
def processActivity():
    """
    Processes a single Strava Activity, will add/remove the activity and all derivatives, or just the Stream data.
    Called by Strava Activity admin page inputs.
    """
    # Get POST request info
    actID = int(request.form['actID'])
    athID = int(request.form['athID'])
    actionType = str(request.form['actionType'])
    procScope = str(request.form['scope'])
    # Verify that athlete ID is in database
    try:
        if athID in DBQueriesStrava.getAthleteList():
            application.logger.debug(
                f"Received a valid POST request to process a Strava Activity: {request.form}"
            )
            # Get Strava API access credentials
            client = OAuthStrava.getAuth()
            if procScope == "scopeFullActivity":
                # Process entire activity and all derived products
                # Delete activity, if it exists
                application.logger.debug(
                    f"Fully deleting the activity {actID}, if it exists")
                APIFunctionsStrava.deleteSingleActivity(actID)
                # Add new activity
                if actionType == "Add":
                    application.logger.debug(
                        f"Fully processing the activity {actID}")
                    # Issue activity Update
                    APIFunctionsStrava.singleActivityProcessing(client, actID)
                # elif actionType == "Delete":
                #     application.logger.debug(f"Fully deleting the activity {actID}, if it exists")
                #     APIFunctionsStrava.deleteSingleActivity(actID)
                else:
                    return Response(status=400)
            elif procScope == "scopeCSV":
                # Delete existing Strava Stream from S3
                application.logger.debug(
                    f"Removing the activity stream {actID}")
                StravaAWSS3.deleteFromS3(os.getenv("S3_TRIMMED_STREAM_BUCKET"),
                                         "trimmedCSV", actID)
                if actionType == "Add":
                    # Add new Strava stream
                    application.logger.debug(
                        f"Adding the activity stream {actID}")
                    APIFunctionsStrava.generateAndUploadCSVStream(
                        client, actID)
                # elif actionType == "Remove":
                #     # Delete existing Strava Stream from S3
                #     application.logger.debug(f"Removing the activity stream {actID}")
                #     StravaAWSS3.deleteFromS3(os.getenv("S3_TRIMMED_STREAM_BUCKET"), "trimmedCSV", actID)
                else:
                    return Response(status=200)
            else:
                return Response(status=400)
            return Response(status=200)
        else:
            return Response(status=400)
    except:
        return Response(status=500)
예제 #6
0
def genTopoJSON():
    """
    Generates a new TopoJSON file using all stored Strava activities and uploads to S3 Bucket, replaces existing file.
    Called by Strava Activity admin page inputs.
    """
    # Create topojson file
    application.logger.debug(f"Received request to generate a new TopoJSON")
    topoJSON = DBQueriesStrava.createStravaPublicActTopoJSON()
    # Upload topoJSON to AWS S3
    StravaAWSS3.uploadToS3(topoJSON)
    application.logger.debug(f"New TopoJSON has been generated")
    return Response(status=200)
def createStravaWebhook(client):
    """
    Creates new Strava webhook subscription. Client information and client generated token are pulled from environmental
    variables and the callback URL is set to a dedicated callback address on the application.

    If subscription is successful, a subscription ID will be provided by Strava and this ID will be inserted into
    Postgres.

    Returns
    -------
    Integer. Strava subscription id

    """
    try:
        # Kick off process to create new webhook
        #### The following may not be true, HTTPS may work, server was experiencing other problems that may have
        #### interferred in the process, need to test.
        # callback URL needs to be a HTTP url, not HTTPS, so the elastic beanstalk base environment URL is provided
        # as all calls to leavittmapping.com are redirected to HTTPS, consider making HTTP only mapping to domain.
        application.logger.debug(
            f"Attempting to create a new Strava webhook subscription with the values: client id: "
            f"{os.getenv('STRAVA_CLIENT_ID')} client_secret: {os.getenv('STRAVA_CLIENT_SECRET')}"
            f"callback url: {(os.getenv('httpSiteIndex') + os.getenv('strava_callback_url'))} "
            f"and the verify token: {os.getenv('STRAVA_VERIFY_TOKEN')}")

        response = client.create_subscription(
            client_id=os.getenv("STRAVA_CLIENT_ID"),
            client_secret=os.getenv("STRAVA_CLIENT_SECRET"),
            callback_url=(os.getenv('httpSiteIndex') +
                          os.getenv('strava_callback_url')),
            verify_token=os.getenv("STRAVA_VERIFY_TOKEN"))
        application.logger.debug(f"Response id is {response.id}")
        # Update database with sub id
        DBQueriesStrava.updateSubId(response.id)
        return response.id
    except Exception as e:
        # Something broke, log error
        application.logger.error(
            f"Create subscription function failed with the error {e}")
예제 #8
0
def addwebhooksub():
    """
    Adds a new Strava webhook subscription to the database and Strava API. Kicks off callback verification process.
    Called by Strava Activity admin page inputs.
    """
    # Get POST request info
    # athID = int(request.form['athID'])
    # callbackurl = str(request.form['callbackURL'])
    # Generate 14 character verify token string
    verifytoken = secrets.token_hex(7)
    # Insert token into database, will be updated if subID if successful, otherwise row will be deleted
    DBQueriesStrava.insertVerifyToken(verifytoken)
    application.logger.debug(
        f"New verification token {verifytoken} has been added to database")
    # Get Strava API access credentials
    client = OAuthStrava.getAuth()
    try:
        # Send request to create webhook subscription, will be given the new subscription ID in response
        application.logger.debug(
            f"Callback url is {os.getenv('STRAVA_CALLBACK_URL')}")
        # postDat = {"client_id": os.getenv("STRAVA_CLIENT_ID"),
        #            "client_secret": os.getenv("STRAVA_CLIENT_SECRET"),
        #            "callback_url": os.getenv('FULL_STRAVA_CALLBACK_URL'),
        #            "verify_token": verifytoken}
        #
        # r = requests.post("https://www.strava.com/api/v3/push_subscriptions", data=postDat)
        # resp = r.json()

        resp = client.create_subscription(
            client_id=os.getenv("STRAVA_CLIENT_ID"),
            client_secret=os.getenv("STRAVA_CLIENT_SECRET"),
            # callback_url=os.getenv('FULL_STRAVA_CALLBACK_URL'),
            callback_url=os.getenv('STRAVA_CALLBACK_URL'),
            verify_token=verifytoken)
        application.logger.debug(resp)
        # application.logger.debug(f"New sub id is {resp['id']}, updating database")
        application.logger.debug(f"New sub id is {resp.id}, updating database")
        # Update database with new sub id
        # DBQueriesStrava.updateSubId(resp["id"], verifytoken)
        DBQueriesStrava.updateSubId(resp.id, verifytoken)
        # application.logger.debug(f"New sub id {resp['id']} has been added to the database")
        application.logger.debug(
            f"New sub id {resp.id} has been added to the database")
        return Response(status=200)
    except Exception as e:
        application.logger.debug(
            f"Webhook creation process failed with the error {e}")
        # logging.error(e, exc_info=True)
        DBQueriesStrava.deleteVerifyTokenRecord(verifytoken)
        return Response(status=400, response=str(e))
def generateAndUploadCSVStream(client, actID, activity=None):
    """
    Generates and uploads a privacy zone masked Strava Stream CSV.

    @param client: stravalib client instance with valid access token
    @param actID: Int. Activity ID of Strava activity to process
    @param activity: Dictionary. Optional. Dictionary of full Strava Activity details, generated if not provided
    @return: Nothing. Uploads file to S3 Bucket
    """
    if not activity:
        # Get all activity details for newly created activity, including stream data
        activity = getFullDetails(client, actID)
    # Create in-memory buffer csv of stream data
    csvBuff = StravaAWSS3.writeMemoryCSV(activity["stream"])
    # Get WKT formatted latlng stream data
    wktStr = formatStreamData(activity["stream"])
    # application.logger.debug(f"wktSTR is: \n {wktStr}")
    # Get list of coordinates which cross privacy areas, these will be removed from the latlng stream CSV data
    removeCoordList = DBQueriesStrava.getIntersectingPoints(wktStr)
    # application.logger.debug(f"Remove cord list is: \n {removeCoordList}")
    # Trim/remove rows from latlng CSV stream which have coordinates that intersect the privacy areas
    trimmedMemCSV = trimStreamCSV(removeCoordList, csvBuff)
    # Upload trimmed buffer csv to AWS S3 bucket
    StravaAWSS3.uploadToS3(trimmedMemCSV, activity["act"]["actId"])
def handleSubCallback(request):
    """
    Handles requests to Strava subscription callback URL.

    GET:
        Webhoook Subscription Creation Process:
            CallbackURL is sent a GET request containing a challenge code. This code is sent back to requester to verify
            the callback.

             The initial request to create a new webhook subscription is then provided with verification and
             the new subscription ID.
    POST:
        Webhook subscription update message. Sent when a activity on a subscribed account is created, updated, or deleted,
        or when a privacy related profile setting is changed.

        All update messages are inputted into Postgres.

        Currently, only activity creation events are handled, additional development is needed to handle other events.

    Returns
    -------
    GET request:
        JSON, echoed Strava challenge text.
    POST request:
        Success code if data are successfully added to Postgres/PostGIS. Strava must receive a 200 code in response to
        POST.
    """
    application.logger.debug(
        f"Request to Strava callback url. Request is: {request}")
    # Check if request is a GET callback request, part of webhook subscription process
    if request.method == 'GET':
        application.logger.debug(
            "Got a GET callback request from Strava to verify webhook. Request args are: "
            f"{request.args}")
        # Extract challenge and verification tokens
        callBackContent = request.args.get("hub.challenge")
        callBackVerifyToken = request.args.get("hub.verify_token")
        # Form callback response as dict
        callBackResponse = {"hub.challenge": callBackContent}
        # Check if verification tokens match, i.e. if GET request is from Strava
        if callBackVerifyToken and DBQueriesStrava.checkVerificationToken(
                callBackVerifyToken):
            application.logger.debug(
                f"Strava callback verification succeeded, "
                f" responding with the challenge code"
                f" message: {callBackResponse}")
            # Verification succeeded, return challenge code as dict
            # Using Flask Response API automatically converts it to JSON with HTTP 200 success code
            return callBackResponse
        else:
            # Verification failed, raise error
            application.logger.error(
                f"Strava verification token doesn't match!")
            raise ValueError(
                'Strava token verification failed, no match found.')
    # POST request containing webhook subscription update message, new activity or other change to Strava account
    elif request.method == 'POST':
        application.logger.debug(
            "New activity incoming! Got a POST callback request from Strava")
        try:
            # Convert JSON body to dict
            callbackContent = json.loads(request.data, strict=False)
            application.logger.debug("JSON content has been extracted")
            application.logger.debug(f"Update content is {callbackContent}")
            # application.logger.debug(f"Update content dir is {dir(callbackContent)}")
            # Call function to handle update message and process new activity, if applicable
            # Get application access credentials
            client = OAuthStrava.getAuth()
            handleSubUpdate(client, callbackContent)
            application.logger.debug(
                "Inserted webhook update and activity details into postgres tables!"
            )
            return Response(status=200)
        except Exception as e:
            application.logger.error(
                f"Strava subscription update failed with the error {e}")