use std::fmt::Display; use url::Url; use crate::error::GementionError; use crate::models::web::C2SMentionRequest; use super::verification::VerificationStatus; #[derive(Debug)] pub(crate) enum MentionType { Reply, Like, } #[derive(Debug)] pub(crate) struct Mention { receiving_endpoint: Url, mention_type: MentionType, target: String, user: String, content: Option, verification_status: VerificationStatus, } impl Mention { pub fn target(&self) -> &str { &self.target } pub fn receiving_endpoint(&self) -> &Url { &self.receiving_endpoint } pub fn target_url(&self) -> Url { Url::parse(&format!("gemini://{}", &self.target)).unwrap() } pub fn mention_type(&self) -> &MentionType { &self.mention_type } pub fn user(&self) -> &str { &self.user } pub fn content(&self) -> Option<&str> { self.content.as_deref() } pub fn verify_status(&self) -> &VerificationStatus { &self.verification_status } pub fn set_verify_status(&mut self, status: VerificationStatus) { self.verification_status = status; } fn valid_gemtext(&self) -> String { let headline = format!("## {} from {}", self.mention_type, self.user); let header = format!("=> {} In response to this page", self.target_url()); let content = match &self.mention_type { MentionType::Reply => self.content.clone().unwrap_or(String::from("[no content]")), MentionType::Like => format!("{} liked this.", self.user), }; format!("{}\n\n{}\n\n### Content\n{}", headline, header, content) } fn invalid_gemtext(&self) -> String { let headline = String::from("## Invalid Mention"); let failure_reason = match &self.verification_status { VerificationStatus::Invalid(failure) => failure.to_string(), VerificationStatus::NotYetVerified => String::from("verification not executed"), _ => String::from("not invalid"), }; format!("{}\n\n{}", headline, failure_reason) } pub fn as_gemtext(&self) -> String { match self.verification_status { VerificationStatus::Verified { .. } => self.valid_gemtext(), _ => self.invalid_gemtext(), } } } impl Display for Mention { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let body = self.as_gemtext(); write!(f, "{}", body) } } // This is for converting an incoming Titan request into a mention // type. It will attempt to parse the first line to identify the type. // If the type cannot be identified, or if the string is not valid // UTF-8, an error is returned. impl TryFrom<&[u8]> for MentionType { type Error = GementionError; fn try_from(content: &[u8]) -> Result { let content = std::str::from_utf8(&content)?; let content_type = content .lines() .next() .map(|line| line.trim()) .unwrap_or("[none provided]"); match content_type.to_lowercase() { value if value == "reply" => Ok(Self::Reply), value if value == "like" => Ok(Self::Like), _ => Err(GementionError::InvalidMentionType(content_type.to_owned())), } } } impl<'a> TryFrom> for Mention { type Error = GementionError; fn try_from(resource: C2SMentionRequest<'a>) -> Result { if resource.mime() == "text/plain" { // Be flexible on mention type: if first line isn't a // mention type, just assume reply. let mention_type = match MentionType::try_from(resource.raw_body()) { Ok(mention_type) => mention_type, Err(GementionError::InvalidMentionType(_)) => MentionType::Reply, Err(e) => return Err(e), }; let target = resource .target() .ok_or(GementionError::TargetNotProvided)? .to_owned(); let user = resource .username() .ok_or(GementionError::UsernameNotProvided)?; let receiving_endpoint = Url::parse( resource .accessed_url() .as_str() .split(&target) .next() .ok_or(GementionError::InvalidBody)?, )?; let content = resource.content()?.to_owned(); let content = if !content.is_empty() { Some(content) } else { None }; Ok(Mention { user, mention_type, content, target, receiving_endpoint, verification_status: VerificationStatus::NotYetVerified, }) } else { Err(GementionError::InvalidBody) } } } impl Display for MentionType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Reply => write!(f, "Reply"), Self::Like => write!(f, "Like"), } } }