initial commit
This commit is contained in:
commit
638abc06d8
5 changed files with 3048 additions and 0 deletions
3
.env.example
Normal file
3
.env.example
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
HELLOASSO_EMAIL=contact@etoiledebethleem.fr
|
||||||
|
HELLOASSO_PASSWORD=XXX
|
||||||
|
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
.env
|
||||||
|
|
2483
Cargo.lock
generated
Normal file
2483
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
20
Cargo.toml
Normal file
20
Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
[package]
|
||||||
|
name = "paheko_helloasso_adapter_rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.0.32", features = ["derive"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"]}
|
||||||
|
serde_json = "1.0"
|
||||||
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
url = "2.4.1"
|
||||||
|
xdg = "2.5.2"
|
||||||
|
anyhow = "1.0.75"
|
||||||
|
thiserror = "1.0" # macro to derive Error
|
||||||
|
surf = "2.3.2"
|
||||||
|
chrono = { version = "0.4.31", features = ["serde"] }
|
||||||
|
envy = "0.4.2"
|
||||||
|
strum = { version = "0.25", features = ["derive"] }
|
||||||
|
dotenvy = "0.15.7"
|
539
src/main.rs
Normal file
539
src/main.rs
Normal file
|
@ -0,0 +1,539 @@
|
||||||
|
use url::Url;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
use chrono::prelude::{NaiveDate, DateTime, Utc};
|
||||||
|
use strum::{Display };
|
||||||
|
|
||||||
|
/// permanent config to store long-term config
|
||||||
|
/// used to ingest env settings
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
struct Config {
|
||||||
|
helloasso_email: String,
|
||||||
|
helloasso_password: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
struct LoginPayload {
|
||||||
|
email: String,
|
||||||
|
password: String
|
||||||
|
}
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
enum APIClientError {
|
||||||
|
#[error("Received non-normal status code from API")]
|
||||||
|
InvalidStatusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
static APP_USER_AGENT: &str = concat!(
|
||||||
|
env!("CARGO_PKG_NAME"),
|
||||||
|
"/",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// start user cache management
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
struct UserCache {
|
||||||
|
helloasso_session: Option<HelloassoSession>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Display, Debug, Error)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
enum LoadError {
|
||||||
|
XDG,
|
||||||
|
Fs,
|
||||||
|
FailedToParse,
|
||||||
|
FailedToEncode,
|
||||||
|
FailedToCreate,
|
||||||
|
FailedToWrite
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn write_user_cache(cache: &UserCache) -> Result<(), LoadError> {
|
||||||
|
let xdg_dirs = xdg::BaseDirectories::with_prefix(env!("CARGO_PKG_NAME"))
|
||||||
|
.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 })?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_user_cache() -> Result<UserCache, LoadError> {
|
||||||
|
let xdg_dirs = xdg::BaseDirectories::with_prefix(env!("CARGO_PKG_NAME"))
|
||||||
|
.map_err(|e| { LoadError::XDG })?;
|
||||||
|
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)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 })?;
|
||||||
|
|
||||||
|
Ok(cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
struct HelloassoSession {
|
||||||
|
jwt: String
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LoginError {
|
||||||
|
TransportFailure(reqwest::Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct HelloassoClient {
|
||||||
|
client: reqwest::Client,
|
||||||
|
base_url: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HelloassoClient {
|
||||||
|
fn default() -> Self {
|
||||||
|
HelloassoClient {
|
||||||
|
client: HelloassoClient::get_base_client_builder()
|
||||||
|
.build()
|
||||||
|
.expect("reqwest client to be built"),
|
||||||
|
base_url: Url::parse("https://api.helloasso.com/v5/")
|
||||||
|
.expect("Valid helloasso API base URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HelloassoClient {
|
||||||
|
|
||||||
|
fn get_base_client_builder() -> reqwest::ClientBuilder {
|
||||||
|
let mut default_headers = reqwest::header::HeaderMap::new();
|
||||||
|
default_headers.insert("Accept", "application/json".parse().unwrap());
|
||||||
|
|
||||||
|
let proxy = reqwest::Proxy::https("https://localhost:8999").unwrap();
|
||||||
|
reqwest::Client::builder()
|
||||||
|
.proxy(proxy)
|
||||||
|
.default_headers(default_headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn login(&mut self, payload: LoginPayload) -> Result<AuthentifiedHelloassoClient> {
|
||||||
|
let mut login_commons_headers = reqwest::header::HeaderMap::new();
|
||||||
|
login_commons_headers.insert(
|
||||||
|
"Origin",
|
||||||
|
"https://auth.helloasso.com".parse().expect("Header value to be OK")
|
||||||
|
);
|
||||||
|
|
||||||
|
let res = self.client.get(self.base_url.join("auth/antiforgerytoken")?)
|
||||||
|
.headers(login_commons_headers.clone())
|
||||||
|
.send().await?;
|
||||||
|
let antiforgerytoken: String = res.json().await?;
|
||||||
|
|
||||||
|
let res = self.client.post(self.base_url.join("auth/login")?)
|
||||||
|
.json(&payload)
|
||||||
|
.headers(login_commons_headers.clone())
|
||||||
|
.header("x-csrf-token", antiforgerytoken)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if res.status() != 200 {
|
||||||
|
return Err(anyhow!("Unexpected status code from login"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_jwt_from_cookies_headers(headers: &reqwest::header::HeaderMap) -> Option<String> {
|
||||||
|
for (name_opt, value_raw) in headers {
|
||||||
|
let name = String::from(name_opt.as_str());
|
||||||
|
if name.to_lowercase() != "set-cookie" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let value = String::from(value_raw.to_str().unwrap());
|
||||||
|
if value.starts_with("tm5-HelloAsso") {
|
||||||
|
let jwt = value.split("tm5-HelloAsso=").nth(1)?.split(";").nth(0)?.trim().to_string();
|
||||||
|
return Some(jwt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
let jwt = get_jwt_from_cookies_headers(&res.headers())
|
||||||
|
.context("Failed to find or parse JWT from login response")?;
|
||||||
|
|
||||||
|
let session = HelloassoSession { jwt };
|
||||||
|
|
||||||
|
Ok(self.authentified_client(session))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authentified_client(&self, session: HelloassoSession) -> AuthentifiedHelloassoClient {
|
||||||
|
AuthentifiedHelloassoClient::new(self.base_url.clone(), session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct AuthentifiedHelloassoClient {
|
||||||
|
session: HelloassoSession,
|
||||||
|
client: reqwest::Client,
|
||||||
|
base_url: Url
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct PaginationMeta {
|
||||||
|
continuation_token: String,
|
||||||
|
page_index: u64,
|
||||||
|
page_size: u64,
|
||||||
|
total_count: u64,
|
||||||
|
total_pages: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct PaginationCapsule {
|
||||||
|
data: serde_json::Value,
|
||||||
|
pagination: PaginationMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct CustomFieldAnswer {
|
||||||
|
answer: String,
|
||||||
|
id: u64,
|
||||||
|
name: String
|
||||||
|
// missing type, it's probably always TextInput, if not, serde will fail to parse
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct UserDetails {
|
||||||
|
country: String,
|
||||||
|
email: String,
|
||||||
|
first_name: String,
|
||||||
|
last_name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// #[derive(Debug, Serialize, Deserialize)]
|
||||||
|
// #[serde(rename_all = "camelCase")]
|
||||||
|
// struct OrderDetails {
|
||||||
|
// date:
|
||||||
|
// form_
|
||||||
|
// }
|
||||||
|
|
||||||
|
impl AuthentifiedHelloassoClient {
|
||||||
|
/// each time we need to change the token, we will need to rebuild the client
|
||||||
|
fn new(base_url: Url, session: HelloassoSession) -> Self {
|
||||||
|
let mut auth_headers = reqwest::header::HeaderMap::new();
|
||||||
|
auth_headers.insert("Authorization", format!("Bearer {}", session.jwt).parse().unwrap());
|
||||||
|
|
||||||
|
AuthentifiedHelloassoClient {
|
||||||
|
base_url,
|
||||||
|
session,
|
||||||
|
client: HelloassoClient::get_base_client_builder()
|
||||||
|
.default_headers(auth_headers)
|
||||||
|
.build()
|
||||||
|
.expect("reqwest client to be built")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify_auth(&self) -> Result<bool> {
|
||||||
|
let res = self.client
|
||||||
|
.get(self.base_url.join("agg/user")?)
|
||||||
|
.send().await?;
|
||||||
|
return Ok(res.status() == 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user_details(&self) -> Result<()> {
|
||||||
|
let res = self.client
|
||||||
|
.get(self.base_url.join("agg/user")?)
|
||||||
|
.send().await?;
|
||||||
|
if res.status() != 200 {
|
||||||
|
return Err(APIClientError::InvalidStatusCode.into());
|
||||||
|
}
|
||||||
|
let user_details: serde_json::Value = res.json().await?;
|
||||||
|
|
||||||
|
dbg!(user_details);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch(&self, path: String) -> Result<serde_json::Value> {
|
||||||
|
let res = self.client
|
||||||
|
.get(self.base_url.join(path.as_str())?)
|
||||||
|
.send().await?;
|
||||||
|
if res.status() != 200 {
|
||||||
|
return Err(APIClientError::InvalidStatusCode.into());
|
||||||
|
}
|
||||||
|
let details: serde_json::Value = res.json().await?;
|
||||||
|
|
||||||
|
// handle pagination
|
||||||
|
// merge into "data", "pagination" is the key that hold details
|
||||||
|
|
||||||
|
Ok(details)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_with_pagination(&self, path: String) -> Result<Vec<serde_json::Value>> {
|
||||||
|
let mut data: Vec<serde_json::Value> = vec![];
|
||||||
|
let mut continuation_token: Option<String> = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut url = self.base_url.join(path.as_str())?;
|
||||||
|
if let Some(token) = &continuation_token {
|
||||||
|
url.query_pairs_mut().append_pair("continuationToken", token);
|
||||||
|
}
|
||||||
|
let res = self.client
|
||||||
|
.get(url)
|
||||||
|
.send().await?;
|
||||||
|
if res.status() != 200 {
|
||||||
|
return Err(APIClientError::InvalidStatusCode.into());
|
||||||
|
}
|
||||||
|
let capsule: PaginationCapsule = res.json().await?;
|
||||||
|
|
||||||
|
// handle pagination
|
||||||
|
// merge into "data", "pagination" is the key that hold details
|
||||||
|
|
||||||
|
let page_items = match capsule.data {
|
||||||
|
serde_json::Value::Array(inner) => inner,
|
||||||
|
_ => {
|
||||||
|
return Err(anyhow!("Unexpected json value in data bundle"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if page_items.len() == 0 {
|
||||||
|
return Ok(data);
|
||||||
|
}
|
||||||
|
data.extend(page_items);
|
||||||
|
if capsule.pagination.page_index == capsule.pagination.total_pages {
|
||||||
|
return Ok(data);
|
||||||
|
}
|
||||||
|
continuation_token = Some(capsule.pagination.continuation_token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn organization(&self, slug: &str) -> Organization {
|
||||||
|
Organization { client: self.clone(), slug: slug.to_string() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Organization {
|
||||||
|
client: AuthentifiedHelloassoClient,
|
||||||
|
slug: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
enum MembershipMode {
|
||||||
|
#[serde(rename = "Individuel")]
|
||||||
|
Individual,
|
||||||
|
#[serde(rename = "Couple")]
|
||||||
|
Couple,
|
||||||
|
#[serde(rename = "Individuel bienfaiteur")]
|
||||||
|
BenefactorIndividual,
|
||||||
|
#[serde(rename = "Couple bienfaiteur")]
|
||||||
|
BenefactorCouple,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct FormAnswer {
|
||||||
|
amount: u64,
|
||||||
|
#[serde(rename = "name")]
|
||||||
|
mode: MembershipMode,
|
||||||
|
#[serde(rename = "payer")]
|
||||||
|
user: UserDetails,
|
||||||
|
id: u64,
|
||||||
|
custom_fields: Vec<CustomFieldAnswer>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Organization {
|
||||||
|
async fn get_details(&self) -> Result<serde_json::Value> {
|
||||||
|
let details = self.client.fetch(format!("organizations/{}", self.slug)).await?;
|
||||||
|
Ok(details)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_form_answers(&self, form_slug: String) -> Result<Vec<FormAnswer>> {
|
||||||
|
let data = self.client.fetch_with_pagination(
|
||||||
|
format!("organizations/{}/forms/Membership/{}/participants?withDetails=true", self.slug, form_slug)
|
||||||
|
).await?;
|
||||||
|
let mut answers: Vec<FormAnswer> = vec![];
|
||||||
|
for entry in data {
|
||||||
|
answers.push(serde_json::from_value(entry).context("Cannot parse FormAnswer")?)
|
||||||
|
}
|
||||||
|
Ok(answers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: find a better way to have the logic implemented
|
||||||
|
async fn get_auth_client_from_cache(user_cache: &mut UserCache, ha_client: &mut HelloassoClient, login_payload: LoginPayload) -> Result<AuthentifiedHelloassoClient> {
|
||||||
|
|
||||||
|
async fn login(user_cache: &mut UserCache, ha_client: &mut HelloassoClient, login_payload: LoginPayload) -> Result<AuthentifiedHelloassoClient> {
|
||||||
|
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");
|
||||||
|
return Ok(login(user_cache, ha_client, login_payload).await?)
|
||||||
|
}
|
||||||
|
println!("Used anterior token");
|
||||||
|
return Ok(auth_client);
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
println!("First time login");
|
||||||
|
return Ok(login(user_cache, ha_client, login_payload).await?);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo:
|
||||||
|
// - make pagination working
|
||||||
|
// - create paheko client
|
||||||
|
// - get current paheko membership
|
||||||
|
// - function to convert participants to paheko members
|
||||||
|
// - clean up names and things
|
||||||
|
// - map custom fields with the right thing
|
||||||
|
// - handle linked users
|
||||||
|
|
||||||
|
fn get_paheko_membership_from_ha_answers() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// for now we include the custom fields into the paheko user
|
||||||
|
/// we don't have time to implement user settings to change the custom fields mapping
|
||||||
|
/// for now, manual mapping
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct PahekoUser {
|
||||||
|
first_name: String,
|
||||||
|
last_name: String,
|
||||||
|
email: String,
|
||||||
|
|
||||||
|
phone: Option<String>,
|
||||||
|
address: String,
|
||||||
|
city: String,
|
||||||
|
postal_code: String,
|
||||||
|
skills: Option<String>,
|
||||||
|
job: Option<String>,
|
||||||
|
birthday: Option<NaiveDate> // we will need to validate some data before
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct PahekoMembership {
|
||||||
|
author: PahekoUser,
|
||||||
|
linked_users: Vec<PahekoUser>,
|
||||||
|
campaign: String,
|
||||||
|
mode: String,
|
||||||
|
inception_datum: DateTime<Utc>
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CustomFieldsMapping {
|
||||||
|
helloasso_id: u64,
|
||||||
|
paheko_slug: String,
|
||||||
|
label: String
|
||||||
|
// address: u64,
|
||||||
|
// postal_code: u64,
|
||||||
|
// city: u64,
|
||||||
|
// phone: u64,
|
||||||
|
// skills: u64,
|
||||||
|
// birthday: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// rust how to access inner enum value
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
enum HelloAssoCustomFields {
|
||||||
|
Address,
|
||||||
|
PostalCode,
|
||||||
|
City,
|
||||||
|
Phone,
|
||||||
|
Job,
|
||||||
|
Skills,
|
||||||
|
Birthday
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<u64> for HelloAssoCustomFields {
|
||||||
|
fn into(self) -> u64 {
|
||||||
|
match self {
|
||||||
|
HelloAssoCustomFields::Address => 12958695,
|
||||||
|
HelloAssoCustomFields::PostalCode => 12958717,
|
||||||
|
HelloAssoCustomFields::City => 12958722,
|
||||||
|
HelloAssoCustomFields::Phone => 13279172,
|
||||||
|
HelloAssoCustomFields::Job => 13279172,
|
||||||
|
HelloAssoCustomFields::Skills => 11231129,
|
||||||
|
HelloAssoCustomFields::Birthday => 12944367
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_custom_field(form_answer: &FormAnswer, custom_field_id: HelloAssoCustomFields) -> Option<String> {
|
||||||
|
let int_repr: u64 = custom_field_id.into();
|
||||||
|
form_answer.custom_fields.iter()
|
||||||
|
.find(|f| f.id == int_repr)
|
||||||
|
.and_then(|cf| Some(cf.answer.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
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")?;
|
||||||
|
let mut ha_client: HelloassoClient = Default::default();
|
||||||
|
|
||||||
|
let login_payload = LoginPayload {
|
||||||
|
email: config.helloasso_email,
|
||||||
|
password: config.helloasso_password
|
||||||
|
};
|
||||||
|
let auth_client: AuthentifiedHelloassoClient = get_auth_client_from_cache(&mut user_cache, &mut ha_client, login_payload).await?;
|
||||||
|
|
||||||
|
// dbg!(auth_client.get_user_details().await?);
|
||||||
|
|
||||||
|
let slug = "l-etoile-de-bethleem-association-des-amis-de-la-chapelle-de-bethleem-d-aubevoye";
|
||||||
|
|
||||||
|
let org: Organization = auth_client.organization(slug);
|
||||||
|
// dbg!(org.get_details().await?);
|
||||||
|
let answers = org.get_form_answers("2023-2024".to_string()).await?;
|
||||||
|
|
||||||
|
// dbg!(&answers);
|
||||||
|
println!("Got {} answers to the membership form. Processing...", &answers.len());
|
||||||
|
|
||||||
|
// first step: output a list of PahekoUser with PahekoMembership
|
||||||
|
let pk_memberships: Vec<PahekoMembership> = vec![];
|
||||||
|
let mut pk_users: Vec<PahekoUser> = vec![];
|
||||||
|
|
||||||
|
for answer in answers {
|
||||||
|
// TODO: parse birthday
|
||||||
|
// NaiveDate::parse_from_str
|
||||||
|
dbg!(&answer);
|
||||||
|
pk_users.push(PahekoUser {
|
||||||
|
first_name: answer.user.first_name.clone(),
|
||||||
|
last_name: answer.user.last_name.clone(),
|
||||||
|
email: answer.user.email.clone(),
|
||||||
|
phone: read_custom_field(&answer, HelloAssoCustomFields::Phone),
|
||||||
|
skills: read_custom_field(&answer, HelloAssoCustomFields::Skills),
|
||||||
|
address: read_custom_field(&answer, HelloAssoCustomFields::Address).expect("to have address"),
|
||||||
|
postal_code: read_custom_field(&answer, HelloAssoCustomFields::PostalCode).expect("to have address"),
|
||||||
|
city: read_custom_field(&answer, HelloAssoCustomFields::City).expect("to have address"),
|
||||||
|
job: read_custom_field(&answer, HelloAssoCustomFields::Job),
|
||||||
|
birthday: None
|
||||||
|
});
|
||||||
|
}
|
||||||
|
dbg!(pk_users);
|
||||||
|
|
||||||
|
// then, request the current list of users
|
||||||
|
// then, upload the PahekoMembership
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let res = launch_adapter().await;
|
||||||
|
dbg!(res);
|
||||||
|
}
|
Loading…
Reference in a new issue