Add mention parser
This commit is contained in:
parent
0fd7c0fae3
commit
fa7bff4b31
4 changed files with 164 additions and 2 deletions
|
@ -19,6 +19,7 @@ use crate::ipfs::store as ipfs_store;
|
|||
use crate::ipfs::utils::{IPFS_LOGO, get_ipfs_url};
|
||||
use crate::mastodon_api::oauth::auth::get_current_user;
|
||||
use crate::models::attachments::queries::set_attachment_ipfs_cid;
|
||||
use crate::models::posts::mentions::{find_mentioned_profiles, replace_mentions};
|
||||
use crate::models::profiles::queries::get_followers;
|
||||
use crate::models::posts::helpers::{
|
||||
get_actions_for_post,
|
||||
|
@ -46,8 +47,21 @@ async fn create_status(
|
|||
) -> Result<HttpResponse, HttpError> {
|
||||
let db_client = &mut **get_database_client(&db_pool).await?;
|
||||
let current_user = get_current_user(db_client, auth.token()).await?;
|
||||
let instance = config.instance();
|
||||
let mut post_data = PostCreateData::from(data.into_inner());
|
||||
post_data.validate()?;
|
||||
// Mentions
|
||||
let mention_map = find_mentioned_profiles(
|
||||
db_client,
|
||||
&instance.host(),
|
||||
&post_data.content,
|
||||
).await?;
|
||||
post_data.content = replace_mentions(
|
||||
&mention_map,
|
||||
&instance.host(),
|
||||
&instance.url(),
|
||||
&post_data.content,
|
||||
);
|
||||
let post = create_post(db_client, ¤t_user.id, post_data).await?;
|
||||
// Federate
|
||||
let maybe_in_reply_to = match post.in_reply_to_id {
|
||||
|
@ -58,7 +72,7 @@ async fn create_status(
|
|||
None => None,
|
||||
};
|
||||
let activity = create_activity_note(
|
||||
&config.instance_url(),
|
||||
&instance.url(),
|
||||
&post,
|
||||
maybe_in_reply_to.as_ref(),
|
||||
);
|
||||
|
@ -79,7 +93,7 @@ async fn create_status(
|
|||
}
|
||||
}
|
||||
deliver_activity(&config, ¤t_user, activity, recipients);
|
||||
let status = Status::from_post(post, &config.instance_url());
|
||||
let status = Status::from_post(post, &instance.url());
|
||||
Ok(HttpResponse::Created().json(status))
|
||||
}
|
||||
|
||||
|
|
129
src/models/posts/mentions.rs
Normal file
129
src/models/posts/mentions.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use regex::{Captures, Regex};
|
||||
use tokio_postgres::GenericClient;
|
||||
|
||||
use crate::errors::DatabaseError;
|
||||
use crate::models::profiles::queries::get_profiles_by_accts;
|
||||
use crate::models::profiles::types::DbActorProfile;
|
||||
|
||||
const MENTION_RE: &str = r"(?m)(?P<space>^|\s)@(?P<user>\w+)@(?P<instance>\S+)";
|
||||
|
||||
fn pattern_to_acct(caps: &Captures, instance_host: &str) -> String {
|
||||
if &caps["instance"] == instance_host {
|
||||
caps["user"].to_string()
|
||||
} else {
|
||||
format!("{}@{}", &caps["user"], &caps["instance"])
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds everything that looks like a mention
|
||||
fn find_mentions(
|
||||
instance_host: &str,
|
||||
text: &str,
|
||||
) -> Vec<String> {
|
||||
let mention_re = Regex::new(MENTION_RE).unwrap();
|
||||
let mut mentions = vec![];
|
||||
for caps in mention_re.captures_iter(text) {
|
||||
let acct = pattern_to_acct(&caps, instance_host);
|
||||
mentions.push(acct);
|
||||
};
|
||||
mentions
|
||||
}
|
||||
|
||||
pub async fn find_mentioned_profiles(
|
||||
db_client: &impl GenericClient,
|
||||
instance_host: &str,
|
||||
text: &str,
|
||||
) -> Result<HashMap<String, DbActorProfile>, DatabaseError> {
|
||||
let mentions = find_mentions(instance_host, text);
|
||||
let profiles = get_profiles_by_accts(db_client, mentions).await?;
|
||||
let mut mention_map: HashMap<String, DbActorProfile> = HashMap::new();
|
||||
for profile in profiles {
|
||||
mention_map.insert(profile.acct.clone(), profile);
|
||||
};
|
||||
Ok(mention_map)
|
||||
}
|
||||
|
||||
pub fn replace_mentions(
|
||||
mention_map: &HashMap<String, DbActorProfile>,
|
||||
instance_host: &str,
|
||||
instance_url: &str,
|
||||
text: &str,
|
||||
) -> String {
|
||||
let mention_re = Regex::new(MENTION_RE).unwrap();
|
||||
let result = mention_re.replace_all(text, |caps: &Captures| {
|
||||
let acct = pattern_to_acct(&caps, instance_host);
|
||||
match mention_map.get(&acct) {
|
||||
Some(profile) => {
|
||||
// Replace with a link
|
||||
let url = profile.actor_id(instance_url).unwrap();
|
||||
format!(
|
||||
r#"{}<a href="{}" target="_blank" rel="noreferrer">@{}</a>"#,
|
||||
caps["space"].to_string(),
|
||||
url,
|
||||
profile.username,
|
||||
)
|
||||
},
|
||||
None => caps[0].to_string(), // leave unchanged if actor is not known
|
||||
}
|
||||
});
|
||||
result.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
use super::*;
|
||||
|
||||
const INSTANCE_HOST: &str = "server1.com";
|
||||
const INSTANCE_URL: &str = "https://server1.com";
|
||||
|
||||
#[test]
|
||||
fn test_find_mentions() {
|
||||
let text = concat!(
|
||||
"@user1@server1.com ",
|
||||
"@user2@server2.com ",
|
||||
"@@invalid@server2.com ",
|
||||
"@test@server3.com@nospace@server4.com ",
|
||||
"@notmention ",
|
||||
"some text",
|
||||
);
|
||||
let results = find_mentions(INSTANCE_HOST, text);
|
||||
assert_eq!(results, vec![
|
||||
"user1",
|
||||
"user2@server2.com",
|
||||
"test@server3.com@nospace@server4.com",
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replace_mentions() {
|
||||
// Local actor
|
||||
let profile_1 = DbActorProfile {
|
||||
username: "user1".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
// Remote actor
|
||||
let profile_2 = DbActorProfile {
|
||||
username: "user2".to_string(),
|
||||
actor_json: Some(json!({
|
||||
"id": "https://server2.com/actors/user2",
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
let text = "@user1@server1.com @user2@server2.com sometext @notmention @test@unknown.org";
|
||||
let mention_map = HashMap::from([
|
||||
("user1".to_string(), profile_1),
|
||||
("user2@server2.com".to_string(), profile_2),
|
||||
]);
|
||||
let result = replace_mentions(&mention_map, INSTANCE_HOST, INSTANCE_URL, text);
|
||||
|
||||
let expected_result = concat!(
|
||||
r#"<a href="https://server1.com/users/user1" target="_blank" rel="noreferrer">@user1</a> "#,
|
||||
r#"<a href="https://server2.com/actors/user2" target="_blank" rel="noreferrer">@user2</a> "#,
|
||||
r#"sometext @notmention @test@unknown.org"#,
|
||||
);
|
||||
assert_eq!(result, expected_result);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod helpers;
|
||||
pub mod mentions;
|
||||
pub mod queries;
|
||||
pub mod types;
|
||||
|
|
|
@ -164,6 +164,24 @@ pub async fn get_profiles(
|
|||
Ok(profiles)
|
||||
}
|
||||
|
||||
pub async fn get_profiles_by_accts(
|
||||
db_client: &impl GenericClient,
|
||||
accts: Vec<String>,
|
||||
) -> Result<Vec<DbActorProfile>, DatabaseError> {
|
||||
let rows = db_client.query(
|
||||
"
|
||||
SELECT actor_profile
|
||||
FROM actor_profile
|
||||
WHERE acct = ANY($1)
|
||||
",
|
||||
&[&accts],
|
||||
).await?;
|
||||
let profiles = rows.iter()
|
||||
.map(|row| row.try_get("actor_profile"))
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(profiles)
|
||||
}
|
||||
|
||||
pub async fn get_followers(
|
||||
db_client: &impl GenericClient,
|
||||
profile_id: &Uuid,
|
||||
|
|
Loading…
Reference in a new issue