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() }; } /// 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 content(&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") } } 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. It // is very flexible: If the incoming value matches a specific format, // it'll accurately convert the stringi nto a proper mention. // Otherwise, it just assumes comment. An error is returned only if // input is not utf8. impl TryFrom<&[u8]> for MentionType { type Error = GementionError; fn try_from(content: &[u8]) -> Result { let content = std::str::from_utf8(&content)?; //TODO 1st line = type (reply, like, etc) rest = comment // content. user should be yoinked from the incoming // connection. so we cannot do from &[u8] let content_type = content .lines() .next() .map(|line| line.trim().to_lowercase()); Ok(match content_type { Some(value) if value == "reply" => Self::Reply, Some(value) if value == "like" => Self::Like, _ => Self::Reply, }) } } impl<'a> TryFrom> for Mention { type Error = GementionError; fn try_from(resource: MentionUpload<'a>) -> Result { if resource.mime() == "text/plain" { let mention_type = MentionType::try_from(resource.content())?; let target = resource.target().ok_or(GementionError::TargetNotProvided)?; Ok(Mention { mention_type, target: target.to_owned(), user: "".to_string(), content: None, }) } 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() } }