fatline-rs 0.2.2

Farcaster grpc client and utility library
Documentation
//! Types that are used by the various frontend implementations

use async_trait::async_trait;
use ed25519_dalek::SigningKey;
use eyre::{bail, Result};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tonic::Response;

use crate::profile_builder::ProfileUpdate;
use crate::proto::on_chain_event::Body;
use crate::proto::{
    FidRequest, Message, SignerEventType, SignerRequest, StorageLimitsResponse, UserDataRequest,
    UserDataType,
};
use crate::to_messages::ToMessages;
use crate::utils::{optional_get_user_data, optional_get_user_data_value};
use crate::{HubService, PUBLIC_KEY_LENGTH};

/// The combined user's profile, holding values from all user update types
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct Profile {
    pub fid: u64,
    pub username: Option<String>,
    pub display_name: Option<String>,
    pub profile_picture: Option<String>,
    pub bio: Option<String>,
    pub url: Option<String>,
}

#[derive(Error, Debug)]
pub enum UserError {
    #[error("no storage available")]
    NoStorage,
}

/// A service to get merged user info form a hubble grpc service
#[async_trait]
pub trait UserService {
    /// Combine all the user's data messages into a serializable Profile type; might be default for values that aren't set
    async fn get_user_profile(&mut self, fid: u64) -> Result<Profile>;
    /// Checks that the key is *currently* valid for the fid, returning false or error in every other case
    async fn key_valid_for_fid(&mut self, key: &[u8; PUBLIC_KEY_LENGTH], fid: u64) -> Result<bool>;
    /// Gets a list of profiles that *follow* the user. maybe paginate this
    async fn get_user_followers(&mut self, fid: u64) -> Result<Vec<Profile>>;
    /// Get a list of profiles that the user is *following*. maybe paginate this
    async fn get_user_following(&mut self, fid: u64) -> Result<Vec<Profile>>;
    /// Seek through all follow events until none remain
    async fn get_all_follow_events(&mut self, fid: u64) -> Result<Vec<u64>>;
    // /// Subscribe to all user events
    // async fn get_all_user_events(&mut self, fid: Option<u64>) -> Result<impl Stream<Item = HubEvent>>;
    async fn update_user(
        &mut self,
        signing_key: &SigningKey,
        fid: u64,
        update: ProfileUpdate,
    ) -> Result<Profile>;
}

fn get_user_data_request(fid: u64, data_type: UserDataType) -> UserDataRequest {
    UserDataRequest {
        fid,
        user_data_type: data_type as i32,
    }
}

fn get_user_data(response: Response<Message>) -> Option<String> {
    optional_get_user_data(response).and_then(optional_get_user_data_value)
}

fn get_reaction_data(response: Response<Message>) -> Option<u64> {
    todo!()
    // optional_get_follow_message(&response.into_inner()).and_then(optional_get_followed_fid)
}

fn contains_any_storage_units(response: Response<StorageLimitsResponse>) -> bool {
    response.into_inner().units > 0
}

#[async_trait]
impl UserService for HubService {
    async fn get_user_profile(&mut self, fid: u64) -> Result<Profile> {
        // Check fid is valid, save subsequent calls if we are looking up non-existent data
        let fid_check = self
            .get_current_storage_limits_by_fid(FidRequest {
                fid,
                page_size: Some(1),
                page_token: None,
                reverse: None,
            })
            .await
            .is_ok_and(contains_any_storage_units);

        if !fid_check {
            bail!(UserError::NoStorage)
        }

        // Username request
        let username = self
            .get_user_data(get_user_data_request(fid, UserDataType::Username))
            .await
            .map_or(None, get_user_data);

        // Display name request
        let display_name = self
            .get_user_data(get_user_data_request(fid, UserDataType::Display))
            .await
            .map_or(None, get_user_data);

        // Profile pictue URL request
        let profile_picture = self
            .get_user_data(get_user_data_request(fid, UserDataType::Pfp))
            .await
            .map_or(None, get_user_data);

        // Bio request
        let bio = self
            .get_user_data(get_user_data_request(fid, UserDataType::Bio))
            .await
            .map_or(None, get_user_data);

        // Extra profile URL request
        let url = self
            .get_user_data(get_user_data_request(fid, UserDataType::Url))
            .await
            .map_or(None, get_user_data);

        // Concat all data together, technically everything except fid here is optional
        Ok(Profile {
            fid,
            username,
            display_name,
            profile_picture,
            bio,
            url,
        })
    }

    async fn key_valid_for_fid(&mut self, key: &[u8; PUBLIC_KEY_LENGTH], fid: u64) -> Result<bool> {
        let onchain_event = self
            .get_on_chain_signer(SignerRequest {
                fid: fid,
                signer: key.to_vec(),
            })
            .await?
            .into_inner();

        match onchain_event.body {
            Some(Body::SignerEventBody(body)) => {
                // return bool of whether the signer event was an add
                // a remove signifies that the signer isn't valid anymore
                Ok(body.event_type == SignerEventType::Add as i32)
            }
            _ => Ok(false),
        }
    }

    async fn get_user_followers(&mut self, fid: u64) -> Result<Vec<Profile>> {
        todo!()
        // self.get_links_by_fid()
    }

    async fn get_user_following(&mut self, fid: u64) -> Result<Vec<Profile>> {
        let all_followed = self.get_all_follow_events(fid).await?;
        todo!()
    }

    async fn get_all_follow_events(&mut self, fid: u64) -> Result<Vec<u64>> {
        // let mut page = None;
        // let results = Vec::new();
        // while page.is_some() || results.is_empty() {
        //     let res = self.get_all_cast_messages_by_fid(FidRequest {
        //         page_token: page.clone(),
        //         fid,
        //         reverse: None,
        //         page_size: None
        //     }).await?;
        //     let inner = res.into_inner();
        //     page = inner.next_page_token;
        // TODO: need to actually map the follow to unfollow adds to removes
        // let all_follows = inner.messages
        //     .iter()
        //     .map();
        // }
        todo!()
    }

    async fn update_user(
        &mut self,
        signing_key: &SigningKey,
        fid: u64,
        update: ProfileUpdate,
    ) -> Result<Profile> {
        let messages = update.to_messages(fid, &signing_key);
        for message in messages {
            self.submit_message(message).await?;
        }
        self.get_user_profile(fid).await
    }

    // async fn get_all_user_events(&mut self, fid: Option<u64>) -> Result<impl Stream<Item = HubEvent>> {
    //     todo!()
    // }
}