2024-02-29 20:41:11 +00:00
|
|
|
use anyhow::Result;
|
2024-02-29 18:20:24 +00:00
|
|
|
use rsa::pkcs1v15::SigningKey;
|
2024-02-29 20:56:19 +00:00
|
|
|
use rsa::RsaPrivateKey;
|
2024-02-29 18:20:24 +00:00
|
|
|
use rsa::sha2::Sha256;
|
2024-02-29 20:56:19 +00:00
|
|
|
use rsa::signature::RandomizedSigner;
|
2024-02-29 20:41:11 +00:00
|
|
|
use serde::{de::DeserializeOwned, Deserialize};
|
2020-10-25 08:58:46 +00:00
|
|
|
|
2024-02-29 20:56:19 +00:00
|
|
|
pub use config::BunqConfig;
|
|
|
|
pub use monetary_account::MonetaryAccount;
|
|
|
|
pub use payment::Payment;
|
|
|
|
|
2024-02-29 20:41:11 +00:00
|
|
|
mod config;
|
|
|
|
mod monetary_account;
|
|
|
|
mod payment;
|
2020-10-25 08:58:46 +00:00
|
|
|
|
2024-02-29 20:41:11 +00:00
|
|
|
const BASE_URL: &str = "https://api.bunq.com";
|
2020-10-25 08:58:46 +00:00
|
|
|
|
2024-02-29 20:41:11 +00:00
|
|
|
pub struct BunqClient {
|
|
|
|
token: String,
|
|
|
|
#[allow(dead_code)] // Required for signing bodies. Not used yet.
|
|
|
|
keypair: RsaPrivateKey,
|
|
|
|
user_id: i64,
|
2020-10-25 08:58:46 +00:00
|
|
|
}
|
|
|
|
|
2024-02-29 20:41:11 +00:00
|
|
|
impl BunqClient {
|
|
|
|
pub fn monetary_accounts(&self) -> Result<Vec<MonetaryAccount>> {
|
|
|
|
monetary_account::get(self)
|
|
|
|
}
|
2020-10-25 08:58:46 +00:00
|
|
|
|
2024-02-29 20:41:11 +00:00
|
|
|
pub fn payments(&self, acc: &MonetaryAccount) -> Result<Vec<Payment>> {
|
|
|
|
self.payments_from_to(acc, None, None)
|
|
|
|
}
|
|
|
|
pub fn payments_from_to(
|
|
|
|
&self,
|
|
|
|
acc: &MonetaryAccount,
|
|
|
|
from: Option<i64>,
|
|
|
|
to: Option<i64>,
|
|
|
|
) -> anyhow::Result<Vec<Payment>> {
|
|
|
|
payment::get_from_to(self, acc, from, to)
|
|
|
|
}
|
2020-10-25 08:58:46 +00:00
|
|
|
}
|
|
|
|
|
2024-02-29 20:41:11 +00:00
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct Pagination {
|
|
|
|
#[allow(dead_code)] // Used for refresh. Not implemented yet.
|
|
|
|
future_url: Option<String>,
|
|
|
|
#[allow(dead_code)] // Used for finding newer items. Not necessary yet.
|
|
|
|
newer_url: Option<String>,
|
|
|
|
older_url: Option<String>,
|
2020-10-25 08:58:46 +00:00
|
|
|
}
|
|
|
|
|
2024-02-29 20:41:11 +00:00
|
|
|
struct Response<T> {
|
|
|
|
response: T,
|
|
|
|
pagination: Option<Pagination>,
|
2020-10-25 08:58:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[serde(rename_all = "PascalCase")]
|
|
|
|
struct RawResponse {
|
|
|
|
response: Vec<serde_json::Value>,
|
|
|
|
pagination: Option<Pagination>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl RawResponse {
|
|
|
|
fn decode_retarded<T: DeserializeOwned>(self) -> Result<Response<T>> {
|
|
|
|
let mut map = serde_json::Map::new();
|
|
|
|
for e in self.response {
|
|
|
|
if let serde_json::Value::Object(e) = e {
|
|
|
|
let (k, v) = e
|
|
|
|
.into_iter()
|
|
|
|
.next()
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("malformed response"))?;
|
|
|
|
map.insert(k, v);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(Response {
|
|
|
|
response: serde_json::from_value(map.into())?,
|
|
|
|
pagination: self.pagination,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn deserialize_retarded_response<T: DeserializeOwned>(r: &str) -> Result<Response<T>> {
|
|
|
|
let r: RawResponse = serde_json::from_str(r)?;
|
|
|
|
r.decode_retarded()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn deserialize_normal_response<T: DeserializeOwned>(r: &str) -> Result<Response<T>> {
|
|
|
|
let r: RawResponse = serde_json::from_str(r)?;
|
|
|
|
Ok(Response {
|
|
|
|
response: serde_json::from_value(r.response.into())?,
|
|
|
|
pagination: r.pagination,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-02-29 20:41:11 +00:00
|
|
|
fn sign(body: &str, key: &RsaPrivateKey) -> Result<String> {
|
2024-02-29 20:56:19 +00:00
|
|
|
use base64::prelude::{BASE64_STANDARD, Engine};
|
|
|
|
|
2024-02-29 20:41:11 +00:00
|
|
|
let signing_key = SigningKey::<Sha256>::new(key.clone());
|
|
|
|
let mut rng = rand::thread_rng();
|
|
|
|
let signature = signing_key.sign_with_rng(&mut rng, body.as_bytes());
|
2024-02-29 20:56:19 +00:00
|
|
|
Ok(BASE64_STANDARD.encode(signature.to_string()))
|
2024-02-29 18:28:13 +00:00
|
|
|
}
|