2021-11-13 17:37:31 +00:00
|
|
|
use std::path::Path;
|
2021-04-09 00:22:17 +00:00
|
|
|
|
|
|
|
use chrono::{DateTime, Utc};
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
2022-01-31 19:10:51 +00:00
|
|
|
use crate::errors::ValidationError;
|
2022-07-23 17:22:10 +00:00
|
|
|
use crate::frontend::get_subscription_page_url;
|
2021-09-16 14:34:24 +00:00
|
|
|
use crate::models::profiles::types::{
|
|
|
|
DbActorProfile,
|
|
|
|
ExtraField,
|
2022-04-26 14:12:26 +00:00
|
|
|
IdentityProof,
|
2022-07-23 15:47:53 +00:00
|
|
|
PaymentOption,
|
2022-07-23 17:22:10 +00:00
|
|
|
PaymentType,
|
2021-09-16 14:34:24 +00:00
|
|
|
ProfileUpdateData,
|
|
|
|
};
|
2022-01-31 19:10:51 +00:00
|
|
|
use crate::models::profiles::validators::validate_username;
|
2022-07-31 10:54:09 +00:00
|
|
|
use crate::models::subscriptions::types::Subscription;
|
2022-01-31 19:10:51 +00:00
|
|
|
use crate::models::users::types::{
|
|
|
|
validate_local_username,
|
|
|
|
User,
|
|
|
|
};
|
2021-04-09 00:22:17 +00:00
|
|
|
use crate::utils::files::{FileError, save_validated_b64_file, get_file_url};
|
|
|
|
|
2022-04-26 14:12:26 +00:00
|
|
|
/// https://docs.joinmastodon.org/entities/field/
|
2021-09-17 14:43:02 +00:00
|
|
|
#[derive(Serialize)]
|
|
|
|
pub struct AccountField {
|
|
|
|
pub name: String,
|
|
|
|
pub value: String,
|
2022-04-26 14:12:26 +00:00
|
|
|
verified_at: Option<DateTime<Utc>>,
|
2021-09-17 14:43:02 +00:00
|
|
|
}
|
|
|
|
|
2021-04-09 00:22:17 +00:00
|
|
|
/// https://docs.joinmastodon.org/entities/source/
|
|
|
|
#[derive(Serialize)]
|
|
|
|
pub struct Source {
|
|
|
|
pub note: Option<String>,
|
2021-09-17 14:43:02 +00:00
|
|
|
pub fields: Vec<AccountField>,
|
2021-04-09 00:22:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// https://docs.joinmastodon.org/entities/account/
|
|
|
|
#[derive(Serialize)]
|
|
|
|
pub struct Account {
|
|
|
|
pub id: Uuid,
|
|
|
|
pub username: String,
|
|
|
|
pub acct: String,
|
2022-01-15 00:18:17 +00:00
|
|
|
pub url: String,
|
2021-04-09 00:22:17 +00:00
|
|
|
pub display_name: Option<String>,
|
|
|
|
pub created_at: DateTime<Utc>,
|
|
|
|
pub note: Option<String>,
|
|
|
|
pub avatar: Option<String>,
|
|
|
|
pub header: Option<String>,
|
2022-04-26 14:12:26 +00:00
|
|
|
pub identity_proofs: Vec<AccountField>,
|
2021-09-17 14:43:02 +00:00
|
|
|
pub fields: Vec<AccountField>,
|
2021-04-09 00:22:17 +00:00
|
|
|
pub followers_count: i32,
|
|
|
|
pub following_count: i32,
|
|
|
|
pub statuses_count: i32,
|
|
|
|
|
|
|
|
pub source: Option<Source>,
|
2021-10-05 16:24:06 +00:00
|
|
|
|
2022-07-23 17:22:10 +00:00
|
|
|
pub subscription_page_url: Option<String>,
|
2021-04-09 00:22:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Account {
|
|
|
|
pub fn from_profile(profile: DbActorProfile, instance_url: &str) -> Self {
|
2022-01-15 00:18:17 +00:00
|
|
|
let profile_url = profile.actor_url(instance_url);
|
2021-10-08 18:15:04 +00:00
|
|
|
let avatar_url = profile.avatar_file_name.as_ref()
|
2021-11-13 17:37:31 +00:00
|
|
|
.map(|name| get_file_url(instance_url, name));
|
2021-10-08 18:15:04 +00:00
|
|
|
let header_url = profile.banner_file_name.as_ref()
|
2021-11-13 17:37:31 +00:00
|
|
|
.map(|name| get_file_url(instance_url, name));
|
2022-04-26 14:12:26 +00:00
|
|
|
|
|
|
|
let mut identity_proofs = vec![];
|
2022-07-23 17:22:10 +00:00
|
|
|
for proof in profile.identity_proofs.clone().into_inner() {
|
2022-04-26 14:12:26 +00:00
|
|
|
// Skip proof if it doesn't map to field name
|
2022-08-04 15:44:48 +00:00
|
|
|
if let Some(currency) = proof.issuer.currency() {
|
|
|
|
let field_name = currency.field_name();
|
2022-04-26 14:12:26 +00:00
|
|
|
let field = AccountField {
|
|
|
|
name: field_name,
|
|
|
|
value: proof.issuer.address,
|
|
|
|
// Use current time because DID proofs are always valid
|
|
|
|
verified_at: Some(Utc::now()),
|
|
|
|
};
|
|
|
|
identity_proofs.push(field);
|
|
|
|
};
|
|
|
|
};
|
2022-07-23 17:22:10 +00:00
|
|
|
|
2022-04-26 14:12:26 +00:00
|
|
|
let mut extra_fields = vec![];
|
2022-07-23 17:22:10 +00:00
|
|
|
for extra_field in profile.extra_fields.clone().into_inner() {
|
2022-04-26 14:12:26 +00:00
|
|
|
let field = AccountField {
|
|
|
|
name: extra_field.name,
|
|
|
|
value: extra_field.value,
|
|
|
|
verified_at: None,
|
|
|
|
};
|
|
|
|
extra_fields.push(field);
|
|
|
|
};
|
|
|
|
|
2022-07-23 17:22:10 +00:00
|
|
|
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();
|
|
|
|
|
2021-04-09 00:22:17 +00:00
|
|
|
Self {
|
|
|
|
id: profile.id,
|
|
|
|
username: profile.username,
|
|
|
|
acct: profile.acct,
|
2022-01-15 00:18:17 +00:00
|
|
|
url: profile_url,
|
2021-04-09 00:22:17 +00:00
|
|
|
display_name: profile.display_name,
|
|
|
|
created_at: profile.created_at,
|
|
|
|
note: profile.bio,
|
|
|
|
avatar: avatar_url,
|
|
|
|
header: header_url,
|
2022-04-26 14:12:26 +00:00
|
|
|
identity_proofs,
|
|
|
|
fields: extra_fields,
|
2021-04-09 00:22:17 +00:00
|
|
|
followers_count: profile.follower_count,
|
|
|
|
following_count: profile.following_count,
|
|
|
|
statuses_count: profile.post_count,
|
2021-10-10 10:15:18 +00:00
|
|
|
source: None,
|
2022-07-23 17:22:10 +00:00
|
|
|
subscription_page_url,
|
2021-04-09 00:22:17 +00:00
|
|
|
}
|
|
|
|
}
|
2021-10-05 16:24:06 +00:00
|
|
|
|
|
|
|
pub fn from_user(user: User, instance_url: &str) -> Self {
|
2021-10-10 10:15:18 +00:00
|
|
|
let fields_sources = user.profile.extra_fields.clone()
|
2022-04-21 22:37:08 +00:00
|
|
|
.into_inner().into_iter()
|
2021-10-10 10:15:18 +00:00
|
|
|
.map(|field| AccountField {
|
|
|
|
name: field.name,
|
|
|
|
value: field.value_source.unwrap_or(field.value),
|
2022-04-26 14:12:26 +00:00
|
|
|
verified_at: None,
|
2021-10-10 10:15:18 +00:00
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
let source = Source {
|
|
|
|
note: user.profile.bio_source.clone(),
|
|
|
|
fields: fields_sources,
|
|
|
|
};
|
2021-10-05 16:24:06 +00:00
|
|
|
let mut account = Self::from_profile(user.profile, instance_url);
|
2021-10-10 10:15:18 +00:00
|
|
|
account.source = Some(source);
|
2021-10-05 16:24:06 +00:00
|
|
|
account
|
|
|
|
}
|
2021-04-09 00:22:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// https://docs.joinmastodon.org/methods/accounts/
|
2021-10-05 20:57:24 +00:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
pub struct AccountCreateData {
|
2022-01-06 19:06:35 +00:00
|
|
|
pub username: String,
|
2022-02-15 19:26:06 +00:00
|
|
|
pub password: Option<String>,
|
2021-10-05 20:57:24 +00:00
|
|
|
|
2022-02-14 18:07:16 +00:00
|
|
|
pub message: Option<String>,
|
|
|
|
pub signature: Option<String>,
|
|
|
|
|
2022-01-06 19:06:35 +00:00
|
|
|
pub invite_code: Option<String>,
|
2021-10-05 20:57:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl AccountCreateData {
|
2022-01-31 19:10:51 +00:00
|
|
|
|
|
|
|
pub fn clean(&self) -> Result<(), ValidationError> {
|
|
|
|
validate_username(&self.username)?;
|
|
|
|
validate_local_username(&self.username)?;
|
2022-02-15 19:26:06 +00:00
|
|
|
if self.password.is_none() && self.message.is_none() {
|
|
|
|
return Err(ValidationError("password or EIP-4361 message is required"));
|
|
|
|
};
|
2022-01-31 19:10:51 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
2021-10-05 20:57:24 +00:00
|
|
|
}
|
|
|
|
|
2021-04-09 00:22:17 +00:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
pub struct AccountUpdateData {
|
|
|
|
pub display_name: Option<String>,
|
|
|
|
pub note: Option<String>,
|
|
|
|
pub note_source: Option<String>,
|
|
|
|
pub avatar: Option<String>,
|
|
|
|
pub header: Option<String>,
|
2021-09-16 14:34:24 +00:00
|
|
|
pub fields_attributes: Option<Vec<ExtraField>>,
|
2021-04-09 00:22:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn process_b64_image_field_value(
|
|
|
|
form_value: Option<String>,
|
|
|
|
db_value: Option<String>,
|
2021-11-13 17:37:31 +00:00
|
|
|
output_dir: &Path,
|
2021-04-09 00:22:17 +00:00
|
|
|
) -> Result<Option<String>, FileError> {
|
|
|
|
let maybe_file_name = match form_value {
|
|
|
|
Some(b64_data) => {
|
2021-11-13 17:37:31 +00:00
|
|
|
if b64_data.is_empty() {
|
2021-04-09 00:22:17 +00:00
|
|
|
// Remove file
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
// Decode and save file
|
|
|
|
let (file_name, _) = save_validated_b64_file(
|
2021-11-13 17:37:31 +00:00
|
|
|
&b64_data, output_dir, "image/",
|
2021-04-09 00:22:17 +00:00
|
|
|
)?;
|
|
|
|
Some(file_name)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
// Keep current value
|
|
|
|
None => db_value,
|
|
|
|
};
|
|
|
|
Ok(maybe_file_name)
|
|
|
|
}
|
|
|
|
|
|
|
|
impl AccountUpdateData {
|
|
|
|
pub fn into_profile_data(
|
|
|
|
self,
|
|
|
|
current_avatar: &Option<String>,
|
|
|
|
current_banner: &Option<String>,
|
2022-04-26 14:12:26 +00:00
|
|
|
current_identity_proofs: &[IdentityProof],
|
2022-07-23 15:47:53 +00:00
|
|
|
current_payment_options: &[PaymentOption],
|
2021-11-13 17:37:31 +00:00
|
|
|
media_dir: &Path,
|
2021-04-09 00:22:17 +00:00
|
|
|
) -> Result<ProfileUpdateData, FileError> {
|
|
|
|
let avatar = process_b64_image_field_value(
|
|
|
|
self.avatar, current_avatar.clone(), media_dir,
|
|
|
|
)?;
|
|
|
|
let banner = process_b64_image_field_value(
|
|
|
|
self.header, current_banner.clone(), media_dir,
|
|
|
|
)?;
|
2022-04-26 14:12:26 +00:00
|
|
|
let identity_proofs = current_identity_proofs.to_vec();
|
2022-07-23 15:47:53 +00:00
|
|
|
let payment_options = current_payment_options.to_vec();
|
2021-09-16 14:34:24 +00:00
|
|
|
let extra_fields = self.fields_attributes.unwrap_or(vec![]);
|
2021-04-09 00:22:17 +00:00
|
|
|
let profile_data = ProfileUpdateData {
|
|
|
|
display_name: self.display_name,
|
|
|
|
bio: self.note,
|
|
|
|
bio_source: self.note_source,
|
|
|
|
avatar,
|
|
|
|
banner,
|
2022-04-26 14:12:26 +00:00
|
|
|
identity_proofs,
|
2022-07-23 15:47:53 +00:00
|
|
|
payment_options,
|
2021-09-16 14:34:24 +00:00
|
|
|
extra_fields,
|
2022-04-26 14:12:26 +00:00
|
|
|
actor_json: None, // always None for local profiles
|
2021-04-09 00:22:17 +00:00
|
|
|
};
|
|
|
|
Ok(profile_data)
|
|
|
|
}
|
|
|
|
}
|
2021-10-10 10:15:18 +00:00
|
|
|
|
2022-04-22 10:10:54 +00:00
|
|
|
#[derive(Serialize)]
|
|
|
|
pub struct IdentityClaim {
|
|
|
|
pub claim: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
pub struct IdentityProofData {
|
|
|
|
pub signature: String,
|
|
|
|
}
|
|
|
|
|
2022-06-26 17:54:51 +00:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
pub struct SubscriptionQueryParams {
|
|
|
|
pub price: u64,
|
|
|
|
}
|
|
|
|
|
2022-02-21 15:27:25 +00:00
|
|
|
// TODO: actix currently doesn't support parameter arrays
|
|
|
|
// https://github.com/actix/actix-web/issues/2044
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
pub struct RelationshipQueryParams {
|
|
|
|
#[serde(rename(deserialize = "id[]"))]
|
|
|
|
pub id: Uuid,
|
|
|
|
}
|
|
|
|
|
2022-02-21 15:20:18 +00:00
|
|
|
#[derive(Serialize)]
|
2022-02-21 22:32:36 +00:00
|
|
|
pub struct RelationshipMap {
|
|
|
|
pub id: Uuid, // target ID
|
|
|
|
pub following: bool,
|
|
|
|
pub followed_by: bool,
|
|
|
|
pub requested: bool,
|
|
|
|
pub subscription_to: bool,
|
|
|
|
pub subscription_from: bool,
|
2022-02-21 15:20:18 +00:00
|
|
|
pub showing_reblogs: bool,
|
2022-03-09 20:37:14 +00:00
|
|
|
pub showing_replies: bool,
|
2022-02-21 15:20:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn default_showing_reblogs() -> bool { true }
|
|
|
|
|
2022-03-09 20:37:14 +00:00
|
|
|
fn default_showing_replies() -> bool { true }
|
|
|
|
|
2022-02-21 15:20:18 +00:00
|
|
|
impl Default for RelationshipMap {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self {
|
|
|
|
id: Default::default(),
|
|
|
|
following: false,
|
|
|
|
followed_by: false,
|
|
|
|
requested: false,
|
|
|
|
subscription_to: false,
|
|
|
|
subscription_from: false,
|
|
|
|
showing_reblogs: default_showing_reblogs(),
|
2022-03-09 20:37:14 +00:00
|
|
|
showing_replies: default_showing_replies(),
|
2022-02-21 15:20:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-04 11:14:54 +00:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
pub struct SearchDidQueryParams {
|
|
|
|
pub did: String,
|
|
|
|
}
|
|
|
|
|
2022-02-21 15:20:18 +00:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
pub struct FollowData {
|
|
|
|
#[serde(default = "default_showing_reblogs")]
|
|
|
|
pub reblogs: bool,
|
2022-03-09 20:37:14 +00:00
|
|
|
#[serde(default = "default_showing_replies")]
|
|
|
|
pub replies: bool,
|
2022-02-21 22:32:36 +00:00
|
|
|
}
|
|
|
|
|
2022-02-08 01:28:02 +00:00
|
|
|
fn default_page_size() -> i64 { 20 }
|
|
|
|
|
2022-07-07 14:56:01 +00:00
|
|
|
fn default_exclude_replies() -> bool { true }
|
|
|
|
|
2022-02-08 01:28:02 +00:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
pub struct StatusListQueryParams {
|
2022-07-07 14:56:01 +00:00
|
|
|
#[serde(default = "default_exclude_replies")]
|
|
|
|
pub exclude_replies: bool,
|
|
|
|
|
2022-02-08 01:28:02 +00:00
|
|
|
#[serde(default)]
|
|
|
|
pub pinned: bool,
|
|
|
|
|
|
|
|
pub max_id: Option<Uuid>,
|
|
|
|
|
|
|
|
#[serde(default = "default_page_size")]
|
|
|
|
pub limit: i64,
|
|
|
|
}
|
|
|
|
|
2022-08-06 22:13:47 +00:00
|
|
|
fn default_follow_list_page_size() -> u8 { 40 }
|
2021-12-29 12:07:56 +00:00
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
pub struct FollowListQueryParams {
|
|
|
|
pub max_id: Option<i32>,
|
|
|
|
|
2022-02-08 01:28:02 +00:00
|
|
|
#[serde(default = "default_follow_list_page_size")]
|
2022-08-06 22:13:47 +00:00
|
|
|
pub limit: u8,
|
2021-12-29 12:07:56 +00:00
|
|
|
}
|
|
|
|
|
2022-07-31 10:54:09 +00:00
|
|
|
#[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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-10 10:15:18 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
const INSTANCE_URL: &str = "https://example.com";
|
|
|
|
|
2022-02-15 19:26:06 +00:00
|
|
|
#[test]
|
|
|
|
fn test_validate_account_create_data() {
|
|
|
|
let account_data = AccountCreateData {
|
|
|
|
username: "test".to_string(),
|
|
|
|
password: None,
|
|
|
|
message: None,
|
|
|
|
signature: Some("test".to_string()),
|
|
|
|
invite_code: None,
|
|
|
|
};
|
|
|
|
let error = account_data.clean().unwrap_err();
|
|
|
|
assert_eq!(error.to_string(), "password or EIP-4361 message is required");
|
|
|
|
}
|
|
|
|
|
2021-10-10 10:15:18 +00:00
|
|
|
#[test]
|
|
|
|
fn test_create_account_from_profile() {
|
|
|
|
let profile = DbActorProfile {
|
|
|
|
avatar_file_name: Some("test".to_string()),
|
|
|
|
..Default::default()
|
|
|
|
};
|
|
|
|
let account = Account::from_profile(profile, INSTANCE_URL);
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
account.avatar.unwrap(),
|
|
|
|
format!("{}/media/test", INSTANCE_URL),
|
|
|
|
);
|
|
|
|
assert!(account.source.is_none());
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_create_account_from_user() {
|
|
|
|
let bio_source = "test";
|
|
|
|
let wallet_address = "0x1234";
|
|
|
|
let profile = DbActorProfile {
|
|
|
|
bio_source: Some(bio_source.to_string()),
|
|
|
|
..Default::default()
|
|
|
|
};
|
|
|
|
let user = User {
|
2021-12-24 17:46:01 +00:00
|
|
|
wallet_address: Some(wallet_address.to_string()),
|
2021-10-10 10:15:18 +00:00
|
|
|
profile,
|
2021-12-01 23:26:59 +00:00
|
|
|
..Default::default()
|
2021-10-10 10:15:18 +00:00
|
|
|
};
|
|
|
|
let account = Account::from_user(user, INSTANCE_URL);
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
account.source.unwrap().note.unwrap(),
|
|
|
|
bio_source,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|