Proper parsing and validation of incoming mentions
This commit is contained in:
parent
51fbea4cb1
commit
d30a272cbf
|
@ -12,12 +12,18 @@ pub(crate) enum GementionError {
|
||||||
#[error("Not a Titan resource")]
|
#[error("Not a Titan resource")]
|
||||||
NotTitanResource,
|
NotTitanResource,
|
||||||
|
|
||||||
|
#[error("Invalid mention type: {0}")]
|
||||||
|
InvalidMentionType(String),
|
||||||
|
|
||||||
#[error("Invalid body")]
|
#[error("Invalid body")]
|
||||||
InvalidBody,
|
InvalidBody,
|
||||||
|
|
||||||
#[error("Gemention Target not provided")]
|
#[error("Gemention Target not provided")]
|
||||||
TargetNotProvided,
|
TargetNotProvided,
|
||||||
|
|
||||||
|
#[error("Username not provided")]
|
||||||
|
UsernameNotProvided,
|
||||||
|
|
||||||
#[error("url parsing error: {0}")]
|
#[error("url parsing error: {0}")]
|
||||||
UrlParsingError(#[from] url::ParseError),
|
UrlParsingError(#[from] url::ParseError),
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,21 @@ macro_rules! titan {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_content_body(raw_body: &[u8]) -> Result<&str, GementionError> {
|
||||||
|
let content = std::str::from_utf8(raw_body)?;
|
||||||
|
|
||||||
|
// Drop 1st line if it is a mention type. Otherwise return whole thing.
|
||||||
|
let mention_type = MentionType::try_from(raw_body);
|
||||||
|
let content: &str = if let Ok(_) = mention_type {
|
||||||
|
let amt_to_chop = content.lines().next().map(|line| line.len()).unwrap();
|
||||||
|
&content[amt_to_chop..]
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(content.trim())
|
||||||
|
}
|
||||||
|
|
||||||
/// Wraps an incoming Titan request and provides handy methods for
|
/// Wraps an incoming Titan request and provides handy methods for
|
||||||
/// extracting the expected information.
|
/// extracting the expected information.
|
||||||
pub(crate) struct MentionUpload<'a> {
|
pub(crate) struct MentionUpload<'a> {
|
||||||
|
@ -25,7 +40,7 @@ impl<'a> MentionUpload<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content(&self) -> &[u8] {
|
pub fn raw_body(&self) -> &[u8] {
|
||||||
titan!(self).content.as_slice()
|
titan!(self).content.as_slice()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +67,10 @@ impl<'a> MentionUpload<'a> {
|
||||||
pub fn target(&self) -> Option<&str> {
|
pub fn target(&self) -> Option<&str> {
|
||||||
self.client.parameter("target")
|
self.client.parameter("target")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn content(&self) -> Result<&str, GementionError> {
|
||||||
|
parse_content_body(self.raw_body())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TryFrom<&'a Client> for MentionUpload<'a> {
|
impl<'a> TryFrom<&'a Client> for MentionUpload<'a> {
|
||||||
|
@ -76,29 +95,26 @@ pub(crate) struct Mention {
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is for converting an incoming Titan request into a mention. It
|
// This is for converting an incoming Titan request into a mention
|
||||||
// is very flexible: If the incoming value matches a specific format,
|
// type. It will attempt to parse the first line to identify the type.
|
||||||
// it'll accurately convert the stringi nto a proper mention.
|
// If the type cannot be identified, or if the string is not valid
|
||||||
// Otherwise, it just assumes comment. An error is returned only if
|
// UTF-8, an error is returned.
|
||||||
// input is not utf8.
|
|
||||||
impl TryFrom<&[u8]> for MentionType {
|
impl TryFrom<&[u8]> for MentionType {
|
||||||
type Error = GementionError;
|
type Error = GementionError;
|
||||||
|
|
||||||
fn try_from(content: &[u8]) -> Result<Self, Self::Error> {
|
fn try_from(content: &[u8]) -> Result<Self, Self::Error> {
|
||||||
let content = std::str::from_utf8(&content)?;
|
let content = std::str::from_utf8(&content)?;
|
||||||
//TODO 1st line = type (reply, like, etc) rest = comment
|
|
||||||
// content. user should be yoinked from the incoming
|
|
||||||
// connection. so we cannot do from &[u8]
|
|
||||||
let content_type = content
|
let content_type = content
|
||||||
.lines()
|
.lines()
|
||||||
.next()
|
.next()
|
||||||
.map(|line| line.trim().to_lowercase());
|
.map(|line| line.trim())
|
||||||
|
.unwrap_or("[none provided]");
|
||||||
|
|
||||||
Ok(match content_type {
|
match content_type.to_lowercase() {
|
||||||
Some(value) if value == "reply" => Self::Reply,
|
value if value == "reply" => Ok(Self::Reply),
|
||||||
Some(value) if value == "like" => Self::Like,
|
value if value == "like" => Ok(Self::Like),
|
||||||
_ => Self::Reply,
|
_ => Err(GementionError::InvalidMentionType(content_type.to_owned())),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,14 +123,35 @@ impl<'a> TryFrom<MentionUpload<'a>> for Mention {
|
||||||
|
|
||||||
fn try_from(resource: MentionUpload<'a>) -> Result<Self, Self::Error> {
|
fn try_from(resource: MentionUpload<'a>) -> Result<Self, Self::Error> {
|
||||||
if resource.mime() == "text/plain" {
|
if resource.mime() == "text/plain" {
|
||||||
let mention_type = MentionType::try_from(resource.content())?;
|
// Be flexible on mention type: if first line isn't a
|
||||||
let target = resource.target().ok_or(GementionError::TargetNotProvided)?;
|
// mention type, just assume reply.
|
||||||
|
let mention_type = match MentionType::try_from(resource.raw_body()) {
|
||||||
|
Ok(mention_type) => mention_type,
|
||||||
|
Err(GementionError::InvalidMentionType(_)) => MentionType::Reply,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
let target = resource
|
||||||
|
.target()
|
||||||
|
.ok_or(GementionError::TargetNotProvided)?
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
let user = resource
|
||||||
|
.username()
|
||||||
|
.ok_or(GementionError::UsernameNotProvided)?;
|
||||||
|
|
||||||
|
let content = resource.content()?.to_owned();
|
||||||
|
let content = if !content.is_empty() {
|
||||||
|
Some(content)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Mention {
|
Ok(Mention {
|
||||||
|
user,
|
||||||
mention_type,
|
mention_type,
|
||||||
target: target.to_owned(),
|
content,
|
||||||
user: "".to_string(),
|
target,
|
||||||
content: None,
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(GementionError::InvalidBody)
|
Err(GementionError::InvalidBody)
|
||||||
|
@ -175,3 +212,28 @@ impl GemBytes for MentionResponse {
|
||||||
format!("20 text/gemini\r\n{}", self.status).into_bytes()
|
format!("20 text/gemini\r\n{}", self.status).into_bytes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_comment_body_with_mention_type() -> Result<(), GementionError> {
|
||||||
|
let body = "reply\nthis is my comment body, which is a reply.".as_bytes();
|
||||||
|
let expected = "this is my comment body, which is a reply.";
|
||||||
|
let parsed = parse_content_body(body)?;
|
||||||
|
|
||||||
|
assert_eq!(expected, parsed);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_comment_body_without_mention_type() -> Result<(), GementionError> {
|
||||||
|
let expected = "this is my comment body, which has no mention type.";
|
||||||
|
let body = expected.as_bytes();
|
||||||
|
let parsed = parse_content_body(body)?;
|
||||||
|
|
||||||
|
assert_eq!(expected, parsed);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue