diff --git a/src/comments.rs b/src/comments.rs index 75a33c9..af56ef1 100644 --- a/src/comments.rs +++ b/src/comments.rs @@ -1,28 +1,44 @@ -use crate::verification::verify; +use crate::error::GementionError; +use crate::models::mentions::{Mention, MentionUpload}; +use crate::models::verification::VerificationStatus; +use crate::{models::mentions::MentionResponse, verification::verify}; use fluffer::Client; // Receive a mention over Titan. // - Make Gemini request to see if target page supports gemention. // - If so, store mention in DB. -pub(crate) async fn receive_mention(client: Client) -> String { +pub(crate) async fn receive_mention(client: Client) -> Result { // TODO change to return MentionResponse or something like that. - let titan = match client.titan { - Some(ref titan) => titan, - _ => return "".to_string(), - }; - + let titan = MentionUpload::try_from(&client)?; let target = client.parameter("target").unwrap_or("not provided"); let verified = verify(&target).await; - format!( - "Target: {}\nVerification status: {}\nSize: {}\nMime: {}\nContent: {}\nToken: {}", - target, - verified.to_string(), - titan.size, - titan.mime, - std::str::from_utf8(&titan.content).unwrap_or("[not utf8]"), - titan.token.as_deref().unwrap_or("[no token]"), - ) + if let VerificationStatus::Verified { .. } = verified { + let mention = Mention::try_from(titan); + match mention { + Ok(mention) => println!("{:?}", mention), + Err(err) => println!("{}", err), + } + } + + match verified { + VerificationStatus::Verified { endpoint, source } => { + Ok(MentionResponse::verified(&endpoint)) + } + VerificationStatus::NotVerified(failure) => { + Ok(MentionResponse::failure(&failure.to_string())) + } + } + + // format!( + // "Target: {}\nVerification status: {}\nSize: {}\nMime: {}\nContent: {}\nToken: {}", + // target, + // verified.to_string(), + // titan.size, + // titan.mime, + // std::str::from_utf8(&titan.content).unwrap_or("[not utf8]"), + // titan.token.as_deref().unwrap_or("[no token]"), + // ) } // Render comments gemtext by requesting comments for a page. diff --git a/src/error.rs b/src/error.rs index 959186e..3dbe3d3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,7 @@ use std::str::Utf8Error; +use async_trait::async_trait; +use fluffer::GemBytes; use thiserror::Error; #[derive(Error, Debug)] @@ -7,6 +9,15 @@ pub(crate) enum GementionError { #[error("No content found for target")] NoContentFoundForTarget, + #[error("Not a Titan resource")] + NotTitanResource, + + #[error("Invalid body")] + InvalidBody, + + #[error("Gemention Target not provided")] + TargetNotProvided, + #[error("url parsing error: {0}")] UrlParsingError(#[from] url::ParseError), @@ -16,3 +27,10 @@ pub(crate) enum GementionError { #[error("generic error: {0}")] UnclassifiedError(#[from] anyhow::Error), } + +#[async_trait] +impl GemBytes for GementionError { + async fn gem_bytes(self) -> Vec { + format!("59 text/gemini\r\n{}", self).into_bytes() + } +} diff --git a/src/models/mentions.rs b/src/models/mentions.rs index 17d9c2c..13a94ff 100644 --- a/src/models/mentions.rs +++ b/src/models/mentions.rs @@ -1,15 +1,77 @@ use async_trait::async_trait; -use fluffer::GemBytes; +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, } @@ -19,19 +81,47 @@ pub(crate) struct Mention { // 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> for Mention { +impl TryFrom<&[u8]> for MentionType { type Error = GementionError; - fn try_from(content: Vec) -> Result { + fn try_from(content: &[u8]) -> Result { let content = std::str::from_utf8(&content)?; - Ok(Mention { - mention_type: MentionType::Reply, - user: "".to_string(), - content: None, + //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 { @@ -54,3 +144,34 @@ impl GemBytes for Mention { 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() + } +}