From dc34c980f689241d04a705d9880024521de668a5 Mon Sep 17 00:00:00 2001 From: silverpill Date: Wed, 11 May 2022 12:50:36 +0000 Subject: [PATCH] Handle Update(Note) activities --- docs/openapi.yaml | 9 ++++ migrations/V0024__post__add_updated_at.sql | 1 + migrations/schema.sql | 1 + src/activitypub/activity.rs | 3 ++ src/activitypub/inbox/create_note.rs | 2 +- src/activitypub/inbox/mod.rs | 1 + src/activitypub/inbox/update_note.rs | 37 +++++++++++++++ src/activitypub/receiver.rs | 8 ++++ src/mastodon_api/statuses/types.rs | 3 ++ src/models/posts/queries.rs | 54 +++++++++++++++++----- src/models/posts/types.rs | 9 ++++ 11 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 migrations/V0024__post__add_updated_at.sql create mode 100644 src/activitypub/inbox/update_note.rs diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 3a77c2d..5b2412f 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -864,6 +864,15 @@ components: id: type: string format: uuid + created_at: + description: The date when this post was created. + type: string + format: dateTime + edited_at: + description: The date when this post was edited. + type: string + format: dateTime + nullable: true content: description: HTML-encoded post content. type: string diff --git a/migrations/V0024__post__add_updated_at.sql b/migrations/V0024__post__add_updated_at.sql new file mode 100644 index 0000000..b4bd6ec --- /dev/null +++ b/migrations/V0024__post__add_updated_at.sql @@ -0,0 +1 @@ +ALTER TABLE post ADD COLUMN updated_at TIMESTAMP WITH TIME ZONE; diff --git a/migrations/schema.sql b/migrations/schema.sql index 0f2ec4c..c4cfb58 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -46,6 +46,7 @@ CREATE TABLE post ( token_id INTEGER, token_tx_id VARCHAR(200), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE, UNIQUE (author_id, repost_of_id) ); diff --git a/src/activitypub/activity.rs b/src/activitypub/activity.rs index bd15b71..d8be331 100644 --- a/src/activitypub/activity.rs +++ b/src/activitypub/activity.rs @@ -90,6 +90,9 @@ pub struct Object { #[serde(skip_serializing_if = "Option::is_none")] pub to: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub updated: Option>, } #[derive(Serialize)] diff --git a/src/activitypub/inbox/create_note.rs b/src/activitypub/inbox/create_note.rs index 7f8ddeb..4a8eea6 100644 --- a/src/activitypub/inbox/create_note.rs +++ b/src/activitypub/inbox/create_note.rs @@ -48,7 +48,7 @@ fn get_note_author_id(object: &Object) -> Result { const CONTENT_MAX_SIZE: usize = 100000; -fn get_note_content(object: &Object) -> Result { +pub fn get_note_content(object: &Object) -> Result { let content = if object.object_type == PAGE { // Lemmy Page object.name.as_ref().ok_or(ValidationError("no content"))? diff --git a/src/activitypub/inbox/mod.rs b/src/activitypub/inbox/mod.rs index d914c40..10a3f06 100644 --- a/src/activitypub/inbox/mod.rs +++ b/src/activitypub/inbox/mod.rs @@ -1,2 +1,3 @@ pub mod create_note; +pub mod update_note; pub mod update_person; diff --git a/src/activitypub/inbox/update_note.rs b/src/activitypub/inbox/update_note.rs new file mode 100644 index 0000000..63d4748 --- /dev/null +++ b/src/activitypub/inbox/update_note.rs @@ -0,0 +1,37 @@ +use chrono::Utc; +use tokio_postgres::GenericClient; + +use crate::activitypub::activity::Object; +use crate::activitypub::fetcher::helpers::ImportError; +use crate::activitypub::receiver::parse_object_id; +use crate::errors::DatabaseError; +use crate::models::posts::queries::{ + get_post_by_object_id, + update_post, +}; +use crate::models::posts::types::PostUpdateData; +use super::create_note::get_note_content; + +pub async fn handle_update_note( + db_client: &mut impl GenericClient, + instance_url: &str, + object: Object, +) -> Result<(), ImportError> { + let post_id = match parse_object_id(instance_url, &object.id) { + Ok(post_id) => post_id, + Err(_) => { + let post = match get_post_by_object_id(db_client, &object.id).await { + Ok(post) => post, + // Ignore Update if post is not found locally + Err(DatabaseError::NotFound(_)) => return Ok(()), + Err(other_error) => return Err(other_error.into()), + }; + post.id + }, + }; + let content = get_note_content(&object)?; + let updated_at = object.updated.unwrap_or(Utc::now()); + let post_data = PostUpdateData { content, updated_at }; + update_post(db_client, &post_id, post_data).await?; + Ok(()) +} diff --git a/src/activitypub/receiver.rs b/src/activitypub/receiver.rs index dfa056e..d429b1e 100644 --- a/src/activitypub/receiver.rs +++ b/src/activitypub/receiver.rs @@ -41,6 +41,7 @@ use super::fetcher::helpers::{ get_or_import_profile_by_actor_id, import_post, }; +use super::inbox::update_note::handle_update_note; use super::inbox::update_person::handle_update_person; use super::vocabulary::*; @@ -392,6 +393,13 @@ pub async fn receive_activity( Err(other_error) => return Err(other_error.into()), } }, + (UPDATE, NOTE) => { + require_actor_signature(&activity.actor, &signer_id)?; + let object: Object = serde_json::from_value(activity.object) + .map_err(|_| ValidationError("invalid object"))?; + handle_update_note(db_client, &config.instance_url(), object).await?; + NOTE + }, (UPDATE, PERSON) => { require_actor_signature(&activity.actor, &signer_id)?; handle_update_person( diff --git a/src/mastodon_api/statuses/types.rs b/src/mastodon_api/statuses/types.rs index 0cf950c..68834dd 100644 --- a/src/mastodon_api/statuses/types.rs +++ b/src/mastodon_api/statuses/types.rs @@ -53,6 +53,8 @@ pub struct Status { pub id: Uuid, pub uri: String, pub created_at: DateTime, + // Undocumented https://github.com/mastodon/mastodon/blob/v3.5.2/app/serializers/rest/status_serializer.rb + edited_at: Option>, pub account: Account, pub content: String, pub in_reply_to_id: Option, @@ -104,6 +106,7 @@ impl Status { id: post.id, uri: object_id, created_at: post.created_at, + edited_at: post.updated_at, account: account, content: post.content, in_reply_to_id: post.in_reply_to_id, diff --git a/src/models/posts/queries.rs b/src/models/posts/queries.rs index 6d952aa..17692d3 100644 --- a/src/models/posts/queries.rs +++ b/src/models/posts/queries.rs @@ -22,7 +22,13 @@ use crate::models::profiles::queries::update_post_count; use crate::models::profiles::types::DbActorProfile; use crate::models::relationships::types::RelationshipType; use crate::utils::id::new_uuid; -use super::types::{DbPost, Post, PostCreateData, Visibility}; +use super::types::{ + DbPost, + Post, + PostCreateData, + PostUpdateData, + Visibility, +}; pub async fn create_post( db_client: &mut impl GenericClient, @@ -660,25 +666,24 @@ pub async fn get_post_by_ipfs_cid( pub async fn update_post( db_client: &impl GenericClient, - post: &Post, + post_id: &Uuid, + post_data: PostUpdateData, ) -> Result<(), DatabaseError> { - // Reposts can't be updated + // Reposts and immutable posts can't be updated let updated_count = db_client.execute( " UPDATE post SET content = $1, - ipfs_cid = $2, - token_id = $3, - token_tx_id = $4 - WHERE id = $5 AND repost_of_id IS NULL + updated_at = $2 + WHERE id = $3 + AND repost_of_id IS NULL + AND ipfs_cid IS NULL ", &[ - &post.content, - &post.ipfs_cid, - &post.token_id, - &post.token_tx_id, - &post.id, + &post_data.content, + &post_data.updated_at, + &post_id, ], ).await?; if updated_count == 0 { @@ -1072,6 +1077,31 @@ mod tests { let post = create_post(db_client, &profile.id, post_data).await.unwrap(); assert_eq!(post.content, "test post"); assert_eq!(post.author.id, profile.id); + assert_eq!(post.updated_at, None); + } + + #[tokio::test] + #[serial] + async fn test_update_post() { + let db_client = &mut create_test_database().await; + let user_data = UserCreateData { + username: "test".to_string(), + ..Default::default() + }; + let user = create_user(db_client, user_data).await.unwrap(); + let post_data = PostCreateData { + content: "test post".to_string(), + ..Default::default() + }; + let post = create_post(db_client, &user.id, post_data).await.unwrap(); + let post_data = PostUpdateData { + content: "test update".to_string(), + updated_at: Utc::now(), + }; + update_post(db_client, &post.id, post_data).await.unwrap(); + let post = get_post_by_id(db_client, &post.id).await.unwrap(); + assert_eq!(post.content, "test update"); + assert_eq!(post.updated_at.is_some(), true); } #[tokio::test] diff --git a/src/models/posts/types.rs b/src/models/posts/types.rs index 97fab8a..db52bc3 100644 --- a/src/models/posts/types.rs +++ b/src/models/posts/types.rs @@ -70,6 +70,7 @@ pub struct DbPost { pub token_id: Option, pub token_tx_id: Option, pub created_at: DateTime, + pub updated_at: Option>, } // List of user's actions @@ -98,6 +99,7 @@ pub struct Post { pub token_id: Option, pub token_tx_id: Option, pub created_at: DateTime, + pub updated_at: Option>, // These fields are not populated automatically // by functions in posts::queries module @@ -139,6 +141,7 @@ impl Post { token_id: db_post.token_id, token_tx_id: db_post.token_tx_id, created_at: db_post.created_at, + updated_at: db_post.updated_at, actions: None, in_reply_to: None, repost_of: None, @@ -179,6 +182,7 @@ impl Default for Post { token_id: None, token_tx_id: None, created_at: Utc::now(), + updated_at: None, actions: None, in_reply_to: None, repost_of: None, @@ -230,6 +234,11 @@ impl PostCreateData { } } +pub struct PostUpdateData { + pub content: String, + pub updated_at: DateTime, +} + #[cfg(test)] mod tests { use super::*;