beeper_desktop_api/models/
chat.rs1use serde::{Deserialize, Deserializer, Serialize};
4use super::message::Message;
5use super::user::User;
6
7fn deserialize_optional_u64_from_string_or_number<'de, D>(
8 deserializer: D,
9) -> Result<Option<u64>, D::Error>
10where
11 D: Deserializer<'de>,
12{
13 #[derive(Deserialize)]
14 #[serde(untagged)]
15 enum U64OrString {
16 U64(u64),
17 String(String),
18 }
19
20 let value = Option::<U64OrString>::deserialize(deserializer)?;
21
22 match value {
23 None => Ok(None),
24 Some(U64OrString::U64(v)) => Ok(Some(v)),
25 Some(U64OrString::String(s)) => s
26 .parse::<u64>()
27 .map(Some)
28 .map_err(serde::de::Error::custom),
29 }
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Participants {
35 pub items: Vec<User>,
37 #[serde(rename = "hasMore")]
39 pub has_more: bool,
40 pub total: u32,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct Chat {
47 pub id: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 #[serde(rename = "localChatID")]
52 pub local_chat_id: Option<String>,
53 #[serde(rename = "accountID")]
55 pub account_id: String,
56 pub network: String,
58 pub title: String,
60 #[serde(rename = "type")]
62 pub chat_type: String,
63 pub participants: Participants,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 #[serde(rename = "lastActivity")]
68 pub last_activity: Option<String>,
69 #[serde(rename = "unreadCount")]
71 pub unread_count: u32,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 #[serde(default)]
75 #[serde(rename = "lastReadMessageSortKey")]
76 #[serde(deserialize_with = "deserialize_optional_u64_from_string_or_number")]
77 pub last_read_message_sort_key: Option<u64>,
78 #[serde(rename = "isArchived")]
80 pub is_archived: bool,
81 #[serde(rename = "isMuted")]
83 pub is_muted: bool,
84 #[serde(rename = "isPinned")]
86 pub is_pinned: bool,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub preview: Option<Box<Message>>,
90}
91
92impl Chat {
93 pub fn display_name(&self) -> String {
98 if self.chat_type == "single" {
99 if let Some(first_participant) = self.participants.items.iter().filter(|p| !p.is_self.unwrap_or(false)).next() {
101 if let Some(full_name) = &first_participant.full_name {
102 return full_name.clone();
103 }
104 if let Some(username) = &first_participant.username {
105 return username.clone();
106 }
107 }
108 }
109
110 self.title.clone()
112 }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct CreateChatInput {
118 #[serde(rename = "accountID")]
120 pub account_id: String,
121 #[serde(rename = "participantIDs")]
123 pub participant_ids: Vec<String>,
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub title: Option<String>,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct CreateChatOutput {
132 #[serde(rename = "chatID")]
134 pub chat_id: String,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct ListChatsOutput {
140 pub items: Vec<Chat>,
142 #[serde(rename = "hasMore")]
144 pub has_more: bool,
145 #[serde(skip_serializing_if = "Option::is_none")]
147 #[serde(rename = "oldestCursor")]
148 pub oldest_cursor: Option<String>,
149 #[serde(skip_serializing_if = "Option::is_none")]
151 #[serde(rename = "newestCursor")]
152 pub newest_cursor: Option<String>,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct SearchChatsOutput {
158 pub items: Vec<Chat>,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub chats: Option<std::collections::HashMap<String, Chat>>,
163 #[serde(rename = "hasMore")]
165 pub has_more: bool,
166 #[serde(skip_serializing_if = "Option::is_none")]
168 #[serde(rename = "oldestCursor")]
169 pub oldest_cursor: Option<String>,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 #[serde(rename = "newestCursor")]
173 pub newest_cursor: Option<String>,
174}
175
176#[cfg(test)]
177mod tests {
178 use super::ListChatsOutput;
179
180 fn list_chats_payload_with_sort_key(sort_key_json: &str) -> String {
181 format!(
182 r#"{{
183 "items": [
184 {{
185 "id": "chat-1",
186 "accountID": "account-1",
187 "network": "WhatsApp",
188 "title": "Alice",
189 "type": "single",
190 "participants": {{
191 "items": [],
192 "hasMore": false,
193 "total": 0
194 }},
195 "unreadCount": 0,
196 "lastReadMessageSortKey": {sort_key_json},
197 "isArchived": false,
198 "isMuted": false,
199 "isPinned": false
200 }}
201 ],
202 "hasMore": false
203 }}"#
204 )
205 }
206
207 #[test]
208 fn list_chats_deserializes_numeric_sort_key() {
209 let payload = list_chats_payload_with_sort_key("453400065536");
210 let output: ListChatsOutput = serde_json::from_str(&payload).expect("should parse");
211 assert_eq!(output.items[0].last_read_message_sort_key, Some(453400065536));
212 }
213
214 #[test]
215 fn list_chats_deserializes_string_sort_key() {
216 let payload = list_chats_payload_with_sort_key("\"453400065536\"");
217 let output: ListChatsOutput = serde_json::from_str(&payload).expect("should parse");
218 assert_eq!(output.items[0].last_read_message_sort_key, Some(453400065536));
219 }
220
221 #[test]
222 fn list_chats_deserializes_null_sort_key() {
223 let payload = list_chats_payload_with_sort_key("null");
224 let output: ListChatsOutput = serde_json::from_str(&payload).expect("should parse");
225 assert_eq!(output.items[0].last_read_message_sort_key, None);
226 }
227
228 #[test]
229 fn list_chats_deserializes_missing_sort_key() {
230 let payload = r#"{
231 "items": [
232 {
233 "id": "chat-1",
234 "accountID": "account-1",
235 "network": "WhatsApp",
236 "title": "Alice",
237 "type": "single",
238 "participants": {
239 "items": [],
240 "hasMore": false,
241 "total": 0
242 },
243 "unreadCount": 0,
244 "isArchived": false,
245 "isMuted": false,
246 "isPinned": false
247 }
248 ],
249 "hasMore": false
250 }"#;
251
252 let output: ListChatsOutput = serde_json::from_str(payload).expect("should parse");
253 assert_eq!(output.items[0].last_read_message_sort_key, None);
254 }
255
256 #[test]
257 fn list_chats_rejects_invalid_sort_key_string() {
258 let payload = list_chats_payload_with_sort_key("\"not-a-number\"");
259 let result: Result<ListChatsOutput, _> = serde_json::from_str(&payload);
260 assert!(result.is_err());
261 }
262}