Return structured responses properly, with validation and descriptive output.

This commit is contained in:
projectmoon 2024-03-30 12:34:48 +01:00
parent d30a272cbf
commit 72a3c2f3cf
6 changed files with 187 additions and 99 deletions

View File

@ -1,7 +1,7 @@
use crate::error::GementionError; use crate::error::GementionError;
use crate::models::mentions::{Mention, MentionUpload}; use crate::models::mentions::{Mention, MentionUpload};
use crate::models::verification::VerificationStatus; use crate::models::web::MentionResponse;
use crate::{models::mentions::MentionResponse, verification::verify}; use crate::{verification::verify};
use fluffer::Client; use fluffer::Client;
// Receive a mention over Titan. // Receive a mention over Titan.
@ -10,35 +10,10 @@ use fluffer::Client;
pub(crate) async fn receive_mention(client: Client) -> Result<MentionResponse, GementionError> { pub(crate) async fn receive_mention(client: Client) -> Result<MentionResponse, GementionError> {
// TODO change to return MentionResponse or something like that. // TODO change to return MentionResponse or something like that.
let titan = MentionUpload::try_from(&client)?; let titan = MentionUpload::try_from(&client)?;
let target = client.parameter("target").unwrap_or("not provided"); let mut mention = Mention::try_from(titan)?;
let verified = verify(&target).await; verify(&mut mention).await?;
if let VerificationStatus::Verified { .. } = verified { Ok(MentionResponse::from(mention))
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]"),
// )
} }
// Render comments gemtext by requesting comments for a page. // Render comments gemtext by requesting comments for a page.

View File

@ -4,6 +4,8 @@ use std::fmt::Display;
use crate::error::GementionError; use crate::error::GementionError;
use super::verification::VerificationStatus;
macro_rules! titan { macro_rules! titan {
($self:expr) => { ($self:expr) => {
$self.client.titan.as_ref().unwrap() $self.client.titan.as_ref().unwrap()
@ -93,6 +95,68 @@ pub(crate) struct Mention {
target: String, target: String,
user: String, user: String,
content: Option<String>, content: Option<String>,
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 // This is for converting an incoming Titan request into a mention
@ -152,6 +216,7 @@ impl<'a> TryFrom<MentionUpload<'a>> for Mention {
mention_type, mention_type,
content, content,
target, target,
verification_status: VerificationStatus::NotYetVerified,
}) })
} else { } else {
Err(GementionError::InvalidBody) Err(GementionError::InvalidBody)
@ -168,51 +233,6 @@ impl Display for MentionType {
} }
} }
#[async_trait]
impl GemBytes for Mention {
async fn gem_bytes(self) -> Vec<u8> {
// TODO change output based on mention type:
// Reply/comment = current format.
// Like = "<user> 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<u8> {
format!("20 text/gemini\r\n{}", self.status).into_bytes()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -1,2 +1,3 @@
pub(crate) mod verification; pub(crate) mod verification;
pub(crate) mod mentions; pub(crate) mod mentions;
pub(crate) mod web;

View File

@ -1,20 +1,29 @@
use std::fmt::Display;
use crate::error::GementionError; use crate::error::GementionError;
use std::fmt::Display;
use super::mentions::Mention;
#[derive(Debug)]
pub(crate) enum VerificationStatus { pub(crate) enum VerificationStatus {
Verified { Verified {
endpoint: String, endpoint: String,
source: VerificationSource, source: VerificationSource,
}, },
NotVerified(VerificationFailureReason), Invalid(VerificationFailureReason),
/// Verification has not yet occurred.
NotYetVerified,
} }
impl ToString for VerificationStatus { impl ToString for VerificationStatus {
fn to_string(&self) -> String { fn to_string(&self) -> String {
match self { match self {
Self::Verified { endpoint, source } => format!("verified: {} [{}]", endpoint, source), Self::Verified { endpoint, source } => {
Self::NotVerified(failure) => failure.to_string(), 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 { pub(crate) enum VerificationFailureReason {
/// No titan link to our endpoint exists on this page. /// No titan link to our endpoint exists on this page.
NoMentionLinkFound, NoMentionLinkFound,

82
src/models/web.rs Normal file
View File

@ -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<Mention> 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<u8> {
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()
}
}

View File

@ -4,6 +4,7 @@ use germ::request::request as germ_request;
use url::Url; use url::Url;
use crate::error::GementionError; use crate::error::GementionError;
use crate::models::mentions::Mention;
use crate::models::verification::*; use crate::models::verification::*;
const OUR_ENDPOINT: &'static str = "titan://localhost/receive/"; 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) gemtext_link.starts_with(OUR_ENDPOINT)
} }
fn scan_for_mentions(meta: GeminiMetadata, ast: GeminiAst) -> (VerificationSource, Vec<String>) { fn scan_for_gemention_endpoints(
meta: GeminiMetadata,
ast: GeminiAst,
) -> (VerificationSource, Vec<String>) {
// Check metadata of the page for a gemention endpoint. // Check metadata of the page for a gemention endpoint.
if let Some(endpoint) = meta.parameters().get("gemention") { if let Some(endpoint) = meta.parameters().get("gemention") {
let endpoint = endpoint.trim_start_matches("="); let endpoint = endpoint.trim_start_matches("=");
@ -36,9 +40,9 @@ fn scan_for_mentions(meta: GeminiMetadata, ast: GeminiAst) -> (VerificationSourc
fn verify_mentions<S: AsRef<str>>( fn verify_mentions<S: AsRef<str>>(
expected_link: S, expected_link: S,
source_and_mentions: (VerificationSource, Vec<String>), source_and_endpoints: (VerificationSource, Vec<String>),
) -> VerificationStatus { ) -> VerificationStatus {
let (verification_source, mentions) = source_and_mentions; let (verification_source, mentions) = source_and_endpoints;
let expected_link = expected_link.as_ref(); let expected_link = expected_link.as_ref();
if mentions.len() > 0 { if mentions.len() > 0 {
@ -50,24 +54,24 @@ fn verify_mentions<S: AsRef<str>>(
.find_map(|link| { .find_map(|link| {
if link == expected_link { if link == expected_link {
Some(VerificationStatus::Verified { Some(VerificationStatus::Verified {
source: verification_source,
endpoint: link, endpoint: link,
source: verification_source,
}) })
} else { } else {
None None
} }
}) })
.unwrap_or(VerificationStatus::NotVerified( .unwrap_or(VerificationStatus::Invalid(
VerificationFailureReason::MentionLinkIncorrect, VerificationFailureReason::MentionLinkIncorrect,
)) ))
} else { } else {
VerificationStatus::NotVerified(VerificationFailureReason::NoMentionLinkFound) VerificationStatus::Invalid(VerificationFailureReason::NoMentionLinkFound)
} }
} }
async fn verify_mention(target_page: &str) -> Result<VerificationStatus, GementionError> { async fn verify_mention(mention: &mut Mention) -> Result<(), GementionError> {
let url = Url::parse(&format!("gemini://{}", target_page))?; let url = Url::parse(&format!("gemini://{}", mention.target()))?;
let expected_link = Url::parse(OUR_ENDPOINT)?.join(target_page)?; let expected_link = Url::parse(OUR_ENDPOINT)?.join(mention.target())?;
let resp = germ_request(&url).await?; let resp = germ_request(&url).await?;
let meta = GeminiMetadata::from_string(resp.meta()); let meta = GeminiMetadata::from_string(resp.meta());
@ -78,17 +82,13 @@ async fn verify_mention(target_page: &str) -> Result<VerificationStatus, Gementi
.ok_or(GementionError::NoContentFoundForTarget)?; .ok_or(GementionError::NoContentFoundForTarget)?;
let ast = GeminiAst::from_string(content); let ast = GeminiAst::from_string(content);
let mentions = scan_for_mentions(meta, ast); let source_and_endpoints = scan_for_gemention_endpoints(meta, ast);
Ok(verify_mentions(expected_link, mentions)) let status = verify_mentions(expected_link, source_and_endpoints);
mention.set_verify_status(status);
Ok(())
} }
pub(crate) async fn verify(target_page: &str) -> VerificationStatus { pub(crate) async fn verify(mention: &mut Mention) -> Result<(), GementionError> {
let result = verify_mention(target_page) verify_mention(mention).await
.await
.map_err(|e| VerificationStatus::NotVerified(VerificationFailureReason::Error(e)));
match result {
Ok(status) => status,
Err(status) => status,
}
} }