use jmap_base_client::{ClientError, WsFrame, WsSession};
use jmap_chat_types::{ChatPresenceEvent, ChatStreamEnable, ChatTypingEvent, EphemeralMessage};
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum ChatWsFrame {
StateChange(jmap_base_client::StateChange),
Response(jmap_types::JmapResponse),
ChatTyping(ChatTypingEvent),
ChatPresence(ChatPresenceEvent),
Unknown {
type_name: String,
},
}
pub fn parse_chat_ws_frame(frame: WsFrame) -> ChatWsFrame {
match frame {
WsFrame::StateChange(sc) => ChatWsFrame::StateChange(sc),
WsFrame::Response(r) => ChatWsFrame::Response(r),
WsFrame::Unknown { type_name, raw } => match type_name.as_str() {
"ChatTypingEvent" => serde_json::from_value::<ChatTypingEvent>(raw)
.map(ChatWsFrame::ChatTyping)
.unwrap_or_else(|_| ChatWsFrame::Unknown {
type_name: "ChatTypingEvent".to_owned(),
}),
"ChatPresenceEvent" => serde_json::from_value::<ChatPresenceEvent>(raw)
.map(ChatWsFrame::ChatPresence)
.unwrap_or_else(|_| ChatWsFrame::Unknown {
type_name: "ChatPresenceEvent".to_owned(),
}),
_ => ChatWsFrame::Unknown { type_name },
},
_ => ChatWsFrame::Unknown {
type_name: "<unknown>".to_owned(),
},
}
}
pub trait ChatWsExt: sealed::Sealed {
fn next_chat_frame(
&mut self,
) -> impl std::future::Future<Output = Option<Result<ChatWsFrame, ClientError>>> + Send;
fn send_stream_enable(
&mut self,
enable: &ChatStreamEnable,
) -> impl std::future::Future<Output = Result<(), ClientError>> + Send;
fn send_stream_disable(
&mut self,
) -> impl std::future::Future<Output = Result<(), ClientError>> + Send;
}
mod sealed {
pub trait Sealed {}
impl Sealed for ::jmap_base_client::WsSession {}
}
impl ChatWsExt for WsSession {
async fn next_chat_frame(&mut self) -> Option<Result<ChatWsFrame, ClientError>> {
self.next_frame().await.map(|r| r.map(parse_chat_ws_frame))
}
async fn send_stream_enable(&mut self, enable: &ChatStreamEnable) -> Result<(), ClientError> {
let msg = EphemeralMessage::Enable(enable.clone());
let text = serde_json::to_string(&msg).map_err(ClientError::from_parse)?;
self.send_text(text).await
}
async fn send_stream_disable(&mut self) -> Result<(), ClientError> {
let msg: EphemeralMessage = serde_json::from_str(r#"{"@type":"ChatStreamDisable"}"#)
.map_err(ClientError::from_parse)?;
let text = serde_json::to_string(&msg).map_err(ClientError::from_parse)?;
self.send_text(text).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use jmap_chat_types::{Presence, SenderId};
use jmap_types::Id;
fn unknown_frame(type_name: &str, json: &str) -> WsFrame {
let raw = serde_json::from_str(json).expect("test fixture must be valid JSON");
WsFrame::Unknown {
type_name: type_name.to_owned(),
raw,
}
}
#[test]
fn parse_state_change_forwarded() {
let sc: jmap_base_client::StateChange =
serde_json::from_str(r#"{"changed":{"acc1":{"Chat":"s1"}}}"#)
.expect("test fixture must deserialize");
let frame = parse_chat_ws_frame(WsFrame::StateChange(sc));
match frame {
ChatWsFrame::StateChange(got) => {
assert_eq!(
got.changed
.get(&Id::from("acc1"))
.and_then(|m| m.get("Chat"))
.map(|s| s.as_ref()),
Some("s1")
);
}
other => panic!("expected StateChange, got {other:?}"),
}
}
#[test]
fn parse_chat_typing_event_valid() {
let frame = unknown_frame(
"ChatTypingEvent",
r#"{"@type":"ChatTypingEvent","chatId":"c1","senderId":"u1","typing":true}"#,
);
let got = parse_chat_ws_frame(frame);
match got {
ChatWsFrame::ChatTyping(evt) => {
assert_eq!(evt.chat_id.as_ref(), "c1");
assert_eq!(evt.sender_id, SenderId::Contact("u1".to_owned()));
assert!(evt.typing);
}
other => panic!("expected ChatTyping, got {other:?}"),
}
}
#[test]
fn parse_chat_presence_event_valid() {
let frame = unknown_frame(
"ChatPresenceEvent",
r#"{"@type":"ChatPresenceEvent","contactId":"u2","presence":"away"}"#,
);
let got = parse_chat_ws_frame(frame);
match got {
ChatWsFrame::ChatPresence(evt) => {
assert_eq!(evt.contact_id.as_ref(), "u2");
assert_eq!(evt.presence, Presence::Away);
}
other => panic!("expected ChatPresence, got {other:?}"),
}
}
#[test]
fn parse_chat_typing_malformed_degrades_to_unknown() {
let frame = WsFrame::Unknown {
type_name: "ChatTypingEvent".to_owned(),
raw: serde_json::json!({"missing": "required fields"}),
};
let got = parse_chat_ws_frame(frame);
match got {
ChatWsFrame::Unknown { type_name } => {
assert_eq!(type_name, "ChatTypingEvent");
}
other => panic!("expected Unknown, got {other:?}"),
}
}
#[test]
fn parse_unknown_type_forwarded() {
let frame = unknown_frame("FutureEvent", r#"{"@type":"FutureEvent","foo":"bar"}"#);
let got = parse_chat_ws_frame(frame);
match got {
ChatWsFrame::Unknown { type_name } => assert_eq!(type_name, "FutureEvent"),
other => panic!("expected Unknown, got {other:?}"),
}
}
#[test]
fn send_stream_enable_serializes_with_at_type() {
let enable: ChatStreamEnable = serde_json::from_str(r#"{"dataTypes":["typing"]}"#)
.expect("test fixture must deserialize");
let msg = EphemeralMessage::Enable(enable);
let text = serde_json::to_string(&msg).expect("must serialize");
assert!(
text.contains("\"@type\":\"ChatStreamEnable\""),
"ChatStreamEnable frame must include @type discriminant; got: {text}"
);
assert!(
text.contains("\"dataTypes\""),
"ChatStreamEnable frame must include dataTypes field; got: {text}"
);
}
#[test]
fn send_stream_disable_serializes_with_at_type() {
let msg: EphemeralMessage =
serde_json::from_str(r#"{"@type":"ChatStreamDisable"}"#).expect("must deserialize");
let text = serde_json::to_string(&msg).expect("must serialize");
assert!(
text.contains("\"@type\":\"ChatStreamDisable\""),
"ChatStreamDisable frame must include @type discriminant; got: {text}"
);
}
}