Skip to main content

im_core/attachments/
selection.rs

1use serde::{Deserialize, Serialize};
2use serde_json::{Map, Value};
3
4pub const ERR_ATTACHMENT_NOT_FOUND: &str = "attachment not found in message content";
5pub const ERR_ATTACHMENT_ID_REQUIRED: &str =
6    "attachment_id is required for messages with multiple attachments";
7pub const ERR_ATTACHMENT_MESSAGE_INVALID: &str = "message is not an attachment manifest";
8
9#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
10pub struct AttachmentSelection {
11    pub message_id: String,
12    pub requested_id: String,
13    pub sender_did: String,
14    pub attachment_id: String,
15    pub filename: String,
16    pub mime_type: String,
17    pub size: String,
18    pub digest_b64u: String,
19    pub object_uri: String,
20    pub caption: String,
21    pub message_security_profile: String,
22    pub object_encryption_mode: String,
23    pub object_cipher: Option<String>,
24    pub plaintext_size: Option<String>,
25}
26
27#[derive(Debug, Clone, Default, PartialEq, Eq)]
28pub(crate) struct InternalAttachmentSelection {
29    pub public: AttachmentSelection,
30    pub object_key_b64u: Option<String>,
31    pub nonce_b64u: Option<String>,
32}
33
34pub(crate) fn find_attachment_selection(
35    messages: &[Value],
36    requested_message_id: &str,
37    requested_attachment_id: &str,
38) -> crate::ImResult<AttachmentSelection> {
39    find_internal_attachment_selection(messages, requested_message_id, requested_attachment_id)
40        .map(|selection| selection.redacted_public())
41}
42
43pub(crate) fn find_internal_attachment_selection(
44    messages: &[Value],
45    requested_message_id: &str,
46    requested_attachment_id: &str,
47) -> crate::ImResult<InternalAttachmentSelection> {
48    for message in messages {
49        let Some(message_object) = message.as_object() else {
50            continue;
51        };
52        let view_id = string_from_value(message_object.get("id"));
53        let raw_message_id = string_from_value(message_object.get("message_id"));
54        let actual_message_id = if raw_message_id.is_empty() {
55            view_id.clone()
56        } else {
57            raw_message_id
58        };
59        if requested_message_id != view_id && requested_message_id != actual_message_id {
60            continue;
61        }
62        let content = decode_attachment_content(message_object.get("content"))?;
63        let attachments = attachments_from_content(content.get("attachments"));
64        if attachments.is_empty() {
65            return Err(attachment_message_invalid());
66        }
67        let selected = select_attachment_entry(&attachments, requested_attachment_id)?;
68        let access_info = selected
69            .get("access_info")
70            .and_then(Value::as_object)
71            .ok_or_else(attachment_message_invalid)?;
72        let digest = selected.get("digest").and_then(Value::as_object);
73        let encryption_info = selected.get("encryption_info").and_then(Value::as_object);
74        let message_security_profile =
75            message_security_profile_from_message(message_object, encryption_info);
76        let object_encryption_mode = encryption_info
77            .and_then(|value| value.get("mode"))
78            .and_then(Value::as_str)
79            .unwrap_or(crate::attachments::manifest::OBJECT_ENCRYPTION_MODE_NONE)
80            .trim()
81            .to_string();
82        return Ok(InternalAttachmentSelection {
83            public: AttachmentSelection {
84                message_id: actual_message_id,
85                requested_id: view_id,
86                sender_did: string_from_value(message_object.get("sender_did")),
87                attachment_id: string_from_value(selected.get("attachment_id")),
88                filename: string_from_value(selected.get("filename")),
89                mime_type: string_from_value(selected.get("mime_type")),
90                size: string_from_value(selected.get("size")),
91                digest_b64u: digest
92                    .and_then(|value| value.get("value_b64u"))
93                    .and_then(Value::as_str)
94                    .unwrap_or_default()
95                    .to_string(),
96                object_uri: access_info
97                    .get("object_uri")
98                    .and_then(Value::as_str)
99                    .unwrap_or_default()
100                    .to_string(),
101                caption: string_from_value(content.get("caption")),
102                message_security_profile,
103                object_encryption_mode: if object_encryption_mode.is_empty() {
104                    crate::attachments::manifest::OBJECT_ENCRYPTION_MODE_NONE.to_string()
105                } else {
106                    object_encryption_mode
107                },
108                object_cipher: encryption_info
109                    .and_then(|value| value.get("object_cipher"))
110                    .and_then(Value::as_str)
111                    .map(ToOwned::to_owned),
112                plaintext_size: encryption_info
113                    .and_then(|value| value.get("plaintext_size"))
114                    .and_then(Value::as_str)
115                    .map(ToOwned::to_owned),
116            },
117            object_key_b64u: encryption_info
118                .and_then(|value| value.get("object_key_b64u"))
119                .and_then(Value::as_str)
120                .map(ToOwned::to_owned),
121            nonce_b64u: encryption_info
122                .and_then(|value| value.get("nonce_b64u"))
123                .and_then(Value::as_str)
124                .map(ToOwned::to_owned),
125        });
126    }
127    Err(crate::ImError::MessageNotFound {
128        message_id: requested_message_id.to_string(),
129    })
130}
131
132pub(crate) fn find_attachment_selection_with_paging<F>(
133    fetch_page: F,
134    requested_message_id: &str,
135    requested_attachment_id: &str,
136) -> crate::ImResult<AttachmentSelection>
137where
138    F: FnMut(i64) -> crate::ImResult<(Vec<Value>, bool)>,
139{
140    find_internal_attachment_selection_with_paging(
141        fetch_page,
142        requested_message_id,
143        requested_attachment_id,
144    )
145    .map(|selection| selection.redacted_public())
146}
147
148pub(crate) fn find_internal_attachment_selection_with_paging<F>(
149    mut fetch_page: F,
150    requested_message_id: &str,
151    requested_attachment_id: &str,
152) -> crate::ImResult<InternalAttachmentSelection>
153where
154    F: FnMut(i64) -> crate::ImResult<(Vec<Value>, bool)>,
155{
156    let mut skip = 0_i64;
157    loop {
158        let (messages, has_more) = fetch_page(skip)?;
159        match find_internal_attachment_selection(
160            &messages,
161            requested_message_id,
162            requested_attachment_id,
163        ) {
164            Ok(selection) => return Ok(selection),
165            Err(crate::ImError::MessageNotFound { .. }) if has_more && !messages.is_empty() => {
166                skip += messages.len() as i64;
167            }
168            Err(crate::ImError::MessageNotFound { message_id }) => {
169                return Err(crate::ImError::MessageNotFound { message_id });
170            }
171            Err(err) => return Err(err),
172        }
173    }
174}
175
176impl InternalAttachmentSelection {
177    pub(crate) fn redacted_public(&self) -> AttachmentSelection {
178        self.public.clone()
179    }
180
181    pub(crate) fn message_security_profile(&self) -> &str {
182        self.public.message_security_profile.trim()
183    }
184
185    pub(crate) fn object_encryption_mode(&self) -> &str {
186        self.public.object_encryption_mode.trim()
187    }
188
189    pub(crate) fn is_object_e2ee(&self) -> bool {
190        self.object_encryption_mode() == crate::attachments::manifest::OBJECT_ENCRYPTION_MODE_E2EE
191    }
192}
193
194fn decode_attachment_content(value: Option<&Value>) -> crate::ImResult<Map<String, Value>> {
195    match value {
196        Some(Value::Object(object)) if !object.is_empty() => Ok(object.clone()),
197        Some(Value::String(text)) if !text.trim().is_empty() => {
198            let decoded: Value =
199                serde_json::from_str(text).map_err(|err| crate::ImError::Serialization {
200                    detail: err.to_string(),
201                })?;
202            decoded
203                .as_object()
204                .filter(|object| !object.is_empty())
205                .cloned()
206                .ok_or_else(attachment_message_invalid)
207        }
208        _ => Err(attachment_message_invalid()),
209    }
210}
211
212fn attachments_from_content(value: Option<&Value>) -> Vec<Map<String, Value>> {
213    value
214        .and_then(Value::as_array)
215        .map(|items| {
216            items
217                .iter()
218                .filter_map(|item| item.as_object().cloned())
219                .collect()
220        })
221        .unwrap_or_default()
222}
223
224fn select_attachment_entry(
225    attachments: &[Map<String, Value>],
226    requested_attachment_id: &str,
227) -> crate::ImResult<Map<String, Value>> {
228    if attachments.is_empty() {
229        return Err(attachment_not_found());
230    }
231    if requested_attachment_id.trim().is_empty() {
232        if attachments.len() > 1 {
233            return Err(attachment_id_required());
234        }
235        return Ok(attachments[0].clone());
236    }
237    attachments
238        .iter()
239        .find(|attachment| {
240            string_from_value(attachment.get("attachment_id")) == requested_attachment_id
241        })
242        .cloned()
243        .ok_or_else(attachment_not_found)
244}
245
246fn string_from_value(value: Option<&Value>) -> String {
247    value
248        .and_then(Value::as_str)
249        .unwrap_or_default()
250        .to_string()
251}
252
253fn message_security_profile_from_message(
254    message: &Map<String, Value>,
255    _encryption_info: Option<&Map<String, Value>>,
256) -> String {
257    for candidate in [
258        message.get("message_security_profile"),
259        message.get("security_profile"),
260        message.get("security"),
261        message
262            .get("metadata")
263            .and_then(Value::as_object)
264            .and_then(|metadata| metadata.get("message_security_profile")),
265        message
266            .get("metadata")
267            .and_then(Value::as_object)
268            .and_then(|metadata| metadata.get("security")),
269    ] {
270        let value = candidate.and_then(Value::as_str).unwrap_or_default().trim();
271        if !value.is_empty() {
272            return normalize_message_security_profile(value);
273        }
274    }
275    String::new()
276}
277
278fn normalize_message_security_profile(value: &str) -> String {
279    match value.trim() {
280        "secure-direct" | "direct" | "direct_e2ee" | "e2ee" => "direct-e2ee".to_string(),
281        "group" | "group_e2ee" => "group-e2ee".to_string(),
282        "" => "transport-protected".to_string(),
283        value => value.to_string(),
284    }
285}
286
287fn attachment_not_found() -> crate::ImError {
288    crate::ImError::invalid_input(Some("attachment_id".to_string()), ERR_ATTACHMENT_NOT_FOUND)
289}
290
291fn attachment_id_required() -> crate::ImError {
292    crate::ImError::invalid_input(
293        Some("attachment_id".to_string()),
294        ERR_ATTACHMENT_ID_REQUIRED,
295    )
296}
297
298fn attachment_message_invalid() -> crate::ImError {
299    crate::ImError::invalid_input(Some("content".to_string()), ERR_ATTACHMENT_MESSAGE_INVALID)
300}