Add API method for adding subscription as payment option
This commit is contained in:
parent
e573ecb27b
commit
1554780b35
7 changed files with 128 additions and 4 deletions
|
@ -159,6 +159,22 @@ paths:
|
|||
description: User's wallet address is not known or not verified
|
||||
418:
|
||||
description: Blockchain integration is not enabled
|
||||
/api/v1/accounts/subscription_enabled:
|
||||
post:
|
||||
summary: Notify server about subscription setup
|
||||
responses:
|
||||
200:
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Account'
|
||||
400:
|
||||
description: User hasn't enabled subscriptions
|
||||
403:
|
||||
description: User's wallet address is not known or not verified
|
||||
418:
|
||||
description: Blockchain integration is not enabled
|
||||
/api/v1/accounts/relationships:
|
||||
get:
|
||||
summary: Find out whether a given actor is followed, blocked, muted, etc.
|
||||
|
@ -717,6 +733,11 @@ components:
|
|||
type: string
|
||||
nullable: true
|
||||
example: '0xd8da6bf...'
|
||||
subscription_page_url:
|
||||
description: Subscription page URL
|
||||
type: string
|
||||
nullable: true
|
||||
example: 'https://example.com/profile/1/subscription'
|
||||
Attachment:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
@ -86,6 +86,7 @@ pub struct ContractSet {
|
|||
pub gate: Option<Contract<Http>>,
|
||||
pub collectible: Option<Contract<Http>>,
|
||||
pub subscription: Option<Contract<Http>>,
|
||||
pub subscription_adapter: Option<Contract<Http>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -115,6 +116,7 @@ pub async fn get_contracts(
|
|||
let mut maybe_gate = None;
|
||||
let mut maybe_collectible = None;
|
||||
let mut maybe_subscription = None;
|
||||
let mut maybe_subscription_adapter = None;
|
||||
let mut sync_targets = vec![];
|
||||
|
||||
let gate_abi = load_abi(&config.contract_dir, GATE)?;
|
||||
|
@ -172,6 +174,7 @@ pub async fn get_contracts(
|
|||
log::info!("subscription contract address is {:?}", subscription.address());
|
||||
sync_targets.push(subscription.address());
|
||||
maybe_subscription = Some(subscription);
|
||||
maybe_subscription_adapter = Some(subscription_adapter);
|
||||
};
|
||||
|
||||
let current_block = get_current_block_number(&web3, storage_dir).await?;
|
||||
|
@ -188,6 +191,7 @@ pub async fn get_contracts(
|
|||
gate: maybe_gate,
|
||||
collectible: maybe_collectible,
|
||||
subscription: maybe_subscription,
|
||||
subscription_adapter: maybe_subscription_adapter,
|
||||
};
|
||||
Ok(Blockchain { contract_set, sync_state })
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ use chrono::{DateTime, TimeZone, Utc};
|
|||
|
||||
use web3::{
|
||||
api::Web3,
|
||||
contract::Contract,
|
||||
contract::{Contract, Options},
|
||||
ethabi::RawLog,
|
||||
transports::Http,
|
||||
types::{BlockId, BlockNumber, FilterBuilder, U256},
|
||||
|
@ -38,6 +38,7 @@ use crate::models::users::queries::{
|
|||
get_user_by_id,
|
||||
get_user_by_wallet_address,
|
||||
};
|
||||
use super::contracts::ContractSet;
|
||||
use super::errors::EthereumError;
|
||||
use super::signatures::{
|
||||
encode_uint256,
|
||||
|
@ -263,3 +264,19 @@ pub fn create_subscription_signature(
|
|||
)?;
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
pub async fn is_registered_recipient(
|
||||
contract_set: &ContractSet,
|
||||
user_address: &str,
|
||||
) -> Result<bool, EthereumError> {
|
||||
let adapter = match &contract_set.subscription_adapter {
|
||||
Some(contract) => contract,
|
||||
None => return Ok(false),
|
||||
};
|
||||
let user_address = parse_address(user_address)?;
|
||||
let result: bool = adapter.query(
|
||||
"isSubscriptionConfigured", (user_address,),
|
||||
None, Options::default(), None,
|
||||
).await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
|
|
@ -13,3 +13,10 @@ pub fn get_post_page_url(instance_url: &str, post_id: &Uuid) -> String {
|
|||
pub fn get_tag_page_url(instance_url: &str, tag_name: &str) -> String {
|
||||
format!("{}/tag/{}", instance_url, tag_name)
|
||||
}
|
||||
|
||||
pub fn get_subscription_page_url(instance_url: &str, profile_id: &Uuid) -> String {
|
||||
format!(
|
||||
"{}/subscription",
|
||||
get_profile_page_url(instance_url, profile_id),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,12 +5,14 @@ use serde::{Deserialize, Serialize};
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::errors::ValidationError;
|
||||
use crate::frontend::get_subscription_page_url;
|
||||
use crate::models::profiles::currencies::get_identity_proof_field_name;
|
||||
use crate::models::profiles::types::{
|
||||
DbActorProfile,
|
||||
ExtraField,
|
||||
IdentityProof,
|
||||
PaymentOption,
|
||||
PaymentType,
|
||||
ProfileUpdateData,
|
||||
};
|
||||
use crate::models::profiles::validators::validate_username;
|
||||
|
@ -56,6 +58,7 @@ pub struct Account {
|
|||
pub source: Option<Source>,
|
||||
|
||||
pub wallet_address: Option<String>,
|
||||
pub subscription_page_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
|
@ -67,7 +70,7 @@ impl Account {
|
|||
.map(|name| get_file_url(instance_url, name));
|
||||
|
||||
let mut identity_proofs = vec![];
|
||||
for proof in profile.identity_proofs.into_inner() {
|
||||
for proof in profile.identity_proofs.clone().into_inner() {
|
||||
// Skip proof if it doesn't map to field name
|
||||
if let Some(field_name) = get_identity_proof_field_name(&proof.proof_type) {
|
||||
let field = AccountField {
|
||||
|
@ -79,8 +82,9 @@ impl Account {
|
|||
identity_proofs.push(field);
|
||||
};
|
||||
};
|
||||
|
||||
let mut extra_fields = vec![];
|
||||
for extra_field in profile.extra_fields.into_inner() {
|
||||
for extra_field in profile.extra_fields.clone().into_inner() {
|
||||
let field = AccountField {
|
||||
name: extra_field.name,
|
||||
value: extra_field.value,
|
||||
|
@ -89,6 +93,18 @@ impl Account {
|
|||
extra_fields.push(field);
|
||||
};
|
||||
|
||||
let subscription_page_url = profile.payment_options.clone()
|
||||
.into_inner().into_iter()
|
||||
.map(|option| {
|
||||
match option.payment_type {
|
||||
PaymentType::Link => option.href.unwrap_or_default(),
|
||||
PaymentType::EthereumSubscription => {
|
||||
get_subscription_page_url(instance_url, &profile.id)
|
||||
},
|
||||
}
|
||||
})
|
||||
.next();
|
||||
|
||||
Self {
|
||||
id: profile.id,
|
||||
username: profile.username,
|
||||
|
@ -106,6 +122,7 @@ impl Account {
|
|||
statuses_count: profile.post_count,
|
||||
source: None,
|
||||
wallet_address: None,
|
||||
subscription_page_url,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,10 @@ use crate::ethereum::identity::{
|
|||
create_identity_claim,
|
||||
verify_identity_proof,
|
||||
};
|
||||
use crate::ethereum::subscriptions::create_subscription_signature;
|
||||
use crate::ethereum::subscriptions::{
|
||||
create_subscription_signature,
|
||||
is_registered_recipient,
|
||||
};
|
||||
use crate::mastodon_api::oauth::auth::get_current_user;
|
||||
use crate::mastodon_api::statuses::helpers::build_status_list;
|
||||
use crate::mastodon_api::statuses::types::Status;
|
||||
|
@ -30,6 +33,7 @@ use crate::models::profiles::queries::{
|
|||
};
|
||||
use crate::models::profiles::types::{
|
||||
IdentityProof,
|
||||
PaymentOption,
|
||||
ProfileUpdateData,
|
||||
};
|
||||
use crate::models::relationships::queries::{
|
||||
|
@ -307,6 +311,44 @@ async fn authorize_subscription(
|
|||
Ok(HttpResponse::Ok().json(signature))
|
||||
}
|
||||
|
||||
#[post("/subscriptions_enabled")]
|
||||
async fn subscriptions_enabled(
|
||||
auth: BearerAuth,
|
||||
config: web::Data<Config>,
|
||||
db_pool: web::Data<Pool>,
|
||||
maybe_blockchain: web::Data<Option<ContractSet>>,
|
||||
) -> Result<HttpResponse, HttpError> {
|
||||
let db_client = &**get_database_client(&db_pool).await?;
|
||||
let mut current_user = get_current_user(db_client, auth.token()).await?;
|
||||
let contract_set = maybe_blockchain.as_ref().as_ref()
|
||||
.ok_or(HttpError::NotSupported)?;
|
||||
let wallet_address = current_user.public_wallet_address()
|
||||
.ok_or(HttpError::PermissionError)?;
|
||||
let is_registered = is_registered_recipient(contract_set, &wallet_address)
|
||||
.await.map_err(|_| HttpError::InternalError)?;
|
||||
if !is_registered {
|
||||
return Err(ValidationError("recipient is not registered").into());
|
||||
};
|
||||
|
||||
if current_user.profile.payment_options.is_empty() {
|
||||
// Add payment option to profile
|
||||
let mut profile_data = ProfileUpdateData::from(¤t_user.profile);
|
||||
profile_data.payment_options = vec![PaymentOption::subscription()];
|
||||
current_user.profile = update_profile(
|
||||
db_client,
|
||||
¤t_user.id,
|
||||
profile_data,
|
||||
).await?;
|
||||
|
||||
// Federate
|
||||
prepare_update_person(db_client, config.instance(), ¤t_user)
|
||||
.await?.spawn_deliver();
|
||||
};
|
||||
|
||||
let account = Account::from_user(current_user, &config.instance_url());
|
||||
Ok(HttpResponse::Ok().json(account))
|
||||
}
|
||||
|
||||
#[get("/relationships")]
|
||||
async fn get_relationships_view(
|
||||
auth: BearerAuth,
|
||||
|
@ -512,6 +554,7 @@ pub fn account_api_scope() -> Scope {
|
|||
.service(get_identity_claim)
|
||||
.service(create_identity_proof)
|
||||
.service(authorize_subscription)
|
||||
.service(subscriptions_enabled)
|
||||
// Routes with account ID
|
||||
.service(get_account)
|
||||
.service(follow_account)
|
||||
|
|
|
@ -94,6 +94,16 @@ pub struct PaymentOption {
|
|||
pub href: Option<String>,
|
||||
}
|
||||
|
||||
impl PaymentOption {
|
||||
pub fn subscription() -> Self {
|
||||
Self {
|
||||
payment_type: PaymentType::EthereumSubscription,
|
||||
name: None,
|
||||
href: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct PaymentOptions(pub Vec<PaymentOption>);
|
||||
|
||||
|
@ -102,6 +112,11 @@ impl PaymentOptions {
|
|||
let Self(payment_options) = self;
|
||||
payment_options
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
let Self(payment_options) = self;
|
||||
payment_options.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
json_from_sql!(PaymentOptions);
|
||||
|
|
Loading…
Reference in a new issue