Add API method for adding subscription as payment option

This commit is contained in:
silverpill 2022-07-23 17:22:10 +00:00
parent e573ecb27b
commit 1554780b35
7 changed files with 128 additions and 4 deletions

View file

@ -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:

View file

@ -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 })
} }

View file

@ -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)
}

View file

@ -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),
)
}

View file

@ -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,
} }
} }

View file

@ -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(&current_user.profile);
profile_data.payment_options = vec![PaymentOption::subscription()];
current_user.profile = update_profile(
db_client,
&current_user.id,
profile_data,
).await?;
// Federate
prepare_update_person(db_client, config.instance(), &current_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)

View file

@ -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);