use as_variant::as_variant;
use ruma_common::{
OwnedDeviceId, OwnedTransactionId,
serde::{JsonObject, StringEnum},
};
use ruma_macros::EventContent;
use serde::{Deserialize, Serialize, de};
use serde_json::{Value as JsonValue, from_value as from_json_value};
use crate::{GlobalAccountDataEventType, PrivOwnedStr};
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
#[ruma_event(type = "m.secret.request", kind = ToDevice)]
pub struct ToDeviceSecretRequestEventContent {
#[serde(flatten)]
pub action: RequestAction,
pub requesting_device_id: OwnedDeviceId,
pub request_id: OwnedTransactionId,
}
impl ToDeviceSecretRequestEventContent {
pub fn new(
action: RequestAction,
requesting_device_id: OwnedDeviceId,
request_id: OwnedTransactionId,
) -> Self {
Self { action, requesting_device_id, request_id }
}
}
#[derive(Clone, Debug, Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum RequestAction {
Request(SecretRequestAction),
RequestCancellation,
#[doc(hidden)]
#[serde(untagged)]
_Custom(CustomRequestAction),
}
impl RequestAction {
pub fn action(&self) -> &str {
match self {
Self::Request(_) => "request",
Self::RequestCancellation => "request_cancellation",
Self::_Custom(custom) => &custom.action,
}
}
}
impl<'de> Deserialize<'de> for RequestAction {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let mut json = JsonObject::deserialize(deserializer)?;
let action = json
.remove("action")
.and_then(|value| as_variant!(value, JsonValue::String))
.ok_or_else(|| de::Error::missing_field("action"))?;
match action.as_ref() {
"request" => from_json_value(json.into()).map(Self::Request),
"request_cancellation" => Ok(Self::RequestCancellation),
_ => Ok(Self::_Custom(CustomRequestAction { action })),
}
.map_err(de::Error::custom)
}
}
impl From<SecretRequestAction> for RequestAction {
fn from(value: SecretRequestAction) -> Self {
Self::Request(value)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct SecretRequestAction {
pub name: SecretName,
}
impl SecretRequestAction {
pub fn new(name: SecretName) -> Self {
Self { name }
}
}
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, StringEnum)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub enum SecretName {
#[ruma_enum(rename = "m.cross_signing.master")]
CrossSigningMasterKey,
#[ruma_enum(rename = "m.cross_signing.user_signing")]
CrossSigningUserSigningKey,
#[ruma_enum(rename = "m.cross_signing.self_signing")]
CrossSigningSelfSigningKey,
#[ruma_enum(rename = "m.megolm_backup.v1")]
RecoveryKey,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
impl From<SecretName> for GlobalAccountDataEventType {
fn from(value: SecretName) -> Self {
GlobalAccountDataEventType::from(value.as_str())
}
}
#[doc(hidden)]
#[derive(Clone, Debug, Serialize)]
pub struct CustomRequestAction {
action: String,
}
#[cfg(test)]
mod tests {
use assert_matches2::assert_matches;
use ruma_common::canonical_json::assert_to_canonical_json_eq;
use serde_json::{from_value as from_json_value, json};
use super::{
RequestAction, SecretName, SecretRequestAction, ToDeviceSecretRequestEventContent,
};
#[test]
fn secret_request_serialization() {
let content = ToDeviceSecretRequestEventContent::new(
RequestAction::Request(SecretRequestAction::new("org.example.some.secret".into())),
"ABCDEFG".into(),
"randomly_generated_id_9573".into(),
);
assert_to_canonical_json_eq!(
content,
json!({
"name": "org.example.some.secret",
"action": "request",
"requesting_device_id": "ABCDEFG",
"request_id": "randomly_generated_id_9573",
}),
);
}
#[test]
fn secret_request_recovery_key_serialization() {
let content = ToDeviceSecretRequestEventContent::new(
RequestAction::Request(SecretRequestAction::new(SecretName::RecoveryKey)),
"XYZxyz".into(),
"this_is_a_request_id".into(),
);
assert_to_canonical_json_eq!(
content,
json!({
"name": "m.megolm_backup.v1",
"action": "request",
"requesting_device_id": "XYZxyz",
"request_id": "this_is_a_request_id",
}),
);
}
#[test]
fn secret_request_cancellation_serialization() {
let content = ToDeviceSecretRequestEventContent::new(
RequestAction::RequestCancellation,
"ABCDEFG".into(),
"randomly_generated_id_9573".into(),
);
assert_to_canonical_json_eq!(
content,
json!({
"action": "request_cancellation",
"requesting_device_id": "ABCDEFG",
"request_id": "randomly_generated_id_9573",
}),
);
}
#[test]
fn secret_request_deserialization() {
let json = json!({
"name": "org.example.some.secret",
"action": "request",
"requesting_device_id": "ABCDEFG",
"request_id": "randomly_generated_id_9573"
});
let content = from_json_value::<ToDeviceSecretRequestEventContent>(json).unwrap();
assert_eq!(content.requesting_device_id, "ABCDEFG");
assert_eq!(content.request_id, "randomly_generated_id_9573");
assert_matches!(content.action, RequestAction::Request(secret));
assert_eq!(secret.name.as_str(), "org.example.some.secret");
}
#[test]
fn secret_request_cancellation_deserialization() {
let json = json!({
"action": "request_cancellation",
"requesting_device_id": "ABCDEFG",
"request_id": "randomly_generated_id_9573"
});
let content = from_json_value::<ToDeviceSecretRequestEventContent>(json).unwrap();
assert_eq!(content.requesting_device_id, "ABCDEFG");
assert_eq!(content.request_id, "randomly_generated_id_9573");
assert_matches!(content.action, RequestAction::RequestCancellation);
}
#[test]
fn secret_request_recovery_key_deserialization() {
let json = json!({
"name": "m.megolm_backup.v1",
"action": "request",
"requesting_device_id": "XYZxyz",
"request_id": "this_is_a_request_id"
});
let content = from_json_value::<ToDeviceSecretRequestEventContent>(json).unwrap();
assert_eq!(content.requesting_device_id, "XYZxyz");
assert_eq!(content.request_id, "this_is_a_request_id");
assert_matches!(content.action, RequestAction::Request(secret));
assert_eq!(secret.name, SecretName::RecoveryKey);
}
#[test]
fn secret_custom_action_serialization_roundtrip() {
let json = json!({
"action": "my_custom_action",
"requesting_device_id": "XYZxyz",
"request_id": "this_is_a_request_id"
});
let content = from_json_value::<ToDeviceSecretRequestEventContent>(json.clone()).unwrap();
assert_eq!(content.requesting_device_id, "XYZxyz");
assert_eq!(content.request_id, "this_is_a_request_id");
assert_eq!(content.action.action(), "my_custom_action");
assert_to_canonical_json_eq!(content, json);
}
}