im-core 0.1.0

Rust IM SDK for Awiki clients built on Agent Network Protocol (ANP)
Documentation
use serde_json::{Map, Value};

use crate::ids::MessageId;
use crate::messages::ThreadRef;
use crate::realtime::{AttachmentDownloadAction, AttachmentMessageSummary};

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AttachmentProjection {
    pub summary: Option<AttachmentMessageSummary>,
    pub download_action: Option<AttachmentDownloadAction>,
    pub warnings: Vec<String>,
}

pub(crate) fn project_attachment(
    content_type: &str,
    body: Option<&Map<String, Value>>,
    thread: &ThreadRef,
    message_id: &MessageId,
) -> Option<AttachmentProjection> {
    if content_type != crate::attachments::manifest::attachment_manifest_content_type() {
        return None;
    }
    let mut warnings = Vec::new();
    let Some(manifest) = manifest_from_body(body) else {
        warnings.push("attachment manifest payload is missing or invalid".to_string());
        return Some(AttachmentProjection {
            summary: None,
            download_action: None,
            warnings,
        });
    };

    let primary_attachment_id = string_from_object(Some(manifest), "primary_attachment_id");
    let attachments = manifest
        .get("attachments")
        .and_then(Value::as_array)
        .map(Vec::as_slice)
        .unwrap_or_default();
    let selected = select_attachment(attachments, &primary_attachment_id);
    if attachments.is_empty() {
        warnings.push("attachment manifest has no attachments".to_string());
    }
    if selected.is_none() {
        warnings.push("attachment manifest primary attachment is missing".to_string());
    }
    if primary_attachment_id.trim().is_empty() {
        warnings.push("attachment manifest primary_attachment_id is missing".to_string());
    }

    let Some(selected) = selected else {
        return Some(AttachmentProjection {
            summary: None,
            download_action: None,
            warnings,
        });
    };

    if let Some(mode) =
        unsupported_encryption_mode(selected).or_else(|| unsupported_encryption_mode(manifest))
    {
        warnings.push(format!(
            "attachment manifest encryption mode {mode} is not supported by realtime projection"
        ));
        return Some(AttachmentProjection {
            summary: None,
            download_action: None,
            warnings,
        });
    }

    let attachment_id = string_from_value(selected.get("attachment_id"))
        .or_else(|| non_empty_string(primary_attachment_id));
    let filename = string_from_value(selected.get("filename"));
    let mime_type = string_from_value(selected.get("mime_type"));
    let size_bytes =
        string_from_value(selected.get("size")).and_then(|size| size.trim().parse::<u64>().ok());
    if attachment_id.is_none() {
        warnings.push("attachment manifest attachment_id is missing".to_string());
    }
    if filename.is_none() {
        warnings.push("attachment manifest filename is missing".to_string());
    }
    if mime_type.is_none() {
        warnings.push("attachment manifest mime_type is missing".to_string());
    }
    if size_bytes.is_none() {
        warnings.push("attachment manifest size_bytes is missing".to_string());
    }

    Some(AttachmentProjection {
        download_action: Some(AttachmentDownloadAction {
            thread: thread.clone(),
            message_id: message_id.clone(),
            attachment_id: attachment_id.clone(),
        }),
        summary: Some(AttachmentMessageSummary {
            attachment_id,
            filename,
            mime_type,
            size_bytes,
            content_type: Some(content_type.to_string()),
        }),
        warnings,
    })
}

pub(crate) fn attachment_summary_attributes(
    summary: &AttachmentMessageSummary,
) -> Vec<crate::messages::MessageMetadataAttribute> {
    let mut attributes = Vec::new();
    push_attribute(
        &mut attributes,
        "attachment_id",
        summary.attachment_id.as_deref(),
    );
    push_attribute(
        &mut attributes,
        "attachment_filename",
        summary.filename.as_deref(),
    );
    push_attribute(
        &mut attributes,
        "attachment_mime_type",
        summary.mime_type.as_deref(),
    );
    let size = summary.size_bytes.map(|value| value.to_string());
    push_attribute(&mut attributes, "attachment_size_bytes", size.as_deref());
    push_attribute(
        &mut attributes,
        "attachment_content_type",
        summary.content_type.as_deref(),
    );
    attributes
}

fn manifest_from_body(body: Option<&Map<String, Value>>) -> Option<&Map<String, Value>> {
    if let Some(payload) = body
        .and_then(|body| body.get("payload"))
        .and_then(Value::as_object)
    {
        return Some(payload);
    }
    body.filter(|body| {
        body.contains_key("attachments") || body.contains_key("primary_attachment_id")
    })
}

fn select_attachment<'a>(
    attachments: &'a [Value],
    primary_attachment_id: &str,
) -> Option<&'a Map<String, Value>> {
    if !primary_attachment_id.trim().is_empty() {
        let selected = attachments.iter().find_map(|value| {
            let object = value.as_object()?;
            (string_from_object(Some(object), "attachment_id") == primary_attachment_id)
                .then_some(object)
        });
        if selected.is_some() {
            return selected;
        }
    }
    attachments.first().and_then(Value::as_object)
}

fn unsupported_encryption_mode(object: &Map<String, Value>) -> Option<String> {
    let mode = string_from_value(
        object
            .get("encryption_info")
            .and_then(Value::as_object)
            .and_then(|encryption| encryption.get("mode")),
    )
    .or_else(|| string_from_value(object.get("encryption_mode")))
    .or_else(|| string_from_value(object.get("object_encryption_mode")))?;
    if mode.eq_ignore_ascii_case("none") {
        None
    } else {
        Some(mode)
    }
}

fn push_attribute(
    attributes: &mut Vec<crate::messages::MessageMetadataAttribute>,
    key: &str,
    value: Option<&str>,
) {
    let Some(value) = value.map(str::trim).filter(|value| !value.is_empty()) else {
        return;
    };
    attributes.push(crate::messages::MessageMetadataAttribute {
        key: key.to_string(),
        value: value.to_string(),
    });
}

fn string_from_object(object: Option<&Map<String, Value>>, key: &str) -> String {
    object
        .and_then(|object| object.get(key))
        .and_then(Value::as_str)
        .unwrap_or_default()
        .to_string()
}

fn string_from_value(value: Option<&Value>) -> Option<String> {
    value
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(str::to_string)
}

fn non_empty_string(value: String) -> Option<String> {
    if value.trim().is_empty() {
        None
    } else {
        Some(value)
    }
}