Add pagination headers to followers/following API responses

This commit is contained in:
silverpill 2022-08-06 22:13:47 +00:00
parent 450e47bcef
commit bdcdb06c51
9 changed files with 127 additions and 34 deletions

View file

@ -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:

View file

@ -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, &current_user.id, None, None).await?;
let followers = get_followers(db_client, &current_user.id).await?;
let mut recipients: Vec<Actor> = Vec::new();
for profile in followers {
if let Some(remote_actor) = profile.actor_json {

View file

@ -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, &current_user.id, None, None).await?;
let followers = get_followers(db_client, &current_user.id).await?;
audience.extend(followers);
},
Visibility::Subscribers => {

View file

@ -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 {

View file

@ -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 {

View file

@ -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)]

View file

@ -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 {

View file

@ -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());
}
}

View file

@ -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 })
}
}