use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::Path;
use trusty_common::memory_core::palace::Drawer;
use uuid::Uuid;
pub const MSG_MARKER_TAG: &str = "msg:v1";
pub const TAG_FROM_PREFIX: &str = "msg:from=";
pub const TAG_TO_PREFIX: &str = "msg:to=";
pub const TAG_PURPOSE_PREFIX: &str = "msg:purpose=";
pub const TAG_SENT_AT_PREFIX: &str = "msg:sent_at=";
pub const TAG_READ_PREFIX: &str = "msg:read=";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub id: Uuid,
pub from_palace: String,
pub to_palace: String,
pub purpose: String,
pub sent_at: DateTime<Utc>,
pub read: bool,
pub content: String,
}
impl Message {
pub fn from_drawer(drawer: &Drawer) -> Option<Self> {
if !drawer.tags.iter().any(|t| t == MSG_MARKER_TAG) {
return None;
}
let from_palace = extract_tag(drawer, TAG_FROM_PREFIX)?.to_string();
let to_palace = extract_tag(drawer, TAG_TO_PREFIX)?.to_string();
let purpose = extract_tag(drawer, TAG_PURPOSE_PREFIX)?.to_string();
let sent_at_raw = extract_tag(drawer, TAG_SENT_AT_PREFIX)?;
let sent_at = DateTime::parse_from_rfc3339(sent_at_raw)
.ok()?
.with_timezone(&Utc);
let read = extract_tag(drawer, TAG_READ_PREFIX)
.map(|v| v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
Some(Message {
id: drawer.id,
from_palace,
to_palace,
purpose,
sent_at,
read,
content: drawer.content.clone(),
})
}
pub fn to_injection_block(&self) -> String {
format!(
"## Message from {from} (purpose: {purpose})\n\
_sent {sent_at} → {to}_\n\
\n\
{content}\n",
from = self.from_palace,
purpose = self.purpose,
sent_at = self.sent_at.to_rfc3339(),
to = self.to_palace,
content = self.content
)
}
}
pub(super) fn extract_tag<'a>(drawer: &'a Drawer, prefix: &str) -> Option<&'a str> {
drawer.tags.iter().find_map(|t| t.strip_prefix(prefix))
}
pub fn build_message_tags(
from_palace: &str,
to_palace: &str,
purpose: &str,
sent_at: DateTime<Utc>,
) -> Vec<String> {
vec![
MSG_MARKER_TAG.to_string(),
format!("{TAG_FROM_PREFIX}{from_palace}"),
format!("{TAG_TO_PREFIX}{to_palace}"),
format!("{TAG_PURPOSE_PREFIX}{purpose}"),
format!("{TAG_SENT_AT_PREFIX}{ts}", ts = sent_at.to_rfc3339()),
format!("{TAG_READ_PREFIX}false"),
]
}
pub fn slugify_for_palace(path: &Path) -> Result<String> {
let raw = path
.file_name()
.and_then(|s| s.to_str())
.ok_or_else(|| anyhow!("path has no final component: {}", path.display()))?;
Ok(slugify_string(raw))
}
pub fn slugify_string(input: &str) -> String {
let lowered = input.trim().to_ascii_lowercase();
let stripped = lowered.strip_suffix(".git").unwrap_or(&lowered);
let mut out = String::with_capacity(stripped.len());
let mut prev_hyphen = false;
for c in stripped.chars() {
let next = match c {
'a'..='z' | '0'..='9' => Some(c),
'_' | '-' | ' ' | '\t' => Some('-'),
_ => None,
};
if let Some(c) = next {
if c == '-' {
if !prev_hyphen && !out.is_empty() {
out.push('-');
prev_hyphen = true;
}
} else {
out.push(c);
prev_hyphen = false;
}
}
}
while out.ends_with('-') {
out.pop();
}
out
}