Enable replies
This commit is contained in:
parent
087a077f7d
commit
520e5399aa
9 changed files with 103 additions and 9 deletions
|
@ -77,6 +77,7 @@ POST /api/v1/media
|
||||||
GET /api/v2/search
|
GET /api/v2/search
|
||||||
POST /api/v1/statuses
|
POST /api/v1/statuses
|
||||||
GET /api/v1/statuses/{status_id}
|
GET /api/v1/statuses/{status_id}
|
||||||
|
GET /api/v1/statuses/{status_id}/context
|
||||||
GET /api/v1/timelines/home
|
GET /api/v1/timelines/home
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
1
migrations/V0003__post__add_in_reply_to.sql
Normal file
1
migrations/V0003__post__add_in_reply_to.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE post ADD COLUMN in_reply_to_id UUID REFERENCES post (id) ON DELETE CASCADE;
|
|
@ -33,6 +33,7 @@ CREATE TABLE post (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
author_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
author_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
|
in_reply_to_id UUID REFERENCES post (id) ON DELETE CASCADE,
|
||||||
ipfs_cid VARCHAR(200),
|
ipfs_cid VARCHAR(200),
|
||||||
token_id INTEGER,
|
token_id INTEGER,
|
||||||
token_tx_id VARCHAR(200),
|
token_tx_id VARCHAR(200),
|
||||||
|
|
|
@ -122,6 +122,8 @@ pub async fn receive_activity(
|
||||||
}
|
}
|
||||||
let post_data = PostCreateData {
|
let post_data = PostCreateData {
|
||||||
content,
|
content,
|
||||||
|
// TODO: parse inReplyTo field
|
||||||
|
in_reply_to_id: None,
|
||||||
attachments: attachments,
|
attachments: attachments,
|
||||||
created_at: object.published,
|
created_at: object.published,
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,4 +13,6 @@ pub const PERSON: &str = "Person";
|
||||||
pub const DOCUMENT: &str = "Document";
|
pub const DOCUMENT: &str = "Document";
|
||||||
pub const IMAGE: &str = "Image";
|
pub const IMAGE: &str = "Image";
|
||||||
pub const NOTE: &str = "Note";
|
pub const NOTE: &str = "Note";
|
||||||
|
|
||||||
|
// Misc
|
||||||
pub const PROPERTY_VALUE: &str = "PropertyValue";
|
pub const PROPERTY_VALUE: &str = "PropertyValue";
|
||||||
|
|
|
@ -13,6 +13,7 @@ pub struct Status {
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub account: Account,
|
pub account: Account,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
pub in_reply_to_id: Option<Uuid>,
|
||||||
pub media_attachments: Vec<Attachment>,
|
pub media_attachments: Vec<Attachment>,
|
||||||
|
|
||||||
// Extra fields
|
// Extra fields
|
||||||
|
@ -32,6 +33,7 @@ impl Status {
|
||||||
created_at: post.created_at,
|
created_at: post.created_at,
|
||||||
account: account,
|
account: account,
|
||||||
content: post.content,
|
content: post.content,
|
||||||
|
in_reply_to_id: post.in_reply_to_id,
|
||||||
media_attachments: attachments,
|
media_attachments: attachments,
|
||||||
ipfs_cid: post.ipfs_cid,
|
ipfs_cid: post.ipfs_cid,
|
||||||
token_id: post.token_id,
|
token_id: post.token_id,
|
||||||
|
@ -47,6 +49,8 @@ pub struct StatusData {
|
||||||
|
|
||||||
#[serde(rename = "media_ids[]")]
|
#[serde(rename = "media_ids[]")]
|
||||||
pub media_ids: Option<Vec<Uuid>>,
|
pub media_ids: Option<Vec<Uuid>>,
|
||||||
|
|
||||||
|
pub in_reply_to_id: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<StatusData> for PostCreateData {
|
impl From<StatusData> for PostCreateData {
|
||||||
|
@ -54,6 +58,7 @@ impl From<StatusData> for PostCreateData {
|
||||||
fn from(value: StatusData) -> Self {
|
fn from(value: StatusData) -> Self {
|
||||||
Self {
|
Self {
|
||||||
content: value.status,
|
content: value.status,
|
||||||
|
in_reply_to_id: value.in_reply_to_id,
|
||||||
attachments: value.media_ids.unwrap_or(vec![]),
|
attachments: value.media_ids.unwrap_or(vec![]),
|
||||||
created_at: None,
|
created_at: None,
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,12 @@ use crate::ipfs::store as ipfs_store;
|
||||||
use crate::ipfs::utils::{IPFS_LOGO, get_ipfs_url};
|
use crate::ipfs::utils::{IPFS_LOGO, get_ipfs_url};
|
||||||
use crate::mastodon_api::users::auth::get_current_user;
|
use crate::mastodon_api::users::auth::get_current_user;
|
||||||
use crate::models::profiles::queries::get_followers;
|
use crate::models::profiles::queries::get_followers;
|
||||||
use crate::models::posts::queries::{create_post, get_post_by_id, update_post};
|
use crate::models::posts::queries::{
|
||||||
|
create_post,
|
||||||
|
get_post_by_id,
|
||||||
|
get_thread,
|
||||||
|
update_post,
|
||||||
|
};
|
||||||
use crate::models::posts::types::PostCreateData;
|
use crate::models::posts::types::PostCreateData;
|
||||||
use super::types::{Status, StatusData};
|
use super::types::{Status, StatusData};
|
||||||
|
|
||||||
|
@ -66,6 +71,20 @@ async fn get_status(
|
||||||
Ok(HttpResponse::Ok().json(status))
|
Ok(HttpResponse::Ok().json(status))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/{status_id}/context")]
|
||||||
|
async fn get_context(
|
||||||
|
config: web::Data<Config>,
|
||||||
|
db_pool: web::Data<Pool>,
|
||||||
|
web::Path(status_id): web::Path<Uuid>,
|
||||||
|
) -> Result<HttpResponse, HttpError> {
|
||||||
|
let db_client = &**get_database_client(&db_pool).await?;
|
||||||
|
let statuses: Vec<Status> = get_thread(db_client, &status_id).await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|post| Status::from_post(post, &config.instance_url()))
|
||||||
|
.collect();
|
||||||
|
Ok(HttpResponse::Ok().json(statuses))
|
||||||
|
}
|
||||||
|
|
||||||
// https://docs.opensea.io/docs/metadata-standards
|
// https://docs.opensea.io/docs/metadata-standards
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct PostMetadata {
|
struct PostMetadata {
|
||||||
|
@ -154,6 +173,7 @@ pub fn status_api_scope() -> Scope {
|
||||||
.service(create_status)
|
.service(create_status)
|
||||||
// Routes with status ID
|
// Routes with status ID
|
||||||
.service(get_status)
|
.service(get_status)
|
||||||
|
.service(get_context)
|
||||||
.service(make_permanent)
|
.service(make_permanent)
|
||||||
.service(get_signature)
|
.service(get_signature)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,8 @@ pub async fn get_posts(
|
||||||
db_client: &impl GenericClient,
|
db_client: &impl GenericClient,
|
||||||
current_user_id: &Uuid,
|
current_user_id: &Uuid,
|
||||||
) -> Result<Vec<Post>, DatabaseError> {
|
) -> Result<Vec<Post>, DatabaseError> {
|
||||||
// Select posts from follows + own posts
|
// Select posts from follows + own posts.
|
||||||
|
// Do not select replies
|
||||||
let rows = db_client.query(
|
let rows = db_client.query(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
|
@ -25,11 +26,14 @@ pub async fn get_posts(
|
||||||
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
|
||||||
|
post.in_reply_to_id IS NULL
|
||||||
|
AND (
|
||||||
post.author_id = $1
|
post.author_id = $1
|
||||||
OR EXISTS (
|
OR EXISTS (
|
||||||
SELECT 1 FROM relationship
|
SELECT 1 FROM relationship
|
||||||
WHERE source_id = $1 AND target_id = post.author_id
|
WHERE source_id = $1 AND target_id = post.author_id
|
||||||
)
|
)
|
||||||
|
)
|
||||||
ORDER BY post.created_at DESC
|
ORDER BY post.created_at DESC
|
||||||
",
|
",
|
||||||
&[¤t_user_id],
|
&[¤t_user_id],
|
||||||
|
@ -76,11 +80,21 @@ pub async fn create_post(
|
||||||
let created_at = data.created_at.unwrap_or(Utc::now());
|
let created_at = data.created_at.unwrap_or(Utc::now());
|
||||||
let post_row = transaction.query_one(
|
let post_row = transaction.query_one(
|
||||||
"
|
"
|
||||||
INSERT INTO post (id, author_id, content, created_at)
|
INSERT INTO post (
|
||||||
VALUES ($1, $2, $3, $4)
|
id, author_id, content,
|
||||||
|
in_reply_to_id,
|
||||||
|
created_at
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
RETURNING post
|
RETURNING post
|
||||||
",
|
",
|
||||||
&[&post_id, &author_id, &data.content, &created_at],
|
&[
|
||||||
|
&post_id,
|
||||||
|
&author_id,
|
||||||
|
&data.content,
|
||||||
|
&data.in_reply_to_id,
|
||||||
|
&created_at,
|
||||||
|
],
|
||||||
).await?;
|
).await?;
|
||||||
let attachment_rows = transaction.query(
|
let attachment_rows = transaction.query(
|
||||||
"
|
"
|
||||||
|
@ -103,6 +117,7 @@ pub async fn create_post(
|
||||||
id: db_post.id,
|
id: db_post.id,
|
||||||
author: author,
|
author: author,
|
||||||
content: db_post.content,
|
content: db_post.content,
|
||||||
|
in_reply_to_id: db_post.in_reply_to_id,
|
||||||
attachments: db_attachments,
|
attachments: db_attachments,
|
||||||
ipfs_cid: db_post.ipfs_cid,
|
ipfs_cid: db_post.ipfs_cid,
|
||||||
token_id: db_post.token_id,
|
token_id: db_post.token_id,
|
||||||
|
@ -137,6 +152,47 @@ pub async fn get_post_by_id(
|
||||||
Ok(post)
|
Ok(post)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_thread(
|
||||||
|
db_client: &impl GenericClient,
|
||||||
|
post_id: &Uuid,
|
||||||
|
) -> Result<Vec<Post>, DatabaseError> {
|
||||||
|
// TODO: limit recursion depth
|
||||||
|
let rows = db_client.query(
|
||||||
|
"
|
||||||
|
WITH RECURSIVE
|
||||||
|
ancestors (id, in_reply_to_id) AS (
|
||||||
|
SELECT post.id, post.in_reply_to_id FROM post
|
||||||
|
WHERE post.id = $1
|
||||||
|
UNION ALL
|
||||||
|
SELECT post.id, post.in_reply_to_id FROM post
|
||||||
|
JOIN ancestors ON post.id = ancestors.in_reply_to_id
|
||||||
|
),
|
||||||
|
context (id, path) AS (
|
||||||
|
SELECT ancestors.id, ARRAY[ancestors.id] FROM ancestors
|
||||||
|
WHERE ancestors.in_reply_to_id IS NULL
|
||||||
|
UNION
|
||||||
|
SELECT post.id, array_append(context.path, post.id) FROM post
|
||||||
|
JOIN context ON post.in_reply_to_id = context.id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
post, actor_profile,
|
||||||
|
ARRAY(
|
||||||
|
SELECT media_attachment
|
||||||
|
FROM media_attachment WHERE post_id = post.id
|
||||||
|
) AS attachments
|
||||||
|
FROM post
|
||||||
|
JOIN context ON post.id = context.id
|
||||||
|
JOIN actor_profile ON post.author_id = actor_profile.id
|
||||||
|
ORDER BY context.path
|
||||||
|
",
|
||||||
|
&[&post_id],
|
||||||
|
).await?;
|
||||||
|
let posts: Vec<Post> = rows.iter()
|
||||||
|
.map(|row| Post::try_from(row))
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
Ok(posts)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_post_by_ipfs_cid(
|
pub async fn get_post_by_ipfs_cid(
|
||||||
db_client: &impl GenericClient,
|
db_client: &impl GenericClient,
|
||||||
ipfs_cid: &str,
|
ipfs_cid: &str,
|
||||||
|
|
|
@ -16,6 +16,7 @@ pub struct DbPost {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub author_id: Uuid,
|
pub author_id: Uuid,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
pub in_reply_to_id: Option<Uuid>,
|
||||||
pub ipfs_cid: Option<String>,
|
pub ipfs_cid: Option<String>,
|
||||||
pub token_id: Option<i32>,
|
pub token_id: Option<i32>,
|
||||||
pub token_tx_id: Option<String>,
|
pub token_tx_id: Option<String>,
|
||||||
|
@ -27,6 +28,7 @@ pub struct Post {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub author: DbActorProfile,
|
pub author: DbActorProfile,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
pub in_reply_to_id: Option<Uuid>,
|
||||||
pub attachments: Vec<DbMediaAttachment>,
|
pub attachments: Vec<DbMediaAttachment>,
|
||||||
pub ipfs_cid: Option<String>,
|
pub ipfs_cid: Option<String>,
|
||||||
pub token_id: Option<i32>,
|
pub token_id: Option<i32>,
|
||||||
|
@ -46,6 +48,7 @@ impl TryFrom<&Row> for Post {
|
||||||
id: db_post.id,
|
id: db_post.id,
|
||||||
author: db_profile,
|
author: db_profile,
|
||||||
content: db_post.content,
|
content: db_post.content,
|
||||||
|
in_reply_to_id: db_post.in_reply_to_id,
|
||||||
attachments: db_attachments,
|
attachments: db_attachments,
|
||||||
ipfs_cid: db_post.ipfs_cid,
|
ipfs_cid: db_post.ipfs_cid,
|
||||||
token_id: db_post.token_id,
|
token_id: db_post.token_id,
|
||||||
|
@ -58,6 +61,7 @@ impl TryFrom<&Row> for Post {
|
||||||
|
|
||||||
pub struct PostCreateData {
|
pub struct PostCreateData {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
pub in_reply_to_id: Option<Uuid>,
|
||||||
pub attachments: Vec<Uuid>,
|
pub attachments: Vec<Uuid>,
|
||||||
pub created_at: Option<DateTime<Utc>>,
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
@ -83,6 +87,7 @@ mod tests {
|
||||||
fn test_validate_post_data() {
|
fn test_validate_post_data() {
|
||||||
let mut post_data_1 = PostCreateData {
|
let mut post_data_1 = PostCreateData {
|
||||||
content: " ".to_string(),
|
content: " ".to_string(),
|
||||||
|
in_reply_to_id: None,
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
created_at: None,
|
created_at: None,
|
||||||
};
|
};
|
||||||
|
@ -93,6 +98,7 @@ mod tests {
|
||||||
fn test_trimming() {
|
fn test_trimming() {
|
||||||
let mut post_data_2 = PostCreateData {
|
let mut post_data_2 = PostCreateData {
|
||||||
content: "test ".to_string(),
|
content: "test ".to_string(),
|
||||||
|
in_reply_to_id: None,
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
created_at: None,
|
created_at: None,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue