use super::MessageV3Event;
use crate::error::ImError;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
pub fn channel(data: Value) -> Result<MessageV3Event, crate::ImError> {
super::encode("im:read:channel", data)
}
pub fn channel_from_member_projection(
channel_id: &str,
projection: &Value,
) -> Result<MessageV3Event, crate::ImError> {
let row = projection.get("dialogPatch").unwrap_or(projection);
channel(serde_json::json!({
"channelId": channel_id,
"userId": row.get("userId").cloned().unwrap_or(Value::Null),
"state": "read",
"unreadCount": row.get("unreadCount").cloned().unwrap_or(Value::Null),
"unreadPostId": row.get("unreadPostId").cloned().unwrap_or(Value::Null),
"mentionCount": row.get("mentionCount").cloned().unwrap_or(Value::Null),
"mentionCountRoot": row.get("mentionCountRoot").cloned().unwrap_or(Value::Null),
"mentionList": row.get("mentionList").cloned().unwrap_or(Value::Null),
"urgentCount": row.get("urgentCount").cloned().unwrap_or(Value::Null),
"urgentPostList": row.get("urgentPostList").cloned().unwrap_or(Value::Null),
"lastReadSeq": row.get("lastReadSeq").cloned().unwrap_or(Value::Null),
"projectionRevision": row.get("projectionRevision").cloned().unwrap_or(Value::Null),
"lastPost": row.get("lastPost").cloned().unwrap_or(Value::Null),
"lastPostAt": row.get("lastPostAt").cloned().unwrap_or(Value::Null),
"lastRootPostAt": row.get("lastRootPostAt").cloned().unwrap_or(Value::Null),
"msgCount": row.get("msgCount").cloned().unwrap_or(Value::Null),
"msgCountRoot": row.get("msgCountRoot").cloned().unwrap_or(Value::Null),
"msgCountPrivate": row.get("msgCountPrivate").cloned().unwrap_or(Value::Null),
}))
}
pub fn channels_from_storage_rows(
reply_bytes: &[u8],
viewer_user_id: &str,
) -> Result<Vec<MessageV3Event>, crate::ImError> {
let rows = parse_dialog_rows(reply_bytes)?;
channels_from_dialog_rows(&rows, viewer_user_id)
}
pub(crate) fn parse_dialog_rows(reply_bytes: &[u8]) -> Result<Vec<Value>, crate::ImError> {
serde_json::from_slice(reply_bytes)
.map_err(|error| crate::ImError::Parse(format!("dialog read rows: {error}")))
}
pub(crate) fn channels_from_dialog_rows(
rows: &[Value],
viewer_user_id: &str,
) -> Result<Vec<MessageV3Event>, crate::ImError> {
rows.iter()
.filter(|row| {
row.get("id")
.or_else(|| row.get("channel_id"))
.and_then(Value::as_str)
.is_some_and(|value| !value.is_empty())
})
.map(|row| {
let channel_id = row
.get("id")
.or_else(|| row.get("channel_id"))
.and_then(Value::as_str)
.unwrap_or_default();
let unread_count = integer(&row, "unread_count", "unreadCount");
channel(serde_json::json!({
"channelId": channel_id,
"userId": viewer_user_id,
"state": if unread_count == 0 { "read" } else { "unread" },
"unreadCount": unread_count,
"mentionCount": integer(&row, "mention_count", "mentionCount"),
"urgentCount": integer(&row, "urgent_count", "urgentCount"),
"lastReadSeq": integer(&row, "last_read_seq", "lastReadSeq"),
"projectionRevision": integer(&row, "projection_revision", "projectionRevision"),
}))
})
.collect()
}
pub(crate) fn merge_dialog_rows_for_viewer(
channel_rows: &[Value],
member_reply_bytes: &[u8],
auth_user_id: &str,
) -> Result<Vec<Value>, crate::ImError> {
if auth_user_id.is_empty() {
return Err(ImError::Parse(
"dialog member readback missing auth_user_id".into(),
));
}
let mut channel_ids = HashSet::with_capacity(channel_rows.len());
for row in channel_rows {
let channel_id = dialog_row_channel_id(row, "channel")?;
if !channel_ids.insert(channel_id) {
return Err(ImError::Parse(format!(
"dialog channel readback duplicate channel_id: {channel_id}"
)));
}
}
let member_rows = parse_dialog_rows(member_reply_bytes)?;
let mut member_tops = HashMap::with_capacity(member_rows.len());
for row in &member_rows {
let user_id = dialog_row_text(row, "user_id", "userId", "member user_id")?;
if user_id != auth_user_id {
return Err(ImError::Parse(format!(
"dialog member readback user scope mismatch: {user_id}"
)));
}
let channel_id = dialog_row_channel_id(row, "member")?;
let top = dialog_row_bool(row, "channel_is_top", "channelIsTop")?;
if member_tops.insert(channel_id.to_string(), top).is_some() {
return Err(ImError::Parse(format!(
"dialog member readback duplicate channel_id: {channel_id}"
)));
}
}
let mut merged = Vec::with_capacity(channel_rows.len());
for row in channel_rows {
let channel_id = dialog_row_channel_id(row, "channel")?;
let top = member_tops.get(channel_id).copied().ok_or_else(|| {
ImError::Parse(format!(
"dialog member readback missing channel_id: {channel_id}"
))
})?;
let mut row = row.clone();
row.as_object_mut()
.ok_or_else(|| ImError::Parse("dialog channel row must be object".into()))?
.insert("channelIsTop".into(), Value::Bool(top));
merged.push(row);
}
Ok(merged)
}
fn dialog_row_channel_id<'a>(row: &'a Value, scope: &str) -> Result<&'a str, ImError> {
let id = row.get("id").and_then(Value::as_str);
let channel_id = row.get("channel_id").and_then(Value::as_str);
if let (Some(id), Some(channel_id)) = (id, channel_id) {
if id != channel_id {
return Err(ImError::Parse(format!(
"dialog {scope} row channel id mismatch"
)));
}
}
id.or(channel_id)
.filter(|value| !value.is_empty())
.ok_or_else(|| ImError::Parse(format!("dialog {scope} row missing channel_id")))
}
fn dialog_row_text<'a>(
row: &'a Value,
snake: &str,
camel: &str,
scope: &str,
) -> Result<&'a str, ImError> {
row.get(snake)
.or_else(|| row.get(camel))
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
.ok_or_else(|| ImError::Parse(format!("dialog {scope} missing text")))
}
fn dialog_row_bool(row: &Value, snake: &str, camel: &str) -> Result<bool, ImError> {
let value = row
.get(snake)
.or_else(|| row.get(camel))
.ok_or_else(|| ImError::Parse("dialog member row missing channel_is_top".into()))?;
if let Some(value) = value.as_bool() {
return Ok(value);
}
match value.as_i64() {
Some(0) => Ok(false),
Some(1) => Ok(true),
_ => Err(ImError::Parse(
"dialog member channel_is_top must be bool or 0/1".into(),
)),
}
}
fn integer(row: &Value, snake: &str, camel: &str) -> i64 {
row.get(snake)
.or_else(|| row.get(camel))
.and_then(|value| {
value
.as_i64()
.or_else(|| value.as_u64().and_then(|number| i64::try_from(number).ok()))
})
.unwrap_or_default()
}
pub fn post(data: Value) -> Result<MessageV3Event, crate::ImError> {
super::encode("im:post:read", data)
}
pub fn post_readers(data: Value) -> Result<MessageV3Event, crate::ImError> {
super::encode("im:post:readers", data)
}
pub fn post_for_viewer(
channel_id: &str,
post_id: &str,
author_user_id: &str,
reader_id: &str,
snapshot_id: &str,
member_ids: &[String],
read_bits: &str,
receipt_revision: i64,
viewer_user_id: &str,
) -> Result<MessageV3Event, crate::ImError> {
let data = serde_json::json!({
"postId": post_id,
"channelId": channel_id,
"authorUserId": author_user_id,
"readerUserId": reader_id,
"snapshotId": snapshot_id,
"memberIds": member_ids,
"readBits": read_bits,
"receiptRevision": receipt_revision,
});
if !reader_id.is_empty() && reader_id == viewer_user_id {
return post(data);
}
post_readers(data)
}