rustybook-messenger 0.2.1

Messenger client for Rustybook
Documentation
use serde_json::Value;

use crate::error::MessengerError;
use crate::gateway::events::{
    Event,
    MessageEvent,
    PresenceEvent,
    TypingEvent,
};

pub(super) fn parse_messages(payload: &[u8]) -> Result<Vec<Event>, MessengerError> {
    let value: Value = serde_json::from_slice(payload)?;
    let message_payload = normalize_message_payload(value);
    let mut output = Vec::new();

    let deltas = extract_deltas(&message_payload);

    for delta in deltas {
        if let Some(event) = parse_message_delta(&delta) {
            output.push(Event::Message(event));
        }
    }

    Ok(output)
}

pub(super) fn parse_typing(payload: &[u8]) -> Result<Vec<Event>, MessengerError> {
    let value: Value = serde_json::from_slice(payload)?;

    let user_id = value
        .get("sender_fbid")
        .or_else(|| value.get("from"))
        .and_then(value_as_string)
        .unwrap_or_default();

    if user_id.is_empty() {
        return Ok(Vec::new());
    }

    let is_typing = value
        .get("state")
        .and_then(Value::as_i64)
        .map(|state| state == 1)
        .unwrap_or(false);

    let thread_id = value
        .get("thread")
        .or_else(|| value.get("thread_fbid"))
        .and_then(value_as_string);

    Ok(vec![Event::Typing(TypingEvent {
        user_id,
        thread_id,
        is_typing,
    })])
}

pub(super) fn parse_presence(payload: &[u8]) -> Result<Vec<Event>, MessengerError> {
    let value: Value = serde_json::from_slice(payload)?;
    let mut output = Vec::new();

    let list = value
        .get("list")
        .and_then(Value::as_array)
        .cloned()
        .unwrap_or_default();

    for item in list {
        let Some(user_id) = item.get("u").and_then(value_as_string) else {
            continue;
        };

        let is_active = item
            .get("p")
            .and_then(Value::as_i64)
            .map(|presence| presence > 0)
            .unwrap_or(false);

        let last_active_ms = item.get("l").and_then(Value::as_i64);

        output.push(Event::Presence(PresenceEvent {
            user_id,
            is_active,
            last_active_ms,
        }));
    }

    Ok(output)
}

fn normalize_message_payload(mut value: Value) -> Value {
    if let Some(inner) = value.get("d").and_then(parse_nested_json) {
        value = inner;
    }

    if let Some(inner) = value.get("payload").and_then(parse_nested_json) {
        value = inner;
    }

    value
}

fn parse_nested_json(value: &Value) -> Option<Value> {
    if value.is_object() || value.is_array() {
        return Some(value.clone());
    }

    let raw = value.as_str()?;
    serde_json::from_str::<Value>(raw).ok()
}

fn extract_deltas(value: &Value) -> Vec<Value> {
    if let Some(deltas) = value.get("deltas").and_then(Value::as_array) {
        return deltas.clone();
    }

    if let Some(delta) = value.get("delta") {
        if let Some(array) = delta.as_array() {
            return array.clone();
        }
        return vec![delta.clone()];
    }

    if let Some(inner) = value.get("d").and_then(parse_nested_json) {
        return extract_deltas(&inner);
    }

    if let Some(inner) = value.get("payload").and_then(parse_nested_json) {
        return extract_deltas(&inner);
    }

    Vec::new()
}

pub(super) fn parse_message_delta(delta: &Value) -> Option<MessageEvent> {
    if let Some(message) = parse_message_like(delta) {
        return Some(message);
    }

    if let Some(inner) = delta.get("delta").and_then(parse_message_like) {
        return Some(inner);
    }

    if let Some(inner) = delta.get("message").and_then(parse_message_like) {
        return Some(inner);
    }

    if let Some(inner) = delta.get("deltaNewMessage").and_then(parse_message_like) {
        return Some(inner);
    }

    if let Some(inner) = delta
        .get("deltaMessageReply")
        .and_then(|reply| reply.get("message"))
        .and_then(parse_message_like)
    {
        return Some(inner);
    }

    None
}

fn parse_message_like(value: &Value) -> Option<MessageEvent> {
    if let Some(class) = value.get("class").and_then(value_as_string)
        && class != "NewMessage"
    {
        return None;
    }

    let metadata = value.get("messageMetadata")?;
    let thread_id = metadata
        .get("threadKey")
        .and_then(|thread_key| {
            thread_key
                .get("threadFbId")
                .or_else(|| thread_key.get("otherUserFbId"))
        })
        .and_then(value_as_string)?;

    let sender_id = metadata
        .get("actorFbId")
        .and_then(value_as_string)
        .unwrap_or_default();

    let message_id = metadata.get("messageId").and_then(value_as_string);

    let timestamp_ms = metadata.get("timestamp").and_then(value_as_i64);

    let text = value.get("body").and_then(value_as_string);

    Some(MessageEvent {
        message_id,
        thread_id,
        sender_id,
        text,
        timestamp_ms,
    })
}

pub(super) fn value_as_string(value: &Value) -> Option<String> {
    match value {
        Value::String(inner) => Some(inner.clone()),
        Value::Number(inner) => Some(inner.to_string()),
        _ => None,
    }
}

pub(super) fn value_as_i64(value: &Value) -> Option<i64> {
    match value {
        Value::Number(inner) => inner.as_i64(),
        Value::String(inner) => inner.parse::<i64>().ok(),
        _ => None,
    }
}