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, LocalLb};
12use libsecp256k1::SecretKey;
13use qrcode_generator::QrCodeEcc;
14
15use crate::io::network::ApiError;
16
17impl LocalLb {
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.get().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.insert(account.clone())?;
53        db.base_metadata.insert(root_id, root)?;
54        db.last_synced.insert(last_synced as i64)?;
55        db.root.insert(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.insert(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.insert(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 async fn delete_account(&self) -> LbResult<()> {
211        let account = self.get_account()?;
212
213        self.client
214            .request(account, DeleteAccountRequest {})
215            .await
216            .map_err(|err| match err {
217                ApiError::SendFailed(_) => LbErrKind::ServerUnreachable,
218                ApiError::ClientUpdateRequired => LbErrKind::ClientUpdateRequired,
219                _ => core_err_unexpected(err),
220            })?;
221
222        let mut tx = self.begin_tx().await;
223        let db = tx.db();
224
225        db.account.clear()?;
226        db.last_synced.clear()?;
227        db.base_metadata.clear()?;
228        db.root.clear()?;
229        db.local_metadata.clear()?;
230        db.pub_key_lookup.clear()?;
231
232        // todo: clear cache?
233
234        Ok(())
235    }
236
237    const WELCOME_MESSAGE: &'static str = r#"# Markdown Syntax
238Markdown is a language for easily formatting your documents. This document can help you get started.
239
240## Styled Text
241To style text, wrap your text in the corresponding characters.
242| Style         | Syntax              | Example           |
243|---------------|---------------------|-------------------|
244| emphasis      | `*emphasis*`        | *emphasis*        |
245| strong        | `**strong**`        | **strong**        |
246| strikethrough | `~~strikethrough~~` | ~~strikethrough~~ |
247| underline     | `__underline__`     | __underline__     |
248| code          | ``code``            | `code`            |
249| highlight     | `==highlight==`     | ==highlight==     |
250| spoiler       | `||spoiler||`       | ||spoiler||       |
251| superscript   | `^superscript^`     | ^superscript^     |
252| subscript     | `~subscript~`       | ~subscript~       |
253
254## Links
255To 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.
256```md
257[Lockbook's website](https://lockbook.net)
258```
259> [Lockbook's website](https://lockbook.net)
260
261## Images
262To embed an image, add a `!` to the beginning of the link syntax.
263```md
264![Lockbook's logo](https://raw.githubusercontent.com/lockbook/lockbook/master/docs/graphics/logo.svg)
265```
266> ![Lockbook's logo](https://raw.githubusercontent.com/lockbook/lockbook/master/docs/graphics/logo.svg)
267
268## Headings
269To create a heading, add up to six `#`'s plus a space before your text. More `#`'s create a smaller heading.
270```md
271# Heading 1
272## Heading 2
273### Heading 3
274#### Heading 4
275##### Heading 5
276###### Heading 6
277```
278> # Heading 1
279> ## Heading 2
280> ### Heading 3
281> #### Heading 4
282> ##### Heading 5
283> ###### Heading 6
284
285## Lists
286Create 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*.
287```md
288* bulleted list item
289- bulleted list item
290+ bulleted list item
291
2921. numbered list item
2931. numbered list item
2941. numbered list item
295
296- [ ] task list item
297- [x] task list item
298```
299>* bulleted list item
300>- bulleted list item
301>+ bulleted list item
302>
303>1. numbered list item
304>1. numbered list item
305>1. numbered list item
306>
307>- [ ] task list item
308>- [x] task list item
309
310List 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).
311```md
312* This is a bulleted list
313    * An inner item needs at least 2 spaces
3141. This is a numbered list
315    1. An inner item needs at least 3 spaces
316* [ ] This is a task list
317    * [ ] An inner item needs at least 2 spaces
318```
319> * This is a bulleted list
320>   * An inner item needs at least 2 spaces
321> 1. This is a numbered list
322>    1. An inner item needs at least 3 spaces
323> * [ ] This is a task list
324>   * [ ] An inner item needs at least 2 spaces
325
326List 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.
327```md
328* This item contains text,
329    > a quote
330    ### and a heading.
331* This item contains two lines of text.
332The second line doesn't need spaces.
333```
334> * This item contains text,
335>   > a quote
336>   ### and a heading.
337> * This item contains two lines of text.
338> The second line doesn't need spaces.
339
340## Quotes
341To create a block quote, add `> ` to each line.
342```md
343> This is a quote
344```
345> This is a quote
346
347Like list items, block quotes can contain formatted content.
348```md
349> This quote contains some text,
350> ```rust
351> // some code
352> fn main() { println!("Hello, world!"); }
353> ```
354> ### and a heading.
355
356> This quote contains two lines of text.
357The second line doesn't need added characters.
358```
359> This quote contains some text,
360> ```rust
361> // some code
362> fn main() { println!("Hello, world!"); }
363> ```
364> ### and a heading.
365
366> This quote contains two lines of text.
367The second line doesn't need added characters.
368
369## Alerts
370To 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.
371```md
372> [!NOTE]
373> This is a note.
374
375> [!TIP]
376> This is a tip.
377
378> [!IMPORTANT]
379> This is important.
380
381> [!WARNING]
382> This is a warning.
383
384> [!CAUTION] Caution!!!!!
385> This is a caution.
386```
387> [!NOTE]
388> This is a note.
389
390> [!TIP]
391> This is a tip.
392
393> [!IMPORTANT]
394> This is important.
395
396> [!WARNING]
397> This is a warning.
398
399> [!CAUTION] Caution!!!!!
400> This is a caution.
401
402## Tables
403A table is written with `|`'s between columns and a row after the header row whose cell's contents are `-`'s.
404```md
405| Style         | Syntax              | Example           |
406|---------------|---------------------|-------------------|
407| emphasis      | `*emphasis*`        | *emphasis*        |
408| strong        | `**strong**`        | **strong**        |
409```
410> | Style         | Syntax              | Example           |
411> |---------------|---------------------|-------------------|
412> | emphasis      | `*emphasis*`        | *emphasis*        |
413> | strong        | `**strong**`        | **strong**        |
414
415## Code
416A code block is wrapped by two lines containing three backticks. A language can be added after the opening backticks.
417```md
418    ```rust
419    // some code
420    fn main() { println!("Hello, world!"); }
421    ```
422```
423> ```rust
424> // some code
425> fn main() { println!("Hello, world!"); }
426> ```
427
428You can also create a code block by indenting each line with four spaces. Indented code blocks cannot have a language.
429```md
430    // some code
431    fn main() { println!("Hello, world!"); }
432```
433>     // some code
434>     fn main() { println!("Hello, world!"); }
435
436## Thematic Breaks
437A thematic break is written with `***`, `---`, or `___` and shows a horizontal line across the page.
438```md
439***
440---
441___
442```
443> ***
444> ---
445> ___
446"#;
447}
448
449impl crate::Lb {
450    #[instrument(level = "debug", skip(self), err(Debug))]
451    pub fn export_account_private_key(&self) -> LbResult<String> {
452        let account = self.get_account()?;
453        Ok(base64::encode(account.private_key.serialize()))
454    }
455
456    pub fn export_account_phrase(&self) -> LbResult<String> {
457        let account = self.get_account()?;
458        Ok(account.get_phrase()?.join(" "))
459    }
460
461    pub fn export_account_qr(&self) -> LbResult<Vec<u8>> {
462        let acct_secret = self.export_account_private_key()?;
463        qrcode_generator::to_png_to_vec(acct_secret, QrCodeEcc::Low, 1024)
464            .map_err(|err| core_err_unexpected(err).into())
465    }
466}