Skip to content

Commit

Permalink
push notifications on training week update
Browse files Browse the repository at this point in the history
  • Loading branch information
voynow committed Nov 1, 2024
1 parent 961ac1a commit e169131
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 45 deletions.
57 changes: 27 additions & 30 deletions src/apn.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import base64
import logging
import os
import time

Expand All @@ -9,6 +10,10 @@
load_dotenv()


logger = logging.getLogger()
logger.setLevel(logging.INFO)


def generate_jwt_token(key_id: str, team_id: str, private_key: str) -> str:
"""
Generate JWT token for APNs authentication.
Expand All @@ -23,55 +28,47 @@ def generate_jwt_token(key_id: str, team_id: str, private_key: str) -> str:
return jwt.encode(payload, private_key, algorithm="ES256", headers=headers)


def send_push_notification(
device_token: str, payload: dict, auth_token: str, use_sandbox: bool = False
) -> None:
def send_push_notification(device_token: str, title: str, body: str) -> dict:
"""
Send a push notification to a device token.
Send a push notification to a user's device.
Args:
device_token: User's device token
payload: Notification payload
auth_token: APNs JWT token
use_sandbox: If True, uses sandbox environment
:param device_token: User's device token
:param title: Notification title
:param body: Notification body
"""
base_url = "api.sandbox.push.apple.com" if use_sandbox else "api.push.apple.com"
# URL encode the device token and ensure no spaces
auth_token = generate_jwt_token(
key_id=os.environ["APN_KEY_ID"],
team_id=os.environ["APN_TEAM_ID"],
private_key=base64.b64decode(os.environ["APN_PRIVATE_KEY"]).decode(),
)

base_url = "api.push.apple.com"
clean_token = device_token.strip().replace(" ", "")
url = f"https://{base_url}/3/device/{clean_token}"
headers = {
"Authorization": f"Bearer {auth_token}",
"apns-topic": "voynow.mobile",
"content-type": "application/json",
}
payload = {
"aps": {
"alert": {"title": title, "body": body},
"sound": "default",
}
}

try:
client = httpx.Client(http2=True, verify=True, timeout=30.0)
response = client.post(url, json=payload, headers=headers)
response.raise_for_status()
print("Notification sent successfully")
client.close()

except httpx.RequestError as e:
print(f"Connection error: {e}")
logging.error(f"Connection error: {e}")
raise
except httpx.HTTPStatusError as e:
except httpx.HTTPStatusError:
error_payload = response.json() if response.content else "No error details"
print(f"APNs error: {response.status_code}, {error_payload}")
logging.error(f"APNs error: {response.status_code}, {error_payload}")
raise ValueError(f"APNs rejected the request: {error_payload}")


auth_token = generate_jwt_token(
key_id=os.environ["APN_KEY_ID"],
team_id=os.environ["APN_TEAM_ID"],
private_key=base64.b64decode(os.environ["APN_PRIVATE_KEY"]).decode(),
)

device_token = "..."
payload = {
"aps": {
"alert": {"title": "Hello", "body": "This is a test notification"},
"sound": "default",
}
}
send_push_notification(device_token, payload, auth_token)
return response.json()
1 change: 0 additions & 1 deletion src/frontend_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ def start_onboarding(athlete_id: str, payload: dict) -> dict:

def update_device_token_handler(athlete_id: str, payload: dict) -> dict:
"""Handle update_device_token request."""
print(payload)
if not payload or "device_token" not in payload:
return {"success": False, "error": "Missing device_token in payload"}
try:
Expand Down
22 changes: 9 additions & 13 deletions src/update_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@
get_day_of_week_summaries,
get_weekly_summaries,
)
from src.apn import send_push_notification
from src.auth_manager import get_strava_client
from src.constants import COACH_ROLE
from src.email_manager import send_alert_email, send_email, training_week_to_html
from src.email_manager import send_alert_email
from src.mid_week_update import generate_mid_week_update
from src.new_training_week import generate_new_training_week
from src.supabase_client import (
get_training_week,
get_training_week_test,
get_user,
get_user_auth,
has_user_updated_today,
list_users,
upsert_training_week,
Expand All @@ -37,24 +39,21 @@
def training_week_update_pipeline(
user: UserRow,
pipeline_function: Callable[[UserRow, Client], TrainingWeek],
email_subject: str = "TrackFlow 🏃‍♂️🎯",
upsert_training_week: Callable[
[int, TrainingWeek], APIResponse
] = upsert_training_week,
send_email: Callable[[Dict[str, str], str, str], None] = send_email,
) -> TrainingWeek:
"""General processing for training week updates."""
strava_client = get_strava_client(user.athlete_id)
athlete = strava_client.get_athlete()
training_week = pipeline_function(user=user, strava_client=strava_client)

upsert_training_week(user.athlete_id, training_week)

if user.email:
send_email(
to={"email": user.email, "name": f"{athlete.firstname} {athlete.lastname}"},
subject=email_subject,
html_content=training_week_to_html(training_week),
user_auth = get_user_auth(user.athlete_id)
if user_auth.device_token:
send_push_notification(
device_token=user_auth.device_token,
title="TrackFlow 🏃‍♂️🎯",
body="Your training week has been updated!",
)
return training_week

Expand Down Expand Up @@ -105,7 +104,6 @@ def webhook_executor(user: UserRow) -> dict:
training_week_update_pipeline(
user=user,
pipeline_function=mid_week_update_pipeline,
email_subject="TrackFlow Update Inbound! 🏃‍♂️🎯",
)
return {"success": True}

Expand All @@ -122,13 +120,11 @@ def training_week_update_executor(
training_week_update_pipeline(
user=user,
pipeline_function=new_training_week_pipeline,
email_subject="Training Schedule Just Dropped 🏃‍♂️🎯",
)
elif exetype == ExeType.MID_WEEK:
training_week_update_pipeline(
user=user,
pipeline_function=mid_week_update_pipeline,
email_subject="TrackFlow Update Inbound! 🏃‍♂️🎯",
)
except Exception as e:
error_msg = f"{invocation_id=} | Error processing user {user.athlete_id} | {str(e)}\nTraceback: {traceback.format_exc()}"
Expand Down
1 change: 0 additions & 1 deletion tests/test_full_week.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ def wrapped(user: UserRow) -> TrainingWeek:
user=user,
pipeline_function=func,
upsert_training_week=upsert_training_week_test,
send_email=lambda *args, **kwargs: None,
)
return training_week

Expand Down

0 comments on commit e169131

Please sign in to comment.