Add support for object link microsyntax
Example: [[https://example.com/objects/1]].
This commit is contained in:
parent
8d271fe4e3
commit
a771e5e4fa
4 changed files with 138 additions and 0 deletions
|
@ -23,6 +23,7 @@ use crate::ipfs::utils::get_ipfs_url;
|
|||
use crate::mastodon_api::oauth::auth::get_current_user;
|
||||
use crate::models::posts::hashtags::{find_hashtags, replace_hashtags};
|
||||
use crate::models::posts::helpers::can_view_post;
|
||||
use crate::models::posts::links::{replace_object_links, find_linked_posts};
|
||||
use crate::models::posts::mentions::{find_mentioned_profiles, replace_mentions};
|
||||
use crate::models::posts::queries::{
|
||||
create_post,
|
||||
|
@ -116,6 +117,36 @@ async fn create_status(
|
|||
);
|
||||
linked.push(post);
|
||||
};
|
||||
let link_map = match find_linked_posts(
|
||||
db_client,
|
||||
&instance.url(),
|
||||
&post_data.content,
|
||||
).await {
|
||||
Ok(link_map) => link_map,
|
||||
Err(DatabaseError::NotFound(_)) => {
|
||||
return Err(ValidationError("referenced post does't exist").into());
|
||||
},
|
||||
Err(other_error) => return Err(other_error.into()),
|
||||
};
|
||||
post_data.content = replace_object_links(
|
||||
&link_map,
|
||||
&post_data.content,
|
||||
);
|
||||
for post in link_map.into_values() {
|
||||
if !post_data.links.contains(&post.id) {
|
||||
if post.repost_of_id.is_some() {
|
||||
return Err(ValidationError("can't reference repost").into());
|
||||
};
|
||||
if post.visibility != Visibility::Public {
|
||||
return Err(ValidationError("can't reference non-public post").into());
|
||||
};
|
||||
if post.author.id != current_user.id {
|
||||
post_data.mentions.push(post.author.id);
|
||||
};
|
||||
post_data.links.push(post.id);
|
||||
linked.push(post);
|
||||
};
|
||||
};
|
||||
if post_data.links.len() > 0 {
|
||||
if post_data.in_reply_to_id.is_some() {
|
||||
return Err(ValidationError("can't add links to reply").into());
|
||||
|
|
90
src/models/posts/links.rs
Normal file
90
src/models/posts/links.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use regex::{Captures, Regex};
|
||||
use tokio_postgres::GenericClient;
|
||||
|
||||
use crate::errors::DatabaseError;
|
||||
use super::helpers::get_post_by_object_id;
|
||||
use super::types::Post;
|
||||
|
||||
const OBJECT_LINK_SEARCH_RE: &str = r"(?m)\[\[(?P<url>\S+)\]\]";
|
||||
|
||||
/// Finds everything that looks like an object link
|
||||
fn find_object_links(text: &str) -> Vec<String> {
|
||||
let link_re = Regex::new(OBJECT_LINK_SEARCH_RE).unwrap();
|
||||
let mut links = vec![];
|
||||
for caps in link_re.captures_iter(text) {
|
||||
let url = caps["url"].to_string();
|
||||
if !links.contains(&url) {
|
||||
links.push(url);
|
||||
};
|
||||
};
|
||||
links
|
||||
}
|
||||
|
||||
pub async fn find_linked_posts(
|
||||
db_client: &impl GenericClient,
|
||||
instance_url: &str,
|
||||
text: &str,
|
||||
) -> Result<HashMap<String, Post>, DatabaseError> {
|
||||
let links = find_object_links(text);
|
||||
let mut link_map: HashMap<String, Post> = HashMap::new();
|
||||
for url in links {
|
||||
// Return error if post doesn't exist
|
||||
let post = get_post_by_object_id(
|
||||
db_client,
|
||||
instance_url,
|
||||
&url,
|
||||
).await?;
|
||||
link_map.insert(url, post);
|
||||
};
|
||||
Ok(link_map)
|
||||
}
|
||||
|
||||
pub fn replace_object_links(
|
||||
link_map: &HashMap<String, Post>,
|
||||
text: &str,
|
||||
) -> String {
|
||||
let mention_re = Regex::new(OBJECT_LINK_SEARCH_RE).unwrap();
|
||||
let result = mention_re.replace_all(text, |caps: &Captures| {
|
||||
let url = caps["url"].to_string();
|
||||
if link_map.contains_key(&url) {
|
||||
return format!(r#"<a href="{0}">{0}</a>"#, url);
|
||||
};
|
||||
// Leave unchanged if post does not exist
|
||||
caps[0].to_string()
|
||||
});
|
||||
result.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const TEXT_WITH_OBJECT_LINKS: &str = concat!(
|
||||
"test [[https://example.org/1]] link ",
|
||||
"test ([[https://example.org/2]])",
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn test_find_object_links() {
|
||||
let results = find_object_links(TEXT_WITH_OBJECT_LINKS);
|
||||
assert_eq!(results, vec![
|
||||
"https://example.org/1",
|
||||
"https://example.org/2",
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replace_object_links() {
|
||||
let mut link_map = HashMap::new();
|
||||
link_map.insert("https://example.org/1".to_string(), Post::default());
|
||||
link_map.insert("https://example.org/2".to_string(), Post::default());
|
||||
let result = replace_object_links(&link_map, TEXT_WITH_OBJECT_LINKS);
|
||||
let expected_result = concat!(
|
||||
r#"test <a href="https://example.org/1">https://example.org/1</a> link "#,
|
||||
r#"test (<a href="https://example.org/2">https://example.org/2</a>)"#,
|
||||
);
|
||||
assert_eq!(result, expected_result);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
pub mod hashtags;
|
||||
pub mod helpers;
|
||||
pub mod links;
|
||||
pub mod mentions;
|
||||
pub mod queries;
|
||||
pub mod types;
|
||||
|
|
|
@ -134,6 +134,22 @@ pub fn markdown_to_html(text: &str) -> Result<String, MarkdownError> {
|
|||
let mut borrowed_node = node.data.borrow_mut();
|
||||
*borrowed_node = Ast::new(NodeValue::Paragraph);
|
||||
},
|
||||
NodeValue::Link(link) => {
|
||||
if let Some(prev) = node.previous_sibling() {
|
||||
if let NodeValue::Text(ref prev_text) = prev.data.borrow().value {
|
||||
let prev_text = String::from_utf8(prev_text.to_vec())?;
|
||||
// Remove autolink if object link syntax is found
|
||||
if prev_text.ends_with("[[") {
|
||||
for child in node.children() {
|
||||
child.detach();
|
||||
};
|
||||
let text = NodeValue::Text(link.url);
|
||||
let mut borrowed_node = node.data.borrow_mut();
|
||||
*borrowed_node = Ast::new(text);
|
||||
};
|
||||
};
|
||||
};
|
||||
},
|
||||
_ => (),
|
||||
};
|
||||
Ok(())
|
||||
|
|
Loading…
Reference in a new issue