Handle Update(Note) activities
This commit is contained in:
parent
50699b5ab5
commit
dc34c980f6
11 changed files with 115 additions and 13 deletions
|
@ -864,6 +864,15 @@ components:
|
||||||
id:
|
id:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
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:
|
content:
|
||||||
description: HTML-encoded post content.
|
description: HTML-encoded post content.
|
||||||
type: string
|
type: string
|
||||||
|
|
1
migrations/V0024__post__add_updated_at.sql
Normal file
1
migrations/V0024__post__add_updated_at.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE post ADD COLUMN updated_at TIMESTAMP WITH TIME ZONE;
|
|
@ -46,6 +46,7 @@ CREATE TABLE post (
|
||||||
token_id INTEGER,
|
token_id INTEGER,
|
||||||
token_tx_id VARCHAR(200),
|
token_tx_id VARCHAR(200),
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE,
|
||||||
UNIQUE (author_id, repost_of_id)
|
UNIQUE (author_id, repost_of_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -90,6 +90,9 @@ pub struct Object {
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub to: Option<Value>,
|
pub to: Option<Value>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub updated: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
|
@ -48,7 +48,7 @@ fn get_note_author_id(object: &Object) -> Result<String, ValidationError> {
|
||||||
|
|
||||||
const CONTENT_MAX_SIZE: usize = 100000;
|
const CONTENT_MAX_SIZE: usize = 100000;
|
||||||
|
|
||||||
fn get_note_content(object: &Object) -> Result<String, ValidationError> {
|
pub fn get_note_content(object: &Object) -> Result<String, ValidationError> {
|
||||||
let content = if object.object_type == PAGE {
|
let content = if object.object_type == PAGE {
|
||||||
// Lemmy Page
|
// Lemmy Page
|
||||||
object.name.as_ref().ok_or(ValidationError("no content"))?
|
object.name.as_ref().ok_or(ValidationError("no content"))?
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
pub mod create_note;
|
pub mod create_note;
|
||||||
|
pub mod update_note;
|
||||||
pub mod update_person;
|
pub mod update_person;
|
||||||
|
|
37
src/activitypub/inbox/update_note.rs
Normal file
37
src/activitypub/inbox/update_note.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
|
@ -41,6 +41,7 @@ use super::fetcher::helpers::{
|
||||||
get_or_import_profile_by_actor_id,
|
get_or_import_profile_by_actor_id,
|
||||||
import_post,
|
import_post,
|
||||||
};
|
};
|
||||||
|
use super::inbox::update_note::handle_update_note;
|
||||||
use super::inbox::update_person::handle_update_person;
|
use super::inbox::update_person::handle_update_person;
|
||||||
use super::vocabulary::*;
|
use super::vocabulary::*;
|
||||||
|
|
||||||
|
@ -392,6 +393,13 @@ pub async fn receive_activity(
|
||||||
Err(other_error) => return Err(other_error.into()),
|
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) => {
|
(UPDATE, PERSON) => {
|
||||||
require_actor_signature(&activity.actor, &signer_id)?;
|
require_actor_signature(&activity.actor, &signer_id)?;
|
||||||
handle_update_person(
|
handle_update_person(
|
||||||
|
|
|
@ -53,6 +53,8 @@ pub struct Status {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub uri: String,
|
pub uri: String,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
|
// Undocumented https://github.com/mastodon/mastodon/blob/v3.5.2/app/serializers/rest/status_serializer.rb
|
||||||
|
edited_at: Option<DateTime<Utc>>,
|
||||||
pub account: Account,
|
pub account: Account,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub in_reply_to_id: Option<Uuid>,
|
pub in_reply_to_id: Option<Uuid>,
|
||||||
|
@ -104,6 +106,7 @@ impl Status {
|
||||||
id: post.id,
|
id: post.id,
|
||||||
uri: object_id,
|
uri: object_id,
|
||||||
created_at: post.created_at,
|
created_at: post.created_at,
|
||||||
|
edited_at: post.updated_at,
|
||||||
account: account,
|
account: account,
|
||||||
content: post.content,
|
content: post.content,
|
||||||
in_reply_to_id: post.in_reply_to_id,
|
in_reply_to_id: post.in_reply_to_id,
|
||||||
|
|
|
@ -22,7 +22,13 @@ use crate::models::profiles::queries::update_post_count;
|
||||||
use crate::models::profiles::types::DbActorProfile;
|
use crate::models::profiles::types::DbActorProfile;
|
||||||
use crate::models::relationships::types::RelationshipType;
|
use crate::models::relationships::types::RelationshipType;
|
||||||
use crate::utils::id::new_uuid;
|
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(
|
pub async fn create_post(
|
||||||
db_client: &mut impl GenericClient,
|
db_client: &mut impl GenericClient,
|
||||||
|
@ -660,25 +666,24 @@ pub async fn get_post_by_ipfs_cid(
|
||||||
|
|
||||||
pub async fn update_post(
|
pub async fn update_post(
|
||||||
db_client: &impl GenericClient,
|
db_client: &impl GenericClient,
|
||||||
post: &Post,
|
post_id: &Uuid,
|
||||||
|
post_data: PostUpdateData,
|
||||||
) -> Result<(), DatabaseError> {
|
) -> Result<(), DatabaseError> {
|
||||||
// Reposts can't be updated
|
// Reposts and immutable posts can't be updated
|
||||||
let updated_count = db_client.execute(
|
let updated_count = db_client.execute(
|
||||||
"
|
"
|
||||||
UPDATE post
|
UPDATE post
|
||||||
SET
|
SET
|
||||||
content = $1,
|
content = $1,
|
||||||
ipfs_cid = $2,
|
updated_at = $2
|
||||||
token_id = $3,
|
WHERE id = $3
|
||||||
token_tx_id = $4
|
AND repost_of_id IS NULL
|
||||||
WHERE id = $5 AND repost_of_id IS NULL
|
AND ipfs_cid IS NULL
|
||||||
",
|
",
|
||||||
&[
|
&[
|
||||||
&post.content,
|
&post_data.content,
|
||||||
&post.ipfs_cid,
|
&post_data.updated_at,
|
||||||
&post.token_id,
|
&post_id,
|
||||||
&post.token_tx_id,
|
|
||||||
&post.id,
|
|
||||||
],
|
],
|
||||||
).await?;
|
).await?;
|
||||||
if updated_count == 0 {
|
if updated_count == 0 {
|
||||||
|
@ -1072,6 +1077,31 @@ mod tests {
|
||||||
let post = create_post(db_client, &profile.id, post_data).await.unwrap();
|
let post = create_post(db_client, &profile.id, post_data).await.unwrap();
|
||||||
assert_eq!(post.content, "test post");
|
assert_eq!(post.content, "test post");
|
||||||
assert_eq!(post.author.id, profile.id);
|
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]
|
#[tokio::test]
|
||||||
|
|
|
@ -70,6 +70,7 @@ pub struct DbPost {
|
||||||
pub token_id: Option<i32>,
|
pub token_id: Option<i32>,
|
||||||
pub token_tx_id: Option<String>,
|
pub token_tx_id: Option<String>,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// List of user's actions
|
// List of user's actions
|
||||||
|
@ -98,6 +99,7 @@ pub struct Post {
|
||||||
pub token_id: Option<i32>,
|
pub token_id: Option<i32>,
|
||||||
pub token_tx_id: Option<String>,
|
pub token_tx_id: Option<String>,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: Option<DateTime<Utc>>,
|
||||||
|
|
||||||
// These fields are not populated automatically
|
// These fields are not populated automatically
|
||||||
// by functions in posts::queries module
|
// by functions in posts::queries module
|
||||||
|
@ -139,6 +141,7 @@ impl Post {
|
||||||
token_id: db_post.token_id,
|
token_id: db_post.token_id,
|
||||||
token_tx_id: db_post.token_tx_id,
|
token_tx_id: db_post.token_tx_id,
|
||||||
created_at: db_post.created_at,
|
created_at: db_post.created_at,
|
||||||
|
updated_at: db_post.updated_at,
|
||||||
actions: None,
|
actions: None,
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
repost_of: None,
|
repost_of: None,
|
||||||
|
@ -179,6 +182,7 @@ impl Default for Post {
|
||||||
token_id: None,
|
token_id: None,
|
||||||
token_tx_id: None,
|
token_tx_id: None,
|
||||||
created_at: Utc::now(),
|
created_at: Utc::now(),
|
||||||
|
updated_at: None,
|
||||||
actions: None,
|
actions: None,
|
||||||
in_reply_to: None,
|
in_reply_to: None,
|
||||||
repost_of: None,
|
repost_of: None,
|
||||||
|
@ -230,6 +234,11 @@ impl PostCreateData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct PostUpdateData {
|
||||||
|
pub content: String,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
Loading…
Reference in a new issue