1use crate::{chat, location};
36use std::collections::{hash_map, HashMap};
37
38use crate::context::Context;
39use crate::message::{Message, MsgId};
40
41use crate::chat::ChatId;
42use crate::color::color_int_to_hex_string;
43use crate::contact::{Contact, ContactId};
44use crate::tools::time;
45use crate::webxdc::{StatusUpdateItem, StatusUpdateItemAndSerial, StatusUpdateSerial};
46use anyhow::Result;
47use serde::{Deserialize, Serialize};
48
49#[derive(Debug, Serialize, Deserialize)]
50struct MapsActionPayload {
51 action: String,
52 lat: Option<f64>,
53 lng: Option<f64>,
54 label: Option<String>,
55}
56
57#[derive(Debug, Serialize, Deserialize)]
58struct LocationItem {
59 action: String,
60 #[serde(rename = "contactId")]
61 contact_id: u32,
62 lat: f64,
63 lng: f64,
64 independent: bool,
65 timestamp: i64,
66 label: String,
67 name: String,
68 color: String,
69}
70
71pub(crate) async fn intercept_send_update(
72 context: &Context,
73 chat_id: Option<ChatId>,
74 status_update: StatusUpdateItem,
75) -> Result<()> {
76 let payload = serde_json::from_value::<MapsActionPayload>(status_update.payload)?;
77 let lat = payload.lat.unwrap_or_default();
78 let lng = payload.lng.unwrap_or_default();
79 let label = payload.label.unwrap_or_default();
80
81 if payload.action == "pos" && !label.is_empty() {
82 let chat_id = if let Some(chat_id) = chat_id {
83 chat_id
84 } else {
85 ChatId::create_for_contact(context, ContactId::SELF).await?
86 };
87
88 let mut poi_msg = Message::new_text(label);
89 poi_msg.set_location(lat, lng);
90 chat::send_msg(context, chat_id, &mut poi_msg).await?;
91 } else {
92 warn!(context, "unknown maps integration action");
93 }
94
95 Ok(())
96}
97
98pub(crate) async fn intercept_get_updates(
99 context: &Context,
100 chat_id: Option<ChatId>,
101 last_known_serial: StatusUpdateSerial,
102) -> Result<String> {
103 let mut json = String::default();
104 let mut contact_data: HashMap<ContactId, (String, String)> = HashMap::new();
105
106 let begin = time() - 24 * 60 * 60;
107 let locations = location::get_range(context, chat_id, None, begin, 0).await?;
108 for location in locations.iter().rev() {
109 if location.location_id > last_known_serial.to_u32() {
110 let (name, color) = match contact_data.entry(location.contact_id) {
111 hash_map::Entry::Vacant(e) => {
112 let contact = Contact::get_by_id(context, location.contact_id).await?;
113 let name = contact.get_display_name().to_string();
114 let color = color_int_to_hex_string(contact.get_color());
115 e.insert((name, color)).clone()
116 }
117 hash_map::Entry::Occupied(e) => e.get().clone(),
118 };
119
120 let mut label = String::new();
121 if location.independent != 0 {
122 if let Some(marker) = &location.marker {
123 label = marker.to_string() } else if location.msg_id != 0 {
125 if let Some(msg) =
126 Message::load_from_db_optional(context, MsgId::new(location.msg_id)).await?
127 {
128 label = msg.get_text()
129 }
130 }
131 }
132
133 let location_item = LocationItem {
134 action: "pos".to_string(),
135 contact_id: location.contact_id.to_u32(),
136 lat: location.latitude,
137 lng: location.longitude,
138 independent: location.independent != 0,
139 timestamp: location.timestamp,
140 label,
141 name,
142 color,
143 };
144
145 let update_item = StatusUpdateItemAndSerial {
146 item: StatusUpdateItem {
147 payload: serde_json::to_value(location_item)?,
148 info: None,
149 href: None,
150 document: None,
151 summary: None,
152 uid: None,
153 notify: None,
154 },
155 serial: StatusUpdateSerial(location.location_id),
156 max_serial: StatusUpdateSerial(location.location_id),
157 };
158
159 if !json.is_empty() {
160 json.push_str(",\n");
161 }
162 json.push_str(&serde_json::to_string(&update_item)?);
163 }
164 }
165
166 Ok(format!("[{json}]"))
167}
168
169#[cfg(test)]
170mod tests {
171 use crate::chat::{create_group_chat, ChatId, ProtectionStatus};
172 use crate::chatlist::Chatlist;
173 use crate::contact::Contact;
174 use crate::message::Message;
175 use crate::test_utils::TestContext;
176 use crate::webxdc::StatusUpdateSerial;
177 use crate::{location, EventType};
178 use anyhow::Result;
179
180 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
181 async fn test_maps_integration() -> Result<()> {
182 let t = TestContext::new_alice().await;
183
184 let bytes = include_bytes!("../../test-data/webxdc/mapstest-integration-set.xdc");
185 let file = t.get_blobdir().join("maps.xdc");
186 tokio::fs::write(&file, bytes).await.unwrap();
187 t.set_webxdc_integration(file.to_str().unwrap()).await?;
188
189 let chatlist = Chatlist::try_load(&t, 0, None, None).await?;
190 let summary = chatlist.get_summary(&t, 0, None).await?;
191 assert_eq!(summary.text, "No messages.");
192
193 let bob_id = Contact::create(&t, "", "bob@example.net").await?;
196 let bob_chat_id = ChatId::create_for_contact(&t, bob_id).await?;
197 let integration_id = t.init_webxdc_integration(Some(bob_chat_id)).await?.unwrap();
198 assert!(!integration_id.is_special());
199
200 let integration = Message::load_from_db(&t, integration_id).await?;
201 let info = integration.get_webxdc_info(&t).await?;
202 assert_eq!(info.name, "Maps Test 2");
203 assert_eq!(info.internet_access, true);
204
205 t.send_webxdc_status_update(
206 integration_id,
207 r#"{"payload": {"action": "pos", "lat": 11.0, "lng": 12.0, "label": "poi #1"}}"#,
208 )
209 .await?;
210 t.evtracker
211 .get_matching(|evt| matches!(evt, EventType::WebxdcStatusUpdate { .. }))
212 .await;
213 let updates = t
214 .get_webxdc_status_updates(integration_id, StatusUpdateSerial(0))
215 .await?;
216 assert!(updates.contains(r#""lat":11"#));
217 assert!(updates.contains(r#""lng":12"#));
218 assert!(updates.contains(r#""label":"poi #1""#));
219 assert!(updates.contains(r#""contactId":"#)); assert!(updates.contains(r#""name":"Me""#));
221 assert!(updates.contains(r##""color":"#"##));
222 let locations = location::get_range(&t, Some(bob_chat_id), None, 0, 0).await?;
223 assert_eq!(locations.len(), 1);
224 let location = locations.last().unwrap();
225 assert_eq!(location.latitude, 11.0);
226 assert_eq!(location.longitude, 12.0);
227 assert_eq!(location.independent, 1);
228 let msg = t.get_last_msg().await;
229 assert_eq!(msg.text, "poi #1");
230 assert_eq!(msg.chat_id, bob_chat_id);
231
232 let group_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
234 let integration_id = t.init_webxdc_integration(Some(group_id)).await?.unwrap();
235
236 let locations = location::get_range(&t, Some(group_id), None, 0, 0).await?;
237 assert_eq!(locations.len(), 0);
238 t.send_webxdc_status_update(
239 integration_id,
240 r#"{"payload": {"action": "pos", "lat": 22.0, "lng": 23.0, "label": "poi #2"}}"#,
241 )
242 .await?;
243 let updates = t
244 .get_webxdc_status_updates(integration_id, StatusUpdateSerial(0))
245 .await?;
246 assert!(!updates.contains(r#""lat":11"#));
247 assert!(!updates.contains(r#""label":"poi #1""#));
248 assert!(updates.contains(r#""lat":22"#));
249 assert!(updates.contains(r#""lng":23"#));
250 assert!(updates.contains(r#""label":"poi #2""#));
251 let locations = location::get_range(&t, Some(group_id), None, 0, 0).await?;
252 assert_eq!(locations.len(), 1);
253 let location = locations.last().unwrap();
254 assert_eq!(location.latitude, 22.0);
255 assert_eq!(location.longitude, 23.0);
256 assert_eq!(location.independent, 1);
257 let msg = t.get_last_msg().await;
258 assert_eq!(msg.text, "poi #2");
259 assert_eq!(msg.chat_id, group_id);
260
261 let integration_id = t.init_webxdc_integration(None).await?.unwrap();
263
264 let updates = t
265 .get_webxdc_status_updates(integration_id, StatusUpdateSerial(0))
266 .await?;
267 assert!(updates.contains(r#""lat":11"#));
268 assert!(updates.contains(r#""label":"poi #1""#));
269 assert!(updates.contains(r#""lat":22"#));
270 assert!(updates.contains(r#""label":"poi #2""#));
271 let locations = location::get_range(&t, None, None, 0, 0).await?;
272 assert_eq!(locations.len(), 2);
273
274 Ok(())
275 }
276}