Skip to content

Commit

Permalink
see who boosted/favourited a post in mastodon api (#367)
Browse files Browse the repository at this point in the history
  • Loading branch information
zeerooth authored Oct 14, 2023
1 parent f794109 commit 5cc9e95
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 2 deletions.
137 changes: 135 additions & 2 deletions crates/kitsune-core/src/service/post.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use super::{
job::{Enqueue, JobService},
notification::NotificationService,
url::UrlService,
LimitContext,
};
use crate::{
error::{ApiError, Error, Result},
Expand All @@ -20,15 +21,16 @@ use crate::{
};
use async_stream::try_stream;
use diesel::{
BelongingToDsl, BoolExpressionMethods, ExpressionMethods, OptionalExtension, QueryDsl,
SelectableHelper,
BelongingToDsl, BoolExpressionMethods, ExpressionMethods, JoinOnDsl, OptionalExtension,
QueryDsl, SelectableHelper,
};
use diesel_async::{AsyncPgConnection, RunQueryDsl};
use futures_util::{stream::BoxStream, Stream, StreamExt, TryStreamExt};
use garde::Validate;
use iso8601_timestamp::Timestamp;
use kitsune_db::{
model::{
account::Account,
favourite::{Favourite, NewFavourite},
media_attachment::NewPostMediaAttachment,
mention::NewMention,
Expand Down Expand Up @@ -253,6 +255,44 @@ pub struct UnrepostPost {
post_id: Uuid,
}

#[derive(Clone, TypedBuilder, Validate)]
#[garde(context(LimitContext as ctx))]
pub struct GetAccountsInteractingWithPost {
/// ID of the account whose posts are getting fetched
#[garde(skip)]
post_id: Uuid,

/// ID of the account that is requesting the posts
#[builder(default)]
#[garde(skip)]
fetching_account_id: Option<Uuid>,

/// Limit of returned posts
#[garde(range(max = ctx.limit))]
limit: usize,

/// Smallest ID, return results starting from this ID
///
/// Used for pagination
#[builder(default)]
#[garde(skip)]
min_id: Option<Uuid>,

/// Smallest ID, return highest results
///
/// Used for pagination
#[builder(default)]
#[garde(skip)]
since_id: Option<Uuid>,

/// Largest ID
///
/// Used for pagination
#[builder(default)]
#[garde(skip)]
max_id: Option<Uuid>,
}

#[derive(Clone, TypedBuilder)]
pub struct PostService {
db_pool: PgPool,
Expand Down Expand Up @@ -901,6 +941,99 @@ impl PostService {
Ok(post)
}

/// Get accounts that favourited a post
///
/// Does checks whether the user has access to the post
///
/// # Panics
///
/// This should never panic. If it does, please open an issue.
pub async fn favourited_by(
&self,
get_favourites: GetAccountsInteractingWithPost,
) -> Result<impl Stream<Item = Result<Account>> + '_> {
get_favourites.validate(&LimitContext::default())?;

let mut query = posts_favourites::table
.inner_join(accounts::table.on(posts_favourites::account_id.eq(accounts::id)))
.filter(posts_favourites::post_id.eq(get_favourites.post_id))
.select(Account::as_select())
.order(accounts::id.desc())
.limit(get_favourites.limit as i64)
.into_boxed();

if let Some(max_id) = get_favourites.max_id {
query = query.filter(posts_favourites::id.lt(max_id));
}
if let Some(since_id) = get_favourites.since_id {
query = query.filter(posts_favourites::id.gt(since_id));
}
if let Some(min_id) = get_favourites.min_id {
query = query
.filter(posts_favourites::id.gt(min_id))
.order(posts_favourites::id.asc());
}

self.db_pool
.with_connection(|db_conn| {
async move {
Ok::<_, Error>(query.load_stream(db_conn).await?.map_err(Error::from))
}
.scoped()
})
.await
.map_err(Error::from)
}

/// Get accounts that reblogged a post
///
/// Does checks whether the user has access to the post
///
/// # Panics
///
/// This should never panic. If it does, please open an issue.
pub async fn reblogged_by(
&self,
get_reblogs: GetAccountsInteractingWithPost,
) -> Result<impl Stream<Item = Result<Account>> + '_> {
get_reblogs.validate(&LimitContext::default())?;

let permission_check = PermissionCheck::builder()
.fetching_account_id(get_reblogs.fetching_account_id)
.build();

let mut query = posts::table
.add_post_permission_check(permission_check)
.filter(posts::reposted_post_id.eq(get_reblogs.post_id))
.into_boxed();

if let Some(max_id) = get_reblogs.max_id {
query = query.filter(posts::id.lt(max_id));
}
if let Some(since_id) = get_reblogs.since_id {
query = query.filter(posts::id.gt(since_id));
}
if let Some(min_id) = get_reblogs.min_id {
query = query.filter(posts::id.gt(min_id)).order(posts::id.asc());
}

let query = query
.inner_join(accounts::table.on(accounts::id.eq(posts::account_id)))
.select(Account::as_select())
.order(accounts::id.desc())
.limit(get_reblogs.limit as i64);

self.db_pool
.with_connection(|db_conn| {
async move {
Ok::<_, Error>(query.load_stream(db_conn).await?.map_err(Error::from))
}
.scoped()
})
.await
.map_err(Error::from)
}

/// Get a post by its ID
///
/// Does checks whether the user is allowed to fetch the post
Expand Down
84 changes: 84 additions & 0 deletions kitsune/src/http/handler/mastodon/api/v1/statuses/favourited_by.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use crate::{
consts::default_limit,
error::Result,
http::{
extractor::MastodonAuthExtractor,
pagination::{LinkHeader, PaginatedJsonResponse},
},
};
use axum::{
debug_handler,
extract::{OriginalUri, Path, State},
Json,
};
use axum_extra::extract::Query;
use futures_util::TryStreamExt;
use kitsune_core::{
mapping::MastodonMapper,
service::{
post::{GetAccountsInteractingWithPost, PostService},
url::UrlService,
},
};
use kitsune_type::mastodon::Account;
use serde::Deserialize;
use speedy_uuid::Uuid;
use utoipa::IntoParams;

#[derive(Deserialize, IntoParams)]
pub struct GetQuery {
min_id: Option<Uuid>,
max_id: Option<Uuid>,
since_id: Option<Uuid>,
#[serde(default = "default_limit")]
limit: usize,
}

#[debug_handler(state = crate::state::Zustand)]
#[utoipa::path(
get,
path = "/api/v1/statuses/{id}/favourited_by",
security(
("oauth_token" = [])
),
params(GetQuery),
responses(
(status = 200, description = "List of accounts that favourited the status", body = Vec<Account>)
),
)]
pub async fn get(
State(post_service): State<PostService>,
State(mastodon_mapper): State<MastodonMapper>,
State(url_service): State<UrlService>,
OriginalUri(original_uri): OriginalUri,
Query(query): Query<GetQuery>,
user_data: Option<MastodonAuthExtractor>,
Path(id): Path<Uuid>,
) -> Result<PaginatedJsonResponse<Account>> {
let fetching_account_id = user_data.map(|user_data| user_data.0.account.id);
let get_favourites = GetAccountsInteractingWithPost::builder()
.post_id(id)
.fetching_account_id(fetching_account_id)
.limit(query.limit)
.since_id(query.since_id)
.min_id(query.min_id)
.max_id(query.max_id)
.build();

let accounts: Vec<Account> = post_service
.favourited_by(get_favourites)
.await?
.and_then(|acc| mastodon_mapper.map(acc))
.try_collect()
.await?;

let link_header = LinkHeader::new(
&accounts,
query.limit,
&url_service.base_url(),
original_uri.path(),
|a| a.id,
);

Ok((link_header, Json(accounts)))
}
4 changes: 4 additions & 0 deletions kitsune/src/http/handler/mastodon/api/v1/statuses/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ use utoipa::ToSchema;

pub mod context;
pub mod favourite;
pub mod favourited_by;
pub mod reblog;
pub mod reblogged_by;
pub mod source;
pub mod unfavourite;
pub mod unreblog;
Expand Down Expand Up @@ -181,7 +183,9 @@ pub fn routes() -> Router<Zustand> {
.route("/:id", routing::get(get).delete(delete).put(put))
.route("/:id/context", routing::get(context::get))
.route("/:id/favourite", routing::post(favourite::post))
.route("/:id/favourited_by", routing::get(favourited_by::get))
.route("/:id/reblog", routing::post(reblog::post))
.route("/:id/reblogged_by", routing::get(reblogged_by::get))
.route("/:id/source", routing::get(source::get))
.route("/:id/unfavourite", routing::post(unfavourite::post))
.route("/:id/unreblog", routing::post(unreblog::post))
Expand Down
84 changes: 84 additions & 0 deletions kitsune/src/http/handler/mastodon/api/v1/statuses/reblogged_by.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use crate::{
consts::default_limit,
error::Result,
http::{
extractor::MastodonAuthExtractor,
pagination::{LinkHeader, PaginatedJsonResponse},
},
};
use axum::{
debug_handler,
extract::{OriginalUri, Path, State},
Json,
};
use axum_extra::extract::Query;
use futures_util::TryStreamExt;
use kitsune_core::{
mapping::MastodonMapper,
service::{
post::{GetAccountsInteractingWithPost, PostService},
url::UrlService,
},
};
use kitsune_type::mastodon::Account;
use serde::Deserialize;
use speedy_uuid::Uuid;
use utoipa::IntoParams;

#[derive(Deserialize, IntoParams)]
pub struct GetQuery {
min_id: Option<Uuid>,
max_id: Option<Uuid>,
since_id: Option<Uuid>,
#[serde(default = "default_limit")]
limit: usize,
}

#[debug_handler(state = crate::state::Zustand)]
#[utoipa::path(
get,
path = "/api/v1/statuses/{id}/reblogged_by",
security(
("oauth_token" = [])
),
params(GetQuery),
responses(
(status = 200, description = "List of accounts that reblogged the status", body = Vec<Account>)
),
)]
pub async fn get(
State(post_service): State<PostService>,
State(mastodon_mapper): State<MastodonMapper>,
State(url_service): State<UrlService>,
OriginalUri(original_uri): OriginalUri,
Query(query): Query<GetQuery>,
user_data: Option<MastodonAuthExtractor>,
Path(id): Path<Uuid>,
) -> Result<PaginatedJsonResponse<Account>> {
let fetching_account_id = user_data.map(|user_data| user_data.0.account.id);
let get_reblogs = GetAccountsInteractingWithPost::builder()
.post_id(id)
.fetching_account_id(fetching_account_id)
.limit(query.limit)
.since_id(query.since_id)
.min_id(query.min_id)
.max_id(query.max_id)
.build();

let accounts: Vec<Account> = post_service
.reblogged_by(get_reblogs)
.await?
.and_then(|acc| mastodon_mapper.map(acc))
.try_collect()
.await?;

let link_header = LinkHeader::new(
&accounts,
query.limit,
&url_service.base_url(),
original_uri.path(),
|a| a.id,
);

Ok((link_header, Json(accounts)))
}

0 comments on commit 5cc9e95

Please sign in to comment.