Return structured responses properly, with validation and descriptive output.
This commit is contained in:
parent
d30a272cbf
commit
72a3c2f3cf
|
@ -1,7 +1,7 @@
|
|||
use crate::error::GementionError;
|
||||
use crate::models::mentions::{Mention, MentionUpload};
|
||||
use crate::models::verification::VerificationStatus;
|
||||
use crate::{models::mentions::MentionResponse, verification::verify};
|
||||
use crate::models::web::MentionResponse;
|
||||
use crate::{verification::verify};
|
||||
use fluffer::Client;
|
||||
|
||||
// Receive a mention over Titan.
|
||||
|
@ -10,35 +10,10 @@ use fluffer::Client;
|
|||
pub(crate) async fn receive_mention(client: Client) -> Result<MentionResponse, GementionError> {
|
||||
// TODO change to return MentionResponse or something like that.
|
||||
let titan = MentionUpload::try_from(&client)?;
|
||||
let target = client.parameter("target").unwrap_or("not provided");
|
||||
let verified = verify(&target).await;
|
||||
let mut mention = Mention::try_from(titan)?;
|
||||
verify(&mut mention).await?;
|
||||
|
||||
if let VerificationStatus::Verified { .. } = verified {
|
||||
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]"),
|
||||
// )
|
||||
Ok(MentionResponse::from(mention))
|
||||
}
|
||||
|
||||
// Render comments gemtext by requesting comments for a page.
|
||||
|
|
|
@ -4,6 +4,8 @@ use std::fmt::Display;
|
|||
|
||||
use crate::error::GementionError;
|
||||
|
||||
use super::verification::VerificationStatus;
|
||||
|
||||
macro_rules! titan {
|
||||
($self:expr) => {
|
||||
$self.client.titan.as_ref().unwrap()
|
||||
|
@ -93,6 +95,68 @@ pub(crate) struct Mention {
|
|||
target: String,
|
||||
user: 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
|
||||
|
@ -152,6 +216,7 @@ impl<'a> TryFrom<MentionUpload<'a>> for Mention {
|
|||
mention_type,
|
||||
content,
|
||||
target,
|
||||
verification_status: VerificationStatus::NotYetVerified,
|
||||
})
|
||||
} else {
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
pub(crate) mod verification;
|
||||
pub(crate) mod mentions;
|
||||
pub(crate) mod web;
|
||||
|
|
|
@ -1,20 +1,29 @@
|
|||
use std::fmt::Display;
|
||||
use crate::error::GementionError;
|
||||
use std::fmt::Display;
|
||||
|
||||
use super::mentions::Mention;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum VerificationStatus {
|
||||
Verified {
|
||||
endpoint: String,
|
||||
source: VerificationSource,
|
||||
},
|
||||
|
||||
NotVerified(VerificationFailureReason),
|
||||
Invalid(VerificationFailureReason),
|
||||
|
||||
/// Verification has not yet occurred.
|
||||
NotYetVerified,
|
||||
}
|
||||
|
||||
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(),
|
||||
Self::Verified { endpoint, source } => {
|
||||
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 {
|
||||
/// No titan link to our endpoint exists on this page.
|
||||
NoMentionLinkFound,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ use germ::request::request as germ_request;
|
|||
use url::Url;
|
||||
|
||||
use crate::error::GementionError;
|
||||
use crate::models::mentions::Mention;
|
||||
use crate::models::verification::*;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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.
|
||||
if let Some(endpoint) = meta.parameters().get("gemention") {
|
||||
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>>(
|
||||
expected_link: S,
|
||||
source_and_mentions: (VerificationSource, Vec<String>),
|
||||
source_and_endpoints: (VerificationSource, Vec<String>),
|
||||
) -> VerificationStatus {
|
||||
let (verification_source, mentions) = source_and_mentions;
|
||||
let (verification_source, mentions) = source_and_endpoints;
|
||||
let expected_link = expected_link.as_ref();
|
||||
|
||||
if mentions.len() > 0 {
|
||||
|
@ -50,24 +54,24 @@ fn verify_mentions<S: AsRef<str>>(
|
|||
.find_map(|link| {
|
||||
if link == expected_link {
|
||||
Some(VerificationStatus::Verified {
|
||||
source: verification_source,
|
||||
endpoint: link,
|
||||
source: verification_source,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(VerificationStatus::NotVerified(
|
||||
.unwrap_or(VerificationStatus::Invalid(
|
||||
VerificationFailureReason::MentionLinkIncorrect,
|
||||
))
|
||||
} else {
|
||||
VerificationStatus::NotVerified(VerificationFailureReason::NoMentionLinkFound)
|
||||
VerificationStatus::Invalid(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)?;
|
||||
async fn verify_mention(mention: &mut Mention) -> Result<(), GementionError> {
|
||||
let url = Url::parse(&format!("gemini://{}", mention.target()))?;
|
||||
let expected_link = Url::parse(OUR_ENDPOINT)?.join(mention.target())?;
|
||||
|
||||
let resp = germ_request(&url).await?;
|
||||
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)?;
|
||||
|
||||
let ast = GeminiAst::from_string(content);
|
||||
let mentions = scan_for_mentions(meta, ast);
|
||||
Ok(verify_mentions(expected_link, mentions))
|
||||
let source_and_endpoints = scan_for_gemention_endpoints(meta, ast);
|
||||
let status = verify_mentions(expected_link, source_and_endpoints);
|
||||
|
||||
mention.set_verify_status(status);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
pub(crate) async fn verify(mention: &mut Mention) -> Result<(), GementionError> {
|
||||
verify_mention(mention).await
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue