diff --git a/src/comments.rs b/src/comments.rs index af56ef1..be945d8 100644 --- a/src/comments.rs +++ b/src/comments.rs @@ -1,7 +1,7 @@ use crate::error::GementionError; use crate::models::mentions::{Mention, MentionUpload}; -use crate::models::verification::VerificationStatus; -use crate::{models::mentions::MentionResponse, verification::verify}; +use crate::models::web::MentionResponse; +use crate::{verification::verify}; use fluffer::Client; // Receive a mention over Titan. @@ -10,35 +10,10 @@ use fluffer::Client; pub(crate) async fn receive_mention(client: Client) -> Result { // TODO change to return MentionResponse or something like that. let titan = MentionUpload::try_from(&client)?; - let target = client.parameter("target").unwrap_or("not provided"); - let verified = verify(&target).await; + let mut mention = Mention::try_from(titan)?; + verify(&mut mention).await?; - 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]"), - // ) + Ok(MentionResponse::from(mention)) } // Render comments gemtext by requesting comments for a page. diff --git a/src/models/mentions.rs b/src/models/mentions.rs index 16bebf4..9dc6cc2 100644 --- a/src/models/mentions.rs +++ b/src/models/mentions.rs @@ -4,6 +4,8 @@ use std::fmt::Display; use crate::error::GementionError; +use super::verification::VerificationStatus; + macro_rules! titan { ($self:expr) => { $self.client.titan.as_ref().unwrap() @@ -93,6 +95,68 @@ pub(crate) struct Mention { target: String, user: String, content: Option, + verification_status: VerificationStatus, +} + +impl Mention { + pub fn target(&self) -> &str { + &self.target + } + + 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 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{}", headline, 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 @@ -152,6 +216,7 @@ impl<'a> TryFrom> for Mention { mention_type, content, target, + verification_status: VerificationStatus::NotYetVerified, }) } else { Err(GementionError::InvalidBody) @@ -168,51 +233,6 @@ impl Display for MentionType { } } -#[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::*; diff --git a/src/models/mod.rs b/src/models/mod.rs index c9142f1..464f016 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,2 +1,3 @@ pub(crate) mod verification; pub(crate) mod mentions; +pub(crate) mod web; diff --git a/src/models/verification.rs b/src/models/verification.rs index 0a660f9..1d98070 100644 --- a/src/models/verification.rs +++ b/src/models/verification.rs @@ -1,20 +1,29 @@ -use std::fmt::Display; use crate::error::GementionError; +use std::fmt::Display; +use super::mentions::Mention; + +#[derive(Debug)] pub(crate) enum VerificationStatus { Verified { endpoint: String, source: VerificationSource, }, - NotVerified(VerificationFailureReason), + Invalid(VerificationFailureReason), + + /// Verification has not yet occurred. + NotYetVerified, } impl ToString for VerificationStatus { fn to_string(&self) -> String { match self { - Self::Verified { endpoint, source } => format!("verified: {} [{}]", endpoint, source), - Self::NotVerified(failure) => failure.to_string(), + Self::Verified { endpoint, source } => { + format!("verified: endpoint={} [{}]", endpoint, source) + } + Self::Invalid(failure) => failure.to_string(), + Self::NotYetVerified => format!("not yet verified"), } } } @@ -34,6 +43,7 @@ impl Display for VerificationSource { } } +#[derive(Debug)] pub(crate) enum VerificationFailureReason { /// No titan link to our endpoint exists on this page. NoMentionLinkFound, diff --git a/src/models/web.rs b/src/models/web.rs new file mode 100644 index 0000000..7b507a1 --- /dev/null +++ b/src/models/web.rs @@ -0,0 +1,82 @@ +use super::{mentions::Mention, verification::VerificationStatus}; +use fluffer::{async_trait, GemBytes}; + +pub(crate) enum MentionResponse { + VerifiedMention(Mention), + InvalidMention(Mention), + NotTitanRequest, +} + +impl From for MentionResponse { + fn from(mention: Mention) -> Self { + match mention.verify_status() { + VerificationStatus::Verified { .. } => Self::VerifiedMention(mention), + VerificationStatus::Invalid(_) => Self::InvalidMention(mention), + VerificationStatus::NotYetVerified => Self::InvalidMention(mention) + } + } +} + +impl MentionResponse { + pub fn should_have_body(&self) -> bool { + match self { + Self::VerifiedMention(_) | Self::InvalidMention(_) => true, + _ => false, + } + } + + pub fn status_line(&self) -> String { + match self { + Self::VerifiedMention(_) => "20 text/gemini", + Self::InvalidMention(_) => "20 text/gemini", + Self::NotTitanRequest => "59 text/gemini", + } + .to_string() + } + + pub fn headline(&self) -> String { + match self { + Self::VerifiedMention(_) => "# Mention Submitted", + Self::InvalidMention(_) => "# Error Submitting Mention", + Self::NotTitanRequest => "# Invalid Protocol For Request", + } + .to_string() + } + + pub fn description(&self) -> String { + match self { + Self::VerifiedMention(_) => "You have successfully submitted a gemention.", + Self::InvalidMention(_) => { + "There was an error submitting the gemention, detailed below." + } + Self::NotTitanRequest => "Your request was not a Titan protocol request.", + } + .to_string() + } + + pub fn mention_body(&self) -> String { + match self { + Self::VerifiedMention(m) | Self::InvalidMention(m) => m.as_gemtext(), + _ => "".to_string(), + } + } +} + +#[async_trait] +impl GemBytes for MentionResponse { + async fn gem_bytes(self) -> Vec { + let status_line = self.status_line(); + let body = if self.should_have_body() { + format!( + "{}\n\n{}\n\n{},", + self.headline(), + self.description(), + self.mention_body() + ) + } else { + String::from("") + }; + + format!("{}\r\n{}", status_line, body).into_bytes() + } +} diff --git a/src/verification/mod.rs b/src/verification/mod.rs index b0c915d..b5f21f6 100644 --- a/src/verification/mod.rs +++ b/src/verification/mod.rs @@ -4,6 +4,7 @@ use germ::request::request as germ_request; use url::Url; use crate::error::GementionError; +use crate::models::mentions::Mention; use crate::models::verification::*; const OUR_ENDPOINT: &'static str = "titan://localhost/receive/"; @@ -12,7 +13,10 @@ fn is_mention_link(gemtext_link: &str) -> bool { gemtext_link.starts_with(OUR_ENDPOINT) } -fn scan_for_mentions(meta: GeminiMetadata, ast: GeminiAst) -> (VerificationSource, Vec) { +fn scan_for_gemention_endpoints( + meta: GeminiMetadata, + ast: GeminiAst, +) -> (VerificationSource, Vec) { // Check metadata of the page for a gemention endpoint. if let Some(endpoint) = meta.parameters().get("gemention") { let endpoint = endpoint.trim_start_matches("="); @@ -36,9 +40,9 @@ fn scan_for_mentions(meta: GeminiMetadata, ast: GeminiAst) -> (VerificationSourc fn verify_mentions>( expected_link: S, - source_and_mentions: (VerificationSource, Vec), + source_and_endpoints: (VerificationSource, Vec), ) -> VerificationStatus { - let (verification_source, mentions) = source_and_mentions; + let (verification_source, mentions) = source_and_endpoints; let expected_link = expected_link.as_ref(); if mentions.len() > 0 { @@ -50,24 +54,24 @@ fn verify_mentions>( .find_map(|link| { if link == expected_link { Some(VerificationStatus::Verified { - source: verification_source, endpoint: link, + source: verification_source, }) } else { None } }) - .unwrap_or(VerificationStatus::NotVerified( + .unwrap_or(VerificationStatus::Invalid( VerificationFailureReason::MentionLinkIncorrect, )) } else { - VerificationStatus::NotVerified(VerificationFailureReason::NoMentionLinkFound) + VerificationStatus::Invalid(VerificationFailureReason::NoMentionLinkFound) } } -async fn verify_mention(target_page: &str) -> Result { - let url = Url::parse(&format!("gemini://{}", target_page))?; - let expected_link = Url::parse(OUR_ENDPOINT)?.join(target_page)?; +async fn verify_mention(mention: &mut Mention) -> Result<(), GementionError> { + let url = Url::parse(&format!("gemini://{}", mention.target()))?; + let expected_link = Url::parse(OUR_ENDPOINT)?.join(mention.target())?; let resp = germ_request(&url).await?; let meta = GeminiMetadata::from_string(resp.meta()); @@ -78,17 +82,13 @@ async fn verify_mention(target_page: &str) -> Result VerificationStatus { - let result = verify_mention(target_page) - .await - .map_err(|e| VerificationStatus::NotVerified(VerificationFailureReason::Error(e))); - - match result { - Ok(status) => status, - Err(status) => status, - } +pub(crate) async fn verify(mention: &mut Mention) -> Result<(), GementionError> { + verify_mention(mention).await }