This commit is contained in:
Jonas Rabenstein 2026-08-12 13:24:01 +02:00
commit a9d22e8312
18 changed files with 2308 additions and 0 deletions

81
src/webhook.rs Normal file
View file

@ -0,0 +1,81 @@
use axum::{
extract::State,
http::StatusCode,
Json,
};
use crate::{
AppState,
model::WebhookEnvelope,
};
pub async fn receive(
State(state): State<AppState>,
Json(payload): Json<WebhookEnvelope>,
) -> Result<&'static str, StatusCode> {
if payload.secret != state.config.intervals_webhook_secret {
tracing::warn!("invalid Intervals.icu webhook secret");
return Err(StatusCode::UNAUTHORIZED);
}
for event in payload.events {
match event.event_type.as_str() {
"ACTIVITY_UPLOADED" |
"ACTIVITY_ANALYZED" => {
let activity_id = event
.activity
.as_ref()
.and_then(|activity| {
activity.get("id")
})
.and_then(|id| {
id.as_str()
});
let Some(activity_id) = activity_id else {
tracing::warn!(
athlete_id = %event.athlete_id,
"activity webhook without activity id"
);
continue;
};
if let Err(error) =
crate::process_activity(
state.clone(),
event.athlete_id.clone(),
activity_id.to_owned(),
).await
{
tracing::error!(
%error,
athlete_id = %event.athlete_id,
activity_id = %activity_id,
"activity processing failed"
);
// Return 500 so Intervals.icu can retry the webhook.
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
}
"APP_SCOPE_CHANGED" => {
tracing::info!(
athlete_id = %event.athlete_id,
"OAuth scopes changed"
);
}
_ => {
tracing::debug!(
event_type = %event.event_type,
"ignoring Intervals.icu webhook"
);
}
}
}
// Intervals.icu has historically retried on 204; use an ordinary 200.
Ok("ok")
}