cargo fmt

This commit is contained in:
Jonas Rabenstein 2026-08-13 13:39:36 +02:00
commit e37a2d9263
6 changed files with 55 additions and 127 deletions

View file

@ -54,9 +54,7 @@ impl IntervalsRateLimiters {
.expect("Intervals IP limiter mutex poisoned"); .expect("Intervals IP limiter mutex poisoned");
let next = match *last { let next = match *last {
Some(previous) => { Some(previous) => std::cmp::max(now, previous + IP_REQUEST_INTERVAL),
std::cmp::max(now, previous + IP_REQUEST_INTERVAL)
}
None => now, None => now,
}; };
@ -183,15 +181,13 @@ impl Config {
cookie_secure, cookie_secure,
intervals_rate_limiters: intervals_rate_limiters: IntervalsRateLimiters::default(),
IntervalsRateLimiters::default(),
}) })
} }
} }
fn required(name: &str) -> Result<String> { fn required(name: &str) -> Result<String> {
let value = env::var(name) let value = env::var(name).with_context(|| format!("{name} is not configured"))?;
.with_context(|| format!("{name} is not configured"))?;
if value.trim().is_empty() { if value.trim().is_empty() {
anyhow::bail!("{name} is empty"); anyhow::bail!("{name} is empty");
@ -201,9 +197,7 @@ fn required(name: &str) -> Result<String> {
} }
fn optional(name: &str) -> Option<String> { fn optional(name: &str) -> Option<String> {
env::var(name) env::var(name).ok().filter(|value| !value.trim().is_empty())
.ok()
.filter(|value| !value.trim().is_empty())
} }
#[cfg(test)] #[cfg(test)]
@ -221,9 +215,7 @@ mod tests {
limiters.wait_for_ip_slot().await; limiters.wait_for_ip_slot().await;
} }
assert!( assert!(Instant::now().duration_since(start) >= Duration::from_millis(190));
Instant::now().duration_since(start) >= Duration::from_millis(190)
);
} }
#[tokio::test] #[tokio::test]
@ -234,14 +226,10 @@ mod tests {
let start = Instant::now(); let start = Instant::now();
limiters.wait_for_caller("caller-a").await; limiters.wait_for_caller("caller-a").await;
assert!( assert!(Instant::now().duration_since(start) >= Duration::from_millis(70));
Instant::now().duration_since(start) >= Duration::from_millis(70)
);
let start = Instant::now(); let start = Instant::now();
limiters.wait_for_caller("caller-b").await; limiters.wait_for_caller("caller-b").await;
assert!( assert!(Instant::now().duration_since(start) < Duration::from_millis(40));
Instant::now().duration_since(start) < Duration::from_millis(40)
);
} }
} }

View file

@ -232,7 +232,6 @@ pub async fn upsert_oauth_athlete(
Ok(row.try_get("id")?) Ok(row.try_get("id")?)
} }
/// Ensure that an athlete accessible through the current session exists /// Ensure that an athlete accessible through the current session exists
/// locally so activities can reference it. The session credential itself is /// locally so activities can reference it. The session credential itself is
/// intentionally not stored here; callers continue to use their authenticated /// intentionally not stored here; callers continue to use their authenticated

View file

@ -20,10 +20,7 @@ pub struct IntervalsClient {
impl IntervalsClient { impl IntervalsClient {
pub fn new(config: Config) -> Self { pub fn new(config: Config) -> Self {
let authentication = config let authentication = config.intervals_api_key.clone().map(Authentication::ApiKey);
.intervals_api_key
.clone()
.map(Authentication::ApiKey);
Self { Self {
client: Client::new(), client: Client::new(),
@ -43,10 +40,7 @@ impl IntervalsClient {
/// Create a client using an OAuth access token. /// Create a client using an OAuth access token.
/// Requests are authenticated with HTTP Bearer auth. /// Requests are authenticated with HTTP Bearer auth.
pub fn with_access_token( pub fn with_access_token(config: Config, access_token: impl Into<String>) -> Self {
config: Config,
access_token: impl Into<String>,
) -> Self {
Self { Self {
client: Client::new(), client: Client::new(),
config, config,
@ -70,9 +64,7 @@ impl IntervalsClient {
.intervals_client_id .intervals_client_id
.clone() .clone()
.unwrap_or_else(|| token.clone())), .unwrap_or_else(|| token.clone())),
None => Err(anyhow!( None => Err(anyhow!("Intervals.icu authentication is not configured")),
"Intervals.icu authentication is not configured"
)),
} }
} }
@ -84,29 +76,17 @@ impl IntervalsClient {
) )
} }
fn authenticated( fn authenticated(&self, request: reqwest::RequestBuilder) -> Result<reqwest::RequestBuilder> {
&self,
request: reqwest::RequestBuilder,
) -> Result<reqwest::RequestBuilder> {
match self.authentication.as_ref() { match self.authentication.as_ref() {
Some(Authentication::ApiKey(token)) => { Some(Authentication::ApiKey(token)) => Ok(request.basic_auth("API_KEY", Some(token))),
Ok(request.basic_auth("API_KEY", Some(token))) Some(Authentication::Bearer(token)) => Ok(request.bearer_auth(token)),
} None => Err(anyhow!("Intervals.icu authentication is not configured")),
Some(Authentication::Bearer(token)) => {
Ok(request.bearer_auth(token))
}
None => Err(anyhow!(
"Intervals.icu authentication is not configured"
)),
} }
} }
async fn get_json(&self, url: String) -> Result<Value> { async fn get_json(&self, url: String) -> Result<Value> {
let caller = self.rate_limit_caller()?; let caller = self.rate_limit_caller()?;
let limiters = self let limiters = self.config.intervals_rate_limiters.clone();
.config
.intervals_rate_limiters
.clone();
// Intervals.icu enforces an additional limit of ten calls/sec per // Intervals.icu enforces an additional limit of ten calls/sec per
// source IP. Pace every request start globally for this process. // source IP. Pace every request start globally for this process.
@ -126,10 +106,8 @@ impl IntervalsClient {
.context("Intervals.icu request failed")?; .context("Intervals.icu request failed")?;
let status = response.status(); let status = response.status();
let retry_after = let retry_after = parse_retry_after(response.headers().get("Retry-After"));
parse_retry_after(response.headers().get("Retry-After")); let rate_limit = parse_rate_limit_headers(response.headers());
let rate_limit =
parse_rate_limit_headers(response.headers());
if status == reqwest::StatusCode::TOO_MANY_REQUESTS { if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
let body = response let body = response
@ -173,11 +151,7 @@ impl IntervalsClient {
.context("cannot read Intervals.icu response")?; .context("cannot read Intervals.icu response")?;
if !status.is_success() { if !status.is_success() {
return Err(anyhow!( return Err(anyhow!("Intervals.icu returned HTTP {}: {}", status, body));
"Intervals.icu returned HTTP {}: {}",
status,
body
));
} }
tracing::debug!( tracing::debug!(
@ -190,12 +164,7 @@ impl IntervalsClient {
); );
return serde_json::from_str(&body) return serde_json::from_str(&body)
.with_context(|| { .with_context(|| format!("invalid JSON returned by Intervals.icu: {}", body));
format!(
"invalid JSON returned by Intervals.icu: {}",
body
)
});
} }
unreachable!("rate-limit retry loop must return") unreachable!("rate-limit retry loop must return")
@ -519,10 +488,7 @@ impl IntervalsClient {
} }
} }
fn parse_retry_after(value: Option<&reqwest::header::HeaderValue>) -> Option<std::time::Duration> {
fn parse_retry_after(
value: Option<&reqwest::header::HeaderValue>,
) -> Option<std::time::Duration> {
let value = value?.to_str().ok()?.trim(); let value = value?.to_str().ok()?.trim();
let seconds = value.parse::<u64>().ok()?; let seconds = value.parse::<u64>().ok()?;
Some(std::time::Duration::from_secs(seconds)) Some(std::time::Duration::from_secs(seconds))
@ -537,9 +503,7 @@ fn parse_retry_after(
fn parse_rate_limit_headers( fn parse_rate_limit_headers(
headers: &reqwest::header::HeaderMap, headers: &reqwest::header::HeaderMap,
) -> (Option<u64>, Option<u64>, Option<u64>, Option<u64>) { ) -> (Option<u64>, Option<u64>, Option<u64>, Option<u64>) {
fn pair( fn pair(value: Option<&reqwest::header::HeaderValue>) -> (Option<u64>, Option<u64>) {
value: Option<&reqwest::header::HeaderValue>,
) -> (Option<u64>, Option<u64>) {
let Some(value) = value else { let Some(value) = value else {
return (None, None); return (None, None);
}; };
@ -556,17 +520,10 @@ fn parse_rate_limit_headers(
(first, second) (first, second)
} }
let (limit_15m, limit_daily) = let (limit_15m, limit_daily) = pair(headers.get("X-RateLimit-Limit"));
pair(headers.get("X-RateLimit-Limit")); let (remaining_15m, remaining_daily) = pair(headers.get("X-RateLimit-Remaining"));
let (remaining_15m, remaining_daily) =
pair(headers.get("X-RateLimit-Remaining"));
( (limit_15m, limit_daily, remaining_15m, remaining_daily)
limit_15m,
limit_daily,
remaining_15m,
remaining_daily,
)
} }
fn parse_datetime(value: &str) -> Result<DateTime<Utc>> { fn parse_datetime(value: &str) -> Result<DateTime<Utc>> {

View file

@ -36,7 +36,10 @@ pub async fn api(
build(&state.db, query.group_id, offset, limit) build(&state.db, query.group_id, offset, limit)
.await .await
.map(|groups| { .map(|groups| {
let returned = groups.iter().map(|group| group.rows.len() as i64).sum::<i64>(); let returned = groups
.iter()
.map(|group| group.rows.len() as i64)
.sum::<i64>();
Json(LeaderboardPage { Json(LeaderboardPage {
groups, groups,
next_offset: offset + returned, next_offset: offset + returned,
@ -52,7 +55,12 @@ pub async fn api(
}) })
} }
pub async fn build(db: &PgPool, group_id: Option<i64>, offset: i64, limit: i64) -> Result<Vec<LeaderboardGroup>> { pub async fn build(
db: &PgPool,
group_id: Option<i64>,
offset: i64,
limit: i64,
) -> Result<Vec<LeaderboardGroup>> {
let rows = sqlx::query( let rows = sqlx::query(
r#" r#"
WITH bucketed AS ( WITH bucketed AS (

View file

@ -162,15 +162,11 @@ async fn sync_index(
jar: axum_extra::extract::cookie::CookieJar, jar: axum_extra::extract::cookie::CookieJar,
) -> Result<Html<String>, (axum::http::StatusCode, String)> { ) -> Result<Html<String>, (axum::http::StatusCode, String)> {
let session = current_session_athlete(&state, &jar) let session = current_session_athlete(&state, &jar)
.await
.map_err(http_error)?;
let client =
IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone());
let athletes = client
.athletes()
.await .await
.map_err(http_error)?; .map_err(http_error)?;
let client = IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone());
let athletes = client.athletes().await.map_err(http_error)?;
let athletes = athletes let athletes = athletes
.as_array() .as_array()
.ok_or_else(|| http_error(anyhow!("athletes response is not an array")))?; .ok_or_else(|| http_error(anyhow!("athletes response is not an array")))?;
@ -310,13 +306,9 @@ async fn sync_athlete(
*/ */
let client = IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone()); let client = IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone());
let target_display_name = ensure_accessible_athlete( let target_display_name = ensure_accessible_athlete(&state, &client, &athlete_id)
&state, .await
&client, .map_err(http_error)?;
&athlete_id,
)
.await
.map_err(http_error)?;
let target_athlete_id = local_athlete(&state.db, &athlete_id) let target_athlete_id = local_athlete(&state.db, &athlete_id)
.await .await
@ -770,13 +762,9 @@ async fn sync_import_activity(
let client = IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone()); let client = IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone());
ensure_accessible_athlete( ensure_accessible_athlete(&state, &client, &athlete_id)
&state, .await
&client, .map_err(http_error)?;
&athlete_id,
)
.await
.map_err(http_error)?;
match process_activity_with_client( match process_activity_with_client(
state.clone(), state.clone(),
@ -876,13 +864,9 @@ async fn sync_import_visible(
let client = IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone()); let client = IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone());
ensure_accessible_athlete( ensure_accessible_athlete(&state, &client, &athlete_id)
&state, .await
&client, .map_err(http_error)?;
&athlete_id,
)
.await
.map_err(http_error)?;
let target_athlete_id = local_athlete(&state.db, &athlete_id) let target_athlete_id = local_athlete(&state.db, &athlete_id)
.await .await
@ -975,15 +959,12 @@ async fn ensure_accessible_athlete(
client: &IntervalsClient, client: &IntervalsClient,
athlete_id: &str, athlete_id: &str,
) -> Result<String> { ) -> Result<String> {
let athlete = client let athlete = client.athlete(athlete_id).await.with_context(|| {
.athlete(athlete_id) format!(
.await "cannot access Intervals.icu athlete {} with current authentication token",
.with_context(|| { athlete_id
format!( )
"cannot access Intervals.icu athlete {} with current authentication token", })?;
athlete_id
)
})?;
let display_name = athlete let display_name = athlete
.get("name") .get("name")
@ -992,12 +973,7 @@ async fn ensure_accessible_athlete(
.unwrap_or(athlete_id) .unwrap_or(athlete_id)
.to_string(); .to_string();
db::ensure_local_athlete( db::ensure_local_athlete(&state.db, athlete_id, &display_name).await?;
&state.db,
athlete_id,
&display_name,
)
.await?;
Ok(display_name) Ok(display_name)
} }

View file

@ -728,7 +728,7 @@ function initLeaderboardElements(root){
const leaderboardList=document.getElementById('leaderboard-list'); const leaderboardList=document.getElementById('leaderboard-list');
if(leaderboardList){ if(leaderboardList){
const status=document.getElementById('leaderboard-status'),sentinel=document.getElementById('leaderboard-sentinel'),groups=new Map(); const status=document.getElementById('leaderboard-status'),sentinel=document.getElementById('leaderboard-sentinel'),groups=new Map();
let offset=0,loading=false,hasMore=true; const pageSize=50; let offset=0,loading=false,hasMore=true; const pageSize=25;
async function loadLeaderboard(){ async function loadLeaderboard(){
if(loading||!hasMore)return; loading=true; if(loading||!hasMore)return; loading=true;
try{ try{