diff --git a/services/reports/src/database/item.rs b/services/reports/src/database/item.rs --- a/services/reports/src/database/item.rs +++ b/services/reports/src/database/item.rs @@ -115,44 +115,6 @@ } size } - - /// Creates a report item from a report input payload - /// - /// WARN: Note that this method stores content as [`ReportStorage::Database`] - /// regardless of its size. Use [`ensure_size_constraints`] to move content to - /// blob storage if necessary. - pub fn from_input( - payload: ReportInput, - user_id: Option, - ) -> Result { - let ReportInput { - platform_details, - report_type, - time, - mut report_content, - } = payload; - - let platform = platform_details.platform.clone(); - - // Add "platformDetails" back to report content - let platform_details_value = serde_json::to_value(platform_details)?; - report_content - .insert("platformDetails".to_string(), platform_details_value); - - // serialize report JSON to bytes - let content_bytes = serde_json::to_vec(&report_content)?; - let content = ReportContent::Database(content_bytes); - - Ok(ReportItem { - id: ReportID::default(), - user_id: user_id.unwrap_or("[null]".to_string()), - platform, - report_type, - creation_time: time.unwrap_or_else(Utc::now), - encryption_key: None, - content, - }) - } } impl TryFrom for ReportItem { diff --git a/services/reports/src/service.rs b/services/reports/src/service.rs --- a/services/reports/src/service.rs +++ b/services/reports/src/service.rs @@ -1,4 +1,5 @@ use actix_web::FromRequest; +use chrono::Utc; use comm_services_lib::{ auth::UserIdentity, blob::client::{BlobServiceClient, BlobServiceError}, @@ -15,9 +16,9 @@ use crate::{ database::{ client::{DatabaseClient, ReportsPage}, - item::ReportItem, + item::{ReportContent, ReportItem}, }, - email::config::EmailConfig, + email::{config::EmailConfig, ReportEmail}, report_types::{ReportID, ReportInput, ReportOutput, ReportType}, }; @@ -77,20 +78,20 @@ pub async fn save_reports( &self, - reports: Vec, + inputs: Vec, ) -> ServiceResult> { - let mut items = Vec::with_capacity(reports.len()); + let mut reports = Vec::with_capacity(inputs.len()); let mut tasks = tokio::task::JoinSet::new(); - // 1. Concurrently upload reports to blob service if needed - for input in reports { + // 1. Concurrently prepare reports. Upload them to blob service if needed + for input in inputs { let blob_client = self.blob_client.clone(); let user_id = self.requesting_user_id.clone(); tasks.spawn(async move { - let mut item = ReportItem::from_input(input, user_id) + let mut report = process_report(input, user_id) .map_err(ReportsServiceError::SerdeError)?; - item.ensure_size_constraints(&blob_client).await?; - Ok(item) + report.db_item.ensure_size_constraints(&blob_client).await?; + Ok(report) }); } @@ -101,12 +102,23 @@ error!("Task failed to join: {err}"); ReportsServiceError::Unexpected })?; - items.push(result?); + reports.push(result?); } - // 3. Store reports in database - let ids = items.iter().map(|item| item.id.clone()).collect(); - self.db.save_reports(items).await?; + let (ids, (db_items, emails)): (Vec<_>, (Vec<_>, Vec<_>)) = reports + .into_iter() + .map(|ProcessedReport { id, db_item, email }| (id, (db_item, email))) + .unzip(); + + // 3. Store the reports in database + self.db.save_reports(db_items).await?; + + // 4. Send e-mails asynchronously + tokio::spawn(async move { + if let Err(err) = crate::email::send_emails(emails).await { + error!("Failed to send e-mails: {err}"); + } + }); Ok(ids) } @@ -201,6 +213,47 @@ } } +struct ProcessedReport { + id: ReportID, + db_item: ReportItem, + email: ReportEmail, +} + +fn process_report( + input: ReportInput, + user_id: Option, +) -> Result { + let id = ReportID::default(); + let email = crate::email::prepare_email(&input, &id, user_id.as_deref()); + + let ReportInput { + platform_details, + report_type, + time, + mut report_content, + } = input; + + // Add "platformDetails" back to report content. + // It was deserialized into a separate field. + let platform_details_value = serde_json::to_value(&platform_details)?; + report_content.insert("platformDetails".to_string(), platform_details_value); + + // serialize report JSON to bytes + let content_bytes = serde_json::to_vec(&report_content)?; + + let db_item = ReportItem { + id: id.clone(), + user_id: user_id.unwrap_or("[null]".to_string()), + platform: platform_details.platform.clone(), + report_type, + creation_time: time.unwrap_or_else(Utc::now), + encryption_key: None, + content: ReportContent::Database(content_bytes), + }; + + Ok(ProcessedReport { id, db_item, email }) +} + /// Transforms report content JSON into format that can be /// imported into Redux DevTools. fn prepare_redux_devtools_import(