im-core 0.1.0

Rust IM SDK for Awiki clients built on Agent Network Protocol (ANP)
Documentation
use serde_json::Value;

#[cfg(all(feature = "sqlite", any(feature = "blocking", test)))]
pub(crate) fn cached_group_snapshot(
    client: &crate::core::ImClient,
    group_did: &str,
) -> crate::ImResult<Option<Value>> {
    let connection = crate::internal::local_state::open_writable(
        &client.core_inner().sdk_paths().local_state.sqlite_path,
    )?;
    for owner_identity_id in owner_identity_ids(client) {
        if let Some(snapshot) =
            crate::internal::local_state::groups::get_group_snapshot_for_owner_identity(
                &connection,
                &owner_identity_id,
                client.did().as_str(),
                &group_storage_key(group_did),
            )?
        {
            return Ok(Some(enrich_cached_group_snapshot(snapshot)));
        }
    }
    Ok(None)
}

#[cfg(all(feature = "sqlite", not(any(feature = "blocking", test))))]
pub(crate) fn cached_group_snapshot(
    _client: &crate::core::ImClient,
    _group_did: &str,
) -> crate::ImResult<Option<Value>> {
    Ok(None)
}

#[cfg(not(feature = "sqlite"))]
pub(crate) fn cached_group_snapshot(
    _client: &crate::core::ImClient,
    _group_did: &str,
) -> crate::ImResult<Option<Value>> {
    Ok(None)
}

#[cfg(feature = "sqlite")]
pub(crate) async fn cached_group_snapshot_async(
    client: &crate::core::ImClient,
    group_did: &str,
) -> crate::ImResult<Option<Value>> {
    let db = client.core_inner().local_state_db().await?;
    for owner_identity_id in owner_identity_ids(client) {
        if let Some(snapshot) = db
            .get_group_snapshot(
                owner_identity_id,
                client.did().as_str(),
                group_storage_key(group_did),
            )
            .await?
        {
            return Ok(Some(enrich_cached_group_snapshot(snapshot)));
        }
    }
    Ok(None)
}

#[cfg(not(feature = "sqlite"))]
pub(crate) async fn cached_group_snapshot_async(
    _client: &crate::core::ImClient,
    _group_did: &str,
) -> crate::ImResult<Option<Value>> {
    Ok(None)
}

pub(crate) fn is_active_group_owner(snapshot: &Value) -> bool {
    let role = default_string(
        &string_value(snapshot.get("my_role")),
        &string_value(snapshot.get("member_role")),
    );
    let status = default_string(
        &string_value(snapshot.get("membership_status")),
        &string_value(snapshot.get("member_status")),
    );
    role == "owner" && status == "active"
}

pub(crate) fn group_snapshot_uses_e2ee(snapshot: &Value) -> bool {
    if snapshot.is_null() {
        return false;
    }
    if value_string(snapshot.get("message_security_profile")) == "group-e2ee" {
        return true;
    }
    if snapshot
        .get("group_policy")
        .and_then(Value::as_object)
        .map(|policy| value_string(policy.get("message_security_profile")))
        .is_some_and(|profile| profile == "group-e2ee")
    {
        return true;
    }
    decoded_metadata(snapshot)
        .as_ref()
        .map(|metadata| value_string(metadata.get("message_security_profile")))
        .is_some_and(|profile| profile == "group-e2ee")
}

#[cfg(feature = "sqlite")]
fn owner_identity_ids(client: &crate::core::ImClient) -> Vec<String> {
    let mut ids = Vec::new();
    push_nonempty_unique(&mut ids, client.current_identity().id.as_str());
    if let Some(alias) = client.current_identity().local_alias.as_deref() {
        push_nonempty_unique(&mut ids, alias);
    }
    ids
}

#[cfg(feature = "sqlite")]
fn push_nonempty_unique(values: &mut Vec<String>, value: &str) {
    let value = value.trim();
    if value.is_empty() || values.iter().any(|known| known == value) {
        return;
    }
    values.push(value.to_string());
}

#[cfg(feature = "sqlite")]
fn enrich_cached_group_snapshot(mut snapshot: Value) -> Value {
    let metadata = snapshot
        .get("metadata")
        .and_then(Value::as_str)
        .and_then(|value| serde_json::from_str::<Value>(value).ok())
        .and_then(|value| normalize_group_snapshot(&value));
    if let (Some(object), Some(Value::Object(metadata_object))) =
        (snapshot.as_object_mut(), metadata)
    {
        for (key, value) in metadata_object {
            object.entry(key).or_insert(value);
        }
    }
    if let Some(object) = snapshot.as_object_mut() {
        let group_did = string_value(object.get("group_did"));
        if !group_did.trim().is_empty() {
            object
                .entry("did".to_string())
                .or_insert(Value::String(group_did));
        }
        let my_role = string_value(object.get("my_role"));
        if !my_role.trim().is_empty() {
            object
                .entry("member_role".to_string())
                .or_insert(Value::String(my_role));
        }
        let status = string_value(object.get("membership_status"));
        if !status.trim().is_empty() {
            object
                .entry("member_status".to_string())
                .or_insert(Value::String(status));
        }
        if !object.contains_key("group_profile") {
            let mut profile = serde_json::Map::new();
            insert_from_object(object, &mut profile, "display_name", "name");
            insert_from_object(object, &mut profile, "description", "description");
            insert_from_object(object, &mut profile, "slug", "slug");
            insert_from_object(object, &mut profile, "goal", "goal");
            insert_from_object(object, &mut profile, "rules", "rules");
            insert_from_object(object, &mut profile, "message_prompt", "message_prompt");
            insert_from_object(object, &mut profile, "doc_url", "doc_url");
            if !profile.is_empty() {
                object.insert("group_profile".to_string(), Value::Object(profile));
            }
        }
    }
    snapshot
}

#[cfg(feature = "sqlite")]
fn insert_from_object(
    source: &serde_json::Map<String, Value>,
    target: &mut serde_json::Map<String, Value>,
    target_key: &str,
    source_key: &str,
) {
    if let Some(value) = source.get(source_key).filter(|value| !value.is_null()) {
        target.insert(target_key.to_string(), value.clone());
    }
}

fn normalize_group_snapshot(raw: &Value) -> Option<Value> {
    if raw.is_null() {
        return None;
    }
    if let Some(snapshot) = raw.get("group_snapshot").filter(|value| value.is_object()) {
        return Some(snapshot.clone());
    }
    let group_did = group_did_from_result(raw);
    if group_did.trim().is_empty() {
        return None;
    }
    if let Some(profile) = raw.get("group_profile").filter(|value| value.is_object()) {
        return Some(serde_json::json!({
            "group_did": group_did,
            "did": group_did,
            "group_state_version": raw.get("group_state_version").cloned().unwrap_or(Value::Null),
            "name": string_value(profile.get("display_name")),
            "description": profile.get("description").cloned().unwrap_or(Value::Null),
            "discoverability": profile.get("discoverability").cloned().unwrap_or(Value::Null),
            "slug": profile.get("slug").cloned().unwrap_or(Value::Null),
            "goal": profile.get("goal").cloned().unwrap_or(Value::Null),
            "rules": profile.get("rules").cloned().unwrap_or(Value::Null),
            "message_prompt": profile.get("message_prompt").cloned().unwrap_or(Value::Null),
            "doc_url": profile.get("doc_url").cloned().unwrap_or(Value::Null),
            "owner_did": raw.get("owner_did").cloned().unwrap_or(Value::Null),
            "member_role": raw.get("member_role").or_else(|| raw.get("my_role")).cloned().unwrap_or(Value::Null),
            "my_role": raw.get("my_role").or_else(|| raw.get("member_role")).cloned().unwrap_or(Value::Null),
            "member_status": raw.get("member_status").or_else(|| raw.get("membership_status")).cloned().unwrap_or(Value::Null),
            "membership_status": raw.get("membership_status").or_else(|| raw.get("member_status")).cloned().unwrap_or(Value::Null),
            "join_enabled": raw.get("join_enabled").cloned().unwrap_or(Value::Null),
            "member_count": raw.get("member_count").cloned().unwrap_or(Value::Null),
            "group_profile": profile,
            "group_policy": raw.get("group_policy").cloned().unwrap_or(Value::Null),
            "created_at": raw.get("created_at").cloned().unwrap_or(Value::Null),
            "updated_at": raw.get("updated_at").cloned().unwrap_or(Value::Null),
        }));
    }
    Some(raw.clone())
}

fn decoded_metadata(snapshot: &Value) -> Option<Value> {
    snapshot
        .get("metadata")
        .and_then(Value::as_str)
        .and_then(|raw| serde_json::from_str::<Value>(raw).ok())
}

fn group_did_from_result(raw: &Value) -> String {
    string_value(raw.get("group_did"))
        .trim()
        .to_string()
        .or_else_nonempty(|| string_value(raw.get("did")))
}

#[cfg(feature = "sqlite")]
fn group_storage_key(group_did: &str) -> String {
    group_did.trim().to_string()
}

fn string_value(value: Option<&Value>) -> String {
    value
        .and_then(Value::as_str)
        .unwrap_or_default()
        .to_string()
}

fn default_string(value: &str, fallback: &str) -> String {
    if value.trim().is_empty() {
        fallback.to_string()
    } else {
        value.to_string()
    }
}

fn value_string(value: Option<&Value>) -> String {
    value
        .and_then(Value::as_str)
        .unwrap_or_default()
        .trim()
        .to_string()
}

trait NonEmptyString {
    fn or_else_nonempty(self, fallback: impl FnOnce() -> String) -> String;
}

impl NonEmptyString for String {
    fn or_else_nonempty(self, fallback: impl FnOnce() -> String) -> String {
        if self.trim().is_empty() {
            fallback()
        } else {
            self
        }
    }
}