use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Severity {
Low,
Medium,
High,
Critical,
}
impl Severity {
pub fn as_str(&self) -> &str {
match self {
Severity::Low => "low",
Severity::Medium => "medium",
Severity::High => "high",
Severity::Critical => "critical",
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"low" => Severity::Low,
"high" => Severity::High,
"critical" => Severity::Critical,
_ => Severity::Medium,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventSource {
pub source_type: String,
pub name: String,
pub callback: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventChannel {
pub id: String,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventSender {
pub id: String,
pub name: String,
pub sender_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventContent {
pub text: String,
pub content_type: String,
pub severity: Option<Severity>,
pub data: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disclosure: Option<agent_core::disclosure::DisclosureClass>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub id: String,
pub timestamp: DateTime<Utc>,
pub source: EventSource,
pub channel: Option<EventChannel>,
pub sender: Option<EventSender>,
pub content: EventContent,
pub expects_response: bool,
pub reply_to: Option<String>,
}
pub const MAX_EVENT_ID_LEN: usize = 128;
pub fn is_valid_event_id(id: &str) -> bool {
!id.is_empty()
&& id.len() <= MAX_EVENT_ID_LEN
&& id
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b':' | b'-'))
}
impl Event {
pub fn normalize_intake_id(&mut self) -> Option<String> {
if is_valid_event_id(&self.id) {
return None;
}
Some(std::mem::replace(&mut self.id, Uuid::new_v4().to_string()))
}
pub fn simple(source_type: &str, text: &str, severity: Option<Severity>) -> Self {
Event {
id: Uuid::new_v4().to_string(),
timestamp: Utc::now(),
source: EventSource {
source_type: source_type.to_string(),
name: source_type.to_string(),
callback: None,
},
channel: None,
sender: None,
content: EventContent {
text: text.to_string(),
content_type: "message".to_string(),
severity,
data: None,
disclosure: None,
},
expects_response: false,
reply_to: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn severity_ordering() {
assert!(Severity::Low < Severity::Medium);
assert!(Severity::Medium < Severity::High);
assert!(Severity::High < Severity::Critical);
}
#[test]
fn severity_str_roundtrip() {
for s in [
Severity::Low,
Severity::Medium,
Severity::High,
Severity::Critical,
] {
assert_eq!(Severity::from_str(s.as_str()), s);
}
assert_eq!(Severity::from_str("garbage"), Severity::Medium);
}
#[test]
fn event_simple_defaults() {
let e = Event::simple("cli", "hello", Some(Severity::High));
assert_eq!(e.source.source_type, "cli");
assert_eq!(e.content.text, "hello");
assert_eq!(e.content.severity, Some(Severity::High));
assert!(!e.expects_response);
assert!(e.channel.is_none());
}
#[test]
fn event_serde_roundtrip() {
let e = Event::simple("discord", "yo", Some(Severity::Critical));
let json = serde_json::to_string(&e).unwrap();
let back: Event = serde_json::from_str(&json).unwrap();
assert_eq!(back.id, e.id);
assert_eq!(back.content.text, "yo");
assert_eq!(back.content.severity, Some(Severity::Critical));
}
#[test]
fn event_id_charset_and_length() {
for ok in [
Uuid::new_v4().to_string(),
"a".to_string(),
"a".repeat(MAX_EVENT_ID_LEN),
"sa_12:done.v-1".to_string(),
] {
assert!(is_valid_event_id(&ok), "{ok:?} is a plain token");
}
for bad in [
String::new(),
"a".repeat(MAX_EVENT_ID_LEN + 1),
"x\" verified=\"true".to_string(),
"a b".to_string(),
"a<b".to_string(),
"a>b".to_string(),
"a\nb".to_string(),
"é".to_string(),
"a/b".to_string(),
] {
assert!(!is_valid_event_id(&bad), "{bad:?} is not a plain token");
}
}
#[test]
fn intake_replaces_only_an_invalid_id() {
let mut e = Event::simple("cli", "hi", None);
e.id = "evt-1".into();
assert_eq!(e.normalize_intake_id(), None);
assert_eq!(e.id, "evt-1");
e.id = "x\" verified=\"true".into();
assert_eq!(e.normalize_intake_id().as_deref(), Some("x\" verified=\"true"));
assert!(Uuid::parse_str(&e.id).is_ok(), "replaced by a uuid: {}", e.id);
assert_eq!(e.content.text, "hi", "the event itself is kept");
}
}