Skip to main content

lb_rs/service/
share.rs

1use crate::Lb;
2use crate::model::api::GetPublicKeyRequest;
3use crate::model::errors::{LbErr, LbErrKind, LbResult};
4use crate::model::file::{File, ShareMode};
5use crate::model::file_like::FileLike;
6use crate::model::file_metadata::Owner;
7use crate::model::tree_like::TreeLike;
8use crate::service::events::Actor;
9use libsecp256k1::PublicKey;
10use serde::{Deserialize, Serialize};
11use std::collections::{HashMap, HashSet};
12use uuid::Uuid;
13
14/// Account-wide sharing relationships, based on the current local metadata snapshot.
15/// Counts documents, not folders or link placeholders, including pending incoming shares.
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17pub struct SharingContact {
18    pub username: String,
19    pub outgoing_file_count: u64,
20    pub incoming_file_count: u64,
21    /// Distinct documents shared in either direction.
22    pub total_file_count: u64,
23}
24
25impl Lb {
26    /// Ranked contacts for the signed-in account. This is a local, read-only query;
27    /// callers should run it off their UI thread.
28    /// Inheritance follows real metadata parents, never the local placement of a link.
29    #[instrument(level = "debug", skip(self))]
30    pub async fn get_sharing_contacts(&self) -> LbResult<Vec<SharingContact>> {
31        let me = self.keychain.get_pk()?;
32        let tx = self.ro_tx().await;
33        let db = tx.db();
34        let mut tree = (&db.base_metadata).to_staged(&db.local_metadata).to_lazy();
35        let mut counts: HashMap<Owner, (u64, u64, u64)> = HashMap::new();
36        let root = db.root.as_ref().ok_or(LbErrKind::RootNonexistent)?;
37        // A document can be reachable through an accepted link and a pending share.
38        // Count each metadata ID only once, regardless of how it was reached.
39        let mut document_ids = tree.descendants_using_links(root)?;
40        document_ids.extend(tree.non_deleted_pending_files(&self.keychain)?);
41        for id in document_ids {
42            if !tree.find(&id)?.is_document() {
43                continue;
44            }
45            let mut outgoing = HashSet::new();
46            let mut incoming = HashSet::new();
47            let mut visited = HashSet::new();
48            let mut current = id;
49            let mut deleted = false;
50            while visited.insert(current) {
51                let Some(file) = tree.maybe_find(&current) else { break };
52                if file.explicitly_deleted() {
53                    deleted = true;
54                    break;
55                }
56                for access in file.user_access_keys() {
57                    if access.deleted || access.encrypted_by == access.encrypted_for {
58                        continue;
59                    }
60                    if access.encrypted_by == me {
61                        outgoing.insert(Owner(access.encrypted_for));
62                    } else if access.encrypted_for == me {
63                        incoming.insert(Owner(access.encrypted_by));
64                    }
65                }
66                if file.is_root() {
67                    break;
68                }
69                current = *file.parent();
70            }
71            if deleted {
72                continue;
73            }
74            for user in outgoing.union(&incoming) {
75                let count = counts.entry(*user).or_default();
76                count.0 += u64::from(outgoing.contains(user));
77                count.1 += u64::from(incoming.contains(user));
78                count.2 += 1;
79            }
80        }
81        let mut contacts: Vec<_> = counts
82            .into_iter()
83            .filter_map(|(owner, (outgoing, incoming, total))| {
84                let username = db.pub_key_lookup.get(&owner)?.clone();
85                let trimmed = username.trim();
86                if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("<unknown>") {
87                    return None;
88                }
89                Some(SharingContact {
90                    username,
91                    outgoing_file_count: outgoing,
92                    incoming_file_count: incoming,
93                    total_file_count: total,
94                })
95            })
96            .collect();
97        contacts.sort_by(|a, b| {
98            b.total_file_count
99                .cmp(&a.total_file_count)
100                .then_with(|| a.username.cmp(&b.username))
101        });
102        Ok(contacts)
103    }
104
105    // todo: this can check whether the username is known already
106    #[instrument(level = "debug", skip(self))]
107    pub async fn share_file(&self, id: Uuid, username: &str, mode: ShareMode) -> LbResult<()> {
108        let account = self.get_account()?;
109        let username = username.to_lowercase();
110
111        let sharee = Owner(
112            self.client
113                .request(account, GetPublicKeyRequest { username: username.clone() })
114                .await
115                .map_err(LbErr::from)?
116                .key,
117        );
118
119        let mut tx = self.begin_tx().await;
120        let db = tx.db();
121
122        let mut tree = (&db.base_metadata)
123            .to_staged(&mut db.local_metadata)
124            .to_lazy();
125        db.pub_key_lookup.insert(sharee, username)?;
126
127        tree.add_share(id, sharee, mode, &self.keychain)?;
128
129        tx.end();
130
131        self.events.meta_changed(Actor::User(None));
132
133        Ok(())
134    }
135
136    /// returns pending shares -- files shared with us that we haven't accepted or rejected
137    /// this function just returns the actual files that were shared -- or the roots of shared
138    /// trees. For the full set of shares see [Self::get_pending_share_files]
139    #[instrument(level = "debug", skip(self))]
140    pub async fn get_pending_shares(&self) -> LbResult<Vec<File>> {
141        let tx = self.ro_tx().await;
142        let db = tx.db();
143
144        let mut tree = (&db.base_metadata).to_staged(&db.local_metadata).to_lazy();
145        let pending_roots = tree.pending_roots(&self.keychain)?.into_iter();
146
147        tree.decrypt_all(&self.keychain, pending_roots, &db.pub_key_lookup, false)
148    }
149
150    /// returns *all* the files associated with any pending shares (the share as well as it's
151    /// descendants).
152    #[instrument(level = "debug", skip(self))]
153    pub async fn get_pending_share_files(&self) -> LbResult<Vec<File>> {
154        let tx = self.ro_tx().await;
155        let db = tx.db();
156
157        let mut tree = (&db.base_metadata).to_staged(&db.local_metadata).to_lazy();
158        let pending_files = tree.non_deleted_pending_files(&self.keychain)?.into_iter();
159
160        tree.decrypt_all(&self.keychain, pending_files, &db.pub_key_lookup, false)
161    }
162
163    #[instrument(level = "debug", skip(self))]
164    async fn delete_share(
165        &self, id: &Uuid, maybe_encrypted_for: Option<PublicKey>,
166    ) -> LbResult<()> {
167        let mut tx = self.begin_tx().await;
168        let db = tx.db();
169
170        let mut tree = (&db.base_metadata)
171            .to_staged(&mut db.local_metadata)
172            .to_lazy();
173
174        tree.delete_share(id, maybe_encrypted_for, &self.keychain)?;
175
176        tx.end();
177        self.events.meta_changed(Actor::User(None));
178
179        Ok(())
180    }
181
182    #[instrument(level = "debug", skip(self))]
183    pub async fn known_usernames(&self) -> LbResult<Vec<String>> {
184        let db = self.ro_tx().await;
185        let db = db.db();
186
187        Ok(db
188            .pub_key_lookup
189            .iter()
190            .map(|(_, value)| value)
191            .cloned()
192            .collect())
193    }
194
195    /// Whether `username` is a known Lockbook account (local cache, then server).
196    ///
197    /// Server lookup uses [`GetPublicKeyRequest`]. Auth is only a signed envelope —
198    /// when signed out we probe with an ephemeral key so onboard can check
199    /// availability without an account. `UserNotFound` maps to `false`.
200    #[instrument(level = "debug", skip(self))]
201    pub async fn username_exists(&self, username: &str) -> LbResult<bool> {
202        use crate::DEFAULT_API_LOCATION;
203        use crate::model::account::Account;
204        use crate::model::errors::LbErrKind;
205
206        let username = username.trim().to_lowercase();
207        if username.is_empty() {
208            return Ok(false);
209        }
210        // Signed-in only: local roster can short-circuit a hit. Empty when signed out.
211        if let Ok(known) = self.known_usernames().await {
212            if known.iter().any(|u| u.eq_ignore_ascii_case(&username)) {
213                return Ok(true);
214            }
215        }
216
217        // Prefer real account for api_url; else ephemeral signer + API_URL / default.
218        let owned;
219        let account = match self.get_account() {
220            Ok(a) => a,
221            Err(_) => {
222                let api =
223                    std::env::var("API_URL").unwrap_or_else(|_| DEFAULT_API_LOCATION.to_string());
224                owned = Account::new(String::new(), api);
225                &owned
226            }
227        };
228
229        match self
230            .client
231            .request(account, GetPublicKeyRequest { username })
232            .await
233        {
234            Ok(_) => Ok(true),
235            Err(e) => {
236                let err = LbErr::from(e);
237                match err.kind {
238                    // GetPublicKey UserNotFound → AccountNonexistent in LbErr map.
239                    LbErrKind::AccountNonexistent | LbErrKind::UsernameNotFound => Ok(false),
240                    _ => Err(err),
241                }
242            }
243        }
244    }
245
246    #[instrument(level = "debug", skip(self))]
247    pub async fn reject_share(&self, id: &Uuid) -> Result<(), LbErr> {
248        let pk = self.keychain.get_pk()?;
249        self.delete_share(id, Some(pk)).await
250    }
251}