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