mod receipt_thread_serde;
use std::{
collections::{btree_map, BTreeMap},
ops::{Deref, DerefMut},
};
use ruma_common::{
EventId, IdParseError, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, UserId,
};
use ruma_macros::{EventContent, OrdAsRefStr, PartialEqAsRefStr, PartialOrdAsRefStr, StringEnum};
use serde::{Deserialize, Serialize};
use crate::PrivOwnedStr;
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
#[allow(clippy::exhaustive_structs)]
#[ruma_event(type = "m.receipt", kind = EphemeralRoom)]
pub struct ReceiptEventContent(pub BTreeMap<OwnedEventId, Receipts>);
impl ReceiptEventContent {
pub fn user_receipt(
&self,
user_id: &UserId,
receipt_type: ReceiptType,
) -> Option<(&EventId, &Receipt)> {
self.iter().find_map(|(event_id, receipts)| {
let receipt = receipts.get(&receipt_type)?.get(user_id)?;
Some((event_id.as_ref(), receipt))
})
}
}
impl Deref for ReceiptEventContent {
type Target = BTreeMap<OwnedEventId, Receipts>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ReceiptEventContent {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl IntoIterator for ReceiptEventContent {
type Item = (OwnedEventId, Receipts);
type IntoIter = btree_map::IntoIter<OwnedEventId, Receipts>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl FromIterator<(OwnedEventId, Receipts)> for ReceiptEventContent {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = (OwnedEventId, Receipts)>,
{
Self(BTreeMap::from_iter(iter))
}
}
pub type Receipts = BTreeMap<ReceiptType, UserReceipts>;
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, PartialOrdAsRefStr, OrdAsRefStr, PartialEqAsRefStr, Eq, StringEnum, Hash)]
#[non_exhaustive]
pub enum ReceiptType {
#[ruma_enum(rename = "m.read")]
Read,
#[ruma_enum(rename = "m.read.private")]
ReadPrivate,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
pub type UserReceipts = BTreeMap<OwnedUserId, Receipt>;
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct Receipt {
#[serde(skip_serializing_if = "Option::is_none")]
pub ts: Option<MilliSecondsSinceUnixEpoch>,
#[serde(rename = "thread_id", default, skip_serializing_if = "ruma_common::serde::is_default")]
pub thread: ReceiptThread,
}
impl Receipt {
pub fn new(ts: MilliSecondsSinceUnixEpoch) -> Self {
Self { ts: Some(ts), thread: ReceiptThread::Unthreaded }
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ReceiptThread {
#[default]
Unthreaded,
Main,
Thread(OwnedEventId),
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
impl ReceiptThread {
pub fn as_str(&self) -> Option<&str> {
match self {
Self::Unthreaded => None,
Self::Main => Some("main"),
Self::Thread(event_id) => Some(event_id.as_str()),
Self::_Custom(s) => Some(&s.0),
}
}
}
impl<T> TryFrom<Option<T>> for ReceiptThread
where
T: AsRef<str> + Into<Box<str>>,
{
type Error = IdParseError;
fn try_from(s: Option<T>) -> Result<Self, Self::Error> {
let res = match s {
None => Self::Unthreaded,
Some(s) => match s.as_ref() {
"main" => Self::Main,
s_ref if s_ref.starts_with('$') => Self::Thread(EventId::parse(s_ref)?),
_ => Self::_Custom(PrivOwnedStr(s.into())),
},
};
Ok(res)
}
}
#[cfg(test)]
mod tests {
use assert_matches2::assert_matches;
use ruma_common::{owned_event_id, MilliSecondsSinceUnixEpoch};
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::{Receipt, ReceiptThread};
#[test]
fn serialize_receipt() {
let mut receipt = Receipt::default();
assert_eq!(to_json_value(receipt.clone()).unwrap(), json!({}));
receipt.thread = ReceiptThread::Main;
assert_eq!(to_json_value(receipt.clone()).unwrap(), json!({ "thread_id": "main" }));
receipt.thread = ReceiptThread::Thread(owned_event_id!("$abcdef76543"));
assert_eq!(to_json_value(receipt).unwrap(), json!({ "thread_id": "$abcdef76543" }));
let mut receipt =
Receipt::new(MilliSecondsSinceUnixEpoch(1_664_702_144_365_u64.try_into().unwrap()));
assert_eq!(to_json_value(receipt.clone()).unwrap(), json!({ "ts": 1_664_702_144_365_u64 }));
receipt.thread = ReceiptThread::try_from(Some("io.ruma.unknown")).unwrap();
assert_eq!(
to_json_value(receipt).unwrap(),
json!({ "ts": 1_664_702_144_365_u64, "thread_id": "io.ruma.unknown" })
);
}
#[test]
fn deserialize_receipt() {
let receipt = from_json_value::<Receipt>(json!({})).unwrap();
assert_eq!(receipt.ts, None);
assert_eq!(receipt.thread, ReceiptThread::Unthreaded);
let receipt = from_json_value::<Receipt>(json!({ "thread_id": "main" })).unwrap();
assert_eq!(receipt.ts, None);
assert_eq!(receipt.thread, ReceiptThread::Main);
let receipt = from_json_value::<Receipt>(json!({ "thread_id": "$abcdef76543" })).unwrap();
assert_eq!(receipt.ts, None);
assert_matches!(receipt.thread, ReceiptThread::Thread(event_id));
assert_eq!(event_id, "$abcdef76543");
let receipt = from_json_value::<Receipt>(json!({ "ts": 1_664_702_144_365_u64 })).unwrap();
assert_eq!(
receipt.ts.unwrap(),
MilliSecondsSinceUnixEpoch(1_664_702_144_365_u64.try_into().unwrap())
);
assert_eq!(receipt.thread, ReceiptThread::Unthreaded);
let receipt = from_json_value::<Receipt>(
json!({ "ts": 1_664_702_144_365_u64, "thread_id": "io.ruma.unknown" }),
)
.unwrap();
assert_eq!(
receipt.ts.unwrap(),
MilliSecondsSinceUnixEpoch(1_664_702_144_365_u64.try_into().unwrap())
);
assert_matches!(&receipt.thread, ReceiptThread::_Custom(_));
assert_eq!(receipt.thread.as_str().unwrap(), "io.ruma.unknown");
}
}