fatline_rs/
profile_builder.rs1use crate::proto::{FarcasterNetwork, HashScheme, Message, MessageData, MessageType, SignatureScheme, UserDataBody, UserDataType};
2use crate::to_messages::ToMessages;
3use crate::utils::{message_hash, sign_hash, FieldUpdate};
4use derive_builder::Builder;
5use ed25519_dalek::{SigningKey, VerifyingKey};
6use crate::proto::message_data::Body;
7use crate::utils;
8
9#[derive(Builder, Default, Clone)]
10#[builder(setter(strip_option), default)]
11pub struct ProfileUpdate {
12 username_update: Option<FieldUpdate<String>>,
13 display_name_update: Option<FieldUpdate<String>>,
14 profile_pic_update: Option<FieldUpdate<String>>,
15 bio_update: Option<FieldUpdate<String>>,
16 url_update: Option<FieldUpdate<String>>,
17 location_update: Option<FieldUpdate<(f32,f32)>>,
18 twitter_update: Option<FieldUpdate<String>>,
19 github_update: Option<FieldUpdate<String>>,
20}
21
22fn field_update_to_message(fid: u64, timestamp: u32, field_update: impl Into<String>, user_data_type: UserDataType) -> MessageData {
23 MessageData {
24 r#type: MessageType::UserDataAdd.into(),
25 fid,
26 timestamp,
27 network: FarcasterNetwork::Mainnet.into(),
28 body: Some(Body::UserDataBody(UserDataBody {
29 r#type: user_data_type.into(),
30 value: field_update.into(),
31 })),
32 }
33}
34
35impl Into<String> for FieldUpdate<String> {
36 fn into(self) -> String {
37 if let FieldUpdate::Add(value) = self {
38 value
39 } else {
40 String::new()
41 }
42 }
43}
44
45impl ToMessages for ProfileUpdate {
46 fn to_messages(self, fid: u64, signing_key: &SigningKey) -> Vec<Message> {
47 let current_time = utils::now();
48
49 let mut messages = Vec::new();
50
51 if let Some(username_update) = self.username_update {
52 messages.push(field_update_to_message(fid, current_time, username_update, UserDataType::Username));
53 }
54
55 if let Some(display_name_update) = self.display_name_update {
56 messages.push(field_update_to_message(fid, current_time, display_name_update, UserDataType::Display));
57 }
58
59 if let Some(profile_pic_update) = self.profile_pic_update {
60 messages.push(field_update_to_message(fid, current_time, profile_pic_update, UserDataType::Pfp));
61 }
62
63 if let Some(bio_update) = self.bio_update {
64 messages.push(field_update_to_message(fid, current_time, bio_update, UserDataType::Bio));
65 }
66
67 if let Some(url_update) = self.url_update {
68 messages.push(field_update_to_message(fid, current_time, url_update, UserDataType::Url));
69 }
70
71 if let Some(location_update) = self.location_update {
72 let encoded = if let FieldUpdate::Add((lat,long)) = location_update {
73 format!("geo:{:.2},{:.2}",lat,long)
74 } else {
75 String::new()
76 };
77 messages.push(field_update_to_message(fid, current_time, encoded, UserDataType::Location));
78 }
79
80 if let Some(twitter_update) = self.twitter_update {
81 messages.push(field_update_to_message(fid, current_time, twitter_update, UserDataType::Twitter));
82 }
83
84 if let Some(github_update) = self.github_update {
85 messages.push(field_update_to_message(fid, current_time, github_update, UserDataType::Github));
86 }
87
88 messages.iter()
89 .cloned()
90 .map(|data| {
91 let hash = message_hash(&data);
92 let signature = sign_hash(signing_key, &hash);
93 Message {
94 data: Some(data),
95 hash: hash.to_vec(),
96 hash_scheme: HashScheme::Blake3.into(),
97 signature: signature.to_vec(),
98 signature_scheme: SignatureScheme::Ed25519.into(),
99 signer: VerifyingKey::from(signing_key).as_bytes().to_vec(),
100 data_bytes: None,
101 }
102 }).collect()
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use ed25519_dalek::{Signature, SigningKey, Verifier};
109 use rand::rngs::OsRng;
110 use crate::profile_builder::{ProfileUpdateBuilder};
111 use crate::proto::MessageType;
112 use crate::utils::FieldUpdate;
113 use crate::to_messages::ToMessages;
114
115 #[tokio::test]
116 async fn test_expected_username_update() -> eyre::Result<()> {
117 let update = ProfileUpdateBuilder::create_empty()
118 .username_update(FieldUpdate::Add("username".to_string()))
119 .display_name_update(FieldUpdate::Add("Username".to_string()))
120 .bio_update(FieldUpdate::Remove)
121 .build()?;
122
123 let signing_key = SigningKey::generate(&mut OsRng);
124
125 let messages = update.to_messages(0, &signing_key);
126
127 let verifying_key = signing_key.verifying_key();
128 assert_eq!(3, messages.len());
129 for message in &messages {
130 assert_eq!(message.signer, verifying_key.as_bytes());
131 assert!(verifying_key.verify(&message.hash, &Signature::from_slice(&message.signature)?).is_ok());
132 }
133 assert_eq!(messages.first().unwrap().data.clone().unwrap().r#type(), MessageType::UserDataAdd);
134
135 Ok(())
136 }
137
138}