Add payment_options field to actor profile

This commit is contained in:
silverpill 2022-07-23 15:47:53 +00:00
parent cef026f89a
commit e573ecb27b
10 changed files with 137 additions and 10 deletions

View file

@ -0,0 +1 @@
ALTER TABLE actor_profile ADD COLUMN payment_options JSONB NOT NULL DEFAULT '[]';

View file

@ -8,6 +8,7 @@ CREATE TABLE actor_profile (
avatar_file_name VARCHAR(100),
banner_file_name VARCHAR(100),
identity_proofs JSONB NOT NULL DEFAULT '[]',
payment_options JSONB NOT NULL DEFAULT '[]',
extra_fields JSONB NOT NULL DEFAULT '[]',
follower_count INTEGER NOT NULL CHECK (follower_count >= 0) DEFAULT 0,
following_count INTEGER NOT NULL CHECK (following_count >= 0) DEFAULT 0,

View file

@ -78,6 +78,7 @@ async fn prepare_remote_profile_data(
avatar,
banner,
identity_proofs,
payment_options: vec![],
extra_fields,
actor_json: Some(actor),
};

View file

@ -64,6 +64,7 @@ pub async fn update_remote_profile(
avatar,
banner,
identity_proofs,
payment_options: vec![],
extra_fields,
actor_json: Some(actor),
};

View file

@ -10,6 +10,7 @@ use crate::models::profiles::types::{
DbActorProfile,
ExtraField,
IdentityProof,
PaymentOption,
ProfileUpdateData,
};
use crate::models::profiles::validators::validate_username;
@ -194,6 +195,7 @@ impl AccountUpdateData {
current_avatar: &Option<String>,
current_banner: &Option<String>,
current_identity_proofs: &[IdentityProof],
current_payment_options: &[PaymentOption],
media_dir: &Path,
) -> Result<ProfileUpdateData, FileError> {
let avatar = process_b64_image_field_value(
@ -203,6 +205,7 @@ impl AccountUpdateData {
self.header, current_banner.clone(), media_dir,
)?;
let identity_proofs = current_identity_proofs.to_vec();
let payment_options = current_payment_options.to_vec();
let extra_fields = self.fields_attributes.unwrap_or(vec![]);
let profile_data = ProfileUpdateData {
display_name: self.display_name,
@ -211,6 +214,7 @@ impl AccountUpdateData {
avatar,
banner,
identity_proofs,
payment_options,
extra_fields,
actor_json: None, // always None for local profiles
};

View file

@ -28,7 +28,10 @@ use crate::models::profiles::queries::{
get_profile_by_id,
update_profile,
};
use crate::models::profiles::types::{IdentityProof, ProfileUpdateData};
use crate::models::profiles::types::{
IdentityProof,
ProfileUpdateData,
};
use crate::models::relationships::queries::{
create_follow_request,
follow,
@ -184,6 +187,7 @@ async fn update_credentials(
&current_user.profile.avatar_file_name,
&current_user.profile.banner_file_name,
&current_user.profile.identity_proofs.into_inner(),
&current_user.profile.payment_options.into_inner(),
&config.media_dir(),
)
.map_err(|err| {

View file

@ -16,6 +16,7 @@ use super::types::{
DbActorProfile,
ExtraFields,
IdentityProofs,
PaymentOptions,
ProfileCreateData,
ProfileUpdateData,
};
@ -31,10 +32,10 @@ pub async fn create_profile(
INSERT INTO actor_profile (
id, username, display_name, acct, bio, bio_source,
avatar_file_name, banner_file_name,
identity_proofs, extra_fields,
identity_proofs, payment_options, extra_fields,
actor_json
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING actor_profile
",
&[
@ -47,6 +48,7 @@ pub async fn create_profile(
&profile_data.avatar,
&profile_data.banner,
&IdentityProofs(profile_data.identity_proofs),
&PaymentOptions(profile_data.payment_options),
&ExtraFields(profile_data.extra_fields),
&profile_data.actor_json,
],
@ -70,10 +72,11 @@ pub async fn update_profile(
avatar_file_name = $4,
banner_file_name = $5,
identity_proofs = $6,
extra_fields = $7,
actor_json = $8,
payment_options = $7,
extra_fields = $8,
actor_json = $9,
updated_at = CURRENT_TIMESTAMP
WHERE id = $9
WHERE id = $10
RETURNING actor_profile
",
&[
@ -83,6 +86,7 @@ pub async fn update_profile(
&data.avatar,
&data.banner,
&IdentityProofs(data.identity_proofs),
&PaymentOptions(data.payment_options),
&ExtraFields(data.extra_fields),
&data.actor_json,
&profile_id,

View file

@ -1,16 +1,22 @@
use std::convert::TryFrom;
use chrono::{DateTime, Duration, Utc};
use postgres_types::FromSql;
use serde::{Deserialize, Serialize};
use serde::{
Deserialize, Deserializer, Serialize, Serializer,
de::Error as DeserializerError,
};
use uuid::Uuid;
use crate::activitypub::actors::types::Actor;
use crate::activitypub::identifiers::local_actor_id;
use crate::database::json_macro::{json_from_sql, json_to_sql};
use crate::errors::ValidationError;
use crate::errors::{ConversionError, ValidationError};
use crate::ethereum::identity::DidPkh;
use super::validators::{
validate_username,
validate_display_name,
validate_payment_options,
clean_bio,
clean_extra_fields,
};
@ -35,6 +41,72 @@ impl IdentityProofs {
json_from_sql!(IdentityProofs);
json_to_sql!(IdentityProofs);
#[derive(Clone, Debug)]
pub enum PaymentType {
Link,
EthereumSubscription,
}
impl From<&PaymentType> for i16 {
fn from(payment_type: &PaymentType) -> i16 {
match payment_type {
PaymentType::Link => 1,
PaymentType::EthereumSubscription => 2,
}
}
}
impl TryFrom<i16> for PaymentType {
type Error = ConversionError;
fn try_from(value: i16) -> Result<Self, Self::Error> {
let payment_type = match value {
1 => Self::Link,
2 => Self::EthereumSubscription,
_ => return Err(ConversionError),
};
Ok(payment_type)
}
}
impl Serialize for PaymentType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer
{
let value: i16 = self.into();
serializer.serialize_i16(value)
}
}
impl<'de> Deserialize<'de> for PaymentType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de>
{
let value: i16 = Deserialize::deserialize(deserializer)?;
Self::try_from(value).map_err(DeserializerError::custom)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PaymentOption {
pub payment_type: PaymentType,
pub name: Option<String>,
pub href: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PaymentOptions(pub Vec<PaymentOption>);
impl PaymentOptions {
pub fn into_inner(self) -> Vec<PaymentOption> {
let Self(payment_options) = self;
payment_options
}
}
json_from_sql!(PaymentOptions);
json_to_sql!(PaymentOptions);
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ExtraField {
pub name: String,
@ -70,6 +142,7 @@ pub struct DbActorProfile {
pub avatar_file_name: Option<String>,
pub banner_file_name: Option<String>,
pub identity_proofs: IdentityProofs,
pub payment_options: PaymentOptions,
pub extra_fields: ExtraFields,
pub follower_count: i32,
pub following_count: i32,
@ -143,6 +216,7 @@ impl Default for DbActorProfile {
avatar_file_name: None,
banner_file_name: None,
identity_proofs: IdentityProofs(vec![]),
payment_options: PaymentOptions(vec![]),
extra_fields: ExtraFields(vec![]),
follower_count: 0,
following_count: 0,
@ -164,6 +238,7 @@ pub struct ProfileCreateData {
pub avatar: Option<String>,
pub banner: Option<String>,
pub identity_proofs: Vec<IdentityProof>,
pub payment_options: Vec<PaymentOption>,
pub extra_fields: Vec<ExtraField>,
pub actor_json: Option<Actor>,
}
@ -178,6 +253,7 @@ impl ProfileCreateData {
let cleaned_bio = clean_bio(bio, self.actor_json.is_some())?;
self.bio = Some(cleaned_bio);
};
validate_payment_options(&self.payment_options)?;
self.extra_fields = clean_extra_fields(&self.extra_fields)?;
Ok(())
}
@ -190,6 +266,7 @@ pub struct ProfileUpdateData {
pub avatar: Option<String>,
pub banner: Option<String>,
pub identity_proofs: Vec<IdentityProof>,
pub payment_options: Vec<PaymentOption>,
pub extra_fields: Vec<ExtraField>,
pub actor_json: Option<Actor>,
}
@ -204,7 +281,7 @@ impl ProfileUpdateData {
let cleaned_bio = clean_bio(bio, self.actor_json.is_some())?;
self.bio = Some(cleaned_bio);
};
// Clean extra fields and remove fields with empty labels
validate_payment_options(&self.payment_options)?;
self.extra_fields = clean_extra_fields(&self.extra_fields)?;
Ok(())
}
@ -220,6 +297,7 @@ impl From<&DbActorProfile> for ProfileUpdateData {
avatar: profile.avatar_file_name,
banner: profile.banner_file_name,
identity_proofs: profile.identity_proofs.into_inner(),
payment_options: profile.payment_options.into_inner(),
extra_fields: profile.extra_fields.into_inner(),
actor_json: profile.actor_json,
}
@ -242,6 +320,18 @@ mod tests {
assert_eq!(serialized, json_data);
}
#[test]
fn test_payment_option_serialization() {
let json_data = r#"{"payment_type":2,"name":null,"href":null}"#;
let payment_option: PaymentOption = serde_json::from_str(json_data).unwrap();
assert!(matches!(
payment_option.payment_type,
PaymentType::EthereumSubscription,
));
let serialized = serde_json::to_string(&payment_option).unwrap();
assert_eq!(serialized, json_data);
}
#[test]
fn test_local_actor_address() {
let local_profile = DbActorProfile {

View file

@ -1,7 +1,7 @@
use regex::Regex;
use crate::errors::ValidationError;
use crate::utils::html::{clean_html, clean_html_strict};
use super::types::ExtraField;
use super::types::{ExtraField, PaymentOption, PaymentType};
const USERNAME_RE: &str = r"^[a-zA-Z0-9_\.-]+$";
@ -46,6 +46,26 @@ pub fn clean_bio(bio: &str, is_remote: bool) -> Result<String, ValidationError>
Ok(cleaned_bio)
}
pub fn validate_payment_options(payment_options: &[PaymentOption])
-> Result<(), ValidationError>
{
for option in payment_options {
match option.payment_type {
PaymentType::Link => {
if option.name.is_none() || option.href.is_none() {
return Err(ValidationError("invalid payment option"));
};
},
PaymentType::EthereumSubscription => {
if option.name.is_some() || option.href.is_some() {
return Err(ValidationError("invalid payment option"));
};
},
};
};
Ok(())
}
const FIELD_NAME_MAX_SIZE: usize = 500;
const FIELD_VALUE_MAX_SIZE: usize = 5000;

View file

@ -81,6 +81,7 @@ pub async fn create_user(
avatar: None,
banner: None,
identity_proofs: vec![],
payment_options: vec![],
extra_fields: vec![],
actor_json: None,
};