diff --git a/services/reports/Cargo.lock b/services/reports/Cargo.lock --- a/services/reports/Cargo.lock +++ b/services/reports/Cargo.lock @@ -518,6 +518,7 @@ "iana-time-zone", "js-sys", "num-traits", + "serde", "time 0.1.45", "wasm-bindgen", "winapi", @@ -813,6 +814,17 @@ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + [[package]] name = "gimli" version = "0.28.0" @@ -1133,6 +1145,17 @@ "winapi", ] +[[package]] +name = "num-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1354,13 +1377,21 @@ dependencies = [ "anyhow", "aws-config", + "chrono", "clap", "comm-services-lib", + "derive_more", + "num-derive", + "num-traits", "once_cell", + "serde", + "serde_json", + "serde_repr", "tokio", "tokio-stream", "tracing", "tracing-subscriber", + "uuid", ] [[package]] @@ -1564,6 +1595,17 @@ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1983,6 +2025,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +dependencies = [ + "getrandom", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/services/reports/Cargo.toml b/services/reports/Cargo.toml --- a/services/reports/Cargo.toml +++ b/services/reports/Cargo.toml @@ -9,12 +9,20 @@ [dependencies] anyhow = "1.0" aws-config = "0.55" +chrono = { version = "0.4", features = ["serde"] } clap = { version = "4.0", features = ["derive", "env"] } comm-services-lib = { path = "../comm-services-lib", features = [ "blob-client", ] } +derive_more = "0.99" +num-traits = "0.2" +num-derive = "0.4" once_cell = "1.17" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_repr = "0.1" tokio = { version = "1.32", features = ["macros", "rt-multi-thread"] } tokio-stream = "0.1" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +uuid = { version = "1.2", features = ["v4"] } diff --git a/services/reports/src/main.rs b/services/reports/src/main.rs --- a/services/reports/src/main.rs +++ b/services/reports/src/main.rs @@ -1,4 +1,5 @@ pub mod config; +pub mod report_types; use anyhow::Result; use tracing_subscriber::filter::{EnvFilter, LevelFilter}; diff --git a/services/reports/src/report_types.rs b/services/reports/src/report_types.rs new file mode 100644 --- /dev/null +++ b/services/reports/src/report_types.rs @@ -0,0 +1,114 @@ +// Report ID + +use std::collections::HashMap; + +use chrono::{serde::ts_milliseconds_option, DateTime, Utc}; +use derive_more::{Deref, Display, Into}; +use num_derive::FromPrimitive; +use serde::{de::Error, Deserialize, Serialize}; +use serde_repr::Deserialize_repr; + +#[derive(Clone, Debug, Deref, Serialize, Into)] +#[repr(transparent)] +pub struct ReportID(String); +impl Default for ReportID { + fn default() -> Self { + let uuid = uuid::Uuid::new_v4(); + ReportID(uuid.to_string()) + } +} +impl From for ReportID { + fn from(value: String) -> Self { + ReportID(value) + } +} + +/// Serialized / deserialized report type. +/// We receive report type from clients as a number, +/// but want to display it as a string. +#[derive( + Copy, Clone, Debug, Default, FromPrimitive, Serialize, Deserialize_repr, +)] +#[repr(u8)] +#[serde(rename_all(serialize = "snake_case"))] +pub enum ReportType { + // NOTE: Keep these in sync with `reportTypes` in lib/types/report-types.js + #[default] + ErrorReport = 0, + ThreadInconsistency = 1, + EntryInconsistency = 2, + MediaMission = 3, + UserInconsistency = 4, +} + +/// Report platform +#[derive(Clone, Debug, Serialize, Deserialize, Display)] +#[serde(rename_all = "lowercase")] +pub enum ReportPlatform { + Android, + IOS, + Web, + Windows, + MacOS, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlatformDetails { + pub platform: ReportPlatform, + code_version: Option, + state_version: Option, +} + +/// Input report payload - this is the JSON we receive from clients +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(remote = "Self")] // we delegate to our custom validation trait +pub struct ReportInput { + pub platform_details: PlatformDetails, + + #[serde(rename = "type")] + #[serde(default)] + pub report_type: ReportType, + + #[serde(default)] + #[serde(with = "ts_milliseconds_option")] + pub time: Option>, + + // we usually don't care about the rest of the fields + // so we just keep them as a JSON object + #[serde(flatten)] + pub report_content: HashMap, +} + +// We can do additional validation here +impl<'de> serde::de::Deserialize<'de> for ReportInput { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + let mut this = Self::deserialize(deserializer)?; + if this.time.is_none() { + if !matches!(this.report_type, ReportType::ThreadInconsistency) { + return Err(Error::custom( + "The 'time' field is optional only for thread inconsistency reports", + )); + } + this.time = Some(Utc::now()); + } + Ok(this) + } +} + +/// Report output payload - this is used to view the report +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReportOutput { + pub id: ReportID, + #[serde(rename = "userID")] + pub user_id: String, + pub platform: ReportPlatform, + pub report_type: ReportType, + pub creation_time: DateTime, + pub content: HashMap, +}