iris-chat 0.1.24

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 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) fn chat_id_for_runtime_message<'a>(
    sender_owner: PublicKey,
    local_owner: PublicKey,
    conversation_owner: Option<PublicKey>,
    tags: impl IntoIterator<Item = &'a nostr::Tag>,
) -> String {
    let chat_id = chat_id_for_tags(sender_owner, local_owner, tags);
    if is_group_chat_id(&chat_id) {
        return chat_id;
    }
    direct_self_sync_chat_id(sender_owner, local_owner, conversation_owner).unwrap_or(chat_id)
}

pub(super) fn direct_self_sync_chat_id(
    sender_owner: PublicKey,
    local_owner: PublicKey,
    conversation_owner: Option<PublicKey>,
) -> Option<String> {
    let owner = conversation_owner?;
    if sender_owner == local_owner && owner != local_owner {
        Some(owner.to_hex())
    } else {
        None
    }
}

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

pub(super) fn parse_runtime_rumor(content: &str) -> Option<RuntimeRumor> {
    if let Ok(mut event) = serde_json::from_str::<UnsignedEvent>(content) {
        event.ensure_id();
        event.verify_id().ok()?;
        let id = event.id.as_ref()?.to_string();
        return Some(RuntimeRumor {
            id,
            pubkey: event.pubkey,
            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(),
        });
    }
    None
}

pub(super) fn looks_like_runtime_rumor(content: &str) -> bool {
    let Ok(value) = serde_json::from_str::<serde_json::Value>(content) else {
        return false;
    };
    value.get("kind").is_some()
        && value.get("content").is_some()
        && value.get("created_at").is_some()
        && value.get("pubkey").is_some()
}

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)
}