Add pagination headers to followers/following API responses
This commit is contained in:
parent
450e47bcef
commit
bdcdb06c51
9 changed files with 127 additions and 34 deletions
|
@ -298,6 +298,12 @@ paths:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Account'
|
||||
headers:
|
||||
Link:
|
||||
description: Link to the next page
|
||||
schema:
|
||||
type: string
|
||||
example: '<https://example.org/api/v1/accounts/1/followers?limit=40&max_id=7628164>; rel="next"'
|
||||
404:
|
||||
description: Profile not found
|
||||
/api/v1/accounts/{account_id}/following:
|
||||
|
@ -328,6 +334,12 @@ paths:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Account'
|
||||
headers:
|
||||
Link:
|
||||
description: Link to the next page
|
||||
schema:
|
||||
type: string
|
||||
example: '<https://example.org/api/v1/accounts/1/following?limit=40&max_id=7628164>; rel="next"'
|
||||
404:
|
||||
description: Profile not found
|
||||
/api/v1/accounts/{account_id}/follow:
|
||||
|
|
|
@ -44,7 +44,7 @@ pub async fn get_announce_note_recipients(
|
|||
current_user: &User,
|
||||
post: &Post,
|
||||
) -> Result<(Vec<Actor>, String), DatabaseError> {
|
||||
let followers = get_followers(db_client, ¤t_user.id, None, None).await?;
|
||||
let followers = get_followers(db_client, ¤t_user.id).await?;
|
||||
let mut recipients: Vec<Actor> = Vec::new();
|
||||
for profile in followers {
|
||||
if let Some(remote_actor) = profile.actor_json {
|
||||
|
|
|
@ -180,7 +180,7 @@ pub async fn get_note_recipients(
|
|||
let mut audience = vec![];
|
||||
match post.visibility {
|
||||
Visibility::Public | Visibility::Followers => {
|
||||
let followers = get_followers(db_client, ¤t_user.id, None, None).await?;
|
||||
let followers = get_followers(db_client, ¤t_user.id).await?;
|
||||
audience.extend(followers);
|
||||
},
|
||||
Visibility::Subscribers => {
|
||||
|
|
|
@ -34,8 +34,8 @@ async fn get_delete_person_recipients(
|
|||
db_client: &impl GenericClient,
|
||||
user_id: &Uuid,
|
||||
) -> Result<Vec<Actor>, DatabaseError> {
|
||||
let followers = get_followers(db_client, user_id, None, None).await?;
|
||||
let following = get_following(db_client, user_id, None, None).await?;
|
||||
let followers = get_followers(db_client, user_id).await?;
|
||||
let following = get_following(db_client, user_id).await?;
|
||||
let mut recipients = vec![];
|
||||
for profile in followers.into_iter().chain(following.into_iter()) {
|
||||
if let Some(remote_actor) = profile.actor_json {
|
||||
|
|
|
@ -41,7 +41,7 @@ async fn get_update_person_recipients(
|
|||
db_client: &impl GenericClient,
|
||||
user_id: &Uuid,
|
||||
) -> Result<Vec<Actor>, DatabaseError> {
|
||||
let followers = get_followers(db_client, user_id, None, None).await?;
|
||||
let followers = get_followers(db_client, user_id).await?;
|
||||
let mut recipients: Vec<Actor> = Vec::new();
|
||||
for profile in followers {
|
||||
if let Some(remote_actor) = profile.actor_json {
|
||||
|
|
|
@ -324,14 +324,14 @@ pub struct StatusListQueryParams {
|
|||
pub limit: i64,
|
||||
}
|
||||
|
||||
fn default_follow_list_page_size() -> i64 { 40 }
|
||||
fn default_follow_list_page_size() -> u8 { 40 }
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct FollowListQueryParams {
|
||||
pub max_id: Option<i32>,
|
||||
|
||||
#[serde(default = "default_follow_list_page_size")]
|
||||
pub limit: i64,
|
||||
pub limit: u8,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use actix_web::{get, post, patch, web, HttpResponse, Scope};
|
||||
use actix_web::{
|
||||
get, patch, post, web,
|
||||
HttpRequest, HttpResponse, Scope,
|
||||
};
|
||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -24,6 +27,7 @@ use crate::ethereum::subscriptions::{
|
|||
is_registered_recipient,
|
||||
};
|
||||
use crate::mastodon_api::oauth::auth::get_current_user;
|
||||
use crate::mastodon_api::pagination::get_paginated_response;
|
||||
use crate::mastodon_api::statuses::helpers::build_status_list;
|
||||
use crate::mastodon_api::statuses::types::Status;
|
||||
use crate::models::posts::queries::get_posts_by_author;
|
||||
|
@ -40,8 +44,8 @@ use crate::models::profiles::types::{
|
|||
use crate::models::relationships::queries::{
|
||||
create_follow_request,
|
||||
follow,
|
||||
get_followers,
|
||||
get_following,
|
||||
get_followers_paginated,
|
||||
get_following_paginated,
|
||||
hide_replies,
|
||||
hide_reposts,
|
||||
show_replies,
|
||||
|
@ -513,6 +517,7 @@ async fn get_account_followers(
|
|||
db_pool: web::Data<Pool>,
|
||||
account_id: web::Path<Uuid>,
|
||||
query_params: web::Query<FollowListQueryParams>,
|
||||
request: HttpRequest,
|
||||
) -> Result<HttpResponse, HttpError> {
|
||||
let db_client = &**get_database_client(&db_pool).await?;
|
||||
let current_user = get_current_user(db_client, auth.token()).await?;
|
||||
|
@ -522,16 +527,24 @@ async fn get_account_followers(
|
|||
let accounts: Vec<Account> = vec![];
|
||||
return Ok(HttpResponse::Ok().json(accounts));
|
||||
};
|
||||
let followers = get_followers(
|
||||
let followers = get_followers_paginated(
|
||||
db_client,
|
||||
&profile.id,
|
||||
query_params.max_id,
|
||||
Some(query_params.limit),
|
||||
query_params.limit.into(),
|
||||
).await?;
|
||||
let max_index = usize::from(query_params.limit.saturating_sub(1));
|
||||
let maybe_last_id = followers.get(max_index).map(|item| item.relationship_id);
|
||||
let accounts: Vec<Account> = followers.into_iter()
|
||||
.map(|profile| Account::from_profile(profile, &config.instance_url()))
|
||||
.map(|item| Account::from_profile(item.profile, &config.instance_url()))
|
||||
.collect();
|
||||
Ok(HttpResponse::Ok().json(accounts))
|
||||
let response = get_paginated_response(
|
||||
&config.instance_url(),
|
||||
request.uri().path(),
|
||||
accounts,
|
||||
maybe_last_id,
|
||||
);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[get("/{account_id}/following")]
|
||||
|
@ -541,6 +554,7 @@ async fn get_account_following(
|
|||
db_pool: web::Data<Pool>,
|
||||
account_id: web::Path<Uuid>,
|
||||
query_params: web::Query<FollowListQueryParams>,
|
||||
request: HttpRequest,
|
||||
) -> Result<HttpResponse, HttpError> {
|
||||
let db_client = &**get_database_client(&db_pool).await?;
|
||||
let current_user = get_current_user(db_client, auth.token()).await?;
|
||||
|
@ -550,16 +564,24 @@ async fn get_account_following(
|
|||
let accounts: Vec<Account> = vec![];
|
||||
return Ok(HttpResponse::Ok().json(accounts));
|
||||
};
|
||||
let following = get_following(
|
||||
let following = get_following_paginated(
|
||||
db_client,
|
||||
&profile.id,
|
||||
query_params.max_id,
|
||||
Some(query_params.limit),
|
||||
query_params.limit.into(),
|
||||
).await?;
|
||||
let max_index = usize::from(query_params.limit.saturating_sub(1));
|
||||
let maybe_last_id = following.get(max_index).map(|item| item.relationship_id);
|
||||
let accounts: Vec<Account> = following.into_iter()
|
||||
.map(|profile| Account::from_profile(profile, &config.instance_url()))
|
||||
.map(|item| Account::from_profile(item.profile, &config.instance_url()))
|
||||
.collect();
|
||||
Ok(HttpResponse::Ok().json(accounts))
|
||||
let response = get_paginated_response(
|
||||
&config.instance_url(),
|
||||
request.uri().path(),
|
||||
accounts,
|
||||
maybe_last_id,
|
||||
);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn account_api_scope() -> Scope {
|
||||
|
|
|
@ -16,6 +16,7 @@ use super::types::{
|
|||
DbFollowRequest,
|
||||
DbRelationship,
|
||||
FollowRequestStatus,
|
||||
RelatedActorProfile,
|
||||
RelationshipType,
|
||||
};
|
||||
|
||||
|
@ -257,8 +258,6 @@ pub async fn get_follow_request_by_path(
|
|||
pub async fn get_followers(
|
||||
db_client: &impl GenericClient,
|
||||
profile_id: &Uuid,
|
||||
max_relationship_id: Option<i32>,
|
||||
limit: Option<i64>,
|
||||
) -> Result<Vec<DbActorProfile>, DatabaseError> {
|
||||
let rows = db_client.query(
|
||||
"
|
||||
|
@ -266,6 +265,30 @@ pub async fn get_followers(
|
|||
FROM actor_profile
|
||||
JOIN relationship
|
||||
ON (actor_profile.id = relationship.source_id)
|
||||
WHERE
|
||||
relationship.target_id = $1
|
||||
AND relationship.relationship_type = $2
|
||||
",
|
||||
&[&profile_id, &RelationshipType::Follow],
|
||||
).await?;
|
||||
let profiles = rows.iter()
|
||||
.map(|row| row.try_get("actor_profile"))
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(profiles)
|
||||
}
|
||||
|
||||
pub async fn get_followers_paginated(
|
||||
db_client: &impl GenericClient,
|
||||
profile_id: &Uuid,
|
||||
max_relationship_id: Option<i32>,
|
||||
limit: i64,
|
||||
) -> Result<Vec<RelatedActorProfile>, DatabaseError> {
|
||||
let rows = db_client.query(
|
||||
"
|
||||
SELECT relationship.id, actor_profile
|
||||
FROM actor_profile
|
||||
JOIN relationship
|
||||
ON (actor_profile.id = relationship.source_id)
|
||||
WHERE
|
||||
relationship.target_id = $1
|
||||
AND relationship.relationship_type = $2
|
||||
|
@ -275,21 +298,43 @@ pub async fn get_followers(
|
|||
",
|
||||
&[&profile_id, &RelationshipType::Follow, &max_relationship_id, &limit],
|
||||
).await?;
|
||||
let related_profiles = rows.iter()
|
||||
.map(RelatedActorProfile::try_from)
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(related_profiles)
|
||||
}
|
||||
|
||||
pub async fn get_following(
|
||||
db_client: &impl GenericClient,
|
||||
profile_id: &Uuid,
|
||||
) -> Result<Vec<DbActorProfile>, DatabaseError> {
|
||||
let rows = db_client.query(
|
||||
"
|
||||
SELECT actor_profile
|
||||
FROM actor_profile
|
||||
JOIN relationship
|
||||
ON (actor_profile.id = relationship.target_id)
|
||||
WHERE
|
||||
relationship.source_id = $1
|
||||
AND relationship.relationship_type = $2
|
||||
",
|
||||
&[&profile_id, &RelationshipType::Follow],
|
||||
).await?;
|
||||
let profiles = rows.iter()
|
||||
.map(|row| row.try_get("actor_profile"))
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(profiles)
|
||||
}
|
||||
|
||||
pub async fn get_following(
|
||||
pub async fn get_following_paginated(
|
||||
db_client: &impl GenericClient,
|
||||
profile_id: &Uuid,
|
||||
max_relationship_id: Option<i32>,
|
||||
limit: Option<i64>,
|
||||
) -> Result<Vec<DbActorProfile>, DatabaseError> {
|
||||
limit: i64,
|
||||
) -> Result<Vec<RelatedActorProfile>, DatabaseError> {
|
||||
let rows = db_client.query(
|
||||
"
|
||||
SELECT actor_profile
|
||||
SELECT relationship.id, actor_profile
|
||||
FROM actor_profile
|
||||
JOIN relationship
|
||||
ON (actor_profile.id = relationship.target_id)
|
||||
|
@ -302,10 +347,10 @@ pub async fn get_following(
|
|||
",
|
||||
&[&profile_id, &RelationshipType::Follow, &max_relationship_id, &limit],
|
||||
).await?;
|
||||
let profiles = rows.iter()
|
||||
.map(|row| row.try_get("actor_profile"))
|
||||
let related_profiles = rows.iter()
|
||||
.map(RelatedActorProfile::try_from)
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(profiles)
|
||||
Ok(related_profiles)
|
||||
}
|
||||
|
||||
pub async fn subscribe(
|
||||
|
@ -486,8 +531,7 @@ mod tests {
|
|||
follow_request.request_status,
|
||||
FollowRequestStatus::Pending,
|
||||
));
|
||||
let following = get_following(db_client, &source.id, None, None)
|
||||
.await.unwrap();
|
||||
let following = get_following(db_client, &source.id).await.unwrap();
|
||||
assert!(following.is_empty());
|
||||
// Accept follow request
|
||||
follow_request_accepted(db_client, &follow_request.id).await.unwrap();
|
||||
|
@ -500,8 +544,7 @@ mod tests {
|
|||
follow_request.request_status,
|
||||
FollowRequestStatus::Accepted,
|
||||
));
|
||||
let following = get_following(db_client, &source.id, None, None)
|
||||
.await.unwrap();
|
||||
let following = get_following(db_client, &source.id).await.unwrap();
|
||||
assert_eq!(following[0].id, target.id);
|
||||
// Unfollow
|
||||
let follow_request_id = unfollow(db_client, &source.id, &target.id)
|
||||
|
@ -513,8 +556,7 @@ mod tests {
|
|||
follow_request_result,
|
||||
Err(DatabaseError::NotFound("follow request")),
|
||||
));
|
||||
let following = get_following(db_client, &source.id, None, None)
|
||||
.await.unwrap();
|
||||
let following = get_following(db_client, &source.id).await.unwrap();
|
||||
assert!(following.is_empty());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@ use tokio_postgres::Row;
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::database::int_enum::{int_enum_from_sql, int_enum_to_sql};
|
||||
use crate::errors::ConversionError;
|
||||
use crate::errors::{ConversionError, DatabaseError};
|
||||
use crate::models::profiles::types::DbActorProfile;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RelationshipType {
|
||||
|
@ -125,3 +126,19 @@ pub struct DbFollowRequest {
|
|||
pub target_id: Uuid,
|
||||
pub request_status: FollowRequestStatus,
|
||||
}
|
||||
|
||||
pub struct RelatedActorProfile {
|
||||
pub relationship_id: i32,
|
||||
pub profile: DbActorProfile,
|
||||
}
|
||||
|
||||
impl TryFrom<&Row> for RelatedActorProfile {
|
||||
|
||||
type Error = DatabaseError;
|
||||
|
||||
fn try_from(row: &Row) -> Result<Self, Self::Error> {
|
||||
let relationship_id = row.try_get("id")?;
|
||||
let profile = row.try_get("actor_profile")?;
|
||||
Ok(Self { relationship_id, profile })
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue