Skip to main content

lockbook_server_lib/
debug_info.rs

1use crate::ServerError::ClientError;
2use std::fmt::Debug;
3use std::ops::DerefMut;
4
5use db_rs::Db;
6use lb_rs::model::{
7    account::BETA_USERS,
8    api::{UpsertDebugInfoError, UpsertDebugInfoRequest},
9    file_metadata::Owner,
10};
11use libsecp256k1::PublicKey;
12use reqwest::multipart;
13use serde_json::json;
14use tracing::{info, warn};
15
16use crate::{
17    RequestContext, ServerError, ServerState,
18    billing::{
19        app_store_client::AppStoreClient, google_play_client::GooglePlayClient,
20        stripe_client::StripeClient,
21    },
22    document_service::DocumentService,
23    schema::ServerDb,
24};
25
26impl<S, A, G, D> ServerState<S, A, G, D>
27where
28    S: StripeClient,
29    A: AppStoreClient,
30    G: GooglePlayClient,
31    D: DocumentService,
32{
33    pub async fn upsert_debug_info(
34        &self, context: RequestContext<UpsertDebugInfoRequest>,
35    ) -> Result<(), ServerError<UpsertDebugInfoError>> {
36        let mut lock = self.index_db.lock().await;
37
38        let db = lock.deref_mut();
39
40        let tx = db.begin_transaction()?;
41
42        if !Self::is_beta_user::<UpsertDebugInfoError>(db, &context.public_key) {
43            return Err(ClientError(UpsertDebugInfoError::NotPermissioned));
44        }
45
46        let owner = Owner(context.public_key);
47
48        let debug_info = context.request.debug_info.clone();
49        let new_panics_count = debug_info.panics.len();
50
51        let maybe_old_debug_info = db.debug_info.insert(
52            owner,
53            context.request.debug_info.lb_id,
54            context.request.debug_info,
55        )?;
56
57        let old_panics_count =
58            if let Some(debug_info) = maybe_old_debug_info { debug_info.panics.len() } else { 0 };
59
60        if new_panics_count > old_panics_count {
61            if let Some(panic) = debug_info.panics.first() {
62                warn!(?debug_info, "beta user experienced a panic");
63
64                self.send_panic_to_discord(&debug_info, panic).await?;
65            }
66        }
67
68        tx.drop_safely()?;
69
70        Ok(())
71    }
72
73    async fn send_panic_to_discord(
74        &self, debug_info: &lb_rs::service::debug::DebugInfo, panic: &str,
75    ) -> Result<(), ServerError<UpsertDebugInfoError>> {
76        let discord_webhook_url = match &self.config.server.discord_webhook_url {
77            Some(url) => url,
78            None => return Ok(()),
79        };
80
81        let maybe_new_line_index = panic.find('\n');
82        let mut panic_title = None;
83
84        if let Some(new_line_index) = maybe_new_line_index {
85            panic_title = Some(panic[0..new_line_index].to_string());
86        }
87
88        let payload = json!({
89            "username": "Panic Reporter",
90            "embeds": [{
91                "color": 14622784,
92                "author": { "name": debug_info.name },
93                "title": panic_title.unwrap_or("".to_string()),
94            }]
95        });
96
97        let debug_info_part = multipart::Part::bytes(serde_json::to_vec_pretty(&debug_info)?)
98            .file_name(format!("{}_{}_debug_info.json", debug_info.name, debug_info.panics.len()))
99            .mime_str("application/json")?;
100
101        let form = multipart::Form::new()
102            .part("file", debug_info_part)
103            .text("payload_json", payload.to_string());
104
105        let response = self
106            .discord_client
107            .post(discord_webhook_url)
108            .multipart(form)
109            .send()
110            .await?;
111
112        if response.status().is_success() {
113            info!("Notifed discord of a panic!");
114        } else {
115            warn!("Failed to notify discord: {:?} {:?}", response.status(), response.text().await?);
116        }
117        Ok(())
118    }
119
120    pub fn is_beta_user<E: Debug>(db: &ServerDb, public_key: &PublicKey) -> bool {
121        let is_beta = match db.accounts.get().get(&Owner(*public_key)) {
122            None => false,
123            Some(account) => BETA_USERS.contains(&account.username.as_str()),
124        };
125
126        is_beta
127    }
128}