Skip to main content

fatline_rs/
profile_builder.rs

1use 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}