Skip to main content

nextcloud_talk/
room.rs

1//! Conversation room model for Nextcloud Talk.
2
3use serde::ser::SerializeStruct;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
6use crate::room_type::RoomType;
7
8/// A Nextcloud Talk conversation or room.
9///
10/// Models metadata about a conversation such as its unique token, name,
11/// conversation type, password protection state, and participant count.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct Room {
14    /// Unique alphanumeric identifier for the conversation.
15    pub token: String,
16    /// Conversation name or display name.
17    pub name: String,
18    /// Type of conversation (one-to-one, group, or public link).
19    pub room_type: RoomType,
20    /// Optional conversation description.
21    pub description: Option<String>,
22    /// Whether the conversation is protected with a password.
23    pub has_password: bool,
24    /// Number of participants in the conversation, if reported.
25    pub participant_count: Option<u32>,
26}
27
28impl Room {
29    /// Creates a new [`Room`] descriptor.
30    pub fn new(
31        token: impl Into<String>,
32        name: impl Into<String>,
33        room_type: RoomType,
34        description: Option<String>,
35        has_password: bool,
36        participant_count: Option<u32>,
37    ) -> Self {
38        Self {
39            token: token.into(),
40            name: name.into(),
41            room_type,
42            description,
43            has_password,
44            participant_count,
45        }
46    }
47}
48
49#[derive(Deserialize)]
50#[serde(rename_all = "camelCase")]
51struct RoomWire {
52    token: String,
53    #[serde(default)]
54    name: Option<String>,
55    #[serde(default, alias = "displayName")]
56    display_name: Option<String>,
57    #[serde(default, alias = "roomName")]
58    room_name: Option<String>,
59    #[serde(alias = "room_type", alias = "roomType", alias = "type")]
60    room_type: RoomType,
61    #[serde(default)]
62    description: Option<String>,
63    #[serde(default, alias = "has_password")]
64    has_password: bool,
65    #[serde(
66        default,
67        alias = "numParticipants",
68        alias = "participant_count",
69        deserialize_with = "crate::wire::lossy_opt_u32"
70    )]
71    participant_count: Option<u32>,
72}
73
74impl From<RoomWire> for Room {
75    fn from(wire: RoomWire) -> Self {
76        let name = wire
77            .name
78            .filter(|s| !s.trim().is_empty())
79            .or_else(|| wire.display_name.filter(|s| !s.trim().is_empty()))
80            .or_else(|| wire.room_name.filter(|s| !s.trim().is_empty()))
81            .unwrap_or_default();
82
83        let description = wire.description.filter(|s| !s.trim().is_empty());
84
85        Self {
86            token: wire.token,
87            name,
88            room_type: wire.room_type,
89            description,
90            has_password: wire.has_password,
91            participant_count: wire.participant_count,
92        }
93    }
94}
95
96impl<'de> Deserialize<'de> for Room {
97    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
98        RoomWire::deserialize(deserializer).map(Self::from)
99    }
100}
101
102impl Serialize for Room {
103    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
104        let mut state = serializer.serialize_struct("Room", 6)?;
105        state.serialize_field("token", &self.token)?;
106        state.serialize_field("name", &self.name)?;
107        state.serialize_field("room_type", &self.room_type)?;
108        state.serialize_field("description", &self.description)?;
109        state.serialize_field("has_password", &self.has_password)?;
110        state.serialize_field("participant_count", &self.participant_count)?;
111        state.end()
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn room_deserialization_from_talk_v4_json() {
121        let json = r#"{
122            "token": "d7a8f9b2",
123            "name": "General Chat",
124            "displayName": "General Chat",
125            "type": 2,
126            "description": "Company wide discussion",
127            "hasPassword": false,
128            "participantCount": 15
129        }"#;
130
131        let room: Room = serde_json::from_str(json).expect("valid room json");
132        assert_eq!(room.token, "d7a8f9b2");
133        assert_eq!(room.name, "General Chat");
134        assert_eq!(room.room_type, RoomType::Group);
135        assert_eq!(room.description.as_deref(), Some("Company wide discussion"));
136        assert!(!room.has_password);
137        assert_eq!(room.participant_count, Some(15));
138    }
139
140    #[test]
141    fn room_deserialization_fallback_display_name_and_room_type() {
142        let json = r#"{
143            "token": "x1y2z3",
144            "displayName": "Alice & Bob",
145            "roomType": 1,
146            "hasPassword": true
147        }"#;
148
149        let room: Room = serde_json::from_str(json).expect("valid room json");
150        assert_eq!(room.token, "x1y2z3");
151        assert_eq!(room.name, "Alice & Bob");
152        assert_eq!(room.room_type, RoomType::OneToOne);
153        assert_eq!(room.description, None);
154        assert!(room.has_password);
155        assert_eq!(room.participant_count, None);
156    }
157
158    #[test]
159    fn room_serialization_round_trip() {
160        let room = Room::new(
161            "public123",
162            "Support Room",
163            RoomType::Public,
164            Some("Public assistance".into()),
165            false,
166            Some(42),
167        );
168
169        let json = serde_json::to_string(&room).expect("serialized");
170        let parsed: Room = serde_json::from_str(&json).expect("deserialized");
171        assert_eq!(room, parsed);
172    }
173}