Prototype first commit
This commit is contained in:
parent
5b382bf75d
commit
863d20a79d
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
cert.pem
|
||||
key.pem
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "gemention"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.81"
|
||||
fluffer = "4"
|
||||
germ = "0.4"
|
||||
thiserror = "1.0.58"
|
||||
tokio = {version = "1.37", features = [ "full" ] }
|
||||
url = "2.5.0"
|
|
@ -0,0 +1,152 @@
|
|||
use fluffer::{App, AppErr, Client, Fluff};
|
||||
use germ::ast::{Ast as GeminiAst, Node as GemtextNode};
|
||||
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(String),
|
||||
NotVerified(VerificationFailureReason),
|
||||
}
|
||||
|
||||
impl ToString for VerificationStatus {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Self::Verified(url) => format!("verified: {}", url),
|
||||
Self::NotVerified(failure) => failure.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(ast: GeminiAst) -> Vec<String> {
|
||||
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()
|
||||
}
|
||||
|
||||
fn verify_mentions<S: AsRef<str>>(expected_link: S, mentions: Vec<String>) -> VerificationStatus {
|
||||
let expected_link = expected_link.as_ref();
|
||||
if mentions.len() > 0 {
|
||||
mentions
|
||||
.into_iter()
|
||||
.find_map(|link| {
|
||||
if link == expected_link {
|
||||
Some(VerificationStatus::Verified(link))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(VerificationStatus::NotVerified(
|
||||
VerificationFailureReason::MentionLinkIncorrect,
|
||||
))
|
||||
} else {
|
||||
VerificationStatus::NotVerified(VerificationFailureReason::NoMentionLinkFound)
|
||||
}
|
||||
}
|
||||
|
||||
async fn verify_mention(target: &str) -> Result<VerificationStatus, GementionError> {
|
||||
let url = Url::parse(&format!("gemini://{}", target))?;
|
||||
let expected_link = Url::parse(OUR_ENDPOINT)?.join(target)?;
|
||||
|
||||
let resp = germ_request(&url).await?;
|
||||
let content = resp
|
||||
.content()
|
||||
.as_deref()
|
||||
.ok_or(GementionError::NoContentFoundForTarget)?;
|
||||
|
||||
let ast = GeminiAst::from_string(content);
|
||||
let mentions = scan_for_mentions(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]
|
||||
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
|
||||
}
|
Loading…
Reference in New Issue