use super::types::Event;
fn regex_strip_event_close(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let chars: Vec<char> = s.chars().collect();
let lower_chars: Vec<char> = s.to_lowercase().chars().collect();
let mut i = 0;
while i < chars.len() {
if i + 7 < chars.len() && lower_chars[i] == '<' && lower_chars[i + 1] == '/' {
let mut j = i + 2;
while j < chars.len() && lower_chars[j] == ' ' { j += 1; }
if j + 5 <= chars.len()
&& lower_chars[j] == 'e'
&& lower_chars[j + 1] == 'v'
&& lower_chars[j + 2] == 'e'
&& lower_chars[j + 3] == 'n'
&& lower_chars[j + 4] == 't'
{
let mut k = j + 5;
while k < chars.len() && lower_chars[k] == ' ' { k += 1; }
if k < chars.len() && chars[k] == '>' {
i = k + 1; continue;
}
}
}
result.push(chars[i]);
i += 1;
}
result
}
pub fn format_event_for_agent(event: &Event) -> String {
let sev = event
.content
.severity
.as_ref()
.map(|s| s.as_str())
.unwrap_or("medium");
let channel_attr = match &event.channel {
Some(ch) => format!(" channel=\"{}\"", ch.name.replace('"', "'")),
None => String::new(),
};
let safe_text = regex_strip_event_close(&event.content.text);
let safe_source = event.source.source_type.replace('"', "'");
let safe_content_type = event.content.content_type.replace('"', "'");
let mut out = format!(
"<event id=\"{}\" type=\"{}\" severity=\"{}\" source=\"{}\"{}>{}",
event.id, safe_content_type, sev, safe_source, channel_attr, safe_text
);
if let Some(data) = &event.content.data {
let data_str = serde_json::to_string(data).unwrap_or_default();
let truncated: String = data_str.chars().take(1000).collect();
let safe_data = regex_strip_event_close(&truncated);
out.push_str(&format!("\nData: {}", safe_data));
}
out.push_str("</event>");
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::events::types::{EventChannel, Severity};
use serde_json::json;
#[test]
fn format_without_channel() {
let e = Event::simple("cli", "ping", Some(Severity::Low));
let s = format_event_for_agent(&e);
assert!(s.starts_with("<event id="));
assert!(s.contains("type=\"message\""));
assert!(s.contains("severity=\"low\""));
assert!(s.contains("source=\"cli\""));
assert!(s.contains("ping"));
assert!(s.ends_with("</event>"));
}
#[test]
fn format_with_channel() {
let mut e = Event::simple("uptime-kuma", "Jellyfin is DOWN. Status 503.", Some(Severity::High));
e.content.content_type = "alert".into();
e.channel = Some(EventChannel {
id: "1".into(),
name: "alerts".into(),
});
let s = format_event_for_agent(&e);
assert!(s.contains("source=\"uptime-kuma\""));
assert!(s.contains("channel=\"alerts\""));
assert!(s.contains("severity=\"high\""));
assert!(s.contains("Jellyfin is DOWN. Status 503."));
assert!(s.ends_with("</event>"));
}
#[test]
fn format_defaults_to_medium_when_no_severity() {
let e = Event::simple("cli", "hi", None);
let s = format_event_for_agent(&e);
assert!(s.contains("severity=\"medium\""));
}
#[test]
fn format_appends_data() {
let mut e = Event::simple("system", "boom", Some(Severity::Critical));
e.content.data = Some(json!({"code": 500}));
let s = format_event_for_agent(&e);
assert!(s.contains("boom"));
assert!(s.contains("\nData: "));
assert!(s.contains("\"code\":500"));
}
}