helloasso-paheko-adapter/src/main.rs

206 lines
6.1 KiB
Rust
Raw Normal View History

2024-01-13 16:51:06 +00:00
#![feature(slice_group_by)]
2023-12-21 16:31:21 +00:00
mod utils;
mod paheko;
mod helloasso;
2024-01-13 16:51:06 +00:00
mod sync_helloasso;
mod sync_csv;
mod sync_paheko;
#[cfg(test)]
mod test_utils;
use thiserror::Error;
use anyhow::{Context, Result, anyhow};
2023-11-09 08:14:14 +00:00
use strum::Display;
use serde::{Serialize, Deserialize};
use url::Url;
2024-01-13 16:51:06 +00:00
use fully_pub::fully_pub;
2024-01-13 16:51:23 +00:00
use argh::FromArgs;
use utils::{parse_normalize_phone, normalize_str, parse_and_get_birthday_year};
2023-11-03 17:44:11 +00:00
/// permanent config to store long-term config
/// used to ingest env settings
2023-12-28 10:40:38 +00:00
/// config loaded from env variables
2023-11-03 17:44:11 +00:00
#[derive(Deserialize, Serialize, Debug)]
2024-01-13 16:51:06 +00:00
#[fully_pub]
2023-11-03 17:44:11 +00:00
struct Config {
helloasso_proxy: Option<String>,
2023-11-03 17:44:11 +00:00
helloasso_email: String,
2023-12-26 15:09:49 +00:00
helloasso_password: String,
helloasso_organization_slug: String,
helloasso_form_name: String,
paheko_proxy: Option<String>,
2023-12-26 15:09:49 +00:00
paheko_base_url: String,
paheko_client_id: String,
paheko_client_secret: String,
paheko_target_activity_name: String,
2024-01-13 16:51:06 +00:00
// paheko_accounting_year_id: u64,
2023-11-03 17:44:11 +00:00
}
// start user cache management
use std::fs;
#[derive(Serialize, Deserialize, Debug)]
2024-01-13 16:51:06 +00:00
#[fully_pub]
2023-11-03 17:44:11 +00:00
struct UserCache {
helloasso_session: Option<helloasso::WebSession>
2023-11-03 17:44:11 +00:00
}
#[derive(Display, Debug, Error)]
#[strum(serialize_all = "snake_case")]
enum LoadError {
XDG,
Fs,
FailedToParse,
FailedToEncode,
FailedToCreate,
FailedToWrite
}
const APP_USER_AGENT: &str = "helloasso_paheko_adapter";
2023-11-03 17:44:11 +00:00
fn write_user_cache(cache: &UserCache) -> Result<(), LoadError> {
let xdg_dirs = xdg::BaseDirectories::with_prefix(env!("CARGO_PKG_NAME"))
2023-12-28 10:40:38 +00:00
.map_err(|_e| { LoadError::XDG })?;
let user_cache_path = xdg_dirs.place_cache_file("session.json").map_err(|_e| { LoadError::FailedToCreate })?;
let encoded_cache = serde_json::to_string(&cache).map_err(|_e| { LoadError::FailedToEncode })?;
fs::write(user_cache_path, encoded_cache.as_str()).map_err(|_e| { LoadError::FailedToWrite })?;
2023-11-03 17:44:11 +00:00
Ok(())
}
fn load_user_cache() -> Result<UserCache, LoadError> {
let xdg_dirs = xdg::BaseDirectories::with_prefix(env!("CARGO_PKG_NAME"))
2023-12-28 10:40:38 +00:00
.map_err(|_e| { LoadError::XDG })?;
2023-11-03 17:44:11 +00:00
let user_cache_path = xdg_dirs.get_cache_file("session.json");
if !user_cache_path.exists() {
let default_cache = UserCache {
helloasso_session: None
};
write_user_cache(&default_cache)?;
}
2023-12-28 10:40:38 +00:00
let session_content = fs::read_to_string(user_cache_path).map_err(|_e| { LoadError::Fs })?;
let cache: UserCache = serde_json::from_str(&session_content).map_err(|_e| { LoadError::FailedToParse })?;
2023-11-03 17:44:11 +00:00
Ok(cache)
}
// TODO: find a better way to have the logic implemented
async fn get_auth_client_from_cache(
user_cache: &mut UserCache,
ha_client: &mut helloasso::Client,
login_payload: helloasso::LoginPayload
) -> Result<helloasso::AuthentifiedClient> {
async fn login(
user_cache: &mut UserCache,
ha_client: &mut helloasso::Client,
login_payload: helloasso::LoginPayload
) -> Result<helloasso::AuthentifiedClient> {
2023-11-03 17:44:11 +00:00
let auth_client = ha_client.login(
login_payload
).await.context("Failed to login")?;
user_cache.helloasso_session = Some(auth_client.session.clone());
write_user_cache(&user_cache).expect("unable to write user cache");
println!("Logged in and wrote token to cache");
Ok(auth_client)
}
match &user_cache.helloasso_session {
Some(cached_session) => {
let auth_client = ha_client.authentified_client(cached_session.clone());
if !auth_client.verify_auth().await? {
println!("Need to relog, token invalid");
2023-12-28 10:40:38 +00:00
return login(user_cache, ha_client, login_payload).await
2023-11-03 17:44:11 +00:00
}
println!("Used anterior token");
2023-12-28 10:40:38 +00:00
Ok(auth_client)
2023-11-03 17:44:11 +00:00
},
None => {
println!("First time login");
2023-12-28 10:40:38 +00:00
login(user_cache, ha_client, login_payload).await
2023-11-03 17:44:11 +00:00
}
2023-12-28 10:40:38 +00:00
}
2023-11-03 17:44:11 +00:00
}
2024-01-13 16:51:06 +00:00
fn get_proxy_from_url(proxy_url: &Option<String>) -> Result<Option<reqwest::Proxy>> {
Ok(match proxy_url {
2024-01-13 16:51:06 +00:00
Some(p) => Some(reqwest::Proxy::all(p)
.context("Expected to build Proxy from paheko_proxy config value")?),
None => None
})
}
2024-01-13 16:51:23 +00:00
async fn launch_adapter(source: SourceType) -> Result<()> {
2023-11-03 17:44:11 +00:00
dotenvy::dotenv()?;
let config: Config = envy::from_env().context("Failed to load env vars")?;
let mut user_cache = load_user_cache().context("Failed to load user cache")?;
2023-12-26 15:09:49 +00:00
if !&config.paheko_base_url.ends_with("/") {
return Err(anyhow!("Invalid paheko base_url, it must end with a slash"))
}
let mut paheko_client: paheko::Client = paheko::Client::new(paheko::ClientConfig {
base_url: Url::parse(&config.paheko_base_url).expect("Expected paheko base url to be a valid URL"),
2024-01-13 16:51:06 +00:00
proxy: get_proxy_from_url(&config.paheko_proxy)?,
user_agent: APP_USER_AGENT.to_string()
});
2023-12-26 15:09:49 +00:00
let paheko_credentials = paheko::Credentials {
2024-01-13 16:51:06 +00:00
client_id: config.paheko_client_id.clone(),
client_secret: config.paheko_client_secret.clone()
2023-12-26 15:09:49 +00:00
};
let paheko_client: paheko::AuthentifiedClient = paheko_client.login(paheko_credentials).await?;
2024-01-13 16:51:23 +00:00
match source {
SourceType::Csv => sync_csv::sync_csv(&paheko_client, &config, &mut user_cache).await?,
SourceType::Helloasso => sync_helloasso::sync_helloasso(&paheko_client, &config, &mut user_cache).await?
}
2023-11-09 08:14:14 +00:00
2023-11-03 17:44:11 +00:00
Ok(())
}
2024-01-13 16:51:23 +00:00
#[derive(FromArgs)]
/// Members and Membership sync adaper for paheko (support Hellosso and CSV)
struct App {
/// the source of sync (CSV or helloasso)
#[argh(option, short = 'm')]
source: String,
}
enum SourceType {
Helloasso,
Csv
}
2023-11-03 17:44:11 +00:00
#[tokio::main]
async fn main() {
2024-01-13 16:51:23 +00:00
let app: App = argh::from_env();
let source = match app.source.as_ref() {
"helloasso" => SourceType::Helloasso,
"csv" => SourceType::Csv,
_ => {
eprintln!("Must provide a valid source argument.");
return;
}
};
let res = launch_adapter(source).await;
match res {
Err(err) => {
eprintln!("Program failed, details bellow");
eprintln!("{:?}", err);
},
Ok(()) => {
eprintln!("Program done");
}
}
2023-11-03 17:44:11 +00:00
}