Add API method for listing incoming subscriptions

This commit is contained in:
silverpill 2022-07-31 10:54:09 +00:00
parent bdcdb06c51
commit 1721eb4a88
6 changed files with 154 additions and 2 deletions

View file

@ -342,6 +342,36 @@ paths:
example: '<https://example.org/api/v1/accounts/1/following?limit=40&max_id=7628164>; rel="next"' example: '<https://example.org/api/v1/accounts/1/following?limit=40&max_id=7628164>; rel="next"'
404: 404:
description: Profile not found description: Profile not found
/api/v1/accounts/{account_id}/subscribers:
get:
summary: Subscriptions to the given actor.
parameters:
- $ref: '#/components/parameters/account_id'
- name: max_id
in: query
description: Return results with subscription 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: Subscription list
type: array
items:
$ref: '#/components/schemas/Subscription'
404:
description: Profile not found
/api/v1/accounts/{account_id}/follow: /api/v1/accounts/{account_id}/follow:
post: post:
summary: Follow the given actor. summary: Follow the given actor.
@ -997,6 +1027,18 @@ components:
type: string type: string
nullable: true nullable: true
example: '0x5fe80cdea7f...' example: '0x5fe80cdea7f...'
Subscription:
type: object
properties:
id:
description: Subscription ID.
type: number
sender:
$ref: '#/components/schemas/Account'
sender_address:
description: Sender address.
type: string
example: '0xd8da6bf...'
Tag: Tag:
type: object type: object
properties: properties:

View file

@ -15,6 +15,7 @@ use crate::models::profiles::types::{
ProfileUpdateData, ProfileUpdateData,
}; };
use crate::models::profiles::validators::validate_username; use crate::models::profiles::validators::validate_username;
use crate::models::subscriptions::types::Subscription;
use crate::models::users::types::{ use crate::models::users::types::{
validate_local_username, validate_local_username,
User, User,
@ -334,6 +335,27 @@ pub struct FollowListQueryParams {
pub limit: u8, pub limit: u8,
} }
#[derive(Serialize)]
pub struct ApiSubscription {
pub id: i32,
pub sender: Account,
pub sender_address: String,
}
impl ApiSubscription {
pub fn from_subscription(
instance_url: &str,
subscription: Subscription,
) -> Self {
let sender = Account::from_profile(subscription.sender, instance_url);
Self {
id: subscription.id,
sender,
sender_address: subscription.sender_address,
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -52,6 +52,7 @@ use crate::models::relationships::queries::{
show_reposts, show_reposts,
unfollow, unfollow,
}; };
use crate::models::subscriptions::queries::get_incoming_subscriptions;
use crate::models::users::queries::{ use crate::models::users::queries::{
is_valid_invite_code, is_valid_invite_code,
create_user, create_user,
@ -76,6 +77,7 @@ use super::types::{
SearchDidQueryParams, SearchDidQueryParams,
StatusListQueryParams, StatusListQueryParams,
SubscriptionQueryParams, SubscriptionQueryParams,
ApiSubscription,
}; };
#[post("")] #[post("")]
@ -584,6 +586,36 @@ async fn get_account_following(
Ok(response) Ok(response)
} }
#[get("/{account_id}/subscribers")]
async fn get_account_subscribers(
auth: BearerAuth,
config: web::Data<Config>,
db_pool: web::Data<Pool>,
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 subscriptions: Vec<ApiSubscription> = vec![];
return Ok(HttpResponse::Ok().json(subscriptions));
};
let instance_url = config.instance_url();
let subscriptions: Vec<ApiSubscription> = get_incoming_subscriptions(
db_client,
&profile.id,
query_params.max_id,
query_params.limit.into(),
)
.await?
.into_iter()
.map(|item| ApiSubscription::from_subscription(&instance_url, item))
.collect();
Ok(HttpResponse::Ok().json(subscriptions))
}
pub fn account_api_scope() -> Scope { pub fn account_api_scope() -> Scope {
web::scope("/api/v1/accounts") web::scope("/api/v1/accounts")
// Routes without account ID // Routes without account ID
@ -603,4 +635,5 @@ pub fn account_api_scope() -> Scope {
.service(get_account_statuses) .service(get_account_statuses)
.service(get_account_followers) .service(get_account_followers)
.service(get_account_following) .service(get_account_following)
.service(get_account_subscribers)
} }

View file

@ -1,2 +1,2 @@
pub mod queries; pub mod queries;
mod types; pub mod types;

View file

@ -1,3 +1,5 @@
use std::convert::TryFrom;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use tokio_postgres::GenericClient; use tokio_postgres::GenericClient;
use uuid::Uuid; use uuid::Uuid;
@ -6,7 +8,7 @@ use crate::database::catch_unique_violation;
use crate::errors::DatabaseError; use crate::errors::DatabaseError;
use crate::models::relationships::queries::{subscribe, subscribe_opt}; use crate::models::relationships::queries::{subscribe, subscribe_opt};
use crate::models::relationships::types::RelationshipType; use crate::models::relationships::types::RelationshipType;
use super::types::DbSubscription; use super::types::{DbSubscription, Subscription};
pub async fn create_subscription( pub async fn create_subscription(
db_client: &mut impl GenericClient, db_client: &mut impl GenericClient,
@ -111,3 +113,29 @@ pub async fn get_expired_subscriptions(
.collect::<Result<_, _>>()?; .collect::<Result<_, _>>()?;
Ok(subscriptions) Ok(subscriptions)
} }
pub async fn get_incoming_subscriptions(
db_client: &impl GenericClient,
recipient_id: &Uuid,
max_subscription_id: Option<i32>,
limit: i64,
) -> Result<Vec<Subscription>, DatabaseError> {
let rows = db_client.query(
"
SELECT subscription, actor_profile AS sender
FROM actor_profile
JOIN subscription
ON (actor_profile.id = subscription.sender_id)
WHERE
subscription.recipient_id = $1
AND ($2::integer IS NULL OR subscription.id < $2)
ORDER BY subscription.id DESC
LIMIT $3
",
&[&recipient_id, &max_subscription_id, &limit],
).await?;
let subscriptions = rows.iter()
.map(Subscription::try_from)
.collect::<Result<_, _>>()?;
Ok(subscriptions)
}

View file

@ -1,7 +1,13 @@
use std::convert::TryFrom;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use postgres_types::FromSql; use postgres_types::FromSql;
use tokio_postgres::Row;
use uuid::Uuid; use uuid::Uuid;
use crate::errors::DatabaseError;
use crate::models::profiles::types::DbActorProfile;
#[derive(FromSql)] #[derive(FromSql)]
#[postgres(name = "subscription")] #[postgres(name = "subscription")]
pub struct DbSubscription { pub struct DbSubscription {
@ -12,3 +18,24 @@ pub struct DbSubscription {
pub expires_at: DateTime<Utc>, pub expires_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
} }
pub struct Subscription {
pub id: i32,
pub sender: DbActorProfile,
pub sender_address: String,
}
impl TryFrom<&Row> for Subscription {
type Error = DatabaseError;
fn try_from(row: &Row) -> Result<Self, Self::Error> {
let db_subscription: DbSubscription = row.try_get("subscription")?;
let db_sender: DbActorProfile = row.try_get("sender")?;
Ok(Self {
id: db_subscription.id,
sender: db_sender,
sender_address: db_subscription.sender_address,
})
}
}