Skip to main content

lb_rs/service/
account.rs

1use crate::experiments::{WelcomeDoc, cohort};
2use crate::model::account::{Account, MAX_USERNAME_LENGTH};
3use crate::model::api::{
4    DeleteAccountRequest, GetPublicKeyRequest, GetUsernameRequest, NewAccountRequestV2,
5};
6use crate::model::errors::{LbErrKind, LbResult, core_err_unexpected};
7use crate::model::file_like::FileLike;
8use crate::model::file_metadata::{FileType, Owner};
9use crate::model::meta::Meta;
10use crate::service::events::Actor;
11use crate::{DEFAULT_API_LOCATION, Lb};
12use libsecp256k1::SecretKey;
13use qrcode_generator::QrCodeEcc;
14
15use crate::io::network::ApiError;
16
17impl Lb {
18    /// CoreError::AccountExists,
19    /// CoreError::UsernameTaken,
20    /// CoreError::UsernameInvalid,
21    /// CoreError::ServerDisabled,
22    /// CoreError::ServerUnreachable,
23    /// CoreError::ClientUpdateRequired,
24    #[instrument(level = "debug", skip(self), err(Debug))]
25    pub async fn create_account(
26        &self, username: &str, api_url: &str, welcome_doc: bool,
27    ) -> LbResult<Account> {
28        let username = String::from(username).to_lowercase();
29
30        if username.len() > MAX_USERNAME_LENGTH {
31            return Err(LbErrKind::UsernameInvalid.into());
32        }
33
34        let mut tx = self.begin_tx().await;
35        let db = tx.db();
36
37        if db.account.as_ref().is_some() {
38            return Err(LbErrKind::AccountExists.into());
39        }
40
41        let account = Account::new(username.clone(), api_url.to_string());
42
43        let root = Meta::create_root(&account)?.sign_with(&account)?;
44        let root_id = *root.id();
45
46        let last_synced = self
47            .client
48            .request(&account, NewAccountRequestV2::new(&account, &root))
49            .await?
50            .last_synced;
51
52        db.account.replace(account.clone())?;
53        db.base_metadata.insert(root_id, root)?;
54        db.last_synced.replace(last_synced as i64)?;
55        db.root.replace(root_id)?;
56        db.pub_key_lookup
57            .insert(Owner(account.public_key()), account.username.clone())?;
58
59        self.keychain.cache_account(account.clone()).await?;
60
61        tx.end();
62
63        if welcome_doc {
64            let cohort: WelcomeDoc = cohort(&account.username);
65            match cohort {
66                WelcomeDoc::NoWelcomeDoc => {}
67                WelcomeDoc::OldWelcomeDoc => {
68                    let welcome_doc = self
69                        .create_file("welcome.md", &root_id, FileType::Document)
70                        .await?;
71                    self.write_document(welcome_doc.id, Self::WELCOME_MESSAGE.as_bytes())
72                        .await?;
73                    self.sync().await?;
74                }
75                WelcomeDoc::FramgentedArchetypes => {
76                    let samples = self
77                        .create_file("samples", &root_id, FileType::Folder)
78                        .await?;
79
80                    let todo = self
81                        .create_file("todo.md", &samples.id, FileType::Document)
82                        .await?;
83                    self.write_document(todo.id, include_bytes!("samples/todo.md"))
84                        .await?;
85
86                    let journal = self
87                        .create_file("journal.md", &samples.id, FileType::Document)
88                        .await?;
89                    let journal_content = include_str!("samples/journal.md")
90                        .replace("{date}", &chrono::Local::now().format("%y-%m-%d").to_string());
91                    self.write_document(journal.id, journal_content.as_bytes())
92                        .await?;
93
94                    let pdf = self
95                        .create_file("bitcoin.pdf", &samples.id, FileType::Document)
96                        .await?;
97                    self.write_document(pdf.id, include_bytes!("samples/bitcoin.pdf"))
98                        .await?;
99
100                    let drawing = self
101                        .create_file("drawing.svg", &samples.id, FileType::Document)
102                        .await?;
103                    self.write_document(drawing.id, include_bytes!("samples/drawing.svg"))
104                        .await?;
105
106                    let welcome = self
107                        .create_file("welcome.md", &root_id, FileType::Document)
108                        .await?;
109                    self.write_document(welcome.id, include_bytes!("samples/welcome.md"))
110                        .await?;
111
112                    self.sync().await?;
113                }
114                WelcomeDoc::HypeWelcomeDoc => {
115                    let welcome_doc = self
116                        .create_file("welcome.md", &root_id, FileType::Document)
117                        .await?;
118                    self.write_document(welcome_doc.id, include_bytes!("samples/hype_welcome.md"))
119                        .await?;
120                    self.sync().await?;
121                }
122            };
123        }
124
125        self.events.meta_changed(Actor::User(None));
126        self.events.signed_in();
127
128        Ok(account)
129    }
130
131    #[instrument(level = "debug", skip(self, key), err(Debug))]
132    pub async fn import_account(&self, key: &str, api_url: Option<&str>) -> LbResult<Account> {
133        if self.get_account().is_ok() {
134            warn!("tried to import an account, but account exists already.");
135            return Err(LbErrKind::AccountExists.into());
136        }
137
138        if let Ok(key) = base64::decode(key) {
139            if let Ok(account) = bincode::deserialize(&key[..]) {
140                return self.import_account_private_key_v1(account).await;
141            } else if let Ok(key) = SecretKey::parse_slice(&key) {
142                return self
143                    .import_account_private_key_v2(key, api_url.unwrap_or(DEFAULT_API_LOCATION))
144                    .await;
145            }
146        }
147
148        let phrase: [&str; 24] = key
149            .split([' ', ','])
150            .filter(|maybe_word| !maybe_word.is_empty())
151            .collect::<Vec<_>>()
152            .try_into()
153            .map_err(|_| LbErrKind::AccountStringCorrupted)?;
154
155        self.import_account_phrase(phrase, api_url.unwrap_or(DEFAULT_API_LOCATION))
156            .await
157    }
158
159    pub async fn import_account_private_key_v1(&self, account: Account) -> LbResult<Account> {
160        let server_public_key = self
161            .client
162            .request(&account, GetPublicKeyRequest { username: account.username.clone() })
163            .await?
164            .key;
165
166        let account_public_key = account.public_key();
167
168        if account_public_key != server_public_key {
169            return Err(LbErrKind::UsernamePublicKeyMismatch.into());
170        }
171
172        let mut tx = self.begin_tx().await;
173        let db = tx.db();
174        db.account.replace(account.clone())?;
175        self.keychain.cache_account(account.clone()).await?;
176
177        Ok(account)
178    }
179
180    pub async fn import_account_private_key_v2(
181        &self, private_key: SecretKey, api_url: &str,
182    ) -> LbResult<Account> {
183        let mut account =
184            Account { username: "".to_string(), api_url: api_url.to_string(), private_key };
185        let public_key = account.public_key();
186
187        account.username = self
188            .client
189            .request(&account, GetUsernameRequest { key: public_key })
190            .await?
191            .username;
192
193        let mut tx = self.begin_tx().await;
194        let db = tx.db();
195        db.account.replace(account.clone())?;
196        self.keychain.cache_account(account.clone()).await?;
197
198        Ok(account)
199    }
200
201    pub async fn import_account_phrase(
202        &self, phrase: [&str; 24], api_url: &str,
203    ) -> LbResult<Account> {
204        let private_key = Account::phrase_to_private_key(phrase)?;
205        self.import_account_private_key_v2(private_key, api_url)
206            .await
207    }
208
209    #[instrument(level = "debug", skip(self), err(Debug))]
210    pub fn export_account_private_key(&self) -> LbResult<String> {
211        let account = self.get_account()?;
212        Ok(base64::encode(account.private_key.serialize()))
213    }
214
215    pub fn export_account_phrase(&self) -> LbResult<String> {
216        let account = self.get_account()?;
217        Ok(account.get_phrase()?.join(" "))
218    }
219
220    pub fn export_account_qr(&self) -> LbResult<Vec<u8>> {
221        let acct_secret = self.export_account_private_key()?;
222        qrcode_generator::to_png_to_vec(acct_secret, QrCodeEcc::Low, 1024)
223            .map_err(|err| core_err_unexpected(err).into())
224    }
225
226    #[instrument(level = "debug", skip(self), err(Debug))]
227    pub async fn delete_account(&self) -> LbResult<()> {
228        let account = self.get_account()?;
229
230        self.client
231            .request(account, DeleteAccountRequest {})
232            .await
233            .map_err(|err| match err {
234                ApiError::SendFailed(_) => LbErrKind::ServerUnreachable,
235                ApiError::ClientUpdateRequired => LbErrKind::ClientUpdateRequired,
236                _ => core_err_unexpected(err),
237            })?;
238
239        let mut tx = self.begin_tx().await;
240        let db = tx.db();
241
242        db.account.take()?;
243        db.last_synced.take()?;
244        db.base_metadata.clear()?;
245        db.root.take()?;
246        db.local_metadata.clear()?;
247        db.pub_key_lookup.clear()?;
248
249        // todo: clear cache?
250
251        Ok(())
252    }
253
254    const WELCOME_MESSAGE: &'static str = r#"# Markdown Syntax
255Markdown is a language for easily formatting your documents. This document can help you get started.
256
257## Styled Text
258To style text, wrap your text in the corresponding characters.
259| Style         | Syntax              | Example           |
260|---------------|---------------------|-------------------|
261| emphasis      | `*emphasis*`        | *emphasis*        |
262| strong        | `**strong**`        | **strong**        |
263| strikethrough | `~~strikethrough~~` | ~~strikethrough~~ |
264| underline     | `__underline__`     | __underline__     |
265| code          | ``code``            | `code`            |
266| highlight     | `==highlight==`     | ==highlight==     |
267| spoiler       | `||spoiler||`       | ||spoiler||       |
268| superscript   | `^superscript^`     | ^superscript^     |
269| subscript     | `~subscript~`       | ~subscript~       |
270
271## Links
272To make text into a link, wrap it with `[` `]`, add a link destination to the end , and wrap the destination with `(` `)`. The link destination can be a web URL or a relative path to another Lockbook file.
273```md
274[Lockbook's website](https://lockbook.net)
275```
276> [Lockbook's website](https://lockbook.net)
277
278## Images
279To embed an image, add a `!` to the beginning of the link syntax.
280```md
281![Lockbook's logo](https://raw.githubusercontent.com/lockbook/lockbook/master/docs/graphics/logo.svg)
282```
283> ![Lockbook's logo](https://raw.githubusercontent.com/lockbook/lockbook/master/docs/graphics/logo.svg)
284
285## Headings
286To create a heading, add up to six `#`'s plus a space before your text. More `#`'s create a smaller heading.
287```md
288# Heading 1
289## Heading 2
290### Heading 3
291#### Heading 4
292##### Heading 5
293###### Heading 6
294```
295> # Heading 1
296> ## Heading 2
297> ### Heading 3
298> #### Heading 4
299> ##### Heading 5
300> ###### Heading 6
301
302## Lists
303Create a list item by adding `- `, `+ `, or `* ` for a bulleted list, `1. ` for a numbered list, or `- [ ] `, `+ [ ] `, or `* [ ] ` for a task list at the start of the line. The added characters are called the *list marker*.
304```md
305* bulleted list item
306- bulleted list item
307+ bulleted list item
308
3091. numbered list item
3101. numbered list item
3111. numbered list item
312
313- [ ] task list item
314- [x] task list item
315```
316>* bulleted list item
317>- bulleted list item
318>+ bulleted list item
319>
320>1. numbered list item
321>1. numbered list item
322>1. numbered list item
323>
324>- [ ] task list item
325>- [x] task list item
326
327List items can be nested. To nest an inner item in an outer one, the inner item's line must start with at least one space for each character in the outer item's list marker: usually 2 for bulleted lists, 3 for numbered lists, or 2 for tasks lists (the trailing `[ ] ` is excluded).
328```md
329* This is a bulleted list
330    * An inner item needs at least 2 spaces
3311. This is a numbered list
332    1. An inner item needs at least 3 spaces
333* [ ] This is a task list
334    * [ ] An inner item needs at least 2 spaces
335```
336> * This is a bulleted list
337>   * An inner item needs at least 2 spaces
338> 1. This is a numbered list
339>    1. An inner item needs at least 3 spaces
340> * [ ] This is a task list
341>   * [ ] An inner item needs at least 2 spaces
342
343List items can contain formatted content. For non-text content, each line must start with the same number of spaces as an inner list item would.
344```md
345* This item contains text,
346    > a quote
347    ### and a heading.
348* This item contains two lines of text.
349The second line doesn't need spaces.
350```
351> * This item contains text,
352>   > a quote
353>   ### and a heading.
354> * This item contains two lines of text.
355> The second line doesn't need spaces.
356
357## Quotes
358To create a block quote, add `> ` to each line.
359```md
360> This is a quote
361```
362> This is a quote
363
364Like list items, block quotes can contain formatted content.
365```md
366> This quote contains some text,
367> ```rust
368> // some code
369> fn main() { println!("Hello, world!"); }
370> ```
371> ### and a heading.
372
373> This quote contains two lines of text.
374The second line doesn't need added characters.
375```
376> This quote contains some text,
377> ```rust
378> // some code
379> fn main() { println!("Hello, world!"); }
380> ```
381> ### and a heading.
382
383> This quote contains two lines of text.
384The second line doesn't need added characters.
385
386## Alerts
387To create an alert, add one of 5 tags to the first line of a quote: `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, or `[!CAUTION]`. An alternate title can be added after the tag.
388```md
389> [!NOTE]
390> This is a note.
391
392> [!TIP]
393> This is a tip.
394
395> [!IMPORTANT]
396> This is important.
397
398> [!WARNING]
399> This is a warning.
400
401> [!CAUTION] Caution!!!!!
402> This is a caution.
403```
404> [!NOTE]
405> This is a note.
406
407> [!TIP]
408> This is a tip.
409
410> [!IMPORTANT]
411> This is important.
412
413> [!WARNING]
414> This is a warning.
415
416> [!CAUTION] Caution!!!!!
417> This is a caution.
418
419## Tables
420A table is written with `|`'s between columns and a row after the header row whose cell's contents are `-`'s.
421```md
422| Style         | Syntax              | Example           |
423|---------------|---------------------|-------------------|
424| emphasis      | `*emphasis*`        | *emphasis*        |
425| strong        | `**strong**`        | **strong**        |
426```
427> | Style         | Syntax              | Example           |
428> |---------------|---------------------|-------------------|
429> | emphasis      | `*emphasis*`        | *emphasis*        |
430> | strong        | `**strong**`        | **strong**        |
431
432## Code
433A code block is wrapped by two lines containing three backticks. A language can be added after the opening backticks.
434```md
435    ```rust
436    // some code
437    fn main() { println!("Hello, world!"); }
438    ```
439```
440> ```rust
441> // some code
442> fn main() { println!("Hello, world!"); }
443> ```
444
445You can also create a code block by indenting each line with four spaces. Indented code blocks cannot have a language.
446```md
447    // some code
448    fn main() { println!("Hello, world!"); }
449```
450>     // some code
451>     fn main() { println!("Hello, world!"); }
452
453## Thematic Breaks
454A thematic break is written with `***`, `---`, or `___` and shows a horizontal line across the page.
455```md
456***
457---
458___
459```
460> ***
461> ---
462> ___
463"#;
464}