iris-chat 0.1.10

Iris Chat command line client and shared encrypted chat core
Documentation
use super::*;

pub(super) fn is_group_chat_id(chat_id: &str) -> bool {
    chat_id.starts_with(GROUP_CHAT_PREFIX)
}

pub(super) fn group_chat_id(group_id: &str) -> String {
    format!("{GROUP_CHAT_PREFIX}{group_id}")
}

pub(super) fn parse_group_id_from_chat_id(chat_id: &str) -> Option<String> {
    chat_id
        .strip_prefix(GROUP_CHAT_PREFIX)
        .map(|group_id| group_id.to_string())
}

pub(super) fn normalize_group_id(value: &str) -> Option<String> {
    if let Some(group_id) = parse_group_id_from_chat_id(value) {
        if !group_id.trim().is_empty() {
            return Some(group_id);
        }
        return None;
    }
    let trimmed = value.trim();
    if trimmed.is_empty() {
        None
    } else {
        Some(trimmed.to_string())
    }
}

pub(super) fn chat_kind_for_id(chat_id: &str) -> ChatKind {
    if is_group_chat_id(chat_id) {
        ChatKind::Group
    } else {
        ChatKind::Direct
    }
}

pub(super) fn first_tag_value<'a>(
    tags: impl IntoIterator<Item = &'a nostr::Tag>,
    name: &str,
) -> Option<String> {
    tags.into_iter()
        .find(|tag| tag.as_slice().first().map(|value| value.as_str()) == Some(name))
        .and_then(|tag| tag.as_slice().get(1).cloned())
}

pub(super) fn message_ids_from_tags<'a>(
    tags: impl IntoIterator<Item = &'a nostr::Tag>,
) -> Vec<String> {
    tags.into_iter()
        .filter(|tag| tag.as_slice().first().map(|value| value.as_str()) == Some("e"))
        .filter_map(|tag| tag.as_slice().get(1).cloned())
        .collect()
}

pub(super) fn event_message_ids(event: &UnsignedEvent) -> Vec<String> {
    message_ids_from_tags(event.tags.iter())
}

pub(super) fn message_expiration_from_tags<'a>(
    tags: impl IntoIterator<Item = &'a nostr::Tag>,
) -> Option<u64> {
    let raw = tags
        .into_iter()
        .find(|tag| tag.as_slice().first().map(|value| value.as_str()) == Some("expiration"))
        .and_then(|tag| tag.as_slice().get(1))?;
    let mut value = raw.parse::<u64>().ok()?;
    if value == 0 {
        return None;
    }
    while value > 9_999_999_999 {
        value /= 1_000;
    }
    (value > 0).then_some(value)
}

pub(super) fn chat_id_for_tags<'a>(
    sender_owner: PublicKey,
    local_owner: PublicKey,
    tags: impl IntoIterator<Item = &'a nostr::Tag>,
) -> String {
    let tags = tags.into_iter().collect::<Vec<_>>();
    if let Some(group_id) = first_tag_value(tags.iter().copied(), "l") {
        return group_chat_id(&group_id);
    }
    if sender_owner == local_owner {
        if let Some(peer_hex) = first_tag_value(tags.iter().copied(), "p") {
            if let Ok(peer) = PublicKey::parse(&peer_hex) {
                if peer != local_owner {
                    return peer.to_hex();
                }
            }
        }
    }
    sender_owner.to_hex()
}

pub(super) struct RuntimeRumor {
    pub(super) id: Option<String>,
    pub(super) kind: u32,
    pub(super) content: String,
    pub(super) created_at_secs: u64,
    pub(super) tags: Vec<nostr::Tag>,
    pub(super) unsigned: Option<UnsignedEvent>,
}

#[derive(Deserialize)]
struct LooseRuntimeRumor {
    #[serde(default)]
    id: Option<String>,
    kind: u32,
    content: String,
    created_at: u64,
    #[serde(default)]
    tags: Vec<Vec<String>>,
}

pub(super) fn parse_runtime_rumor(content: &str) -> Option<RuntimeRumor> {
    if let Ok(event) = serde_json::from_str::<UnsignedEvent>(content) {
        return Some(RuntimeRumor {
            id: event.id.as_ref().map(ToString::to_string),
            kind: event.kind.as_u16() as u32,
            content: event.content.clone(),
            created_at_secs: event.created_at.as_secs(),
            tags: event.tags.iter().cloned().collect(),
            unsigned: Some(event),
        });
    }

    let loose = serde_json::from_str::<LooseRuntimeRumor>(content).ok()?;
    let tags = loose
        .tags
        .iter()
        .filter_map(|tag| nostr::Tag::parse(tag.iter().map(String::as_str)).ok())
        .collect();
    Some(RuntimeRumor {
        id: loose.id,
        kind: loose.kind,
        content: loose.content,
        created_at_secs: loose.created_at,
        tags,
        unsigned: None,
    })
}

pub(super) fn chat_settings_ttl_seconds(content: &str) -> Option<u64> {
    let value = serde_json::from_str::<serde_json::Value>(content).ok()?;
    if let Some(ttl) = value.as_u64() {
        return Some(ttl);
    }
    value
        .get("messageTtlSeconds")
        .or_else(|| value.get("message_ttl_seconds"))
        .and_then(serde_json::Value::as_u64)
}

pub(super) fn send_options_for_expiration(expires_at_secs: Option<u64>) -> Option<SendOptions> {
    expires_at_secs.map(|expires_at| SendOptions {
        expires_at: Some(expires_at),
        ttl_seconds: None,
    })
}