caracal 0.2.9

Nostr client for Gemini
use http::Uri;
use nostr_sdk::prelude::*;
use std::collections::HashSet;
use std::str::FromStr;
use std::time::Duration;
use url::Url;

pub async fn get_user_metadata(
    client: &Client,
    pubkey: PublicKey,
) -> Option<Metadata> {
    let filter_meta = Filter::new().author(pubkey).kind(Kind::Metadata);

    match client.database().query(filter_meta).await {
        Ok(mevents) => {
            if !mevents.is_empty() {
                match Metadata::from_json(&mevents.first().unwrap().content) {
                    Ok(usermeta) => return Some(usermeta.clone()),
                    Err(_err) => return None,
                }
            }
        }
        Err(_err) => {
            return None;
        }
    }

    None
}

pub async fn fetch_user_metadata(
    client: &Client,
    pubk: PublicKey,
) -> Option<Metadata> {
    if let Ok(Some(metadata)) =
        client.fetch_metadata(pubk, Duration::from_secs(5)).await
    {
        Some(metadata)
    } else {
        None
    }
}

pub fn notes_filter() -> Filter {
    Filter::new()
        .kind(Kind::TextNote)
        .kind(Kind::LongFormTextNote)
}

pub fn parse_note_urls(event: &Event) -> Vec<(Uri, Uri)> {
    let mut vec: Vec<(Uri, Uri)> = Vec::new();

    for elem in event.content.split_whitespace() {
        if let Ok(url) = Url::parse(elem) {
            let scheme = url.scheme();
            let Ok(orig_uri) = Uri::from_str(url.as_str()) else {
                continue;
            };

            match scheme {
                "nostr" => {
                    let path = url.path();

                    if path.is_empty() {
                        continue;
                    }

                    let uri: Uri = if path.starts_with("nevent") {
                        Uri::builder()
                            .path_and_query(format!("/note/{}/thread", path))
                            .build()
                            .unwrap()
                    } else {
                        Uri::from_str(url.as_str()).unwrap()
                    };

                    vec.push((orig_uri, uri));
                }
                _ => {
                    vec.push((orig_uri.clone(), orig_uri.clone()));
                    continue;
                }
            }
        }
    }

    vec
}

pub fn parse_note_ids(event: &Event) -> (Vec<EventId>, Vec<PublicKey>) {
    let mut vec_evid: Vec<EventId> = Vec::new();
    let mut vec_pk: Vec<PublicKey> = Vec::new();

    for elem in event.content.split_whitespace() {
        let Ok(url) = Url::parse(elem) else { continue };

        let scheme = url.scheme();

        match scheme {
            "nostr" => {
                let urls = url.as_str();
                let path = url.path();

                if path.starts_with("nevent") {
                    let Ok(res) = Nip19Event::from_nostr_uri(urls) else {
                        continue;
                    };

                    vec_evid.push(res.event_id);
                }
                if path.starts_with("npub") {
                    let Ok(pk) = PublicKey::parse(path) else {
                        continue;
                    };

                    vec_pk.push(pk);
                }
            }
            _ => {
                continue;
            }
        }
    }

    (vec_evid, vec_pk)
}

pub fn reactions_summary(reaction_events: &Events) -> String {
    let mut summary = String::new();
    let rlist: HashSet<_> =
        reaction_events.iter().map(|e| e.content.clone()).collect();

    for emoji in rlist {
        let count = reaction_events
            .iter()
            .filter(|e| e.content == emoji)
            .count();

        let emojis = if emoji == "+" { "\u{1F44D}" } else { &emoji };

        summary.push_str(&format!("{} {} ", count, emojis));
    }

    summary
}