Add API methods for retrieving followers and following lists

This commit is contained in:
silverpill 2021-12-29 12:07:56 +00:00
parent 40958500c1
commit 3993c76c65
6 changed files with 175 additions and 7 deletions

View file

@ -118,6 +118,9 @@ PATCH /api/v1/accounts/update_credentials
GET /api/v1/accounts/relationships
POST /api/v1/accounts/{account_id}/follow
POST /api/v1/accounts/{account_id}/unfollow
GET /api/v1/accounts/{account_id}/statuses
GET /api/v1/accounts/{account_id}/followers
GET /api/v1/accounts/{account_id}/following
GET /api/v1/directory
GET /api/v1/instance
GET /api/v1/markers
@ -145,7 +148,7 @@ GET /api/v1/statuses/{status_id}/signature
POST /api/v1/statuses/{status_id}/token_minted
```
[OpenAPI spec](./docs/openapi.yaml)
[OpenAPI spec](./docs/openapi.yaml) (incomplete)
## CLI commands

View file

@ -144,6 +144,66 @@ paths:
type: array
items:
$ref: '#/components/schemas/Status'
/api/v1/accounts/{account_id}/followers:
get:
summary: Actors which follow the given actor.
parameters:
- $ref: '#/components/parameters/account_id'
- name: max_id
in: query
description: Return results with relationship ID older than this value.
required: false
schema:
type: integer
- name: limit
in: query
description: Maximum number of results to return.
required: false
schema:
type: integer
default: 40
responses:
200:
description: Successful operation
content:
application/json:
schema:
description: Profile list
type: array
items:
$ref: '#/components/schemas/Account'
404:
description: Profile not found
/api/v1/accounts/{account_id}/following:
get:
summary: Actors which the given actor is following.
parameters:
- $ref: '#/components/parameters/account_id'
- name: max_id
in: query
description: Return results with relationship ID older than this value.
required: false
schema:
type: integer
- name: limit
in: query
description: Maximum number of results to return.
required: false
schema:
type: integer
default: 40
responses:
200:
description: Successful operation
content:
application/json:
schema:
description: Profile list
type: array
items:
$ref: '#/components/schemas/Account'
404:
description: Profile not found
/api/v1/statuses/{status_id}:
delete:
summary: Delete post

View file

@ -174,6 +174,16 @@ impl AccountUpdateData {
}
}
fn default_page_size() -> i64 { 40 }
#[derive(Deserialize)]
pub struct FollowListQueryParams {
pub max_id: Option<i32>,
#[serde(default = "default_page_size")]
pub limit: i64,
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -31,6 +31,7 @@ use crate::models::relationships::queries::{
follow,
get_follow_request_by_path,
get_followers,
get_following,
get_relationship,
get_relationships,
unfollow,
@ -45,7 +46,12 @@ use crate::utils::crypto::{
serialize_private_key,
};
use crate::utils::files::FileError;
use super::types::{Account, AccountCreateData, AccountUpdateData};
use super::types::{
Account,
AccountCreateData,
AccountUpdateData,
FollowListQueryParams,
};
#[post("")]
pub async fn create_account(
@ -154,7 +160,7 @@ async fn update_credentials(
// Federate
let activity = create_activity_update_person(&current_user, &config.instance_url())
.map_err(|_| HttpError::InternalError)?;
let followers = get_followers(db_client, &current_user.id).await?;
let followers = get_followers(db_client, &current_user.id, None, None).await?;
let mut recipients: Vec<Actor> = Vec::new();
for follower in followers {
if let Some(remote_actor) = follower.actor_json {
@ -298,6 +304,62 @@ async fn get_account_statuses(
Ok(HttpResponse::Ok().json(statuses))
}
#[get("/{account_id}/followers")]
async fn get_account_followers(
auth: BearerAuth,
config: web::Data<Config>,
db_pool: web::Data<Pool>,
web::Path(account_id): web::Path<Uuid>,
query_params: web::Query<FollowListQueryParams>,
) -> Result<HttpResponse, HttpError> {
let db_client = &**get_database_client(&db_pool).await?;
let current_user = get_current_user(db_client, auth.token()).await?;
let profile = get_profile_by_id(db_client, &account_id).await?;
if profile.id != current_user.id {
// Social graph is hidden
let accounts: Vec<Account> = vec![];
return Ok(HttpResponse::Ok().json(accounts));
};
let followers = get_followers(
db_client,
&profile.id,
query_params.max_id,
Some(query_params.limit),
).await?;
let accounts: Vec<Account> = followers.into_iter()
.map(|profile| Account::from_profile(profile, &config.instance_url()))
.collect();
Ok(HttpResponse::Ok().json(accounts))
}
#[get("/{account_id}/following")]
async fn get_account_following(
auth: BearerAuth,
config: web::Data<Config>,
db_pool: web::Data<Pool>,
web::Path(account_id): web::Path<Uuid>,
query_params: web::Query<FollowListQueryParams>,
) -> Result<HttpResponse, HttpError> {
let db_client = &**get_database_client(&db_pool).await?;
let current_user = get_current_user(db_client, auth.token()).await?;
let profile = get_profile_by_id(db_client, &account_id).await?;
if profile.id != current_user.id {
// Social graph is hidden
let accounts: Vec<Account> = vec![];
return Ok(HttpResponse::Ok().json(accounts));
};
let following = get_following(
db_client,
&profile.id,
query_params.max_id,
Some(query_params.limit),
).await?;
let accounts: Vec<Account> = following.into_iter()
.map(|profile| Account::from_profile(profile, &config.instance_url()))
.collect();
Ok(HttpResponse::Ok().json(accounts))
}
pub fn account_api_scope() -> Scope {
web::scope("/api/v1/accounts")
// Routes without account ID
@ -310,4 +372,6 @@ pub fn account_api_scope() -> Scope {
.service(follow_account)
.service(unfollow_account)
.service(get_account_statuses)
.service(get_account_followers)
.service(get_account_following)
}

View file

@ -12,7 +12,7 @@ pub async fn get_note_audience(
current_user: &User,
post: &Post,
) -> Result<Vec<Actor>, DatabaseError> {
let mut audience = get_followers(db_client, &current_user.id).await?;
let mut audience = get_followers(db_client, &current_user.id, None, None).await?;
if let Some(in_reply_to_id) = post.in_reply_to_id {
// TODO: use post.in_reply_to ?
let in_reply_to_author = get_post_author(db_client, &in_reply_to_id).await?;
@ -51,7 +51,7 @@ pub async fn get_announce_audience(
current_user: &User,
post: &Post,
) -> Result<Audience, DatabaseError> {
let followers = get_followers(db_client, &current_user.id).await?;
let followers = get_followers(db_client, &current_user.id, None, None).await?;
let mut recipients: Vec<Actor> = Vec::new();
for profile in followers {
if let Some(remote_actor) = profile.actor_json {

View file

@ -242,6 +242,8 @@ 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(
"
@ -249,10 +251,39 @@ pub async fn get_followers(
FROM actor_profile
JOIN relationship
ON (actor_profile.id = relationship.source_id)
WHERE relationship.target_id = $1
WHERE
relationship.target_id = $1
AND ($2::integer IS NULL OR relationship.id < $2)
ORDER BY relationship.id DESC
LIMIT $3
",
&[&profile_id],
&[&profile_id, &max_relationship_id, &limit],
).await?;
let profiles = rows.iter()
.map(|row| row.try_get("actor_profile"))
.collect::<Result<_, _>>()?;
Ok(profiles)
}
pub async fn get_following(
db_client: &impl GenericClient,
profile_id: &Uuid,
max_relationship_id: Option<i32>,
limit: Option<i64>,
) -> 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 ($2::integer IS NULL OR relationship.id < $2)
ORDER BY relationship.id DESC
LIMIT $3
",
&[&profile_id, &max_relationship_id, &limit],
).await?;
let profiles = rows.iter()
.map(|row| row.try_get("actor_profile"))