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::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
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 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;
|
||||||
|
|
|
@ -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(())
|
||||||
|
|
Loading…
Reference in a new issue