use crate::proto::{FarcasterNetwork, HashScheme, Message, MessageData, MessageType, SignatureScheme, UserDataBody, UserDataType};
use crate::to_messages::ToMessages;
use crate::utils::{message_hash, sign_hash, FieldUpdate};
use derive_builder::Builder;
use ed25519_dalek::{SigningKey, VerifyingKey};
use crate::proto::message_data::Body;
use crate::utils;
#[derive(Builder, Default, Clone)]
#[builder(setter(strip_option), default)]
pub struct ProfileUpdate {
username_update: Option<FieldUpdate<String>>,
display_name_update: Option<FieldUpdate<String>>,
profile_pic_update: Option<FieldUpdate<String>>,
bio_update: Option<FieldUpdate<String>>,
url_update: Option<FieldUpdate<String>>,
location_update: Option<FieldUpdate<(f32,f32)>>,
twitter_update: Option<FieldUpdate<String>>,
github_update: Option<FieldUpdate<String>>,
}
fn field_update_to_message(fid: u64, timestamp: u32, field_update: impl Into<String>, user_data_type: UserDataType) -> MessageData {
MessageData {
r#type: MessageType::UserDataAdd.into(),
fid,
timestamp,
network: FarcasterNetwork::Mainnet.into(),
body: Some(Body::UserDataBody(UserDataBody {
r#type: user_data_type.into(),
value: field_update.into(),
})),
}
}
impl Into<String> for FieldUpdate<String> {
fn into(self) -> String {
if let FieldUpdate::Add(value) = self {
value
} else {
String::new()
}
}
}
impl ToMessages for ProfileUpdate {
fn to_messages(self, fid: u64, signing_key: &SigningKey) -> Vec<Message> {
let current_time = utils::now();
let mut messages = Vec::new();
if let Some(username_update) = self.username_update {
messages.push(field_update_to_message(fid, current_time, username_update, UserDataType::Username));
}
if let Some(display_name_update) = self.display_name_update {
messages.push(field_update_to_message(fid, current_time, display_name_update, UserDataType::Display));
}
if let Some(profile_pic_update) = self.profile_pic_update {
messages.push(field_update_to_message(fid, current_time, profile_pic_update, UserDataType::Pfp));
}
if let Some(bio_update) = self.bio_update {
messages.push(field_update_to_message(fid, current_time, bio_update, UserDataType::Bio));
}
if let Some(url_update) = self.url_update {
messages.push(field_update_to_message(fid, current_time, url_update, UserDataType::Url));
}
if let Some(location_update) = self.location_update {
let encoded = if let FieldUpdate::Add((lat,long)) = location_update {
format!("geo:{:.2},{:.2}",lat,long)
} else {
String::new()
};
messages.push(field_update_to_message(fid, current_time, encoded, UserDataType::Location));
}
if let Some(twitter_update) = self.twitter_update {
messages.push(field_update_to_message(fid, current_time, twitter_update, UserDataType::Twitter));
}
if let Some(github_update) = self.github_update {
messages.push(field_update_to_message(fid, current_time, github_update, UserDataType::Github));
}
messages.iter()
.cloned()
.map(|data| {
let hash = message_hash(&data);
let signature = sign_hash(signing_key, &hash);
Message {
data: Some(data),
hash: hash.to_vec(),
hash_scheme: HashScheme::Blake3.into(),
signature: signature.to_vec(),
signature_scheme: SignatureScheme::Ed25519.into(),
signer: VerifyingKey::from(signing_key).as_bytes().to_vec(),
data_bytes: None,
}
}).collect()
}
}
#[cfg(test)]
mod tests {
use ed25519_dalek::{Signature, SigningKey, Verifier};
use rand::rngs::OsRng;
use crate::profile_builder::{ProfileUpdateBuilder};
use crate::proto::MessageType;
use crate::utils::FieldUpdate;
use crate::to_messages::ToMessages;
#[tokio::test]
async fn test_expected_username_update() -> eyre::Result<()> {
let update = ProfileUpdateBuilder::create_empty()
.username_update(FieldUpdate::Add("username".to_string()))
.display_name_update(FieldUpdate::Add("Username".to_string()))
.bio_update(FieldUpdate::Remove)
.build()?;
let signing_key = SigningKey::generate(&mut OsRng);
let messages = update.to_messages(0, &signing_key);
let verifying_key = signing_key.verifying_key();
assert_eq!(3, messages.len());
for message in &messages {
assert_eq!(message.signer, verifying_key.as_bytes());
assert!(verifying_key.verify(&message.hash, &Signature::from_slice(&message.signature)?).is_ok());
}
assert_eq!(messages.first().unwrap().data.clone().unwrap().r#type(), MessageType::UserDataAdd);
Ok(())
}
}