caracal 0.2.0

Nostr client for Gemini
use http::Uri;
use nostr_sdk::prelude::*;
use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::str::FromStr;
use std::time::Duration;
use url::Url;
use crate::hookstr::DEFAULT_RELAYS;

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 async fn advertise_relay_list(
    client: &Client,
) -> Result<(), Box<dyn Error>> {
    let mut relay_metadata: HashMap<RelayUrl, Option<RelayMetadata>> =
        HashMap::new();

    for (rurl, _relay) in client.pool().relays_with_flag(RelayServiceFlags::WRITE, FlagCheck::Any).await {
        relay_metadata.insert(rurl, Some(RelayMetadata::Write));
    }

    let evb_meta = EventBuilder::relay_list(relay_metadata);

    client.send_event_builder_to(DEFAULT_RELAYS.into_iter().map(|u| RelayUrl::parse(u).unwrap()), evb_meta).await?;

    /* Send (fixed list for now to make the DMs UI work) inbox relays event */
    let inboxr: Vec<&str> =
        vec!["wss://relay.damus.io", "wss://nostr.bitcoiner.social"];

    let evb_ir = EventBuilder::new(Kind::InboxRelays, "")
        .tags(inboxr.into_iter().map(|ir| Tag::custom(TagKind::Relay, vec![ir])));

    client.send_event_builder(evb_ir).await?;

    Ok(())
}

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

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
}