Add support for object link microsyntax

Example: [[https://example.com/objects/1]].
This commit is contained in:
silverpill 2022-10-14 17:52:05 +00:00
parent 8d271fe4e3
commit a771e5e4fa
4 changed files with 138 additions and 0 deletions

View file

@ -23,6 +23,7 @@ use crate::ipfs::utils::get_ipfs_url;
use crate::mastodon_api::oauth::auth::get_current_user; use crate::mastodon_api::oauth::auth::get_current_user;
use crate::models::posts::hashtags::{find_hashtags, replace_hashtags}; use crate::models::posts::hashtags::{find_hashtags, replace_hashtags};
use crate::models::posts::helpers::can_view_post; 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::mentions::{find_mentioned_profiles, replace_mentions};
use crate::models::posts::queries::{ use crate::models::posts::queries::{
create_post, create_post,
@ -116,6 +117,36 @@ async fn create_status(
); );
linked.push(post); 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.links.len() > 0 {
if post_data.in_reply_to_id.is_some() { if post_data.in_reply_to_id.is_some() {
return Err(ValidationError("can't add links to reply").into()); return Err(ValidationError("can't add links to reply").into());

90
src/models/posts/links.rs Normal file
View 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);
}
}

View file

@ -1,5 +1,6 @@
pub mod hashtags; pub mod hashtags;
pub mod helpers; pub mod helpers;
pub mod links;
pub mod mentions; pub mod mentions;
pub mod queries; pub mod queries;
pub mod types; pub mod types;

View file

@ -134,6 +134,22 @@ pub fn markdown_to_html(text: &str) -> Result<String, MarkdownError> {
let mut borrowed_node = node.data.borrow_mut(); let mut borrowed_node = node.data.borrow_mut();
*borrowed_node = Ast::new(NodeValue::Paragraph); *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(()) Ok(())