Created
April 24, 2025 16:48
-
-
Save lukelittle/9efecf450535f17ccdbce7f942d18d99 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
import logging | |
import json | |
from datetime import datetime | |
from flask import Flask, request, jsonify | |
import requests | |
from dotenv import load_dotenv | |
# Load environment variables | |
load_dotenv() | |
# Configure logging | |
logging.basicConfig( | |
level=logging.INFO, | |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' | |
) | |
logger = logging.getLogger(__name__) | |
# Initialize Flask app | |
app = Flask(__name__) | |
# Environment variables | |
WEBEX_BEARER_TOKEN = os.environ.get('WEBEX_BEARER_TOKEN') | |
HUBSPOT_PRIVATE_APP_TOKEN = os.environ.get('HUBSPOT_PRIVATE_APP_TOKEN') | |
# API endpoints | |
HUBSPOT_API_BASE = 'https://api.hubapi.com' | |
HUBSPOT_SEARCH_ENDPOINT = f'{HUBSPOT_API_BASE}/crm/v3/objects/contacts/search' | |
HUBSPOT_CREATE_CONTACT_ENDPOINT = f'{HUBSPOT_API_BASE}/crm/v3/objects/contacts' | |
HUBSPOT_ENGAGEMENTS_ENDPOINT = f'{HUBSPOT_API_BASE}/crm/v3/objects/notes' | |
# Constants | |
DEFAULT_NAME = "Unknown Caller" | |
def search_contact_by_phone(phone_number): | |
""" | |
Search for a contact in HubSpot by phone number | |
Args: | |
phone_number (str): Phone number to search for | |
Returns: | |
dict or None: Contact data if found, None otherwise | |
""" | |
headers = { | |
'Authorization': f'Bearer {HUBSPOT_PRIVATE_APP_TOKEN}', | |
'Content-Type': 'application/json' | |
} | |
# Format the phone number to remove special characters for search | |
formatted_phone = ''.join(filter(str.isdigit, phone_number)) | |
if formatted_phone.startswith('1') and len(formatted_phone) == 11: | |
# Handle US numbers with country code | |
formatted_phone = formatted_phone[1:] | |
# Create the search payload | |
payload = { | |
"filterGroups": [{ | |
"filters": [{ | |
"propertyName": "phone", | |
"operator": "CONTAINS_TOKEN", | |
"value": formatted_phone | |
}] | |
}], | |
"properties": ["firstname", "lastname", "phone", "email"], | |
"limit": 1 | |
} | |
try: | |
logger.info(f"Searching for contact with phone: {phone_number}") | |
response = requests.post( | |
HUBSPOT_SEARCH_ENDPOINT, | |
headers=headers, | |
json=payload | |
) | |
response.raise_for_status() | |
results = response.json() | |
if results.get('total', 0) > 0: | |
contact = results['results'][0] | |
logger.info(f"Contact found: {contact['properties'].get('firstname', '')} {contact['properties'].get('lastname', '')}") | |
return contact | |
else: | |
logger.info(f"No contact found for phone: {phone_number}") | |
return None | |
except requests.exceptions.RequestException as e: | |
logger.error(f"Error searching for contact: {str(e)}") | |
return None | |
def create_contact(phone_number): | |
""" | |
Create a new contact in HubSpot | |
Args: | |
phone_number (str): Phone number for the new contact | |
Returns: | |
dict or None: Created contact data if successful, None otherwise | |
""" | |
headers = { | |
'Authorization': f'Bearer {HUBSPOT_PRIVATE_APP_TOKEN}', | |
'Content-Type': 'application/json' | |
} | |
payload = { | |
"properties": { | |
"firstname": DEFAULT_NAME, | |
"phone": phone_number | |
} | |
} | |
try: | |
logger.info(f"Creating new contact with phone: {phone_number}") | |
response = requests.post( | |
HUBSPOT_CREATE_CONTACT_ENDPOINT, | |
headers=headers, | |
json=payload | |
) | |
response.raise_for_status() | |
contact = response.json() | |
logger.info(f"Contact created with ID: {contact['id']}") | |
return contact | |
except requests.exceptions.RequestException as e: | |
logger.error(f"Error creating contact: {str(e)}") | |
return None | |
def log_call_activity(contact_id, call_data): | |
""" | |
Log a call activity as a note for a contact in HubSpot | |
Args: | |
contact_id (str): HubSpot contact ID | |
call_data (dict): Call data from Webex | |
Returns: | |
dict or None: Created note data if successful, None otherwise | |
""" | |
headers = { | |
'Authorization': f'Bearer {HUBSPOT_PRIVATE_APP_TOKEN}', | |
'Content-Type': 'application/json' | |
} | |
# Extract call details | |
call_direction = call_data.get('direction', 'Unknown') | |
call_timestamp = call_data.get('timestamp', datetime.now().isoformat()) | |
call_duration = call_data.get('duration', 'N/A') | |
# Format the note content | |
note_content = ( | |
f"Call Type: {call_direction}\n" | |
f"Timestamp: {call_timestamp}\n" | |
f"Duration: {call_duration} seconds\n" | |
) | |
if call_data.get('status'): | |
note_content += f"Status: {call_data.get('status')}\n" | |
payload = { | |
"properties": { | |
"hs_note_body": note_content, | |
"hs_timestamp": datetime.now().timestamp() * 1000, # HubSpot uses milliseconds | |
"hubspot_owner_id": "1", # Default owner ID, update as needed | |
}, | |
"associations": [ | |
{ | |
"to": { | |
"id": contact_id | |
}, | |
"types": [ | |
{ | |
"associationCategory": "HUBSPOT_DEFINED", | |
"associationTypeId": 202 # Contact to Note association | |
} | |
] | |
} | |
] | |
} | |
try: | |
logger.info(f"Logging call activity for contact ID: {contact_id}") | |
response = requests.post( | |
HUBSPOT_ENGAGEMENTS_ENDPOINT, | |
headers=headers, | |
json=payload | |
) | |
response.raise_for_status() | |
note = response.json() | |
logger.info(f"Call activity logged with ID: {note['id']}") | |
return note | |
except requests.exceptions.RequestException as e: | |
logger.error(f"Error logging call activity: {str(e)}") | |
return None | |
def validate_webex_request(request): | |
""" | |
Validate if the request is coming from Webex | |
Args: | |
request: Flask request object | |
Returns: | |
bool: True if valid, False otherwise | |
""" | |
auth_header = request.headers.get('Authorization') | |
# Simple validation - check if token matches | |
if auth_header and auth_header == f'Bearer {WEBEX_BEARER_TOKEN}': | |
return True | |
return False | |
def extract_phone_number(call_data): | |
""" | |
Extract phone number from Webex Calling event data | |
Args: | |
call_data (dict): Call data from Webex | |
Returns: | |
str or None: Phone number if found, None otherwise | |
""" | |
# The actual structure depends on Webex Calling webhook format | |
# This is a simplified example - adjust based on actual payload structure | |
if call_data.get('from') and call_data['from'].get('phoneNumber'): | |
return call_data['from']['phoneNumber'] | |
if call_data.get('callerNumber'): | |
return call_data['callerNumber'] | |
if call_data.get('origin') and call_data['origin'].get('address'): | |
return call_data['origin']['address'] | |
return None | |
@app.route('/webhook/webex-calling', methods=['POST']) | |
def webex_calling_webhook(): | |
""" | |
Webhook endpoint for Webex Calling events | |
""" | |
# Validate request | |
if not validate_webex_request(request): | |
logger.warning("Invalid authentication") | |
return jsonify({"status": "error", "message": "Invalid authentication"}), 401 | |
# Parse the incoming JSON | |
try: | |
call_data = request.json | |
logger.info(f"Received webhook: {json.dumps(call_data, indent=2)}") | |
# Extract phone number from the call data | |
phone_number = extract_phone_number(call_data) | |
if not phone_number: | |
logger.warning("No phone number found in call data") | |
return jsonify({"status": "error", "message": "No phone number found"}), 400 | |
# Search for the contact in HubSpot | |
contact = search_contact_by_phone(phone_number) | |
# If contact doesn't exist, create one | |
if not contact: | |
contact = create_contact(phone_number) | |
if not contact: | |
return jsonify({"status": "error", "message": "Failed to create contact"}), 500 | |
# Log the call activity | |
note = log_call_activity(contact['id'], call_data) | |
if not note: | |
return jsonify({"status": "error", "message": "Failed to log call activity"}), 500 | |
return jsonify({ | |
"status": "success", | |
"message": "Call processed successfully", | |
"contact_id": contact['id'] | |
}) | |
except Exception as e: | |
logger.error(f"Error processing webhook: {str(e)}") | |
return jsonify({"status": "error", "message": str(e)}), 500 | |
@app.route('/health', methods=['GET']) | |
def health_check(): | |
"""Health check endpoint""" | |
return jsonify({"status": "healthy"}), 200 | |
if __name__ == '__main__': | |
# Get port from environment variable for Heroku compatibility | |
port = int(os.environ.get('PORT', 5000)) | |
app.run(host='0.0.0.0', port=port, debug=False) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment