#[cfg(test)]
#[path = "message_fact_render/tests.rs"]
mod tests;
use tau_proto::{
AgentId, Event, MessageAgentTarget, MessageConversation, MessageFactRef, MessageParty,
MessagePublisherId, project_message_fact, visible_escape_metadata,
};
use crate::terminal_text::sanitize_terminal_body;
enum UiMessageFact<'a> {
Delivered(&'a tau_proto::MessageDelivered),
Edited(&'a tau_proto::MessageEdited),
Deleted(&'a tau_proto::MessageDeleted),
ReactionAdded(&'a tau_proto::MessageReactionAdded),
ReactionRemoved(&'a tau_proto::MessageReactionRemoved),
Sent(&'a tau_proto::MessageSent),
}
impl<'a> UiMessageFact<'a> {
fn from_event(event: &'a Event) -> Option<Self> {
match event {
Event::MessageDelivered(fact) => Some(Self::Delivered(fact)),
Event::MessageEdited(fact) => Some(Self::Edited(fact)),
Event::MessageDeleted(fact) => Some(Self::Deleted(fact)),
Event::MessageReactionAdded(fact) => Some(Self::ReactionAdded(fact)),
Event::MessageReactionRemoved(fact) => Some(Self::ReactionRemoved(fact)),
Event::MessageSent(fact) => Some(Self::Sent(fact)),
_ => None,
}
}
fn event_name(&self) -> &'static str {
match self {
Self::Delivered(_) => "message.delivered",
Self::Edited(_) => "message.edited",
Self::Deleted(_) => "message.deleted",
Self::ReactionAdded(_) => "message.reaction_added",
Self::ReactionRemoved(_) => "message.reaction_removed",
Self::Sent(_) => "message.sent",
}
}
fn description(&self) -> &'static str {
match self {
Self::Delivered(_) => "message",
Self::Edited(_) => "message edited",
Self::Deleted(_) => "message deleted",
Self::ReactionAdded(_) => "reaction added",
Self::ReactionRemoved(_) => "reaction removed",
Self::Sent(_) => "message sent",
}
}
fn publisher(&self) -> &MessagePublisherId {
match self {
Self::Delivered(fact) => &fact.publisher_extension_id,
Self::Edited(fact) => &fact.publisher_extension_id,
Self::Deleted(fact) => &fact.publisher_extension_id,
Self::ReactionAdded(fact) => &fact.publisher_extension_id,
Self::ReactionRemoved(fact) => &fact.publisher_extension_id,
Self::Sent(fact) => &fact.publisher_extension_id,
}
}
fn agent_id(&self) -> &MessageAgentTarget {
match self {
Self::Delivered(fact) => &fact.agent_id,
Self::Edited(fact) => &fact.agent_id,
Self::Deleted(fact) => &fact.agent_id,
Self::ReactionAdded(fact) => &fact.agent_id,
Self::ReactionRemoved(fact) => &fact.agent_id,
Self::Sent(fact) => &fact.agent_id,
}
}
fn reference(&self) -> Option<&MessageFactRef> {
match self {
Self::Edited(fact) => Some(&fact.target),
Self::Deleted(fact) => Some(&fact.target),
Self::ReactionAdded(fact) => Some(&fact.target),
Self::ReactionRemoved(fact) => Some(&fact.target),
Self::Delivered(_) | Self::Sent(_) => None,
}
}
fn party(&self) -> Option<(&'static str, &MessageParty)> {
match self {
Self::Delivered(fact) => Some(("from", &fact.sender)),
Self::Edited(fact) => fact.actor.as_ref().map(|party| ("by", party)),
Self::Deleted(fact) => fact.actor.as_ref().map(|party| ("by", party)),
Self::ReactionAdded(fact) => fact.actor.as_ref().map(|party| ("by", party)),
Self::ReactionRemoved(fact) => fact.actor.as_ref().map(|party| ("by", party)),
Self::Sent(fact) => fact.recipient.as_ref().map(|party| ("to", party)),
}
}
fn conversation(&self) -> Option<&MessageConversation> {
match self {
Self::Delivered(fact) => fact.conversation.as_ref(),
Self::Edited(fact) => fact.conversation.as_ref(),
Self::Deleted(fact) => fact.conversation.as_ref(),
Self::ReactionAdded(fact) => fact.conversation.as_ref(),
Self::ReactionRemoved(fact) => fact.conversation.as_ref(),
Self::Sent(fact) => fact.conversation.as_ref(),
}
}
fn reaction(&self) -> Option<&str> {
match self {
Self::ReactionAdded(fact) => Some(&fact.reaction),
Self::ReactionRemoved(fact) => Some(&fact.reaction),
Self::Delivered(_) | Self::Edited(_) | Self::Deleted(_) | Self::Sent(_) => None,
}
}
fn text(&self) -> Option<&str> {
match self {
Self::Delivered(fact) => Some(&fact.text),
Self::Edited(fact) => Some(&fact.text),
Self::Sent(fact) => Some(&fact.text),
Self::Deleted(_) | Self::ReactionAdded(_) | Self::ReactionRemoved(_) => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) enum MessageFactTarget {
Valid(AgentId),
Invalid,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum MessageFactTargetContext {
Explicit,
Implied,
}
pub(super) fn target_agent_id(event: &Event) -> Option<MessageFactTarget> {
let view = UiMessageFact::from_event(event)?;
Some(
AgentId::parse(view.agent_id().as_str())
.map(MessageFactTarget::Valid)
.unwrap_or(MessageFactTarget::Invalid),
)
}
pub(super) fn render(event: &Event, target_context: MessageFactTargetContext) -> Option<String> {
let view = UiMessageFact::from_event(event)?;
let classification = project_message_fact(event)
.expect("the shared classifier recognizes every UI message fact variant");
if let Err(reason) = classification {
return Some(format!(
"Unprojectable message fact\nEvent: {}\nPublisher: {}\nReason: {}",
view.event_name(),
escape(view.publisher().as_str()),
reason.as_str()
));
}
let mut output = format!(
"External `{}` {}",
escape(view.publisher().as_str()),
view.description()
);
let has_party = view.party().is_some();
let has_conversation = view.conversation().is_some();
if let Some((preposition, party)) = view.party() {
output.push(' ');
output.push_str(preposition);
output.push_str(" \"");
output.push_str(&escape_quoted(preferred_party_name(party)));
output.push('"');
}
if let Some(conversation) = view.conversation() {
output.push_str(" in ");
push_context_value(&mut output, preferred_conversation_name(conversation));
}
if !has_party
&& !has_conversation
&& let Some(reference) = view.reference()
{
output.push_str(" for message `");
output.push_str(&escape(reference.publisher_extension_id.as_str()));
output.push_str("`/");
push_context_value(&mut output, reference.message_id.as_str());
}
if matches!(target_context, MessageFactTargetContext::Explicit) {
output.push_str(" for Tau target ");
push_context_value(&mut output, view.agent_id().as_str());
}
output.push(':');
if let Some(text) = view.text() {
output.push('\n');
output.push_str(&sanitize_terminal_body(text));
} else if let Some(reaction) = view.reaction() {
output.push('\n');
output.push_str(&escape(reaction));
}
Some(output)
}
fn preferred_party_name(party: &MessageParty) -> &str {
party
.display_name
.as_deref()
.filter(|display| useful_display(display))
.unwrap_or(&party.stable_id)
}
fn preferred_conversation_name(conversation: &MessageConversation) -> &str {
conversation
.display_name
.as_deref()
.filter(|display| useful_display(display))
.or_else(|| {
conversation
.alias
.as_deref()
.filter(|alias| useful_display(alias))
})
.unwrap_or(&conversation.stable_id)
}
fn useful_display(display: &str) -> bool {
display.chars().any(|character| !character.is_whitespace())
}
fn push_context_value(output: &mut String, value: &str) {
if value.chars().all(context_character_is_unambiguous) {
output.push_str(&escape(value));
} else {
output.push('"');
output.push_str(&escape_quoted(value));
output.push('"');
}
}
fn context_character_is_unambiguous(character: char) -> bool {
character.is_alphanumeric() || matches!(character, '-' | '_' | '.' | '/' | '#' | '@')
}
fn escape_quoted(value: &str) -> String {
escape(value).replace('"', r#"\""#)
}
fn escape(value: &str) -> String {
let mut output = String::with_capacity(value.len());
let mut plain_start = 0;
for (index, character) in value.char_indices() {
let replacement = match character {
'\\' => Some(r"\\"),
'[' => Some(r"\["),
']' => Some(r"\]"),
_ => None,
};
let Some(replacement) = replacement else {
continue;
};
output.push_str(&visible_escape_metadata(&value[plain_start..index]));
output.push_str(replacement);
plain_start = index + character.len_utf8();
}
output.push_str(&visible_escape_metadata(&value[plain_start..]));
output
}