From 4da44159eda9371deac1d24f1bf7d20af3b65956 Mon Sep 17 00:00:00 2001 From: silverpill Date: Thu, 11 Nov 2021 19:10:28 +0000 Subject: [PATCH] Store mentions in database --- migrations/V0011__mention.sql | 5 +++ migrations/schema.sql | 6 ++++ src/activitypub/receiver.rs | 1 + src/mastodon_api/statuses/types.rs | 27 +++++++++++++++ src/mastodon_api/statuses/views.rs | 2 ++ src/models/notifications/queries.rs | 6 ++-- src/models/notifications/types.rs | 3 +- src/models/posts/queries.rs | 54 +++++++++++++++++++++++++---- src/models/posts/types.rs | 10 +++++- 9 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 migrations/V0011__mention.sql diff --git a/migrations/V0011__mention.sql b/migrations/V0011__mention.sql new file mode 100644 index 0000000..c6088ee --- /dev/null +++ b/migrations/V0011__mention.sql @@ -0,0 +1,5 @@ +CREATE TABLE mention ( + post_id UUID NOT NULL REFERENCES post (id) ON DELETE CASCADE, + profile_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE, + PRIMARY KEY (post_id, profile_id) +); diff --git a/migrations/schema.sql b/migrations/schema.sql index 7d45361..99d4353 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -75,6 +75,12 @@ CREATE TABLE media_attachment ( created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() ); +CREATE TABLE mention ( + post_id UUID NOT NULL REFERENCES post (id) ON DELETE CASCADE, + profile_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE, + PRIMARY KEY (post_id, profile_id) +); + CREATE TABLE oauth_token ( id SERIAL PRIMARY KEY, owner_id UUID NOT NULL REFERENCES user_account (id) ON DELETE CASCADE, diff --git a/src/activitypub/receiver.rs b/src/activitypub/receiver.rs index ae3c8b7..0c94921 100644 --- a/src/activitypub/receiver.rs +++ b/src/activitypub/receiver.rs @@ -196,6 +196,7 @@ pub async fn process_note( content, in_reply_to_id, attachments: attachments, + mentions: vec![], object_id: Some(object.id), created_at: object.published, }; diff --git a/src/mastodon_api/statuses/types.rs b/src/mastodon_api/statuses/types.rs index a2a25ff..cc8a133 100644 --- a/src/mastodon_api/statuses/types.rs +++ b/src/mastodon_api/statuses/types.rs @@ -5,6 +5,27 @@ use uuid::Uuid; use crate::mastodon_api::accounts::types::Account; use crate::mastodon_api::media::types::Attachment; use crate::models::posts::types::{Post, PostCreateData}; +use crate::models::profiles::types::DbActorProfile; + +/// https://docs.joinmastodon.org/entities/mention/ +#[derive(Serialize)] +pub struct Mention { + id: String, + username: String, + acct: String, + url: String, +} + +impl Mention { + fn from_profile(profile: DbActorProfile, instance_url: &str) -> Self { + Mention { + id: profile.id.to_string(), + username: profile.username.clone(), + acct: profile.acct.clone(), + url: profile.actor_id(instance_url).unwrap(), + } + } +} /// https://docs.joinmastodon.org/entities/status/ #[derive(Serialize)] @@ -17,6 +38,7 @@ pub struct Status { pub replies_count: i32, pub favourites_count: i32, pub media_attachments: Vec, + mentions: Vec, // Authorized user attributes pub favourited: bool, @@ -32,6 +54,9 @@ impl Status { let attachments: Vec = post.attachments.into_iter() .map(|item| Attachment::from_db(item, instance_url)) .collect(); + let mentions: Vec = post.mentions.into_iter() + .map(|item| Mention::from_profile(item, instance_url)) + .collect(); let account = Account::from_profile(post.author, instance_url); Self { id: post.id, @@ -42,6 +67,7 @@ impl Status { replies_count: post.reply_count, favourites_count: post.reaction_count, media_attachments: attachments, + mentions: mentions, favourited: post.actions.map_or(false, |actions| actions.favourited), ipfs_cid: post.ipfs_cid, token_id: post.token_id, @@ -68,6 +94,7 @@ impl From for PostCreateData { content: value.status, in_reply_to_id: value.in_reply_to_id, attachments: value.media_ids.unwrap_or(vec![]), + mentions: vec![], object_id: None, created_at: None, } diff --git a/src/mastodon_api/statuses/views.rs b/src/mastodon_api/statuses/views.rs index 0036b84..6b9f2a3 100644 --- a/src/mastodon_api/statuses/views.rs +++ b/src/mastodon_api/statuses/views.rs @@ -62,6 +62,8 @@ async fn create_status( &instance.url(), &post_data.content, ); + post_data.mentions = mention_map.values() + .map(|profile| profile.id).collect(); let post = create_post(db_client, ¤t_user.id, post_data).await?; // Federate let maybe_in_reply_to = match post.in_reply_to_id { diff --git a/src/models/notifications/queries.rs b/src/models/notifications/queries.rs index bec2b13..0173e5f 100644 --- a/src/models/notifications/queries.rs +++ b/src/models/notifications/queries.rs @@ -5,7 +5,7 @@ use uuid::Uuid; use crate::errors::DatabaseError; use crate::models::posts::helpers::get_actions_for_posts; -use crate::models::posts::queries::RELATED_ATTACHMENTS; +use crate::models::posts::queries::{RELATED_ATTACHMENTS, RELATED_MENTIONS}; use super::types::{EventType, Notification}; async fn create_notification( @@ -73,7 +73,8 @@ pub async fn get_notifications( " SELECT notification, sender, post, post_author, - {related_attachments} + {related_attachments}, + {related_mentions} FROM notification JOIN actor_profile AS sender ON notification.sender_id = sender.id @@ -85,6 +86,7 @@ pub async fn get_notifications( ORDER BY notification.created_at DESC ", related_attachments=RELATED_ATTACHMENTS, + related_mentions=RELATED_MENTIONS, ); let rows = db_client.query( statement.as_str(), diff --git a/src/models/notifications/types.rs b/src/models/notifications/types.rs index 3d1b2d0..33d6e34 100644 --- a/src/models/notifications/types.rs +++ b/src/models/notifications/types.rs @@ -75,7 +75,8 @@ impl TryFrom<&Row> for Notification { Some(db_post) => { let db_post_author: DbActorProfile = row.try_get("post_author")?; let db_attachments: Vec = row.try_get("attachments")?; - Some(Post::new(db_post, db_post_author, db_attachments)) + let db_mentions: Vec = row.try_get("mentions")?; + Some(Post::new(db_post, db_post_author, db_attachments, db_mentions)) }, None => None, }; diff --git a/src/models/posts/queries.rs b/src/models/posts/queries.rs index c484f1c..b23021c 100644 --- a/src/models/posts/queries.rs +++ b/src/models/posts/queries.rs @@ -13,6 +13,7 @@ use crate::models::cleanup::{ }; use crate::models::notifications::queries::create_reply_notification; use crate::models::profiles::queries::update_post_count; +use crate::models::profiles::types::DbActorProfile; use super::types::{DbPost, Post, PostCreateData}; pub const RELATED_ATTACHMENTS: &str = @@ -21,6 +22,14 @@ pub const RELATED_ATTACHMENTS: &str = FROM media_attachment WHERE post_id = post.id ) AS attachments"; +pub const RELATED_MENTIONS: &str = + "ARRAY( + SELECT actor_profile + FROM mention + JOIN actor_profile ON mention.profile_id = actor_profile.id + WHERE post_id = post.id + ) AS mentions"; + pub async fn get_home_timeline( db_client: &impl GenericClient, current_user_id: &Uuid, @@ -31,7 +40,8 @@ pub async fn get_home_timeline( " SELECT post, actor_profile, - {related_attachments} + {related_attachments}, + {related_mentions} FROM post JOIN actor_profile ON post.author_id = actor_profile.id WHERE @@ -46,6 +56,7 @@ pub async fn get_home_timeline( ORDER BY post.created_at DESC ", related_attachments=RELATED_ATTACHMENTS, + related_mentions=RELATED_MENTIONS, ); let rows = db_client.query( statement.as_str(), @@ -71,13 +82,15 @@ pub async fn get_posts_by_author( " SELECT post, actor_profile, - {related_attachments} + {related_attachments}, + {related_mentions} FROM post JOIN actor_profile ON post.author_id = actor_profile.id WHERE {condition} ORDER BY post.created_at DESC ", related_attachments=RELATED_ATTACHMENTS, + related_mentions=RELATED_MENTIONS, condition=condition, ); let rows = db_client.query( @@ -136,6 +149,25 @@ pub async fn create_post( let db_attachments: Vec = attachments_rows.iter() .map(|row| row.try_get("media_attachment")) .collect::>()?; + // Create mentions + let mentions_rows = transaction.query( + " + INSERT INTO mention (post_id, profile_id) + SELECT $1, unnest($2::uuid[]) + RETURNING ( + SELECT actor_profile FROM actor_profile + WHERE actor_profile.id = profile_id + ) AS actor_profile + ", + &[&db_post.id, &data.mentions], + ).await?; + if mentions_rows.len() != data.mentions.len() { + // Some profiles were not found + return Err(DatabaseError::NotFound("profile")); + }; + let db_mentions: Vec = mentions_rows.iter() + .map(|row| row.try_get("actor_profile")) + .collect::>()?; // Update counters let author = update_post_count(&transaction, &db_post.author_id, 1).await?; if let Some(in_reply_to_id) = &db_post.in_reply_to_id { @@ -152,7 +184,7 @@ pub async fn create_post( } transaction.commit().await?; - let post = Post::new(db_post, author, db_attachments); + let post = Post::new(db_post, author, db_attachments, db_mentions); Ok(post) } @@ -164,12 +196,14 @@ pub async fn get_post_by_id( " SELECT post, actor_profile, - {related_attachments} + {related_attachments}, + {related_mentions} FROM post JOIN actor_profile ON post.author_id = actor_profile.id WHERE post.id = $1 ", related_attachments=RELATED_ATTACHMENTS, + related_mentions=RELATED_MENTIONS, ); let maybe_row = db_client.query_opt( statement.as_str(), @@ -208,13 +242,15 @@ pub async fn get_thread( ) SELECT post, actor_profile, - {related_attachments} + {related_attachments}, + {related_mentions} FROM post JOIN context ON post.id = context.id JOIN actor_profile ON post.author_id = actor_profile.id ORDER BY context.path ", related_attachments=RELATED_ATTACHMENTS, + related_mentions=RELATED_MENTIONS, ); let rows = db_client.query( statement.as_str(), @@ -237,12 +273,14 @@ pub async fn get_post_by_object_id( " SELECT post, actor_profile, - {related_attachments} + {related_attachments}, + {related_mentions} FROM post JOIN actor_profile ON post.author_id = actor_profile.id WHERE post.object_id = $1 ", related_attachments=RELATED_ATTACHMENTS, + related_mentions=RELATED_MENTIONS, ); let maybe_row = db_client.query_opt( statement.as_str(), @@ -261,12 +299,14 @@ pub async fn get_post_by_ipfs_cid( " SELECT post, actor_profile, - {related_attachments} + {related_attachments}, + {related_mentions} FROM post JOIN actor_profile ON post.author_id = actor_profile.id WHERE post.ipfs_cid = $1 ", related_attachments=RELATED_ATTACHMENTS, + related_mentions=RELATED_MENTIONS, ); let result = db_client.query_opt( statement.as_str(), diff --git a/src/models/posts/types.rs b/src/models/posts/types.rs index 31ae653..6c99eb0 100644 --- a/src/models/posts/types.rs +++ b/src/models/posts/types.rs @@ -39,6 +39,7 @@ pub struct Post { pub reply_count: i32, pub reaction_count: i32, pub attachments: Vec, + pub mentions: Vec, pub object_id: Option, pub ipfs_cid: Option, pub token_id: Option, @@ -52,6 +53,7 @@ impl Post { db_post: DbPost, db_author: DbActorProfile, db_attachments: Vec, + db_mentions: Vec, ) -> Self { assert_eq!(db_post.author_id, db_author.id); Self { @@ -62,6 +64,7 @@ impl Post { reply_count: db_post.reply_count, reaction_count: db_post.reaction_count, attachments: db_attachments, + mentions: db_mentions, object_id: db_post.object_id, ipfs_cid: db_post.ipfs_cid, token_id: db_post.token_id, @@ -83,6 +86,7 @@ impl Default for Post { reply_count: 0, reaction_count: 0, attachments: vec![], + mentions: vec![], object_id: None, ipfs_cid: None, token_id: None, @@ -101,7 +105,8 @@ impl TryFrom<&Row> for Post { let db_post: DbPost = row.try_get("post")?; let db_profile: DbActorProfile = row.try_get("actor_profile")?; let db_attachments: Vec = row.try_get("attachments")?; - let post = Self::new(db_post, db_profile, db_attachments); + let db_mentions: Vec = row.try_get("mentions")?; + let post = Self::new(db_post, db_profile, db_attachments, db_mentions); Ok(post) } } @@ -110,6 +115,7 @@ pub struct PostCreateData { pub content: String, pub in_reply_to_id: Option, pub attachments: Vec, + pub mentions: Vec, pub object_id: Option, pub created_at: Option>, } @@ -137,6 +143,7 @@ mod tests { content: " ".to_string(), in_reply_to_id: None, attachments: vec![], + mentions: vec![], object_id: None, created_at: None, }; @@ -149,6 +156,7 @@ mod tests { content: "test ".to_string(), in_reply_to_id: None, attachments: vec![], + mentions: vec![], object_id: None, created_at: None, };