use async_trait::async_trait; use fluffer::{Client, GemBytes}; use std::fmt::Display; use crate::error::GementionError; macro_rules! titan { ($self:expr) => { $self.client.titan.as_ref().unwrap() }; } fn parse_content_body(raw_body: &[u8]) -> Result<&str, GementionError> { let content = std::str::from_utf8(raw_body)?; // Drop 1st line if it is a mention type. Otherwise return whole thing. let mention_type = MentionType::try_from(raw_body); let content: &str = if let Ok(_) = mention_type { let amt_to_chop = content.lines().next().map(|line| line.len()).unwrap(); &content[amt_to_chop..] } else { content }; Ok(content.trim()) } /// Wraps an incoming Titan request and provides handy methods for /// extracting the expected information. pub(crate) struct MentionUpload<'a> { client: &'a Client, } impl<'a> MentionUpload<'a> { pub fn from_client(client: &'a Client) -> Result { if let Some(_) = client.titan { Ok(Self { client: &client }) } else { Err(GementionError::NotTitanResource) } } pub fn raw_body(&self) -> &[u8] { titan!(self).content.as_slice() } pub fn mime(&self) -> &str { titan!(self).mime.as_ref() } pub fn token(&self) -> Option<&str> { titan!(self).token.as_deref() } pub fn size(&self) -> usize { titan!(self).size } pub fn fingerprint(&self) -> Option { self.client.fingerprint() } pub fn username(&self) -> Option { self.client.name() } pub fn target(&self) -> Option<&str> { self.client.parameter("target") } pub fn content(&self) -> Result<&str, GementionError> { parse_content_body(self.raw_body()) } } impl<'a> TryFrom<&'a Client> for MentionUpload<'a> { type Error = GementionError; fn try_from(client: &'a Client) -> Result { MentionUpload::from_client(client) } } #[derive(Debug)] pub(crate) enum MentionType { Reply, Like, } #[derive(Debug)] pub(crate) struct Mention { mention_type: MentionType, target: String, user: String, content: Option, } // 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: MentionUpload<'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 content = resource.content()?.to_owned(); let content = if !content.is_empty() { Some(content) } else { None }; Ok(Mention { user, mention_type, content, target, }) } 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"), } } } #[async_trait] impl GemBytes for Mention { async fn gem_bytes(self) -> Vec { // TODO change output based on mention type: // Reply/comment = current format. // Like = " liked this" let headline = format!("## {} from {}", self.mention_type, self.user); let content = self.content.unwrap_or(String::from("[no content]")); format!("20 text/gemini\r\n{}\n\n{}", headline, content).into_bytes() } } pub(crate) struct MentionResponse { status: String, } impl MentionResponse { pub fn not_titan() -> MentionResponse { Self { status: "not a titan request".to_string(), } } pub fn verified(url: &str) -> MentionResponse { Self { status: format!("verified: {}", url), } } pub fn failure(reason: &str) -> MentionResponse { Self { status: format!("invalid: {}", reason), } } } #[async_trait] impl GemBytes for MentionResponse { async fn gem_bytes(self) -> Vec { format!("20 text/gemini\r\n{}", self.status).into_bytes() } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_comment_body_with_mention_type() -> Result<(), GementionError> { let body = "reply\nthis is my comment body, which is a reply.".as_bytes(); let expected = "this is my comment body, which is a reply."; let parsed = parse_content_body(body)?; assert_eq!(expected, parsed); Ok(()) } #[test] fn parse_comment_body_without_mention_type() -> Result<(), GementionError> { let expected = "this is my comment body, which has no mention type."; let body = expected.as_bytes(); let parsed = parse_content_body(body)?; assert_eq!(expected, parsed); Ok(()) } }