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 #[serde(skip_serializing_if = "Option::is_none")]
58 #[serde(default)]
59 pub network: Option<String>,
60 pub title: String,
62 #[serde(rename = "type")]
64 pub chat_type: String,
65 pub participants: Participants,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 #[serde(rename = "lastActivity")]
70 pub last_activity: Option<String>,
71 #[serde(rename = "unreadCount")]
73 pub unread_count: u32,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 #[serde(default)]
77 #[serde(rename = "lastReadMessageSortKey")]
78 #[serde(deserialize_with = "deserialize_optional_u64_from_string_or_number")]
79 pub last_read_message_sort_key: Option<u64>,
80 #[serde(rename = "isArchived")]
82 pub is_archived: bool,
83 #[serde(rename = "isMuted")]
85 pub is_muted: bool,
86 #[serde(rename = "isPinned")]
88 pub is_pinned: bool,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub preview: Option<Box<Message>>,
92}
93
94impl Chat {
95 pub fn display_name(&self) -> String {
100 if self.chat_type == "single" {
101 if let Some(first_participant) = self.participants.items.iter().filter(|p| !p.is_self.unwrap_or(false)).next() {
103 if let Some(full_name) = &first_participant.full_name {
104 return full_name.clone();
105 }
106 if let Some(username) = &first_participant.username {
107 return username.clone();
108 }
109 }
110 }
111
112 self.title.clone()
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct CreateChatInput {
120 #[serde(rename = "accountID")]
122 pub account_id: String,
123 #[serde(rename = "participantIDs")]
125 pub participant_ids: Vec<String>,
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub title: Option<String>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct CreateChatOutput {
134 #[serde(rename = "chatID")]
136 pub chat_id: String,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ListChatsOutput {
142 pub items: Vec<Chat>,
144 #[serde(rename = "hasMore")]
146 pub has_more: bool,
147 #[serde(skip_serializing_if = "Option::is_none")]
149 #[serde(rename = "oldestCursor")]
150 pub oldest_cursor: Option<String>,
151 #[serde(skip_serializing_if = "Option::is_none")]
153 #[serde(rename = "newestCursor")]
154 pub newest_cursor: Option<String>,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct SearchChatsOutput {
160 pub items: Vec<Chat>,
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub chats: Option<std::collections::HashMap<String, Chat>>,
165 #[serde(rename = "hasMore")]
167 pub has_more: bool,
168 #[serde(skip_serializing_if = "Option::is_none")]
170 #[serde(rename = "oldestCursor")]
171 pub oldest_cursor: Option<String>,
172 #[serde(skip_serializing_if = "Option::is_none")]
174 #[serde(rename = "newestCursor")]
175 pub newest_cursor: Option<String>,
176}
177
178#[cfg(test)]
179mod tests {
180 use super::ListChatsOutput;
181
182 fn list_chats_payload_with_sort_key(sort_key_json: &str) -> String {
183 format!(
184 r#"{{
185 "items": [
186 {{
187 "id": "chat-1",
188 "accountID": "account-1",
189 "network": "WhatsApp",
190 "title": "Alice",
191 "type": "single",
192 "participants": {{
193 "items": [],
194 "hasMore": false,
195 "total": 0
196 }},
197 "unreadCount": 0,
198 "lastReadMessageSortKey": {sort_key_json},
199 "isArchived": false,
200 "isMuted": false,
201 "isPinned": false
202 }}
203 ],
204 "hasMore": false
205 }}"#
206 )
207 }
208
209 #[test]
210 fn list_chats_deserializes_numeric_sort_key() {
211 let payload = list_chats_payload_with_sort_key("453400065536");
212 let output: ListChatsOutput = serde_json::from_str(&payload).expect("should parse");
213 assert_eq!(output.items[0].last_read_message_sort_key, Some(453400065536));
214 }
215
216 #[test]
217 fn list_chats_deserializes_string_sort_key() {
218 let payload = list_chats_payload_with_sort_key("\"453400065536\"");
219 let output: ListChatsOutput = serde_json::from_str(&payload).expect("should parse");
220 assert_eq!(output.items[0].last_read_message_sort_key, Some(453400065536));
221 }
222
223 #[test]
224 fn list_chats_deserializes_null_sort_key() {
225 let payload = list_chats_payload_with_sort_key("null");
226 let output: ListChatsOutput = serde_json::from_str(&payload).expect("should parse");
227 assert_eq!(output.items[0].last_read_message_sort_key, None);
228 }
229
230 #[test]
231 fn list_chats_deserializes_missing_sort_key() {
232 let payload = r#"{
233 "items": [
234 {
235 "id": "chat-1",
236 "accountID": "account-1",
237 "network": "WhatsApp",
238 "title": "Alice",
239 "type": "single",
240 "participants": {
241 "items": [],
242 "hasMore": false,
243 "total": 0
244 },
245 "unreadCount": 0,
246 "isArchived": false,
247 "isMuted": false,
248 "isPinned": false
249 }
250 ],
251 "hasMore": false
252 }"#;
253
254 let output: ListChatsOutput = serde_json::from_str(payload).expect("should parse");
255 assert_eq!(output.items[0].last_read_message_sort_key, None);
256 }
257
258 #[test]
259 fn list_chats_rejects_invalid_sort_key_string() {
260 let payload = list_chats_payload_with_sort_key("\"not-a-number\"");
261 let result: Result<ListChatsOutput, _> = serde_json::from_str(&payload);
262 assert!(result.is_err());
263 }
264}