Store mentions in database

This commit is contained in:
silverpill 2021-11-11 19:10:28 +00:00
parent 06010e4403
commit 4da44159ed
9 changed files with 103 additions and 11 deletions

View file

@ -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)
);

View file

@ -75,6 +75,12 @@ CREATE TABLE media_attachment (
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() 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 ( CREATE TABLE oauth_token (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
owner_id UUID NOT NULL REFERENCES user_account (id) ON DELETE CASCADE, owner_id UUID NOT NULL REFERENCES user_account (id) ON DELETE CASCADE,

View file

@ -196,6 +196,7 @@ pub async fn process_note(
content, content,
in_reply_to_id, in_reply_to_id,
attachments: attachments, attachments: attachments,
mentions: vec![],
object_id: Some(object.id), object_id: Some(object.id),
created_at: object.published, created_at: object.published,
}; };

View file

@ -5,6 +5,27 @@ use uuid::Uuid;
use crate::mastodon_api::accounts::types::Account; use crate::mastodon_api::accounts::types::Account;
use crate::mastodon_api::media::types::Attachment; use crate::mastodon_api::media::types::Attachment;
use crate::models::posts::types::{Post, PostCreateData}; 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/ /// https://docs.joinmastodon.org/entities/status/
#[derive(Serialize)] #[derive(Serialize)]
@ -17,6 +38,7 @@ pub struct Status {
pub replies_count: i32, pub replies_count: i32,
pub favourites_count: i32, pub favourites_count: i32,
pub media_attachments: Vec<Attachment>, pub media_attachments: Vec<Attachment>,
mentions: Vec<Mention>,
// Authorized user attributes // Authorized user attributes
pub favourited: bool, pub favourited: bool,
@ -32,6 +54,9 @@ impl Status {
let attachments: Vec<Attachment> = post.attachments.into_iter() let attachments: Vec<Attachment> = post.attachments.into_iter()
.map(|item| Attachment::from_db(item, instance_url)) .map(|item| Attachment::from_db(item, instance_url))
.collect(); .collect();
let mentions: Vec<Mention> = post.mentions.into_iter()
.map(|item| Mention::from_profile(item, instance_url))
.collect();
let account = Account::from_profile(post.author, instance_url); let account = Account::from_profile(post.author, instance_url);
Self { Self {
id: post.id, id: post.id,
@ -42,6 +67,7 @@ impl Status {
replies_count: post.reply_count, replies_count: post.reply_count,
favourites_count: post.reaction_count, favourites_count: post.reaction_count,
media_attachments: attachments, media_attachments: attachments,
mentions: mentions,
favourited: post.actions.map_or(false, |actions| actions.favourited), favourited: post.actions.map_or(false, |actions| actions.favourited),
ipfs_cid: post.ipfs_cid, ipfs_cid: post.ipfs_cid,
token_id: post.token_id, token_id: post.token_id,
@ -68,6 +94,7 @@ impl From<StatusData> for PostCreateData {
content: value.status, content: value.status,
in_reply_to_id: value.in_reply_to_id, in_reply_to_id: value.in_reply_to_id,
attachments: value.media_ids.unwrap_or(vec![]), attachments: value.media_ids.unwrap_or(vec![]),
mentions: vec![],
object_id: None, object_id: None,
created_at: None, created_at: None,
} }

View file

@ -62,6 +62,8 @@ async fn create_status(
&instance.url(), &instance.url(),
&post_data.content, &post_data.content,
); );
post_data.mentions = mention_map.values()
.map(|profile| profile.id).collect();
let post = create_post(db_client, &current_user.id, post_data).await?; let post = create_post(db_client, &current_user.id, post_data).await?;
// Federate // Federate
let maybe_in_reply_to = match post.in_reply_to_id { let maybe_in_reply_to = match post.in_reply_to_id {

View file

@ -5,7 +5,7 @@ use uuid::Uuid;
use crate::errors::DatabaseError; use crate::errors::DatabaseError;
use crate::models::posts::helpers::get_actions_for_posts; 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}; use super::types::{EventType, Notification};
async fn create_notification( async fn create_notification(
@ -73,7 +73,8 @@ pub async fn get_notifications(
" "
SELECT SELECT
notification, sender, post, post_author, notification, sender, post, post_author,
{related_attachments} {related_attachments},
{related_mentions}
FROM notification FROM notification
JOIN actor_profile AS sender JOIN actor_profile AS sender
ON notification.sender_id = sender.id ON notification.sender_id = sender.id
@ -85,6 +86,7 @@ pub async fn get_notifications(
ORDER BY notification.created_at DESC ORDER BY notification.created_at DESC
", ",
related_attachments=RELATED_ATTACHMENTS, related_attachments=RELATED_ATTACHMENTS,
related_mentions=RELATED_MENTIONS,
); );
let rows = db_client.query( let rows = db_client.query(
statement.as_str(), statement.as_str(),

View file

@ -75,7 +75,8 @@ impl TryFrom<&Row> for Notification {
Some(db_post) => { Some(db_post) => {
let db_post_author: DbActorProfile = row.try_get("post_author")?; let db_post_author: DbActorProfile = row.try_get("post_author")?;
let db_attachments: Vec<DbMediaAttachment> = row.try_get("attachments")?; let db_attachments: Vec<DbMediaAttachment> = row.try_get("attachments")?;
Some(Post::new(db_post, db_post_author, db_attachments)) let db_mentions: Vec<DbActorProfile> = row.try_get("mentions")?;
Some(Post::new(db_post, db_post_author, db_attachments, db_mentions))
}, },
None => None, None => None,
}; };

View file

@ -13,6 +13,7 @@ use crate::models::cleanup::{
}; };
use crate::models::notifications::queries::create_reply_notification; use crate::models::notifications::queries::create_reply_notification;
use crate::models::profiles::queries::update_post_count; use crate::models::profiles::queries::update_post_count;
use crate::models::profiles::types::DbActorProfile;
use super::types::{DbPost, Post, PostCreateData}; use super::types::{DbPost, Post, PostCreateData};
pub const RELATED_ATTACHMENTS: &str = pub const RELATED_ATTACHMENTS: &str =
@ -21,6 +22,14 @@ pub const RELATED_ATTACHMENTS: &str =
FROM media_attachment WHERE post_id = post.id FROM media_attachment WHERE post_id = post.id
) AS attachments"; ) 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( pub async fn get_home_timeline(
db_client: &impl GenericClient, db_client: &impl GenericClient,
current_user_id: &Uuid, current_user_id: &Uuid,
@ -31,7 +40,8 @@ pub async fn get_home_timeline(
" "
SELECT SELECT
post, actor_profile, post, actor_profile,
{related_attachments} {related_attachments},
{related_mentions}
FROM post FROM post
JOIN actor_profile ON post.author_id = actor_profile.id JOIN actor_profile ON post.author_id = actor_profile.id
WHERE WHERE
@ -46,6 +56,7 @@ pub async fn get_home_timeline(
ORDER BY post.created_at DESC ORDER BY post.created_at DESC
", ",
related_attachments=RELATED_ATTACHMENTS, related_attachments=RELATED_ATTACHMENTS,
related_mentions=RELATED_MENTIONS,
); );
let rows = db_client.query( let rows = db_client.query(
statement.as_str(), statement.as_str(),
@ -71,13 +82,15 @@ pub async fn get_posts_by_author(
" "
SELECT SELECT
post, actor_profile, post, actor_profile,
{related_attachments} {related_attachments},
{related_mentions}
FROM post FROM post
JOIN actor_profile ON post.author_id = actor_profile.id JOIN actor_profile ON post.author_id = actor_profile.id
WHERE {condition} WHERE {condition}
ORDER BY post.created_at DESC ORDER BY post.created_at DESC
", ",
related_attachments=RELATED_ATTACHMENTS, related_attachments=RELATED_ATTACHMENTS,
related_mentions=RELATED_MENTIONS,
condition=condition, condition=condition,
); );
let rows = db_client.query( let rows = db_client.query(
@ -136,6 +149,25 @@ pub async fn create_post(
let db_attachments: Vec<DbMediaAttachment> = attachments_rows.iter() let db_attachments: Vec<DbMediaAttachment> = attachments_rows.iter()
.map(|row| row.try_get("media_attachment")) .map(|row| row.try_get("media_attachment"))
.collect::<Result<_, _>>()?; .collect::<Result<_, _>>()?;
// 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<DbActorProfile> = mentions_rows.iter()
.map(|row| row.try_get("actor_profile"))
.collect::<Result<_, _>>()?;
// Update counters // Update counters
let author = update_post_count(&transaction, &db_post.author_id, 1).await?; 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 { 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?; 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) Ok(post)
} }
@ -164,12 +196,14 @@ pub async fn get_post_by_id(
" "
SELECT SELECT
post, actor_profile, post, actor_profile,
{related_attachments} {related_attachments},
{related_mentions}
FROM post FROM post
JOIN actor_profile ON post.author_id = actor_profile.id JOIN actor_profile ON post.author_id = actor_profile.id
WHERE post.id = $1 WHERE post.id = $1
", ",
related_attachments=RELATED_ATTACHMENTS, related_attachments=RELATED_ATTACHMENTS,
related_mentions=RELATED_MENTIONS,
); );
let maybe_row = db_client.query_opt( let maybe_row = db_client.query_opt(
statement.as_str(), statement.as_str(),
@ -208,13 +242,15 @@ pub async fn get_thread(
) )
SELECT SELECT
post, actor_profile, post, actor_profile,
{related_attachments} {related_attachments},
{related_mentions}
FROM post FROM post
JOIN context ON post.id = context.id JOIN context ON post.id = context.id
JOIN actor_profile ON post.author_id = actor_profile.id JOIN actor_profile ON post.author_id = actor_profile.id
ORDER BY context.path ORDER BY context.path
", ",
related_attachments=RELATED_ATTACHMENTS, related_attachments=RELATED_ATTACHMENTS,
related_mentions=RELATED_MENTIONS,
); );
let rows = db_client.query( let rows = db_client.query(
statement.as_str(), statement.as_str(),
@ -237,12 +273,14 @@ pub async fn get_post_by_object_id(
" "
SELECT SELECT
post, actor_profile, post, actor_profile,
{related_attachments} {related_attachments},
{related_mentions}
FROM post FROM post
JOIN actor_profile ON post.author_id = actor_profile.id JOIN actor_profile ON post.author_id = actor_profile.id
WHERE post.object_id = $1 WHERE post.object_id = $1
", ",
related_attachments=RELATED_ATTACHMENTS, related_attachments=RELATED_ATTACHMENTS,
related_mentions=RELATED_MENTIONS,
); );
let maybe_row = db_client.query_opt( let maybe_row = db_client.query_opt(
statement.as_str(), statement.as_str(),
@ -261,12 +299,14 @@ pub async fn get_post_by_ipfs_cid(
" "
SELECT SELECT
post, actor_profile, post, actor_profile,
{related_attachments} {related_attachments},
{related_mentions}
FROM post FROM post
JOIN actor_profile ON post.author_id = actor_profile.id JOIN actor_profile ON post.author_id = actor_profile.id
WHERE post.ipfs_cid = $1 WHERE post.ipfs_cid = $1
", ",
related_attachments=RELATED_ATTACHMENTS, related_attachments=RELATED_ATTACHMENTS,
related_mentions=RELATED_MENTIONS,
); );
let result = db_client.query_opt( let result = db_client.query_opt(
statement.as_str(), statement.as_str(),

View file

@ -39,6 +39,7 @@ pub struct Post {
pub reply_count: i32, pub reply_count: i32,
pub reaction_count: i32, pub reaction_count: i32,
pub attachments: Vec<DbMediaAttachment>, pub attachments: Vec<DbMediaAttachment>,
pub mentions: Vec<DbActorProfile>,
pub object_id: Option<String>, pub object_id: Option<String>,
pub ipfs_cid: Option<String>, pub ipfs_cid: Option<String>,
pub token_id: Option<i32>, pub token_id: Option<i32>,
@ -52,6 +53,7 @@ impl Post {
db_post: DbPost, db_post: DbPost,
db_author: DbActorProfile, db_author: DbActorProfile,
db_attachments: Vec<DbMediaAttachment>, db_attachments: Vec<DbMediaAttachment>,
db_mentions: Vec<DbActorProfile>,
) -> Self { ) -> Self {
assert_eq!(db_post.author_id, db_author.id); assert_eq!(db_post.author_id, db_author.id);
Self { Self {
@ -62,6 +64,7 @@ impl Post {
reply_count: db_post.reply_count, reply_count: db_post.reply_count,
reaction_count: db_post.reaction_count, reaction_count: db_post.reaction_count,
attachments: db_attachments, attachments: db_attachments,
mentions: db_mentions,
object_id: db_post.object_id, object_id: db_post.object_id,
ipfs_cid: db_post.ipfs_cid, ipfs_cid: db_post.ipfs_cid,
token_id: db_post.token_id, token_id: db_post.token_id,
@ -83,6 +86,7 @@ impl Default for Post {
reply_count: 0, reply_count: 0,
reaction_count: 0, reaction_count: 0,
attachments: vec![], attachments: vec![],
mentions: vec![],
object_id: None, object_id: None,
ipfs_cid: None, ipfs_cid: None,
token_id: None, token_id: None,
@ -101,7 +105,8 @@ impl TryFrom<&Row> for Post {
let db_post: DbPost = row.try_get("post")?; let db_post: DbPost = row.try_get("post")?;
let db_profile: DbActorProfile = row.try_get("actor_profile")?; let db_profile: DbActorProfile = row.try_get("actor_profile")?;
let db_attachments: Vec<DbMediaAttachment> = row.try_get("attachments")?; let db_attachments: Vec<DbMediaAttachment> = row.try_get("attachments")?;
let post = Self::new(db_post, db_profile, db_attachments); let db_mentions: Vec<DbActorProfile> = row.try_get("mentions")?;
let post = Self::new(db_post, db_profile, db_attachments, db_mentions);
Ok(post) Ok(post)
} }
} }
@ -110,6 +115,7 @@ pub struct PostCreateData {
pub content: String, pub content: String,
pub in_reply_to_id: Option<Uuid>, pub in_reply_to_id: Option<Uuid>,
pub attachments: Vec<Uuid>, pub attachments: Vec<Uuid>,
pub mentions: Vec<Uuid>,
pub object_id: Option<String>, pub object_id: Option<String>,
pub created_at: Option<DateTime<Utc>>, pub created_at: Option<DateTime<Utc>>,
} }
@ -137,6 +143,7 @@ mod tests {
content: " ".to_string(), content: " ".to_string(),
in_reply_to_id: None, in_reply_to_id: None,
attachments: vec![], attachments: vec![],
mentions: vec![],
object_id: None, object_id: None,
created_at: None, created_at: None,
}; };
@ -149,6 +156,7 @@ mod tests {
content: "test ".to_string(), content: "test ".to_string(),
in_reply_to_id: None, in_reply_to_id: None,
attachments: vec![], attachments: vec![],
mentions: vec![],
object_id: None, object_id: None,
created_at: None, created_at: None,
}; };