Store mentions in database
This commit is contained in:
parent
06010e4403
commit
4da44159ed
9 changed files with 103 additions and 11 deletions
5
migrations/V0011__mention.sql
Normal file
5
migrations/V0011__mention.sql
Normal 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)
|
||||||
|
);
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, ¤t_user.id, post_data).await?;
|
let post = create_post(db_client, ¤t_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 {
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue