diff --git a/Cargo.lock b/Cargo.lock index a5189b6..3f69a20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1668,6 +1668,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 31ff702..517d023 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,4 @@ reqwest = { version = "0.12.5", features = ["json"] } serde = { version = "1.0.204", features = ["derive"] } serde_json = "1.0.120" tokio = { version = "1.38.0", features = ["full"] } +url = "2.5.2" diff --git a/src/client.rs b/src/client.rs index bcbf697..0a52011 100644 --- a/src/client.rs +++ b/src/client.rs @@ -79,7 +79,9 @@ impl Client { } } - pub async fn get_upload_token(&self) -> anyhow::Result { + pub async fn get_upload_token( + &self, + ) -> anyhow::Result { if self.last_refresh.read().await.elapsed().as_secs() > self.token_valid_time.into() { self.refresh_token().await?; } @@ -162,12 +164,10 @@ impl Client { match serde_json::from_str(&text) { std::result::Result::Ok(json) => Ok(json), - Err(err) => { - match serde_json::from_str::(&text) { - std::result::Result::Ok(json) => Err(anyhow::anyhow!("{:?}", json)), - Err(_) => Err(anyhow::Error::new(err).context(text.trim().to_string())), - } - } + Err(err) => match serde_json::from_str::(&text) { + std::result::Result::Ok(json) => Err(anyhow::anyhow!("{:?}", json)), + Err(_) => Err(anyhow::Error::new(err).context(text.trim().to_string())), + }, } } @@ -302,6 +302,39 @@ pub async fn refresh_token( // .map_err(Into::into) } +pub async fn rid_token_auth(rid_code: &str) -> anyhow::Result { + let client = reqwest::Client::new(); + let request = client + .get(format!("https://www.rakuten-drive.com/api/v1/auth/rd/custom/token?rid_code={}&is_extension=false", rid_code)) + .send() + .await?; + + let text = request.text().await?; + + let json: types::response::RIDTokenResponse = serde_json::from_str(&text)?; + Ok(json) +} + +pub async fn get_refresh_token( + token: &str, +) -> anyhow::Result { + let client = reqwest::Client::new(); + let req = types::request::VerifyCustomTokenRequest { + token: token.to_string(), + return_secure_token: true, + }; + let request = client + .post("https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=AIzaSyDyp5IGr4nXbYin_oduNGi6ci-AnWcuAYE") + .json(&req) + .send() + .await?; + + let text = request.text().await?; + + let json: types::response::VerifyCustomTokenResponse = serde_json::from_str(&text)?; + Ok(json) +} + // https://www.rakuten-drive.com/api/account/refreshtoken POST RefreshTokenRequest RefreshTokenResponse // https://forest.sendy.jp/cloud/service/file/v1/file POST FileDetailRequest FileDetailResponse // https://forest.sendy.jp/cloud/service/file/v1/files POST ListFilesRequest ListFilesResponse diff --git a/src/main.rs b/src/main.rs index 6e9fb2d..6e26073 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,6 +106,7 @@ enum Commands { }, #[clap(about = "Print file detail")] Info { path: String }, + Auth {}, } #[tokio::main] @@ -140,8 +141,8 @@ async fn main() -> anyhow::Result<()> { Commands::Mkdir { name, path } => { client.mkdir(name, path.as_deref()).await.unwrap(); } - Commands::Copy { src: _, dest: _ } => { - todo!("Copy"); + Commands::Copy { src, dest } => { + client.copy(src, dest).await.unwrap(); } Commands::Rename { path, name } => { client.rename(path, name).await.unwrap(); @@ -149,6 +150,29 @@ async fn main() -> anyhow::Result<()> { Commands::Info { path } => { client.info(path).await.unwrap(); } + Commands::Auth {} => { + println!("Click the link below to authorize the app:\n"); + let link = "https://login.account.rakuten.com/sso/authorize?response_type=code&client_id=rakuten_drive_web&redirect_uri=https://www.rakuten-drive.com/oauth-callback&scope=openid+profile+email&prompt=login&ui_locales=en"; + println!("{}\n", link); + + println!("Paste the URL you were redirected to:"); + let mut auth_url = String::new(); + std::io::stdin().read_line(&mut auth_url).unwrap(); + let auth_url = url::Url::parse(auth_url.trim())?; + + let params = auth_url.query_pairs().collect::>(); + + let rid_code = params + .iter() + .find(|(key, _)| key == "code") + .map(|(_, value)| value.to_string()) + .ok_or_else(|| anyhow::anyhow!("Code not found in URL"))?; + + let rid_token_auth_res = client::rid_token_auth(rid_code.as_str()).await?; + let token_verify_res = client::get_refresh_token(&rid_token_auth_res.custom_token).await?; + + println!("Refresh token: {}", token_verify_res.refresh_token); + } } Ok(()) diff --git a/src/types/request.rs b/src/types/request.rs index d87800a..ffbec9c 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -151,4 +151,11 @@ pub struct CopyFileRequestFile { pub path: String, pub size: i64, pub version_id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifyCustomTokenRequest { + pub return_secure_token: bool, + pub token: String, } \ No newline at end of file diff --git a/src/types/response.rs b/src/types/response.rs index 2393071..06de32f 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -176,3 +176,20 @@ pub enum SendyErrorType { SendyErrShareUpwardShareExist, SendyErrShareFolderIncludedOrInclude, } + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct RIDTokenResponse { + pub custom_token: String, + pub is_new_user: String, // "false" +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifyCustomTokenResponse { + pub kind: String, + pub id_token: String, + pub refresh_token: String, + pub expires_in: String, + pub is_new_user: bool, +} \ No newline at end of file