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::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.
|
||||||
|
|
|
@ -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::*;
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
pub(crate) mod verification;
|
pub(crate) mod verification;
|
||||||
pub(crate) mod mentions;
|
pub(crate) mod mentions;
|
||||||
|
pub(crate) mod web;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue