Refactor to proper(ish) codebase

This commit is contained in:
projectmoon 2024-03-29 20:19:16 +01:00
parent 968fe675cd
commit 4a6379b9fa
6 changed files with 210 additions and 195 deletions

29
src/comments.rs Normal file
View File

@ -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.

13
src/error.rs Normal file
View File

@ -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),
}

View File

@ -1,200 +1,12 @@
use std::fmt::Display; use fluffer::AppErr;
use fluffer::{App, AppErr, Client, Fluff}; mod comments;
use germ::ast::{Ast as GeminiAst, Node as GemtextNode}; mod error;
use germ::meta::Meta as GeminiMetadata; mod models;
use germ::request::request as germ_request; mod routes;
use thiserror::Error; mod verification;
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<bool, GementionError> {
Ok(gemtext_link.starts_with(OUR_ENDPOINT))
}
fn scan_for_mentions(meta: GeminiMetadata, ast: GeminiAst) -> (VerificationSource, Vec<String>) {
// 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<S: AsRef<str>>(
expected_link: S,
source_and_mentions: (VerificationSource, Vec<String>),
) -> 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<VerificationStatus, GementionError> {
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.
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), AppErr> { async fn main() -> Result<(), AppErr> {
App::default() crate::routes::create_app().run().await
.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
} }

57
src/models.rs Normal file
View File

@ -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(),
}
}
}

10
src/routes.rs Normal file
View File

@ -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"
})
}

94
src/verification/mod.rs Normal file
View File

@ -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<String>) {
// 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<S: AsRef<str>>(
expected_link: S,
source_and_mentions: (VerificationSource, Vec<String>),
) -> 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<VerificationStatus, GementionError> {
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,
}
}