fatline-rs 0.2.2

Farcaster grpc client and utility library
Documentation
use crate::posts::{Embed, Mention, Parent};
use derive_builder::Builder;
use ed25519_dalek::{SigningKey, VerifyingKey};
use crate::proto::{CastAddBody, CastType, FarcasterNetwork, HashScheme, Message, MessageData, MessageType, SignatureScheme};
use crate::proto::message_data::Body;
use crate::to_messages::ToMessages;
use crate::utils::{message_hash, now, sign_hash};

#[derive(Builder, Default, Clone)]
#[builder(setter(strip_option), default, build_fn(validate = "validate_cast"))]
pub struct CastCreate {
    body: String,
    mentions: Option<Vec<Mention>>,
    embeds: Option<Vec<Embed>>,
    parent: Option<Parent>,
}

fn validate_cast(builder: &CastCreateBuilder) -> Result<(), String> {
    if let Some(body) = &builder.body {
        // TODO: maybe add utf-8 validation rule
        if body.len() > 1024 {
            return Err("CastCreate body text was larger than 1024 bytes".to_string())
        }
    };
    if let Some(Some(embeds)) = &builder.embeds {
        if embeds.len() > 2 {
            return Err("CastCreate contains more than two embeds".to_string())
        }
    };
    Ok(())
}

#[inline]
fn fid_from_mention(mention: &Mention) -> u64 {
    mention.fid
}

#[inline]
fn start_pos_from_mention(mention: &Mention) -> u32 {
    mention.start_pos
}

#[inline]
fn extract_mentions(mention: Mention) -> (u64, u32) {
    (fid_from_mention(&mention), start_pos_from_mention(&mention))
}

impl ToMessages for CastCreate {
    fn to_messages(self, fid: u64, signing_key: &SigningKey) -> Vec<Message> {
        let (mentions, mentions_positions) = self.mentions.map_or(
            (Vec::new(), Vec::new()),
            | mentions| mentions.into_iter().map(extract_mentions).collect()
        );

        let cast_type = if self.body.len() > 320 {
            CastType::LongCast
        } else {
            CastType::Cast
        };

        let cast = CastAddBody {
            embeds_deprecated: vec![],
            mentions,
            text: self.body,
            mentions_positions,
            embeds: self.embeds.map_or(Vec::new(), |embeds| embeds.into_iter().map(|e| e.into()).collect()),
            r#type: cast_type.into(),
            parent: None,
        };

        let data = MessageData {
            r#type: MessageType::CastAdd.into(),
            fid,
            timestamp: now(),
            network: FarcasterNetwork::Mainnet.into(),
            body: Some(Body::CastAddBody(cast)),
        };

        let hash = message_hash(&data);
        let signature = sign_hash(signing_key, &hash);

        let message = 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,
        };

        vec![message]
    }
}

#[cfg(test)]
mod test {
    use ed25519_dalek::{Signature, SigningKey, Verifier};
    use rand::rngs::OsRng;
    use crate::cast_builder::CastCreateBuilder;
    use crate::posts::{Embed, Mention};
    use crate::proto::MessageType;
    use crate::to_messages::ToMessages;

    #[test]
    pub fn test_cast_builder() {
        let update = CastCreateBuilder::create_empty()
            .body("@Farcaster cool cast".to_string())
            .embeds(vec![Embed::Url("https://something.com".to_string())])
            .mentions(vec![Mention { fid: 1, start_pos: 0 }])
            .build().expect("Couldn't build cast create");

        let signing_key = SigningKey::generate(&mut OsRng);

        let messages = update.to_messages(0, &signing_key);

        let verifying_key = signing_key.verifying_key();

        for message in &messages {
            assert_eq!(message.signer, verifying_key.as_bytes());
            assert!(verifying_key.verify(&message.hash, &Signature::from_slice(&message.signature).unwrap()).is_ok());
        }
        assert_eq!(messages.first().unwrap().data.clone().unwrap().r#type(), MessageType::CastAdd);
    }

}