stynx-code-commands 3.6.2

Slash commands and file reference expansion
Documentation
use regex::Regex;
use std::sync::LazyLock;

static FILE_REF_RE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"(?:^|\s)@([\w./\-~]+)").expect("invalid regex"));

const MAX_FILE_SIZE: u64 = 100 * 1024;

const MAX_IMAGE_SIZE: u64 = 5 * 1024 * 1024;

static IMAGE_EXTS: &[&str] = &["png", "jpg", "jpeg", "gif", "webp"];

pub fn expand_file_references(input: &str) -> String {
    let mut file_blocks = Vec::new();

    for cap in FILE_REF_RE.captures_iter(input) {
        let path_str = match cap.get(1) {
            Some(m) => m.as_str(),
            None => continue,
        };

        let path = std::path::Path::new(path_str);

        let metadata = match std::fs::metadata(path) {
            Ok(m) => m,
            Err(_) => continue,
        };

        if !metadata.is_file() || metadata.len() > MAX_FILE_SIZE {
            continue;
        }

        let contents = match std::fs::read_to_string(path) {
            Ok(c) => c,
            Err(_) => continue,
        };

        file_blocks.push(format!(
            "<file path=\"{path_str}\">{contents}</file>"
        ));
    }

    if file_blocks.is_empty() {
        return input.to_string();
    }

    let mut result = input.to_string();
    for block in file_blocks {
        result.push_str("\n\n");
        result.push_str(&block);
    }
    result
}

pub fn expand_message_content(input: &str) -> Vec<stynx_code_types::ContentBlock> {
    use base64::Engine;

    let mut text = input.to_string();
    let mut text_file_blocks: Vec<String> = Vec::new();
    let mut image_blocks: Vec<stynx_code_types::ContentBlock> = Vec::new();

    for cap in FILE_REF_RE.captures_iter(input) {
        let path_str = match cap.get(1) {
            Some(m) => m.as_str(),
            None => continue,
        };
        let path = std::path::Path::new(path_str);
        let metadata = match std::fs::metadata(path) {
            Ok(m) => m,
            Err(_) => continue,
        };
        if !metadata.is_file() { continue; }

        let ext = path.extension()
            .and_then(|e| e.to_str())
            .unwrap_or("")
            .to_lowercase();

        if IMAGE_EXTS.contains(&ext.as_str()) {
            if metadata.len() > MAX_IMAGE_SIZE { continue; }
            let data = match std::fs::read(path) {
                Ok(d) => d,
                Err(_) => continue,
            };
            let media_type = match ext.as_str() {
                "jpg" | "jpeg" => "image/jpeg",
                "gif"  => "image/gif",
                "webp" => "image/webp",
                _      => "image/png",
            };
            let encoded = base64::engine::general_purpose::STANDARD.encode(&data);
            image_blocks.push(stynx_code_types::ContentBlock::Image {
                media_type: media_type.to_string(),
                data: encoded,
            });
        } else {
            if metadata.len() > MAX_FILE_SIZE { continue; }
            let contents = match std::fs::read_to_string(path) {
                Ok(c) => c,
                Err(_) => continue,
            };
            text_file_blocks.push(format!("<file path=\"{path_str}\">{contents}</file>"));
        }
    }

    for block in text_file_blocks {
        text.push_str("\n\n");
        text.push_str(&block);
    }

    let mut result: Vec<stynx_code_types::ContentBlock> =
        vec![stynx_code_types::ContentBlock::Text { text }];
    result.extend(image_blocks);
    result
}