autoendpoint/extractors/
message_id.rs

1use crate::error::{ApiError, ApiErrorKind, ApiResult};
2use crate::server::AppState;
3use actix_web::dev::Payload;
4use actix_web::{web::Data, FromRequest, HttpRequest};
5use autopush_common::notification::{STANDARD_NOTIFICATION_PREFIX, TOPIC_NOTIFICATION_PREFIX};
6use fernet::MultiFernet;
7use futures::future;
8use uuid::Uuid;
9
10/// Holds information about a notification. The information is encoded and
11/// encrypted into a "message ID" which is presented to the user. Later, the
12/// user can send us the message ID to perform operations on the associated
13/// notification (e.g. delete it).
14#[derive(Debug)]
15pub enum MessageId {
16    WithTopic {
17        uaid: Uuid,
18        channel_id: Uuid,
19        topic: String,
20    },
21    WithoutTopic {
22        uaid: Uuid,
23        channel_id: Uuid,
24        timestamp: u64,
25    },
26}
27
28impl FromRequest for MessageId {
29    type Error = ApiError;
30    type Future = future::Ready<Result<Self, Self::Error>>;
31
32    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
33        let message_id_param = req
34            .match_info()
35            .get("message_id")
36            .expect("{message_id} must be part of the path");
37        let app_state: Data<AppState> = Data::extract(req)
38            .into_inner()
39            .expect("No server state found");
40
41        future::ready(MessageId::decrypt(&app_state.fernet, message_id_param))
42    }
43}
44
45impl MessageId {
46    /// Encode and encrypt the message ID
47    pub fn encrypt(&self, fernet: &MultiFernet) -> String {
48        let id_str = match self {
49            MessageId::WithTopic {
50                uaid,
51                channel_id,
52                topic,
53            } => format!(
54                "{}:{}:{}:{}",
55                TOPIC_NOTIFICATION_PREFIX,
56                &uaid.as_simple(),
57                &channel_id.as_simple(),
58                topic
59            ),
60            MessageId::WithoutTopic {
61                uaid,
62                channel_id,
63                timestamp,
64            } => format!(
65                "{}:{}:{}:{}",
66                STANDARD_NOTIFICATION_PREFIX,
67                uaid.as_simple(),
68                channel_id.as_simple(),
69                timestamp
70            ),
71        };
72
73        fernet.encrypt(id_str.as_bytes())
74    }
75
76    /// Decrypt and decode the message ID
77    pub fn decrypt(fernet: &MultiFernet, message_id: &str) -> ApiResult<Self> {
78        let decrypted_bytes = fernet
79            .decrypt(message_id)
80            .map_err(|_| ApiErrorKind::InvalidMessageId)?;
81        let decrypted_str = String::from_utf8_lossy(&decrypted_bytes);
82        let segments: Vec<_> = decrypted_str.split(':').collect();
83
84        if segments.len() != 4 {
85            return Err(ApiErrorKind::InvalidMessageId.into());
86        }
87
88        let (version, uaid, chid, topic_or_timestamp) =
89            (segments[0], segments[1], segments[2], segments[3]);
90
91        match version {
92            "01" => Ok(MessageId::WithTopic {
93                uaid: Uuid::parse_str(uaid).map_err(|_| ApiErrorKind::InvalidMessageId)?,
94                channel_id: Uuid::parse_str(chid).map_err(|_| ApiErrorKind::InvalidMessageId)?,
95                topic: topic_or_timestamp.to_string(),
96            }),
97            "02" => Ok(MessageId::WithoutTopic {
98                uaid: Uuid::parse_str(uaid).map_err(|_| ApiErrorKind::InvalidMessageId)?,
99                channel_id: Uuid::parse_str(chid).map_err(|_| ApiErrorKind::InvalidMessageId)?,
100                timestamp: topic_or_timestamp
101                    .parse()
102                    .map_err(|_| ApiErrorKind::InvalidMessageId)?,
103            }),
104            _ => Err(ApiErrorKind::InvalidMessageId.into()),
105        }
106    }
107
108    /// Get the UAID of the associated notification
109    pub fn uaid(&self) -> Uuid {
110        match self {
111            MessageId::WithTopic { uaid, .. } => *uaid,
112            MessageId::WithoutTopic { uaid, .. } => *uaid,
113        }
114    }
115
116    /// Get the sort-key for the associated notification
117    pub fn sort_key(&self) -> String {
118        match self {
119            MessageId::WithTopic {
120                channel_id, topic, ..
121            } => format!(
122                "{}:{}:{}",
123                TOPIC_NOTIFICATION_PREFIX,
124                channel_id.as_hyphenated(),
125                topic
126            ),
127            MessageId::WithoutTopic {
128                channel_id,
129                timestamp,
130                ..
131            } => format!(
132                "{}:{}:{}",
133                STANDARD_NOTIFICATION_PREFIX,
134                timestamp,
135                channel_id.as_hyphenated()
136            ),
137        }
138    }
139}