Federate mentions

This commit is contained in:
silverpill 2021-11-11 21:51:47 +00:00
parent 4da44159ed
commit 2bfb6253f8
6 changed files with 115 additions and 8 deletions

View file

@ -22,6 +22,17 @@ pub struct Attachment {
pub url: String, pub url: String,
} }
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Tag {
pub name: String,
#[serde(rename = "type")]
pub tag_type: String,
pub href: String,
}
#[derive(Default, Deserialize, Serialize)] #[derive(Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Object { pub struct Object {
@ -54,6 +65,9 @@ pub struct Object {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>, pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<Vec<Tag>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<Value>, pub to: Option<Value>,
} }
@ -99,6 +113,7 @@ fn create_activity(
} }
pub fn create_note( pub fn create_note(
instance_host: &str,
instance_url: &str, instance_url: &str,
post: &Post, post: &Post,
in_reply_to: Option<&Post>, in_reply_to: Option<&Post>,
@ -122,6 +137,17 @@ pub fn create_note(
} }
}).collect(); }).collect();
let mut recipients = vec![AP_PUBLIC.to_string()]; let mut recipients = vec![AP_PUBLIC.to_string()];
let mentions: Vec<Tag> = post.mentions.iter().map(|profile| {
let actor_id = profile.actor_id(instance_url).unwrap();
if !profile.is_local() {
recipients.push(actor_id.clone());
};
Tag {
name: profile.actor_address(instance_host),
tag_type: MENTION.to_string(),
href: actor_id,
}
}).collect();
let in_reply_to_object_id = match post.in_reply_to_id { let in_reply_to_object_id = match post.in_reply_to_id {
Some(in_reply_to_id) => { Some(in_reply_to_id) => {
let post = in_reply_to.unwrap(); let post = in_reply_to.unwrap();
@ -131,7 +157,9 @@ pub fn create_note(
} else { } else {
// Replying to remote post // Replying to remote post
let remote_actor_id = post.author.actor_id(instance_url).unwrap(); let remote_actor_id = post.author.actor_id(instance_url).unwrap();
if !recipients.contains(&remote_actor_id) {
recipients.push(remote_actor_id); recipients.push(remote_actor_id);
};
post.object_id.clone() post.object_id.clone()
} }
}, },
@ -148,16 +176,18 @@ pub fn create_note(
attributed_to: Some(actor_id), attributed_to: Some(actor_id),
in_reply_to: in_reply_to_object_id, in_reply_to: in_reply_to_object_id,
content: Some(post.content.clone()), content: Some(post.content.clone()),
tag: Some(mentions),
to: Some(json!(recipients)), to: Some(json!(recipients)),
} }
} }
pub fn create_activity_note( pub fn create_activity_note(
instance_host: &str,
instance_url: &str, instance_url: &str,
post: &Post, post: &Post,
in_reply_to: Option<&Post>, in_reply_to: Option<&Post>,
) -> Activity { ) -> Activity {
let object = create_note(instance_url, post, in_reply_to); let object = create_note(instance_host, instance_url, post, in_reply_to);
let activity = create_activity( let activity = create_activity(
instance_url, instance_url,
&post.author.username, &post.author.username,
@ -292,6 +322,7 @@ impl OrderedCollection {
mod tests { mod tests {
use super::*; use super::*;
const INSTANCE_HOST: &str = "example.com";
const INSTANCE_URL: &str = "https://example.com"; const INSTANCE_URL: &str = "https://example.com";
#[test] #[test]
@ -301,7 +332,7 @@ mod tests {
..Default::default() ..Default::default()
}; };
let post = Post { author, ..Default::default() }; let post = Post { author, ..Default::default() };
let note = create_note(INSTANCE_URL, &post, None); let note = create_note(INSTANCE_HOST, INSTANCE_URL, &post, None);
assert_eq!( assert_eq!(
note.id, note.id,
@ -323,7 +354,7 @@ mod tests {
in_reply_to_id: Some(parent.id), in_reply_to_id: Some(parent.id),
..Default::default() ..Default::default()
}; };
let note = create_note(INSTANCE_URL, &post, Some(&parent)); let note = create_note(INSTANCE_HOST, INSTANCE_URL, &post, Some(&parent));
assert_eq!( assert_eq!(
note.in_reply_to.unwrap(), note.in_reply_to.unwrap(),
@ -334,28 +365,35 @@ mod tests {
#[test] #[test]
fn test_create_note_with_remote_parent() { fn test_create_note_with_remote_parent() {
let parent_author_acct = "test@test.net";
let parent_author_actor_id = "https://test.net/user/test"; let parent_author_actor_id = "https://test.net/user/test";
let parent_author = DbActorProfile { let parent_author = DbActorProfile {
acct: parent_author_acct.to_string(),
actor_json: Some(json!({ actor_json: Some(json!({
"id": parent_author_actor_id, "id": parent_author_actor_id,
})), })),
..Default::default() ..Default::default()
}; };
let parent = Post { let parent = Post {
author: parent_author, author: parent_author.clone(),
object_id: Some("https://test.net/obj/123".to_string()), object_id: Some("https://test.net/obj/123".to_string()),
..Default::default() ..Default::default()
}; };
let post = Post { let post = Post {
in_reply_to_id: Some(parent.id), in_reply_to_id: Some(parent.id),
mentions: vec![parent_author],
..Default::default() ..Default::default()
}; };
let note = create_note(INSTANCE_URL, &post, Some(&parent)); let note = create_note(INSTANCE_HOST, INSTANCE_URL, &post, Some(&parent));
assert_eq!( assert_eq!(
note.in_reply_to.unwrap(), note.in_reply_to.unwrap(),
parent.object_id.unwrap(), parent.object_id.unwrap(),
); );
let tags = note.tag.unwrap();
assert_eq!(tags.len(), 1);
assert_eq!(tags[0].name, parent_author_acct);
assert_eq!(tags[0].href, parent_author_actor_id);
assert_eq!( assert_eq!(
note.to.unwrap(), note.to.unwrap(),
json!([AP_PUBLIC, parent_author_actor_id]), json!([AP_PUBLIC, parent_author_actor_id]),

View file

@ -176,6 +176,19 @@ pub async fn process_note(
attachments.push(db_attachment.id); attachments.push(db_attachment.id);
} }
} }
let mut mentions: Vec<Uuid> = Vec::new();
if let Some(list) = object.tag {
for tag in list {
if tag.tag_type == MENTION {
let profile = get_or_fetch_profile_by_actor_id(
db_client,
&tag.href,
&config.media_dir(),
).await?;
mentions.push(profile.id);
};
};
};
let in_reply_to_id = match object.in_reply_to { let in_reply_to_id = match object.in_reply_to {
Some(object_id) => { Some(object_id) => {
match parse_object_id(&config.instance_url(), &object_id) { match parse_object_id(&config.instance_url(), &object_id) {
@ -196,7 +209,7 @@ pub async fn process_note(
content, content,
in_reply_to_id, in_reply_to_id,
attachments: attachments, attachments: attachments,
mentions: vec![], mentions: mentions,
object_id: Some(object.id), object_id: Some(object.id),
created_at: object.published, created_at: object.published,
}; };

View file

@ -139,7 +139,12 @@ pub async fn get_object(
}, },
None => None, None => None,
}; };
let object = create_note(&config.instance_url(), post, in_reply_to); let object = create_note(
&config.instance().host(),
&config.instance().url(),
post,
in_reply_to,
);
let response = HttpResponse::Ok() let response = HttpResponse::Ok()
.content_type(ACTIVITY_CONTENT_TYPE) .content_type(ACTIVITY_CONTENT_TYPE)
.json(object); .json(object);

View file

@ -13,6 +13,7 @@ pub const PERSON: &str = "Person";
// Object types // Object types
pub const DOCUMENT: &str = "Document"; pub const DOCUMENT: &str = "Document";
pub const IMAGE: &str = "Image"; pub const IMAGE: &str = "Image";
pub const MENTION: &str = "Mention";
pub const NOTE: &str = "Note"; pub const NOTE: &str = "Note";
// Misc // Misc

View file

@ -74,6 +74,7 @@ async fn create_status(
None => None, None => None,
}; };
let activity = create_activity_note( let activity = create_activity_note(
&instance.host(),
&instance.url(), &instance.url(),
&post, &post,
maybe_in_reply_to.as_ref(), maybe_in_reply_to.as_ref(),
@ -94,6 +95,13 @@ async fn create_status(
recipients.push(remote_actor); recipients.push(remote_actor);
} }
} }
for profile in post.mentions.iter() {
let maybe_remote_actor = profile.actor()
.map_err(|_| HttpError::InternalError)?;
if let Some(remote_actor) = maybe_remote_actor {
recipients.push(remote_actor);
};
};
deliver_activity(&config, &current_user, activity, recipients); deliver_activity(&config, &current_user, activity, recipients);
let status = Status::from_post(post, &instance.url()); let status = Status::from_post(post, &instance.url());
Ok(HttpResponse::Created().json(status)) Ok(HttpResponse::Created().json(status))

View file

@ -85,6 +85,14 @@ impl DbActorProfile {
}; };
Ok(actor_id) Ok(actor_id)
} }
pub fn actor_address(&self, instance_host: &str) -> String {
if self.is_local() {
format!("{}@{}", self.acct, instance_host)
} else {
self.acct.clone()
}
}
} }
#[cfg(test)] #[cfg(test)]
@ -156,3 +164,37 @@ impl ProfileUpdateData {
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
const INSTANCE_HOST: &str = "example.com";
#[test]
fn test_local_actor_address() {
let local_profile = DbActorProfile {
acct: "user".to_string(),
actor_json: None,
..Default::default()
};
assert_eq!(
local_profile.actor_address(INSTANCE_HOST),
"user@example.com",
);
}
#[test]
fn test_remote_actor_address() {
let remote_profile = DbActorProfile {
acct: "test@remote.com".to_string(),
actor_json: Some(json!({"id": "https://test"})),
..Default::default()
};
assert_eq!(
remote_profile.actor_address(INSTANCE_HOST),
remote_profile.acct,
);
}
}