diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eca742a..a98f8ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -224,8 +224,6 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max release: name: Create Release diff --git a/Cargo.lock b/Cargo.lock index 72ec7e3..e4649c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -972,9 +972,9 @@ dependencies = [ [[package]] name = "euclid" -version = "0.22.11" +version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" dependencies = [ "num-traits", ] @@ -2083,7 +2083,9 @@ version = "0.2.10" dependencies = [ "reqwest", "serde", + "serde_json", "thiserror 1.0.69", + "tracing", ] [[package]] @@ -4631,6 +4633,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/ndl-core/Cargo.toml b/ndl-core/Cargo.toml index da511e3..60afabc 100644 --- a/ndl-core/Cargo.toml +++ b/ndl-core/Cargo.toml @@ -10,4 +10,6 @@ description = "Shared library for ndl and ndld" [dependencies] reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } serde = { version = "1", features = ["derive"] } +serde_json = "1" thiserror = "1" +tracing = "0.1" diff --git a/ndl-core/src/oauth.rs b/ndl-core/src/oauth.rs index 8966ee7..3cd20c6 100644 --- a/ndl-core/src/oauth.rs +++ b/ndl-core/src/oauth.rs @@ -5,8 +5,8 @@ pub const TOKEN_URL: &str = "https://graph.threads.net/oauth/access_token"; pub const OAUTH_SCOPES: &str = "threads_basic,threads_read_replies,threads_manage_replies,threads_content_publish"; -/// Deserialize user_id from either a string or number (Threads API returns both) -fn deserialize_user_id<'de, D>(deserializer: D) -> Result +/// Deserialize user_id from either a string or number (Threads API returns both), or None if missing +fn deserialize_user_id_opt<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { @@ -17,9 +17,11 @@ where Number(u64), } - match StringOrNumber::deserialize(deserializer)? { - StringOrNumber::String(s) => s.parse().map_err(de::Error::custom), - StringOrNumber::Number(n) => Ok(n), + let opt: Option = Option::deserialize(deserializer)?; + match opt { + Some(StringOrNumber::String(s)) => s.parse().map(Some).map_err(de::Error::custom), + Some(StringOrNumber::Number(n)) => Ok(Some(n)), + None => Ok(None), } } @@ -27,8 +29,8 @@ where pub struct TokenResponse { pub access_token: String, #[allow(dead_code)] - #[serde(deserialize_with = "deserialize_user_id")] - pub user_id: u64, + #[serde(default, deserialize_with = "deserialize_user_id_opt")] + pub user_id: Option, /// Number of seconds until the token expires (3600 for short-lived, 5184000 for long-lived) #[serde(default)] pub expires_in: Option, @@ -44,6 +46,21 @@ pub enum TokenExchangeError { Parse(String), } +/// Parse response body as TokenResponse, logging body at debug level on failure +async fn parse_token_response( + response: reqwest::Response, +) -> Result { + let body = response + .text() + .await + .map_err(|e| TokenExchangeError::Parse(e.to_string()))?; + + serde_json::from_str(&body).map_err(|e| { + tracing::debug!(response_body = %body, "Failed to parse token response"); + TokenExchangeError::Parse(e.to_string()) + }) +} + /// Exchange an authorization code for an access token pub async fn exchange_code( client_id: &str, @@ -74,10 +91,7 @@ pub async fn exchange_code( return Err(TokenExchangeError::Http { status, body }); } - response - .json::() - .await - .map_err(|e| TokenExchangeError::Parse(e.to_string())) + parse_token_response(response).await } /// Exchange a short-lived access token for a long-lived one (60 days) @@ -104,10 +118,7 @@ pub async fn exchange_for_long_lived_token( return Err(TokenExchangeError::Http { status, body }); } - response - .json::() - .await - .map_err(|e| TokenExchangeError::Parse(e.to_string())) + parse_token_response(response).await } /// Refresh a long-lived access token (extends validity by another 60 days) @@ -133,8 +144,5 @@ pub async fn refresh_access_token( return Err(TokenExchangeError::Http { status, body }); } - response - .json::() - .await - .map_err(|e| TokenExchangeError::Parse(e.to_string())) + parse_token_response(response).await } diff --git a/ndl/src/oauth.rs b/ndl/src/oauth.rs index e7b3d3f..3ea8b47 100644 --- a/ndl/src/oauth.rs +++ b/ndl/src/oauth.rs @@ -296,7 +296,7 @@ pub async fn hosted_login(auth_server: &str) -> Result