remarkable_cloud_api/
lib.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::fs;
4use std::io;
5use std::path;
6
7use serde::de::Deserialize;
8use thiserror::Error;
9use uuid::Uuid;
10
11#[derive(Error, Debug)]
12pub enum RemarkableError {
13    #[error("result empty")]
14    EmptyResult,
15
16    #[error(transparent)]
17    IoError(#[from] io::Error),
18
19    #[error(transparent)]
20    HttpError(#[from] reqwest::Error),
21
22    #[error(transparent)]
23    JsonError(#[from] serde_json::Error),
24}
25
26#[derive(serde::Deserialize, serde::Serialize, Debug)]
27pub struct AuthTokens {
28    device_token: String,
29    user_token: String,
30}
31
32#[derive(serde::Deserialize, Debug)]
33pub struct Document {
34    // The serde renames are to map rust-style names to the JSON api.
35    #[serde(rename = "ID")]
36    pub id: Uuid,
37    #[serde(rename = "VissibleName")]
38    pub visible_name: String,
39    #[serde(rename = "Parent", deserialize_with = "deserialize_optional_uuid")]
40    pub parent: Option<Uuid>,
41    #[serde(rename = "Type")]
42    pub doc_type: String,
43    #[serde(rename = "CurrentPage")]
44    pub current_page: i32,
45    #[serde(rename = "Bookmarked")]
46    pub bookmarked: bool,
47    #[serde(rename = "Message")]
48    pub message: String,
49    #[serde(rename = "ModifiedClient")]
50    pub modified_client: chrono::DateTime<chrono::Utc>,
51    #[serde(rename = "BlobURLGet")]
52    pub blob_url_get: String,
53    #[serde(rename = "BlobURLGetExpires")]
54    pub blob_url_get_expires: chrono::DateTime<chrono::Utc>,
55}
56
57// Extends UUID parsing by representing empty string as None
58fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
59where
60    D: serde::de::Deserializer<'de>,
61{
62    let buf = String::deserialize(deserializer)?;
63
64    if buf == "" {
65        Ok(None)
66    } else {
67        Uuid::parse_str(&buf)
68            .map(Some)
69            .map_err(serde::de::Error::custom)
70    }
71}
72
73#[derive(Default)]
74pub struct Documents {
75    by_id: HashMap<Uuid, Document>,
76}
77
78impl Documents {
79    pub fn len(&self) -> usize {
80        self.by_id.len()
81    }
82
83    pub fn is_empty(&self) -> bool {
84        self.len() == 0
85    }
86
87    pub fn get(&self, uuid: &Uuid) -> Option<&Document> {
88        self.by_id.get(uuid)
89    }
90
91    pub fn get_by_path(&self, path: &path::Path) -> Option<&Document> {
92        // TODO: The runtime of this is O(n^m) where n is the total number of
93        // documents and m is the number of path components. Since we have O(1)
94        // lookup by id this should be doable in O(n).
95        for d in self.by_id.values() {
96            if d.visible_name
97                == path
98                    .file_name()
99                    .unwrap_or_default()
100                    .to_str()
101                    .unwrap_or_default()
102            {
103                match path.parent().zip(d.parent) {
104                    None => return Some(d),
105                    Some((parent_path, parent_id)) => match self.get_by_path(parent_path) {
106                        None => continue,
107                        Some(parent) => {
108                            if parent.id == parent_id {
109                                return Some(d);
110                            }
111                        }
112                    },
113                }
114            }
115        }
116        None
117    }
118
119    pub fn get_children(&self, uuid: &Option<Uuid>) -> Vec<&Document> {
120        let mut acc: Vec<&Document> = vec![];
121        for d in self.by_id.values() {
122            if d.parent == *uuid {
123                acc.push(&d);
124            }
125        }
126        acc
127    }
128}
129
130impl<'de> serde::de::Deserialize<'de> for Documents {
131    fn deserialize<D>(deserializer: D) -> Result<Documents, D::Error>
132    where
133        D: serde::Deserializer<'de>,
134    {
135        struct DocumentsVisitor;
136
137        impl<'de> serde::de::Visitor<'de> for DocumentsVisitor {
138            type Value = Documents;
139
140            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
141                formatter.write_str("a JSON sequence")
142            }
143
144            fn visit_seq<V>(self, mut visitor: V) -> Result<Self::Value, V::Error>
145            where
146                V: serde::de::SeqAccess<'de>,
147            {
148                let mut documents: Documents = Default::default();
149
150                while let Some(doc) = visitor.next_element::<Document>()? {
151                    documents.by_id.insert(doc.id, doc);
152                }
153
154                Ok(documents)
155            }
156        }
157
158        deserializer.deserialize_any(DocumentsVisitor)
159    }
160}
161
162#[derive(serde::Serialize, serde::Deserialize, Default, Debug)]
163pub struct ClientState {
164    device_token: String,
165    user_token: String,
166}
167
168impl ClientState {
169    pub fn new() -> Self {
170        Default::default()
171    }
172
173    pub fn initialize(&mut self) {
174        todo!();
175    }
176
177    pub fn load<R>(&mut self, f: R) -> Result<(), RemarkableError>
178    where
179        R: io::Read,
180    {
181        Ok(*self = serde_json::from_reader(f)?)
182    }
183
184    pub fn load_from_path(&mut self, p: &path::Path) -> Result<(), RemarkableError> {
185        Ok(self.load(io::BufReader::new(fs::File::open(p)?))?)
186    }
187
188    pub fn save<W>(&self, f: W) -> Result<(), RemarkableError>
189    where
190        W: io::Write,
191    {
192        Ok(serde_json::to_writer_pretty(f, self)?)
193    }
194
195    pub fn save_to_path(self, p: &path::Path) -> Result<(), RemarkableError> {
196        // TODO: Make this be properly atomic
197        Ok(self.save(io::BufWriter::new(fs::File::create(p)?))?)
198    }
199}
200
201pub struct Client {
202    client_state: ClientState,
203    http_client: reqwest::Client,
204}
205
206impl Client {
207    pub fn new(client_state: ClientState, http_client: reqwest::Client) -> Self {
208        Client {
209            client_state,
210            http_client,
211        }
212    }
213
214    pub fn state(&mut self) -> &mut ClientState {
215        &mut self.client_state
216    }
217
218    pub async fn refresh_token(&mut self) -> Result<(), RemarkableError> {
219        let userurl = "https://my.remarkable.com/token/json/2/user/new";
220        let request = self
221            .http_client
222            .post(userurl)
223            .bearer_auth(&self.client_state.device_token)
224            .body("")
225            .header(reqwest::header::CONTENT_LENGTH, "0");
226        let response = request.send().await?;
227        self.client_state.user_token = response.text().await?;
228        Ok(())
229    }
230
231    pub async fn get_documents(&self) -> Result<Documents, RemarkableError> {
232        let endpoint = "https://document-storage-production-dot-remarkable-production.appspot.com";
233        let list = "document-storage/json/2/docs";
234        let listurl = format!("{}/{}", endpoint, list);
235        let request = self
236            .http_client
237            .get(&listurl)
238            .bearer_auth(&self.client_state.user_token);
239        let response = request.send().await?;
240        let body = response.text().await?;
241        let docs = serde_json::from_str::<Documents>(&body)?;
242        Ok(docs)
243    }
244
245    pub async fn get_document_by_id(&self, id: &Uuid) -> Result<Document, RemarkableError> {
246        let endpoint = "https://document-storage-production-dot-remarkable-production.appspot.com";
247        let list = "document-storage/json/2/docs";
248        let listurl = format!("{}/{}", endpoint, list);
249        let request = self
250            .http_client
251            .get(&listurl)
252            .bearer_auth(&self.client_state.user_token)
253            .query(&[("withBlob", "1"), ("doc", &id.to_string())]);
254        let response = request.send().await?;
255        let body = response.text().await?;
256        let mut docs = serde_json::from_str::<Documents>(&body)?;
257        match docs.by_id.remove(id) {
258            Some(d) => Ok(d),
259            None => Err(RemarkableError::EmptyResult),
260        }
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    #[test]
267    fn it_works() {
268        assert_eq!(2 + 2, 4);
269    }
270}