1use reqwest::{
2 header::{self, HeaderMap, HeaderValue},
3 StatusCode,
4};
5
6use crate::{tasks, History};
7
8const THINGS_USER_AGENT: &str = "ThingsMac/31516502";
10
11#[derive(Debug)]
12pub struct Account {
13 pub email: String,
14 pub maildrop_email: String,
15 pub(crate) history_key: String,
16 pub(crate) client: reqwest::Client,
17}
18
19#[derive(Debug, thiserror::Error)]
20pub enum Error {
21 #[error("The provided credentials are invalid")]
23 InvalidCredentials,
24
25 #[error("The account has issues: {0:?}")]
27 AccountHasIssues(Vec<serde_json::Value>),
28
29 #[error("The account has an unknown status: {0}")]
31 UnknownAccountStatus(String),
32
33 #[error("An error occurred while communicating with the Things Cloud API: {0}")]
35 Reqwest(#[from] reqwest::Error),
36}
37
38impl Account {
39 pub async fn login(email: &str, password: &str) -> Result<Self, Error> {
48 let mut headers = HeaderMap::new();
49 headers.insert(
50 header::USER_AGENT,
51 HeaderValue::from_static(THINGS_USER_AGENT),
52 );
53 headers.insert(
54 header::AUTHORIZATION,
55 HeaderValue::from_str(&format!("Password {password}"))
56 .map_err(|_| Error::InvalidCredentials)?,
57 );
58
59 let client = reqwest::Client::builder()
60 .default_headers(headers)
61 .build()?;
62
63 let response = client
64 .get(format!(
65 "https://cloud.culturedcode.com/version/1/account/{email}"
66 ))
67 .header("Authorization", format!("Password {password}"))
68 .send()
69 .await?;
70
71 if response.status() == StatusCode::UNAUTHORIZED {
72 return Err(Error::InvalidCredentials);
73 }
74
75 let response = response.json::<AccountAPIResponse>().await?;
76
77 if !response.issues.is_empty() {
78 return Err(Error::AccountHasIssues(response.issues));
79 }
80
81 if response.status != Status::Active {
82 return Err(Error::InvalidCredentials);
83 }
84
85 Ok(Self {
86 client,
87 email: response.email,
88 history_key: response.history_key,
89 maildrop_email: response.maildrop_email,
90 })
91 }
92
93 pub async fn history(&self) -> Result<History, tasks::Error> {
99 History::from_account(self).await
100 }
101}
102
103#[derive(Debug, serde::Deserialize)]
104#[serde(rename_all = "kebab-case")]
105struct AccountAPIResponse {
106 status: Status,
107 #[serde(rename = "SLA-version-accepted")]
108 sla_version: String,
109 email: String,
110 history_key: String,
111 maildrop_email: String,
112 issues: Vec<serde_json::Value>,
113}
114
115#[derive(Debug, serde::Deserialize, PartialEq, Eq)]
116enum Status {
117 #[serde(rename = "SYAccountStatusActive")]
118 Active,
119 Other(String),
120}