Add API method for marking posts as favourite
This commit is contained in:
parent
bc65186f00
commit
d2462e9e96
11 changed files with 120 additions and 1 deletions
8
migrations/V0009__post_reaction.sql
Normal file
8
migrations/V0009__post_reaction.sql
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
CREATE TABLE post_reaction (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
author_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
||||||
|
post_id UUID NOT NULL REFERENCES post (id) ON DELETE CASCADE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (author_id, post_id)
|
||||||
|
);
|
||||||
|
ALTER TABLE post ADD COLUMN reaction_count INTEGER NOT NULL CHECK (reaction_count >= 0) DEFAULT 0;
|
|
@ -35,6 +35,7 @@ CREATE TABLE post (
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
in_reply_to_id UUID REFERENCES post (id) ON DELETE CASCADE,
|
in_reply_to_id UUID REFERENCES post (id) ON DELETE CASCADE,
|
||||||
reply_count INTEGER NOT NULL CHECK (reply_count >= 0) DEFAULT 0,
|
reply_count INTEGER NOT NULL CHECK (reply_count >= 0) DEFAULT 0,
|
||||||
|
reaction_count INTEGER NOT NULL CHECK (reaction_count >= 0) DEFAULT 0,
|
||||||
object_id VARCHAR(200) UNIQUE,
|
object_id VARCHAR(200) UNIQUE,
|
||||||
ipfs_cid VARCHAR(200),
|
ipfs_cid VARCHAR(200),
|
||||||
token_id INTEGER,
|
token_id INTEGER,
|
||||||
|
@ -42,6 +43,14 @@ CREATE TABLE post (
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE post_reaction (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
author_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
||||||
|
post_id UUID NOT NULL REFERENCES post (id) ON DELETE CASCADE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (author_id, post_id)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE relationship (
|
CREATE TABLE relationship (
|
||||||
source_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
source_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
||||||
target_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
target_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
||||||
|
|
|
@ -14,6 +14,7 @@ pub fn create_pool(database_url: &str) -> Pool {
|
||||||
pool
|
pool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use tokio_postgres::error::{Error as PgError, SqlState};
|
||||||
use crate::errors::DatabaseError;
|
use crate::errors::DatabaseError;
|
||||||
|
|
||||||
pub async fn get_database_client(pool: &Pool)
|
pub async fn get_database_client(pool: &Pool)
|
||||||
|
@ -24,3 +25,16 @@ pub async fn get_database_client(pool: &Pool)
|
||||||
let client = pool.get().await?;
|
let client = pool.get().await?;
|
||||||
Ok(client)
|
Ok(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn catch_unique_violation(
|
||||||
|
object_type: &'static str,
|
||||||
|
) -> impl Fn(PgError) -> DatabaseError {
|
||||||
|
move |err| {
|
||||||
|
if let Some(code) = err.code() {
|
||||||
|
if code == &SqlState::UNIQUE_VIOLATION {
|
||||||
|
return DatabaseError::AlreadyExists(object_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,8 +15,12 @@ pub struct Status {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub in_reply_to_id: Option<Uuid>,
|
pub in_reply_to_id: Option<Uuid>,
|
||||||
pub replies_count: i32,
|
pub replies_count: i32,
|
||||||
|
pub favourites_count: i32,
|
||||||
pub media_attachments: Vec<Attachment>,
|
pub media_attachments: Vec<Attachment>,
|
||||||
|
|
||||||
|
// Authorized user attributes
|
||||||
|
pub favourited: bool,
|
||||||
|
|
||||||
// Extra fields
|
// Extra fields
|
||||||
pub ipfs_cid: Option<String>,
|
pub ipfs_cid: Option<String>,
|
||||||
pub token_id: Option<i32>,
|
pub token_id: Option<i32>,
|
||||||
|
@ -36,7 +40,9 @@ impl Status {
|
||||||
content: post.content,
|
content: post.content,
|
||||||
in_reply_to_id: post.in_reply_to_id,
|
in_reply_to_id: post.in_reply_to_id,
|
||||||
replies_count: post.reply_count,
|
replies_count: post.reply_count,
|
||||||
|
favourites_count: post.reaction_count,
|
||||||
media_attachments: attachments,
|
media_attachments: attachments,
|
||||||
|
favourited: false,
|
||||||
ipfs_cid: post.ipfs_cid,
|
ipfs_cid: post.ipfs_cid,
|
||||||
token_id: post.token_id,
|
token_id: post.token_id,
|
||||||
token_tx_id: post.token_tx_id,
|
token_tx_id: post.token_tx_id,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/// https://docs.joinmastodon.org/methods/statuses/
|
||||||
use actix_web::{get, post, web, HttpResponse, Scope};
|
use actix_web::{get, post, web, HttpResponse, Scope};
|
||||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
@ -8,7 +9,7 @@ use crate::activitypub::actor::Actor;
|
||||||
use crate::activitypub::deliverer::deliver_activity;
|
use crate::activitypub::deliverer::deliver_activity;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::database::{Pool, get_database_client};
|
use crate::database::{Pool, get_database_client};
|
||||||
use crate::errors::HttpError;
|
use crate::errors::{DatabaseError, HttpError};
|
||||||
use crate::ethereum::nft::create_mint_signature;
|
use crate::ethereum::nft::create_mint_signature;
|
||||||
use crate::ipfs::store as ipfs_store;
|
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};
|
||||||
|
@ -22,6 +23,7 @@ use crate::models::posts::queries::{
|
||||||
update_post,
|
update_post,
|
||||||
};
|
};
|
||||||
use crate::models::posts::types::PostCreateData;
|
use crate::models::posts::types::PostCreateData;
|
||||||
|
use crate::models::reactions::queries::create_reaction;
|
||||||
use super::types::{Status, StatusData};
|
use super::types::{Status, StatusData};
|
||||||
|
|
||||||
#[post("")]
|
#[post("")]
|
||||||
|
@ -98,6 +100,24 @@ async fn get_context(
|
||||||
Ok(HttpResponse::Ok().json(statuses))
|
Ok(HttpResponse::Ok().json(statuses))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("/{status_id}/favourite")]
|
||||||
|
async fn favourite(
|
||||||
|
auth: BearerAuth,
|
||||||
|
config: web::Data<Config>,
|
||||||
|
db_pool: web::Data<Pool>,
|
||||||
|
web::Path(status_id): web::Path<Uuid>,
|
||||||
|
) -> Result<HttpResponse, HttpError> {
|
||||||
|
let db_client = &mut **get_database_client(&db_pool).await?;
|
||||||
|
let current_user = get_current_user(db_client, auth.token()).await?;
|
||||||
|
match create_reaction(db_client, ¤t_user.id, &status_id).await {
|
||||||
|
Err(DatabaseError::AlreadyExists(_)) => (), // post already favourited
|
||||||
|
other_result => other_result?,
|
||||||
|
}
|
||||||
|
let post = get_post_by_id(db_client, &status_id).await?;
|
||||||
|
let status = Status::from_post(post, &config.instance_url());
|
||||||
|
Ok(HttpResponse::Ok().json(status))
|
||||||
|
}
|
||||||
|
|
||||||
// https://docs.opensea.io/docs/metadata-standards
|
// https://docs.opensea.io/docs/metadata-standards
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct PostMetadata {
|
struct PostMetadata {
|
||||||
|
@ -188,6 +208,7 @@ pub fn status_api_scope() -> Scope {
|
||||||
// Routes with status ID
|
// Routes with status ID
|
||||||
.service(get_status)
|
.service(get_status)
|
||||||
.service(get_context)
|
.service(get_context)
|
||||||
|
.service(favourite)
|
||||||
.service(make_permanent)
|
.service(make_permanent)
|
||||||
.service(get_signature)
|
.service(get_signature)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,5 +4,6 @@ pub mod notifications;
|
||||||
pub mod oauth;
|
pub mod oauth;
|
||||||
pub mod posts;
|
pub mod posts;
|
||||||
pub mod profiles;
|
pub mod profiles;
|
||||||
|
pub mod reactions;
|
||||||
pub mod relationships;
|
pub mod relationships;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
|
|
@ -302,6 +302,25 @@ pub async fn update_reply_count(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn update_reaction_count(
|
||||||
|
db_client: &impl GenericClient,
|
||||||
|
post_id: &Uuid,
|
||||||
|
change: i32,
|
||||||
|
) -> Result<(), DatabaseError> {
|
||||||
|
let updated_count = db_client.execute(
|
||||||
|
"
|
||||||
|
UPDATE post
|
||||||
|
SET reaction_count = reaction_count + $1
|
||||||
|
WHERE id = $2
|
||||||
|
",
|
||||||
|
&[&change, &post_id],
|
||||||
|
).await?;
|
||||||
|
if updated_count == 0 {
|
||||||
|
return Err(DatabaseError::NotFound("post"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_token_waitlist(
|
pub async fn get_token_waitlist(
|
||||||
db_client: &impl GenericClient,
|
db_client: &impl GenericClient,
|
||||||
) -> Result<Vec<Uuid>, DatabaseError> {
|
) -> Result<Vec<Uuid>, DatabaseError> {
|
||||||
|
|
|
@ -18,6 +18,7 @@ pub struct DbPost {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub in_reply_to_id: Option<Uuid>,
|
pub in_reply_to_id: Option<Uuid>,
|
||||||
pub reply_count: i32,
|
pub reply_count: i32,
|
||||||
|
pub reaction_count: i32,
|
||||||
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>,
|
||||||
|
@ -32,6 +33,7 @@ pub struct Post {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub in_reply_to_id: Option<Uuid>,
|
pub in_reply_to_id: Option<Uuid>,
|
||||||
pub reply_count: i32,
|
pub reply_count: i32,
|
||||||
|
pub reaction_count: i32,
|
||||||
pub attachments: Vec<DbMediaAttachment>,
|
pub attachments: Vec<DbMediaAttachment>,
|
||||||
pub object_id: Option<String>,
|
pub object_id: Option<String>,
|
||||||
pub ipfs_cid: Option<String>,
|
pub ipfs_cid: Option<String>,
|
||||||
|
@ -53,6 +55,7 @@ impl Post {
|
||||||
content: db_post.content,
|
content: db_post.content,
|
||||||
in_reply_to_id: db_post.in_reply_to_id,
|
in_reply_to_id: db_post.in_reply_to_id,
|
||||||
reply_count: db_post.reply_count,
|
reply_count: db_post.reply_count,
|
||||||
|
reaction_count: db_post.reaction_count,
|
||||||
attachments: db_attachments,
|
attachments: db_attachments,
|
||||||
object_id: db_post.object_id,
|
object_id: db_post.object_id,
|
||||||
ipfs_cid: db_post.ipfs_cid,
|
ipfs_cid: db_post.ipfs_cid,
|
||||||
|
@ -72,6 +75,7 @@ impl Default for Post {
|
||||||
content: "".to_string(),
|
content: "".to_string(),
|
||||||
in_reply_to_id: None,
|
in_reply_to_id: None,
|
||||||
reply_count: 0,
|
reply_count: 0,
|
||||||
|
reaction_count: 0,
|
||||||
attachments: vec![],
|
attachments: vec![],
|
||||||
object_id: None,
|
object_id: None,
|
||||||
ipfs_cid: None,
|
ipfs_cid: None,
|
||||||
|
|
|
@ -256,6 +256,17 @@ pub async fn delete_profile(
|
||||||
",
|
",
|
||||||
&[&profile_id],
|
&[&profile_id],
|
||||||
).await?;
|
).await?;
|
||||||
|
transaction.execute(
|
||||||
|
"
|
||||||
|
UPDATE post
|
||||||
|
SET reaction_count = reaction_count - 1
|
||||||
|
FROM post_reaction
|
||||||
|
WHERE
|
||||||
|
post_reaction.post_id = post.id
|
||||||
|
AND post_reaction.author_id = $1
|
||||||
|
",
|
||||||
|
&[&profile_id],
|
||||||
|
).await?;
|
||||||
// Delete profile
|
// Delete profile
|
||||||
let deleted_count = transaction.execute(
|
let deleted_count = transaction.execute(
|
||||||
"
|
"
|
||||||
|
|
1
src/models/reactions/mod.rs
Normal file
1
src/models/reactions/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod queries;
|
25
src/models/reactions/queries.rs
Normal file
25
src/models/reactions/queries.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use tokio_postgres::GenericClient;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::database::catch_unique_violation;
|
||||||
|
use crate::errors::DatabaseError;
|
||||||
|
use crate::models::posts::queries::update_reaction_count;
|
||||||
|
|
||||||
|
pub async fn create_reaction(
|
||||||
|
db_client: &mut impl GenericClient,
|
||||||
|
author_id: &Uuid,
|
||||||
|
post_id: &Uuid,
|
||||||
|
) -> Result<(), DatabaseError> {
|
||||||
|
let transaction = db_client.transaction().await?;
|
||||||
|
let reaction_id = Uuid::new_v4();
|
||||||
|
transaction.execute(
|
||||||
|
"
|
||||||
|
INSERT INTO post_reaction (id, author_id, post_id)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
",
|
||||||
|
&[&reaction_id, &author_id, &post_id],
|
||||||
|
).await.map_err(catch_unique_violation("reaction"))?;
|
||||||
|
update_reaction_count(&transaction, post_id, 1).await?;
|
||||||
|
transaction.commit().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in a new issue