From 4a6379b9fa8d075d0977380fb23d7012a3bfccaf Mon Sep 17 00:00:00 2001 From: projectmoon Date: Fri, 29 Mar 2024 20:19:16 +0100 Subject: [PATCH] Refactor to proper(ish) codebase --- src/comments.rs | 29 ++++++ src/error.rs | 13 +++ src/main.rs | 202 ++-------------------------------------- src/models.rs | 57 ++++++++++++ src/routes.rs | 10 ++ src/verification/mod.rs | 94 +++++++++++++++++++ 6 files changed, 210 insertions(+), 195 deletions(-) create mode 100644 src/comments.rs create mode 100644 src/error.rs create mode 100644 src/models.rs create mode 100644 src/routes.rs create mode 100644 src/verification/mod.rs diff --git a/src/comments.rs b/src/comments.rs new file mode 100644 index 0000000..f1284b8 --- /dev/null +++ b/src/comments.rs @@ -0,0 +1,29 @@ +use crate::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 { + let titan = match client.titan { + Some(ref titan) => titan, + _ => return "".to_string(), + }; + + 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]"), + ) +} + +// Render comments gemtext by requesting comments for a page. + +// Proxy a webmention to Titan endpoint. diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..e0bf26e --- /dev/null +++ b/src/error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub(crate) enum GementionError { + #[error("No content found for target")] + NoContentFoundForTarget, + + #[error("url parsing error: {0}")] + UrlParsingError(#[from] url::ParseError), + + #[error("generic error: {0}")] + UnclassifiedError(#[from] anyhow::Error), +} diff --git a/src/main.rs b/src/main.rs index d31e7c0..050ad2a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,200 +1,12 @@ -use std::fmt::Display; +use fluffer::AppErr; -use fluffer::{App, AppErr, Client, Fluff}; -use germ::ast::{Ast as GeminiAst, Node as GemtextNode}; -use germ::meta::Meta as GeminiMetadata; -use germ::request::request as germ_request; -use thiserror::Error; -use url::Url; - -#[derive(Error, Debug)] -enum GementionError { - #[error("No content found for target")] - NoContentFoundForTarget, - - #[error("url parsing error: {0}")] - UrlParsingError(#[from] url::ParseError), - - #[error("generic error: {0}")] - UnclassifiedError(#[from] anyhow::Error), -} - -enum VerificationStatus { - Verified { - endpoint: String, - source: VerificationSource, - }, - - NotVerified(VerificationFailureReason), -} - -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(), - } - } -} - -#[derive(Debug, Clone, Copy)] -enum VerificationSource { - Meta, - Page, -} - -impl Display for VerificationSource { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Meta => write!(f, "source=meta"), - Self::Page => write!(f, "source=page"), - } - - } -} - -enum VerificationFailureReason { - /// No titan link to our endpoint exists on this page. - NoMentionLinkFound, - - /// One or more mention links exist, but they are not to this - /// endpoint, or for this page. - MentionLinkIncorrect, - - /// There was an error during the verification process. - Error(GementionError), -} - -impl ToString for VerificationFailureReason { - fn to_string(&self) -> String { - match self { - Self::NoMentionLinkFound => String::from("No mention link found"), - Self::MentionLinkIncorrect => String::from("Mention link points to wrong target"), - Self::Error(err) => err.to_string(), - } - } -} - -const OUR_ENDPOINT: &'static str = "titan://localhost/receive/"; - -fn is_mention_link(gemtext_link: &str) -> Result { - Ok(gemtext_link.starts_with(OUR_ENDPOINT)) -} - -fn scan_for_mentions(meta: GeminiMetadata, ast: GeminiAst) -> (VerificationSource, Vec) { - // Check metadata of the page for a gemention endpoint. - println!("meta is {:?}", meta); - if let Some(endpoint) = meta.parameters().get("gemention") { - let endpoint = endpoint.trim_start_matches("="); - return (VerificationSource::Meta, vec![endpoint.to_owned()]); - } - - // If that fails, check the page itself for the first available - // link that matches. - let endpoints = ast - .inner() - .into_iter() - .filter_map(|node| match node { - GemtextNode::Link { ref to, .. } if is_mention_link(to).unwrap_or(false) => Some(to), - _ => None, - }) - .cloned() - .collect(); - - (VerificationSource::Page, endpoints) -} - -fn verify_mentions>( - expected_link: S, - source_and_mentions: (VerificationSource, Vec), -) -> VerificationStatus { - let (verification_source, mentions) = source_and_mentions; - let expected_link = expected_link.as_ref(); - - if mentions.len() > 0 { - // We have links that go to our endpoint. Scan links for the - // one we expect (i.e. for the target), otherwise we say - // incorrect link. - mentions - .into_iter() - .find_map(|link| { - if link == expected_link { - Some(VerificationStatus::Verified { - source: verification_source, - endpoint: link, - }) - } else { - None - } - }) - .unwrap_or(VerificationStatus::NotVerified( - VerificationFailureReason::MentionLinkIncorrect, - )) - } else { - VerificationStatus::NotVerified(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)?; - - let resp = germ_request(&url).await?; - let meta = GeminiMetadata::from_string(resp.meta()); - - let content = resp - .content() - .as_deref() - .ok_or(GementionError::NoContentFoundForTarget)?; - - let ast = GeminiAst::from_string(content); - let mentions = scan_for_mentions(meta, ast); - Ok(verify_mentions(expected_link, mentions)) -} - -// Receive a mention over Titan. -// - Make Gemini request to see if target page supports gemention. -// - If so, store mention in DB. -async fn receive_mention(client: Client) -> String { - let titan = match client.titan { - Some(ref titan) => titan, - _ => return "".to_string(), - }; - - let target = client.parameter("target").unwrap_or("not provided"); - - let verified = verify_mention(&target).await; - let verified = match verified - .map(|status| status.to_string()) - .map_err(|e| e.to_string()) - { - Ok(value) => value, - Err(value) => value, - }; - - return format!( - "Target: {}\nVerification status: {}\nSize: {}\nMime: {}\nContent: {}\nToken: {}", - target, - verified, - 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. - -// Proxy a webmention to Titan endpoint. +mod comments; +mod error; +mod models; +mod routes; +mod verification; #[tokio::main] async fn main() -> Result<(), AppErr> { - App::default() - .titan("/receive/*target", receive_mention, 20_000) - .route("/", |_| async { - "# Welcome\n=> titan://localhost/receive/agnos.is/posts/webmentions-test.gmi Receive Mention" - }) - //.route("/{*p}", |_| async { "hello" }) - .run() - .await + crate::routes::create_app().run().await } diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..0a660f9 --- /dev/null +++ b/src/models.rs @@ -0,0 +1,57 @@ +use std::fmt::Display; +use crate::error::GementionError; + +pub(crate) enum VerificationStatus { + Verified { + endpoint: String, + source: VerificationSource, + }, + + NotVerified(VerificationFailureReason), +} + +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(), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum VerificationSource { + Meta, + Page, +} + +impl Display for VerificationSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Meta => write!(f, "source=meta"), + Self::Page => write!(f, "source=page"), + } + } +} + +pub(crate) enum VerificationFailureReason { + /// No titan link to our endpoint exists on this page. + NoMentionLinkFound, + + /// One or more mention links exist, but they are not to this + /// endpoint, or for this page. + MentionLinkIncorrect, + + /// There was an error during the verification process. + Error(GementionError), +} + +impl ToString for VerificationFailureReason { + fn to_string(&self) -> String { + match self { + Self::NoMentionLinkFound => String::from("No mention link found"), + Self::MentionLinkIncorrect => String::from("Mention link points to wrong target"), + Self::Error(err) => err.to_string(), + } + } +} diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 0000000..c04d85d --- /dev/null +++ b/src/routes.rs @@ -0,0 +1,10 @@ +use fluffer::App; +use crate::comments::receive_mention; + +pub(crate) fn create_app() -> App { + App::default() + .titan("/receive/*target", receive_mention, 20_000) + .route("/", |_| async { + "# Welcome\n=> titan://localhost/receive/agnos.is/posts/webmentions-test.gmi Receive Mention" + }) +} diff --git a/src/verification/mod.rs b/src/verification/mod.rs new file mode 100644 index 0000000..bf4f4b7 --- /dev/null +++ b/src/verification/mod.rs @@ -0,0 +1,94 @@ +use germ::ast::{Ast as GeminiAst, Node as GemtextNode}; +use germ::meta::Meta as GeminiMetadata; +use germ::request::request as germ_request; +use url::Url; + +use crate::error::GementionError; +use crate::models::*; + +const OUR_ENDPOINT: &'static str = "titan://localhost/receive/"; + +fn is_mention_link(gemtext_link: &str) -> bool { + gemtext_link.starts_with(OUR_ENDPOINT) +} + +fn scan_for_mentions(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("="); + return (VerificationSource::Meta, vec![endpoint.to_owned()]); + } + + // If that fails, check the page itself for the first available + // link that matches. + let endpoints = ast + .inner() + .into_iter() + .filter_map(|node| match node { + GemtextNode::Link { ref to, .. } if is_mention_link(to) => Some(to), + _ => None, + }) + .cloned() + .collect(); + + (VerificationSource::Page, endpoints) +} + +fn verify_mentions>( + expected_link: S, + source_and_mentions: (VerificationSource, Vec), +) -> VerificationStatus { + let (verification_source, mentions) = source_and_mentions; + let expected_link = expected_link.as_ref(); + + if mentions.len() > 0 { + // We have links that go to our endpoint. Scan links for the + // one we expect (i.e. for the target), otherwise we say + // incorrect link. + mentions + .into_iter() + .find_map(|link| { + if link == expected_link { + Some(VerificationStatus::Verified { + source: verification_source, + endpoint: link, + }) + } else { + None + } + }) + .unwrap_or(VerificationStatus::NotVerified( + VerificationFailureReason::MentionLinkIncorrect, + )) + } else { + VerificationStatus::NotVerified(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)?; + + let resp = germ_request(&url).await?; + let meta = GeminiMetadata::from_string(resp.meta()); + + let content = resp + .content() + .as_deref() + .ok_or(GementionError::NoContentFoundForTarget)?; + + let ast = GeminiAst::from_string(content); + let mentions = scan_for_mentions(meta, ast); + Ok(verify_mentions(expected_link, mentions)) +} + +pub(crate) async fn verify(target_page: &str) -> VerificationStatus { + let result = verify_mention(target_page) + .await + .map_err(|e| VerificationStatus::NotVerified(VerificationFailureReason::Error(e))); + + match result { + Ok(status) => status, + Err(status) => status, + } +}