2024-01-13 16:51:06 +00:00
|
|
|
#![feature(slice_group_by)]
|
|
|
|
|
2023-12-21 16:31:21 +00:00
|
|
|
mod utils;
|
2023-12-20 15:47:13 +00:00
|
|
|
mod paheko;
|
|
|
|
mod helloasso;
|
2024-01-13 16:51:06 +00:00
|
|
|
mod sync_helloasso;
|
|
|
|
mod sync_csv;
|
|
|
|
mod sync_paheko;
|
2023-12-20 15:47:13 +00:00
|
|
|
|
|
|
|
use thiserror::Error;
|
2023-12-30 22:48:23 +00:00
|
|
|
use anyhow::{Context, Result, anyhow};
|
2024-01-13 16:51:06 +00:00
|
|
|
use chrono::prelude::{NaiveDate, Datelike};
|
2023-11-09 08:14:14 +00:00
|
|
|
use strum::Display;
|
|
|
|
use serde::{Serialize, Deserialize};
|
2023-12-30 22:48:23 +00:00
|
|
|
use url::Url;
|
2024-01-13 16:51:06 +00:00
|
|
|
use fully_pub::fully_pub;
|
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 {
|
2023-12-30 22:48:23 +00:00
|
|
|
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,
|
2023-12-31 00:03:27 +00:00
|
|
|
|
|
|
|
helloasso_organization_slug: String,
|
|
|
|
helloasso_form_name: String,
|
|
|
|
|
2023-12-30 22:48:23 +00:00
|
|
|
paheko_proxy: Option<String>,
|
2023-12-26 15:09:49 +00:00
|
|
|
paheko_base_url: String,
|
|
|
|
paheko_client_id: String,
|
|
|
|
paheko_client_secret: String,
|
2023-12-31 00:03:27 +00:00
|
|
|
|
|
|
|
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 {
|
2023-12-20 15:47:13 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-12-30 22:48:23 +00:00
|
|
|
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
|
2023-12-20 15:47:13 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2023-11-09 08:14:14 +00:00
|
|
|
fn parse_normalize_phone(phone_number_opt: Option<String>) -> Option<String> {
|
|
|
|
let number_raw = phone_number_opt?;
|
|
|
|
|
|
|
|
let parsed = match phonenumber::parse(Some(phonenumber::country::Id::FR), number_raw) {
|
|
|
|
Ok(r) => {
|
|
|
|
r
|
|
|
|
},
|
|
|
|
Err(_e) => {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Some(parsed.to_string())
|
|
|
|
}
|
|
|
|
|
2023-12-20 15:47:13 +00:00
|
|
|
|
2023-12-26 15:09:49 +00:00
|
|
|
fn normalize_str(subject: String) -> String {
|
|
|
|
subject.trim().replace("\n", ";").to_string()
|
|
|
|
}
|
|
|
|
|
2023-12-20 15:47:13 +00:00
|
|
|
/// remove year precision to comply with GDPR eu rules
|
|
|
|
fn parse_and_get_birthday_year(raw_date: String) -> Option<u32> {
|
2023-12-28 10:40:38 +00:00
|
|
|
let d_res = NaiveDate::parse_from_str(raw_date.trim(), "%d/%m/%Y");
|
2023-12-20 15:47:13 +00:00
|
|
|
let d = d_res.ok()?;
|
2023-12-28 10:40:38 +00:00
|
|
|
d.year().try_into().ok()
|
2023-12-20 15:47:13 +00:00
|
|
|
}
|
|
|
|
|
2024-01-13 16:51:06 +00:00
|
|
|
fn get_proxy_from_url(proxy_url: &Option<String>) -> Result<Option<reqwest::Proxy>> {
|
2023-12-30 22:48:23 +00:00
|
|
|
Ok(match proxy_url {
|
2024-01-13 16:51:06 +00:00
|
|
|
Some(p) => Some(reqwest::Proxy::all(p)
|
2023-12-30 22:48:23 +00:00
|
|
|
.context("Expected to build Proxy from paheko_proxy config value")?),
|
|
|
|
None => None
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-11-03 17:44:11 +00:00
|
|
|
async fn launch_adapter() -> Result<()> {
|
|
|
|
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
|
|
|
|
2023-12-30 22:48:23 +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)?,
|
2023-12-30 22:48:23 +00:00
|
|
|
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:06 +00:00
|
|
|
sync_helloasso::sync_helloasso(&paheko_client, &config, &mut user_cache).await?;
|
|
|
|
// sync_csv::sync(&paheko_client, &config, &mut user_cache).await?;
|
2023-11-09 08:14:14 +00:00
|
|
|
|
2023-11-03 17:44:11 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::main]
|
|
|
|
async fn main() {
|
2023-12-28 10:40:38 +00:00
|
|
|
// TODO: add argument parser to have handle config file
|
2023-12-28 12:25:25 +00:00
|
|
|
let res = launch_adapter().await;
|
|
|
|
match res {
|
|
|
|
Err(err) => {
|
|
|
|
eprintln!("Program failed, details bellow");
|
|
|
|
eprintln!("{:?}", err);
|
|
|
|
},
|
|
|
|
Ok(()) => {
|
|
|
|
eprintln!("Program done");
|
|
|
|
}
|
|
|
|
}
|
2023-11-03 17:44:11 +00:00
|
|
|
}
|