diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 112dfdd..f4b6b1a 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -342,6 +342,36 @@ paths: example: '; rel="next"' 404: 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: post: summary: Follow the given actor. @@ -997,6 +1027,18 @@ components: type: string nullable: true 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: type: object properties: diff --git a/src/mastodon_api/accounts/types.rs b/src/mastodon_api/accounts/types.rs index fdc7fb3..9eaeb63 100644 --- a/src/mastodon_api/accounts/types.rs +++ b/src/mastodon_api/accounts/types.rs @@ -15,6 +15,7 @@ use crate::models::profiles::types::{ ProfileUpdateData, }; use crate::models::profiles::validators::validate_username; +use crate::models::subscriptions::types::Subscription; use crate::models::users::types::{ validate_local_username, User, @@ -334,6 +335,27 @@ pub struct FollowListQueryParams { 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)] mod tests { use super::*; diff --git a/src/mastodon_api/accounts/views.rs b/src/mastodon_api/accounts/views.rs index 34e2ab3..634fd9b 100644 --- a/src/mastodon_api/accounts/views.rs +++ b/src/mastodon_api/accounts/views.rs @@ -52,6 +52,7 @@ use crate::models::relationships::queries::{ show_reposts, unfollow, }; +use crate::models::subscriptions::queries::get_incoming_subscriptions; use crate::models::users::queries::{ is_valid_invite_code, create_user, @@ -76,6 +77,7 @@ use super::types::{ SearchDidQueryParams, StatusListQueryParams, SubscriptionQueryParams, + ApiSubscription, }; #[post("")] @@ -584,6 +586,36 @@ async fn get_account_following( Ok(response) } +#[get("/{account_id}/subscribers")] +async fn get_account_subscribers( + auth: BearerAuth, + config: web::Data, + db_pool: web::Data, + account_id: web::Path, + query_params: web::Query, +) -> Result { + 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 = vec![]; + return Ok(HttpResponse::Ok().json(subscriptions)); + }; + let instance_url = config.instance_url(); + let subscriptions: Vec = 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 { web::scope("/api/v1/accounts") // Routes without account ID @@ -603,4 +635,5 @@ pub fn account_api_scope() -> Scope { .service(get_account_statuses) .service(get_account_followers) .service(get_account_following) + .service(get_account_subscribers) } diff --git a/src/models/subscriptions/mod.rs b/src/models/subscriptions/mod.rs index ec98503..0333ab5 100644 --- a/src/models/subscriptions/mod.rs +++ b/src/models/subscriptions/mod.rs @@ -1,2 +1,2 @@ pub mod queries; -mod types; +pub mod types; diff --git a/src/models/subscriptions/queries.rs b/src/models/subscriptions/queries.rs index 8c8411b..3710f10 100644 --- a/src/models/subscriptions/queries.rs +++ b/src/models/subscriptions/queries.rs @@ -1,3 +1,5 @@ +use std::convert::TryFrom; + use chrono::{DateTime, Utc}; use tokio_postgres::GenericClient; use uuid::Uuid; @@ -6,7 +8,7 @@ use crate::database::catch_unique_violation; use crate::errors::DatabaseError; use crate::models::relationships::queries::{subscribe, subscribe_opt}; use crate::models::relationships::types::RelationshipType; -use super::types::DbSubscription; +use super::types::{DbSubscription, Subscription}; pub async fn create_subscription( db_client: &mut impl GenericClient, @@ -111,3 +113,29 @@ pub async fn get_expired_subscriptions( .collect::>()?; Ok(subscriptions) } + +pub async fn get_incoming_subscriptions( + db_client: &impl GenericClient, + recipient_id: &Uuid, + max_subscription_id: Option, + limit: i64, +) -> Result, 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::>()?; + Ok(subscriptions) +} diff --git a/src/models/subscriptions/types.rs b/src/models/subscriptions/types.rs index 7972e0f..a403ec3 100644 --- a/src/models/subscriptions/types.rs +++ b/src/models/subscriptions/types.rs @@ -1,7 +1,13 @@ +use std::convert::TryFrom; + use chrono::{DateTime, Utc}; use postgres_types::FromSql; +use tokio_postgres::Row; use uuid::Uuid; +use crate::errors::DatabaseError; +use crate::models::profiles::types::DbActorProfile; + #[derive(FromSql)] #[postgres(name = "subscription")] pub struct DbSubscription { @@ -12,3 +18,24 @@ pub struct DbSubscription { pub expires_at: DateTime, pub updated_at: DateTime, } + +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 { + 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, + }) + } +}