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
|
description: User's wallet address is not known or not verified
|
||||||
418:
|
418:
|
||||||
description: Blockchain integration is not enabled
|
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:
|
/api/v1/accounts/relationships:
|
||||||
get:
|
get:
|
||||||
summary: Find out whether a given actor is followed, blocked, muted, etc.
|
summary: Find out whether a given actor is followed, blocked, muted, etc.
|
||||||
|
@ -717,6 +733,11 @@ components:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
example: '0xd8da6bf...'
|
example: '0xd8da6bf...'
|
||||||
|
subscription_page_url:
|
||||||
|
description: Subscription page URL
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
example: 'https://example.com/profile/1/subscription'
|
||||||
Attachment:
|
Attachment:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
|
@ -86,6 +86,7 @@ pub struct ContractSet {
|
||||||
pub gate: Option<Contract<Http>>,
|
pub gate: Option<Contract<Http>>,
|
||||||
pub collectible: Option<Contract<Http>>,
|
pub collectible: Option<Contract<Http>>,
|
||||||
pub subscription: Option<Contract<Http>>,
|
pub subscription: Option<Contract<Http>>,
|
||||||
|
pub subscription_adapter: Option<Contract<Http>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -115,6 +116,7 @@ pub async fn get_contracts(
|
||||||
let mut maybe_gate = None;
|
let mut maybe_gate = None;
|
||||||
let mut maybe_collectible = None;
|
let mut maybe_collectible = None;
|
||||||
let mut maybe_subscription = None;
|
let mut maybe_subscription = None;
|
||||||
|
let mut maybe_subscription_adapter = None;
|
||||||
let mut sync_targets = vec![];
|
let mut sync_targets = vec![];
|
||||||
|
|
||||||
let gate_abi = load_abi(&config.contract_dir, GATE)?;
|
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());
|
log::info!("subscription contract address is {:?}", subscription.address());
|
||||||
sync_targets.push(subscription.address());
|
sync_targets.push(subscription.address());
|
||||||
maybe_subscription = Some(subscription);
|
maybe_subscription = Some(subscription);
|
||||||
|
maybe_subscription_adapter = Some(subscription_adapter);
|
||||||
};
|
};
|
||||||
|
|
||||||
let current_block = get_current_block_number(&web3, storage_dir).await?;
|
let current_block = get_current_block_number(&web3, storage_dir).await?;
|
||||||
|
@ -188,6 +191,7 @@ pub async fn get_contracts(
|
||||||
gate: maybe_gate,
|
gate: maybe_gate,
|
||||||
collectible: maybe_collectible,
|
collectible: maybe_collectible,
|
||||||
subscription: maybe_subscription,
|
subscription: maybe_subscription,
|
||||||
|
subscription_adapter: maybe_subscription_adapter,
|
||||||
};
|
};
|
||||||
Ok(Blockchain { contract_set, sync_state })
|
Ok(Blockchain { contract_set, sync_state })
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ use chrono::{DateTime, TimeZone, Utc};
|
||||||
|
|
||||||
use web3::{
|
use web3::{
|
||||||
api::Web3,
|
api::Web3,
|
||||||
contract::Contract,
|
contract::{Contract, Options},
|
||||||
ethabi::RawLog,
|
ethabi::RawLog,
|
||||||
transports::Http,
|
transports::Http,
|
||||||
types::{BlockId, BlockNumber, FilterBuilder, U256},
|
types::{BlockId, BlockNumber, FilterBuilder, U256},
|
||||||
|
@ -38,6 +38,7 @@ use crate::models::users::queries::{
|
||||||
get_user_by_id,
|
get_user_by_id,
|
||||||
get_user_by_wallet_address,
|
get_user_by_wallet_address,
|
||||||
};
|
};
|
||||||
|
use super::contracts::ContractSet;
|
||||||
use super::errors::EthereumError;
|
use super::errors::EthereumError;
|
||||||
use super::signatures::{
|
use super::signatures::{
|
||||||
encode_uint256,
|
encode_uint256,
|
||||||
|
@ -263,3 +264,19 @@ pub fn create_subscription_signature(
|
||||||
)?;
|
)?;
|
||||||
Ok(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 {
|
pub fn get_tag_page_url(instance_url: &str, tag_name: &str) -> String {
|
||||||
format!("{}/tag/{}", instance_url, tag_name)
|
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 uuid::Uuid;
|
||||||
|
|
||||||
use crate::errors::ValidationError;
|
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::currencies::get_identity_proof_field_name;
|
||||||
use crate::models::profiles::types::{
|
use crate::models::profiles::types::{
|
||||||
DbActorProfile,
|
DbActorProfile,
|
||||||
ExtraField,
|
ExtraField,
|
||||||
IdentityProof,
|
IdentityProof,
|
||||||
PaymentOption,
|
PaymentOption,
|
||||||
|
PaymentType,
|
||||||
ProfileUpdateData,
|
ProfileUpdateData,
|
||||||
};
|
};
|
||||||
use crate::models::profiles::validators::validate_username;
|
use crate::models::profiles::validators::validate_username;
|
||||||
|
@ -56,6 +58,7 @@ pub struct Account {
|
||||||
pub source: Option<Source>,
|
pub source: Option<Source>,
|
||||||
|
|
||||||
pub wallet_address: Option<String>,
|
pub wallet_address: Option<String>,
|
||||||
|
pub subscription_page_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Account {
|
impl Account {
|
||||||
|
@ -67,7 +70,7 @@ impl Account {
|
||||||
.map(|name| get_file_url(instance_url, name));
|
.map(|name| get_file_url(instance_url, name));
|
||||||
|
|
||||||
let mut identity_proofs = vec![];
|
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
|
// Skip proof if it doesn't map to field name
|
||||||
if let Some(field_name) = get_identity_proof_field_name(&proof.proof_type) {
|
if let Some(field_name) = get_identity_proof_field_name(&proof.proof_type) {
|
||||||
let field = AccountField {
|
let field = AccountField {
|
||||||
|
@ -79,8 +82,9 @@ impl Account {
|
||||||
identity_proofs.push(field);
|
identity_proofs.push(field);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut extra_fields = vec![];
|
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 {
|
let field = AccountField {
|
||||||
name: extra_field.name,
|
name: extra_field.name,
|
||||||
value: extra_field.value,
|
value: extra_field.value,
|
||||||
|
@ -89,6 +93,18 @@ impl Account {
|
||||||
extra_fields.push(field);
|
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 {
|
Self {
|
||||||
id: profile.id,
|
id: profile.id,
|
||||||
username: profile.username,
|
username: profile.username,
|
||||||
|
@ -106,6 +122,7 @@ impl Account {
|
||||||
statuses_count: profile.post_count,
|
statuses_count: profile.post_count,
|
||||||
source: None,
|
source: None,
|
||||||
wallet_address: None,
|
wallet_address: None,
|
||||||
|
subscription_page_url,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,10 @@ use crate::ethereum::identity::{
|
||||||
create_identity_claim,
|
create_identity_claim,
|
||||||
verify_identity_proof,
|
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::oauth::auth::get_current_user;
|
||||||
use crate::mastodon_api::statuses::helpers::build_status_list;
|
use crate::mastodon_api::statuses::helpers::build_status_list;
|
||||||
use crate::mastodon_api::statuses::types::Status;
|
use crate::mastodon_api::statuses::types::Status;
|
||||||
|
@ -30,6 +33,7 @@ use crate::models::profiles::queries::{
|
||||||
};
|
};
|
||||||
use crate::models::profiles::types::{
|
use crate::models::profiles::types::{
|
||||||
IdentityProof,
|
IdentityProof,
|
||||||
|
PaymentOption,
|
||||||
ProfileUpdateData,
|
ProfileUpdateData,
|
||||||
};
|
};
|
||||||
use crate::models::relationships::queries::{
|
use crate::models::relationships::queries::{
|
||||||
|
@ -307,6 +311,44 @@ async fn authorize_subscription(
|
||||||
Ok(HttpResponse::Ok().json(signature))
|
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")]
|
#[get("/relationships")]
|
||||||
async fn get_relationships_view(
|
async fn get_relationships_view(
|
||||||
auth: BearerAuth,
|
auth: BearerAuth,
|
||||||
|
@ -512,6 +554,7 @@ pub fn account_api_scope() -> Scope {
|
||||||
.service(get_identity_claim)
|
.service(get_identity_claim)
|
||||||
.service(create_identity_proof)
|
.service(create_identity_proof)
|
||||||
.service(authorize_subscription)
|
.service(authorize_subscription)
|
||||||
|
.service(subscriptions_enabled)
|
||||||
// Routes with account ID
|
// Routes with account ID
|
||||||
.service(get_account)
|
.service(get_account)
|
||||||
.service(follow_account)
|
.service(follow_account)
|
||||||
|
|
|
@ -94,6 +94,16 @@ pub struct PaymentOption {
|
||||||
pub href: Option<String>,
|
pub href: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PaymentOption {
|
||||||
|
pub fn subscription() -> Self {
|
||||||
|
Self {
|
||||||
|
payment_type: PaymentType::EthereumSubscription,
|
||||||
|
name: None,
|
||||||
|
href: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
pub struct PaymentOptions(pub Vec<PaymentOption>);
|
pub struct PaymentOptions(pub Vec<PaymentOption>);
|
||||||
|
|
||||||
|
@ -102,6 +112,11 @@ impl PaymentOptions {
|
||||||
let Self(payment_options) = self;
|
let Self(payment_options) = self;
|
||||||
payment_options
|
payment_options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
let Self(payment_options) = self;
|
||||||
|
payment_options.is_empty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
json_from_sql!(PaymentOptions);
|
json_from_sql!(PaymentOptions);
|
||||||
|
|
Loading…
Reference in a new issue