use anyhow::{Context, Result, bail};
use reqwest::Client;
use serde::Deserialize;
use serde_json::json;
use tracing::info;
use super::{display_qr_terminal, load_token, save_token};
const AUTH_URL: &str = "https://login.dingtalk.com/oauth2/auth";
const TOKEN_URL: &str = "https://api.dingtalk.com/v1.0/oauth2/userAccessToken";
const CORP_TOKEN_URL: &str = "https://oapi.dingtalk.com/gettoken";
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct CorpTokenResponse {
access_token: Option<String>,
expires_in: Option<u64>,
errcode: Option<i64>,
errmsg: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct UserTokenResponse {
access_token: Option<String>,
refresh_token: Option<String>,
expires_in: Option<u64>,
corp_id: Option<String>,
}
pub async fn login(
client: &Client,
app_key: &str,
app_secret: &str,
redirect_uri: Option<&str>,
) -> Result<String> {
if let Some(saved) = load_token("dingtalk")
&& let Some(token) = saved.get("access_token").and_then(|v| v.as_str())
{
info!("using saved DingTalk token");
return Ok(token.to_owned());
}
let redirect = redirect_uri.unwrap_or("https://oapi.dingtalk.com/connect/oauth2/sns_authorize");
let auth_url = format!(
"{AUTH_URL}?client_id={app_key}&response_type=code&scope=openid&redirect_uri={redirect}&state=rsclaw&prompt=consent"
);
println!("=== DingTalk Login ===");
display_qr_terminal(&auth_url)?;
println!("Scan with DingTalk app, then paste the authorization code below.");
print!("Authorization code: ");
use std::io::Write;
std::io::stdout().flush()?;
let mut code = String::new();
std::io::stdin().read_line(&mut code)?;
let code = code.trim().to_owned();
if code.is_empty() {
bail!("no authorization code provided");
}
let resp = client
.post(TOKEN_URL)
.header("Content-Type", "application/json")
.json(&json!({
"clientId": app_key,
"clientSecret": app_secret,
"code": code,
"grantType": "authorization_code",
}))
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
bail!("DingTalk token exchange failed: {status} {body}");
}
let data: UserTokenResponse = resp.json().await?;
let access_token = data
.access_token
.context("no access_token in DingTalk response")?;
info!(
corp_id = data.corp_id.as_deref().unwrap_or("?"),
"DingTalk login successful"
);
save_token(
"dingtalk",
&json!({
"access_token": access_token,
"refresh_token": data.refresh_token,
"expires_in": data.expires_in,
"corp_id": data.corp_id,
}),
)?;
Ok(access_token)
}
pub async fn get_corp_token(client: &Client, app_key: &str, app_secret: &str) -> Result<String> {
let resp: CorpTokenResponse = client
.get(CORP_TOKEN_URL)
.query(&[("appkey", app_key), ("appsecret", app_secret)])
.send()
.await?
.json()
.await?;
if resp.errcode.unwrap_or(0) != 0 {
bail!(
"DingTalk corp token failed: {} (code {})",
resp.errmsg.as_deref().unwrap_or("unknown"),
resp.errcode.unwrap_or(-1)
);
}
resp.access_token.context("no access_token in response")
}