From e92a976226ec5a970217934ac2f214bc3b1fb913 Mon Sep 17 00:00:00 2001 From: argenis de la rosa Date: Wed, 25 Feb 2026 22:17:41 -0500 Subject: [PATCH] feat(discord): forward inbound image attachments as markers (cherry picked from commit a37cebd33a3914c666bfcc897c07ee80b17afb51) --- src/channels/discord.rs | 114 +++++++++++----------------------------- 1 file changed, 30 insertions(+), 84 deletions(-) diff --git a/src/channels/discord.rs b/src/channels/discord.rs index 45fb659b3..b96ee46a3 100644 --- a/src/channels/discord.rs +++ b/src/channels/discord.rs @@ -58,43 +58,9 @@ impl DiscordChannel { /// Process Discord message attachments and return a string to append to the /// agent message context. /// -/// `text/*` MIME types are fetched and inlined. If Discord omits `content_type` -/// or reports `application/octet-stream`, text-like filenames are inferred via -/// extension (for example `message.txt` for auto-converted long messages). -/// Unsupported attachments are skipped with warning-level logs. -const DISCORD_TEXT_ATTACHMENT_EXTENSIONS: &[&str] = &[ - "txt", "md", "json", "csv", "log", "py", "js", "ts", "rs", "toml", "yaml", "yml", "xml", - "html", "css", "sh", -]; - -fn is_text_like_discord_attachment(content_type: Option<&str>, filename: &str) -> bool { - let normalized_content_type = content_type - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(|value| value.to_ascii_lowercase()); - - if let Some(content_type) = normalized_content_type.as_deref() { - if content_type.starts_with("text/") { - return true; - } - if content_type != "application/octet-stream" { - return false; - } - } - - if filename.eq_ignore_ascii_case("message.txt") { - return true; - } - - let Some(extension) = Path::new(filename).extension().and_then(|ext| ext.to_str()) else { - return false; - }; - - DISCORD_TEXT_ATTACHMENT_EXTENSIONS - .iter() - .any(|allowed| extension.eq_ignore_ascii_case(allowed)) -} - +/// `text/*` MIME types are fetched and inlined, while `image/*` MIME types are +/// forwarded as `[IMAGE:]` markers. Other types are skipped. Fetch errors +/// are logged as warnings. async fn process_attachments( attachments: &[serde_json::Value], client: &reqwest::Client, @@ -127,6 +93,8 @@ async fn process_attachments( tracing::warn!(name, error = %e, "discord attachment fetch error"); } } + } else if ct.starts_with("image/") { + parts.push(format!("[IMAGE:{url}]")); } else { tracing::warn!( name, @@ -1474,62 +1442,40 @@ mod tests { } #[tokio::test] - async fn process_attachments_infers_text_when_content_type_missing() { - let mock_server = MockServer::start().await; - Mock::given(method("GET")) - .and(path("/message.txt")) - .respond_with(ResponseTemplate::new(200).set_body_string("hello from discord")) - .mount(&mock_server) - .await; - + async fn process_attachments_emits_single_image_marker() { let client = reqwest::Client::new(); let attachments = vec![serde_json::json!({ - "url": format!("{}/message.txt", mock_server.uri()), - "filename": "message.txt" + "url": "https://cdn.discordapp.com/attachments/123/456/photo.png", + "filename": "photo.png", + "content_type": "image/png" })]; - let result = process_attachments(&attachments, &client).await; - assert!(result.contains("[message.txt]")); - assert!(result.contains("hello from discord")); + assert_eq!( + result, + "[IMAGE:https://cdn.discordapp.com/attachments/123/456/photo.png]" + ); } #[tokio::test] - async fn process_attachments_infers_text_for_octet_stream_txt_extension() { - let mock_server = MockServer::start().await; - Mock::given(method("GET")) - .and(path("/notes.TXT")) - .respond_with(ResponseTemplate::new(200).set_body_string("line one\nline two")) - .mount(&mock_server) - .await; - + async fn process_attachments_emits_multiple_image_markers() { let client = reqwest::Client::new(); - let attachments = vec![serde_json::json!({ - "url": format!("{}/notes.TXT", mock_server.uri()), - "filename": "notes.TXT", - "content_type": "application/octet-stream" - })]; - + let attachments = vec![ + serde_json::json!({ + "url": "https://cdn.discordapp.com/attachments/123/456/one.jpg", + "filename": "one.jpg", + "content_type": "image/jpeg" + }), + serde_json::json!({ + "url": "https://cdn.discordapp.com/attachments/123/456/two.webp", + "filename": "two.webp", + "content_type": "image/webp" + }), + ]; let result = process_attachments(&attachments, &client).await; - assert!(result.contains("[notes.TXT]")); - assert!(result.contains("line one")); - } - - #[test] - fn text_like_discord_attachment_detection_respects_mime_and_filename_fallback() { - assert!(is_text_like_discord_attachment( - Some("text/plain"), - "report.bin" - )); - assert!(is_text_like_discord_attachment(None, "message.txt")); - assert!(is_text_like_discord_attachment( - Some("application/octet-stream"), - "trace.log" - )); - assert!(!is_text_like_discord_attachment( - Some("application/pdf"), - "notes.txt" - )); - assert!(!is_text_like_discord_attachment(None, "image.png")); + assert_eq!( + result, + "[IMAGE:https://cdn.discordapp.com/attachments/123/456/one.jpg]\n---\n[IMAGE:https://cdn.discordapp.com/attachments/123/456/two.webp]" + ); } #[test]