Refactor to proper(ish) codebase
This commit is contained in:
parent
968fe675cd
commit
4a6379b9fa
|
@ -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.
|
|
@ -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),
|
||||||
|
}
|
202
src/main.rs
202
src/main.rs
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
})
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue