gsm-translator 0.4.35

Message translation utilities and templates for Greentic messaging connectors.
Documentation
use anyhow::{Result, anyhow};
use serde_json::{Value, json};

use crate::render_via_engine;
use crate::{secure_action_url, translate_with_span};
use gsm_core::{CardAction, CardBlock, MessageCard, OutKind, OutMessage};

/// Build a Webex message payload from an internal [`OutMessage`].
pub fn to_webex_payload(out: &OutMessage) -> Result<Value> {
    translate_with_span(out, "webex", || build_payload(out))
}

fn build_payload(out: &OutMessage) -> Result<Value> {
    let mut map = serde_json::Map::new();
    map.insert("roomId".into(), Value::String(out.chat_id.clone()));

    if let Some(content) = render_via_engine(out, "webex") {
        if let Some(text) = out.text.clone()
            && !text.is_empty()
        {
            map.insert("markdown".into(), Value::String(text));
        }
        let attachment = json!({
            "contentType": "application/vnd.microsoft.card.adaptive",
            "content": content,
        });
        map.insert("attachments".into(), Value::Array(vec![attachment]));
        return Ok(Value::Object(map));
    }

    match out.kind {
        OutKind::Text => {
            let text = out
                .text
                .clone()
                .ok_or_else(|| anyhow!("text payload missing for text message"))?;
            map.insert("markdown".into(), Value::String(text));
        }
        OutKind::Card => {
            let card = out
                .message_card
                .clone()
                .ok_or_else(|| anyhow!("missing message card for card payload"))?;
            let markdown = out.text.clone().unwrap_or_default();
            if !markdown.is_empty() {
                map.insert("markdown".into(), Value::String(markdown));
            }
            let attachment = json!({
                "contentType": "application/vnd.microsoft.card.adaptive",
                "content": card_to_adaptive(out, card)?,
            });
            map.insert("attachments".into(), Value::Array(vec![attachment]));
        }
    }

    Ok(Value::Object(map))
}

fn card_to_adaptive(out: &OutMessage, card: MessageCard) -> Result<Value> {
    let mut body: Vec<Value> = Vec::new();

    if let Some(title) = card.title {
        body.push(json!({
            "type": "TextBlock",
            "text": title,
            "weight": "Bolder",
            "size": "Medium",
            "wrap": true,
        }));
    }

    for block in card.body {
        match block {
            CardBlock::Text { text, markdown } => {
                body.push(json!({
                    "type": "TextBlock",
                    "text": text,
                    "wrap": true,
                    "isSubtle": false,
                    "weight": if markdown { "Bolder" } else { "Default" },
                }));
            }
            CardBlock::Fact { label, value } => {
                body.push(json!({
                    "type": "FactSet",
                    "facts": [{"title": label, "value": value}],
                }));
            }
            CardBlock::Image { url } => {
                body.push(json!({
                    "type": "Image",
                    "url": url,
                }));
            }
        }
    }

    let mut actions: Vec<Value> = Vec::new();
    for action in card.actions {
        match action {
            CardAction::OpenUrl { title, url, jwt } => {
                let href = secure_action_url(out, &title, &url);
                actions.push(json!({
                    "type": "Action.OpenUrl",
                    "title": title,
                    "url": href,
                    "requiresAuthentication": jwt,
                }));
            }
            CardAction::Postback { title, data } => {
                actions.push(json!({
                    "type": "Action.Submit",
                    "title": title,
                    "data": data,
                }));
            }
        }
    }

    Ok(json!({
        "type": "AdaptiveCard",
        "version": "1.4",
        "body": body,
        "actions": actions,
    }))
}

#[cfg(test)]
mod tests {
    use super::*;
    use gsm_core::{CardBlock, MessageCard, Platform, make_tenant_ctx};

    fn sample_out(kind: OutKind, card: Option<MessageCard>) -> OutMessage {
        OutMessage {
            ctx: make_tenant_ctx("acme".into(), None, None),
            tenant: "acme".into(),
            platform: Platform::Webex,
            chat_id: "room-1".into(),
            thread_id: None,
            kind,
            text: Some("Hello".into()),
            message_card: card,

            adaptive_card: None,
            meta: Default::default(),
        }
    }

    #[test]
    fn text_payload() {
        let out = sample_out(OutKind::Text, None);
        let payload = to_webex_payload(&out).expect("payload");
        assert_eq!(payload["roomId"], "room-1");
        assert_eq!(payload["markdown"], "Hello");
        assert!(payload.get("attachments").is_none());
    }

    #[test]
    fn card_payload() {
        let card = MessageCard {
            title: Some("Card".into()),
            body: vec![CardBlock::Text {
                text: "Body".into(),
                markdown: true,
            }],
            actions: vec![CardAction::OpenUrl {
                title: "Open".into(),
                url: "https://example.com".into(),
                jwt: false,
            }],
        };
        let out = sample_out(OutKind::Card, Some(card));
        let payload = to_webex_payload(&out).expect("payload");
        let attachments = payload["attachments"].as_array().expect("attachments");
        assert_eq!(attachments.len(), 1);
        assert_eq!(
            attachments[0]["contentType"],
            "application/vnd.microsoft.card.adaptive"
        );
    }
}