1use std::time::Duration;
14
15use anyhow::{Context as _, Result, ensure};
16use async_channel::Receiver;
17use quick_xml::events::{BytesEnd, BytesStart, BytesText};
18use tokio::time::timeout;
19
20use crate::chat::{self, ChatId};
21use crate::constants::DC_CHAT_ID_TRASH;
22use crate::contact::ContactId;
23use crate::context::Context;
24use crate::events::EventType;
25use crate::log::warn;
26use crate::message::{Message, MsgId, Viewtype};
27use crate::mimeparser::SystemMessage;
28use crate::tools::{duration_to_str, time};
29use crate::{chatlist_events, stock_str};
30
31#[derive(Debug, Clone, Default)]
33pub struct Location {
34 pub location_id: u32,
36
37 pub latitude: f64,
39
40 pub longitude: f64,
42
43 pub accuracy: f64,
45
46 pub timestamp: i64,
48
49 pub contact_id: ContactId,
51
52 pub msg_id: u32,
54
55 pub chat_id: ChatId,
57
58 pub marker: Option<String>,
60
61 pub independent: u32,
63}
64
65impl Location {
66 pub fn new() -> Self {
68 Default::default()
69 }
70}
71
72#[derive(Debug, Clone, Default)]
77pub struct Kml {
78 pub addr: Option<String>,
80
81 pub locations: Vec<Location>,
83
84 tag: KmlTag,
86
87 pub curr: Location,
89}
90
91#[derive(Default, Debug, Clone, PartialEq, Eq)]
92enum KmlTag {
93 #[default]
94 Undefined,
95 Placemark,
96 PlacemarkTimestamp,
97 PlacemarkTimestampWhen,
98 PlacemarkPoint,
99 PlacemarkPointCoordinates,
100}
101
102impl Kml {
103 pub fn new() -> Self {
105 Default::default()
106 }
107
108 pub fn parse(to_parse: &[u8]) -> Result<Self> {
110 ensure!(to_parse.len() <= 1024 * 1024, "kml-file is too large");
111
112 let mut reader = quick_xml::Reader::from_reader(to_parse);
113 reader.config_mut().trim_text(true);
114
115 let mut kml = Kml::new();
116 kml.locations = Vec::with_capacity(100);
117
118 let mut buf = Vec::new();
119
120 loop {
121 match reader.read_event_into(&mut buf).with_context(|| {
122 format!(
123 "location parsing error at position {}",
124 reader.buffer_position()
125 )
126 })? {
127 quick_xml::events::Event::Start(ref e) => kml.starttag_cb(e, &reader),
128 quick_xml::events::Event::End(ref e) => kml.endtag_cb(e),
129 quick_xml::events::Event::Text(ref e) => kml.text_cb(e),
130 quick_xml::events::Event::Eof => break,
131 _ => (),
132 }
133 buf.clear();
134 }
135
136 Ok(kml)
137 }
138
139 fn text_cb(&mut self, event: &BytesText) {
140 if self.tag == KmlTag::PlacemarkTimestampWhen
141 || self.tag == KmlTag::PlacemarkPointCoordinates
142 {
143 let val = event.xml_content().unwrap_or_default();
144
145 let val = val.replace(['\n', '\r', '\t', ' '], "");
146
147 if self.tag == KmlTag::PlacemarkTimestampWhen && val.len() >= 19 {
148 match chrono::NaiveDateTime::parse_from_str(&val, "%Y-%m-%dT%H:%M:%SZ") {
151 Ok(res) => {
152 self.curr.timestamp = res.and_utc().timestamp();
153 let now = time();
154 if self.curr.timestamp > now {
155 self.curr.timestamp = now;
156 }
157 }
158 Err(_err) => {
159 self.curr.timestamp = time();
160 }
161 }
162 } else if self.tag == KmlTag::PlacemarkPointCoordinates {
163 let parts = val.splitn(2, ',').collect::<Vec<_>>();
164 if let [longitude, latitude] = &parts[..] {
165 self.curr.longitude = longitude.parse().unwrap_or_default();
166 self.curr.latitude = latitude.parse().unwrap_or_default();
167 }
168 }
169 }
170 }
171
172 fn endtag_cb(&mut self, event: &BytesEnd) {
173 let tag = String::from_utf8_lossy(event.name().as_ref())
174 .trim()
175 .to_lowercase();
176
177 match self.tag {
178 KmlTag::PlacemarkTimestampWhen => {
179 if tag == "when" {
180 self.tag = KmlTag::PlacemarkTimestamp
181 }
182 }
183 KmlTag::PlacemarkTimestamp => {
184 if tag == "timestamp" {
185 self.tag = KmlTag::Placemark
186 }
187 }
188 KmlTag::PlacemarkPointCoordinates => {
189 if tag == "coordinates" {
190 self.tag = KmlTag::PlacemarkPoint
191 }
192 }
193 KmlTag::PlacemarkPoint => {
194 if tag == "point" {
195 self.tag = KmlTag::Placemark
196 }
197 }
198 KmlTag::Placemark => {
199 if tag == "placemark" {
200 if 0 != self.curr.timestamp
201 && 0. != self.curr.latitude
202 && 0. != self.curr.longitude
203 {
204 self.locations
205 .push(std::mem::replace(&mut self.curr, Location::new()));
206 }
207 self.tag = KmlTag::Undefined;
208 }
209 }
210 KmlTag::Undefined => {}
211 }
212 }
213
214 fn starttag_cb<B: std::io::BufRead>(
215 &mut self,
216 event: &BytesStart,
217 reader: &quick_xml::Reader<B>,
218 ) {
219 let tag = String::from_utf8_lossy(event.name().as_ref())
220 .trim()
221 .to_lowercase();
222 if tag == "document" {
223 if let Some(addr) = event.attributes().filter_map(|a| a.ok()).find(|attr| {
224 String::from_utf8_lossy(attr.key.as_ref())
225 .trim()
226 .to_lowercase()
227 == "addr"
228 }) {
229 self.addr = addr
230 .decode_and_unescape_value(reader.decoder())
231 .ok()
232 .map(|a| a.into_owned());
233 }
234 } else if tag == "placemark" {
235 self.tag = KmlTag::Placemark;
236 self.curr.timestamp = 0;
237 self.curr.latitude = 0.0;
238 self.curr.longitude = 0.0;
239 self.curr.accuracy = 0.0
240 } else if tag == "timestamp" && self.tag == KmlTag::Placemark {
241 self.tag = KmlTag::PlacemarkTimestamp;
242 } else if tag == "when" && self.tag == KmlTag::PlacemarkTimestamp {
243 self.tag = KmlTag::PlacemarkTimestampWhen;
244 } else if tag == "point" && self.tag == KmlTag::Placemark {
245 self.tag = KmlTag::PlacemarkPoint;
246 } else if tag == "coordinates" && self.tag == KmlTag::PlacemarkPoint {
247 self.tag = KmlTag::PlacemarkPointCoordinates;
248 if let Some(acc) = event.attributes().find_map(|attr| {
249 attr.ok().filter(|a| {
250 String::from_utf8_lossy(a.key.as_ref())
251 .trim()
252 .eq_ignore_ascii_case("accuracy")
253 })
254 }) {
255 let v = acc
256 .decode_and_unescape_value(reader.decoder())
257 .unwrap_or_default();
258
259 self.curr.accuracy = v.trim().parse().unwrap_or_default();
260 }
261 }
262 }
263}
264
265#[expect(clippy::arithmetic_side_effects)]
267pub async fn send_to_chat(context: &Context, chat_id: ChatId, seconds: i64) -> Result<()> {
268 ensure!(seconds >= 0);
269 ensure!(!chat_id.is_special());
270 let now = time();
271 let is_sending_locations_before = is_sending_to_chat(context, chat_id).await?;
272 context
273 .sql
274 .execute(
275 "UPDATE chats \
276 SET locations_send_begin=?, \
277 locations_send_until=? \
278 WHERE id=?",
279 (
280 if 0 != seconds { now } else { 0 },
281 if 0 != seconds { now + seconds } else { 0 },
282 chat_id,
283 ),
284 )
285 .await?;
286 if 0 != seconds && !is_sending_locations_before {
287 let mut msg = Message::new_text(stock_str::msg_location_enabled(context));
288 msg.param.set_cmd(SystemMessage::LocationStreamingEnabled);
289 chat::send_msg(context, chat_id, &mut msg)
290 .await
291 .unwrap_or_default();
292 } else if 0 == seconds && is_sending_locations_before {
293 let stock_str = stock_str::msg_location_disabled(context);
294 chat::add_info_msg(context, chat_id, &stock_str).await?;
295 }
296 context.emit_event(EventType::ChatModified(chat_id));
297 chatlist_events::emit_chatlist_item_changed(context, chat_id);
298 if 0 != seconds {
299 context.scheduler.interrupt_location().await;
300 }
301 Ok(())
302}
303
304pub async fn is_sending(context: &Context) -> Result<bool> {
306 context
307 .sql
308 .exists(
309 "SELECT COUNT(id) FROM chats WHERE locations_send_until>?",
310 (time(),),
311 )
312 .await
313}
314
315pub async fn is_sending_to_chat(context: &Context, chat_id: ChatId) -> Result<bool> {
317 context
318 .sql
319 .exists(
320 "SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?",
321 (chat_id, time()),
322 )
323 .await
324}
325
326async fn get_chats_with_location_streaming(context: &Context) -> Result<Vec<ChatId>> {
328 context
329 .sql
330 .query_map_vec(
331 "SELECT id FROM chats WHERE locations_send_until>?",
332 (time(),),
333 |row| {
334 let chat_id: ChatId = row.get(0)?;
335 Ok(chat_id)
336 },
337 )
338 .await
339}
340
341pub async fn stop_sending(context: &Context) -> Result<()> {
343 for chat_id in get_chats_with_location_streaming(context).await? {
344 send_to_chat(context, chat_id, 0).await?;
345 }
346 Ok(())
347}
348
349pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> Result<bool> {
351 if latitude == 0.0 && longitude == 0.0 {
352 return Ok(true);
353 }
354 let mut continue_streaming = false;
355 let now = time();
356
357 let chats = context
358 .sql
359 .query_map_vec(
360 "SELECT id FROM chats WHERE locations_send_until>?;",
361 (now,),
362 |row| {
363 let id: i32 = row.get(0)?;
364 Ok(id)
365 },
366 )
367 .await?;
368
369 let mut stored_location = false;
370 for chat_id in chats {
371 context.sql.execute(
372 "INSERT INTO locations \
373 (latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
374 (
375 latitude,
376 longitude,
377 accuracy,
378 now,
379 chat_id,
380 ContactId::SELF,
381 )).await.context("Failed to store location")?;
382 stored_location = true;
383
384 info!(context, "Stored location for chat {chat_id}.");
385 continue_streaming = true;
386 }
387 if continue_streaming {
388 context.emit_location_changed(Some(ContactId::SELF)).await?;
389 };
390 if stored_location {
391 context.scheduler.interrupt_location().await;
393 }
394
395 Ok(continue_streaming)
396}
397
398#[expect(clippy::arithmetic_side_effects)]
400pub async fn get_range(
401 context: &Context,
402 chat_id: Option<ChatId>,
403 contact_id: Option<u32>,
404 timestamp_from: i64,
405 mut timestamp_to: i64,
406) -> Result<Vec<Location>> {
407 if timestamp_to == 0 {
408 timestamp_to = time() + 10;
409 }
410
411 let (disable_chat_id, chat_id) = match chat_id {
412 Some(chat_id) => (0, chat_id),
413 None => (1, ChatId::new(0)), };
415 let (disable_contact_id, contact_id) = match contact_id {
416 Some(contact_id) => (0, contact_id),
417 None => (1, 0), };
419 let list = context
420 .sql
421 .query_map_vec(
422 "SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \
423 COALESCE(m.id, 0) AS msg_id, l.from_id, l.chat_id, COALESCE(m.txt, '') AS txt \
424 FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \
425 AND (? OR l.from_id=?) \
426 AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
427 ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
428 (
429 disable_chat_id,
430 chat_id,
431 disable_contact_id,
432 contact_id as i32,
433 timestamp_from,
434 timestamp_to,
435 ),
436 |row| {
437 let msg_id = row.get(6)?;
438 let txt: String = row.get(9)?;
439 let marker = if msg_id != 0 && is_marker(&txt) {
440 Some(txt)
441 } else {
442 None
443 };
444 let loc = Location {
445 location_id: row.get(0)?,
446 latitude: row.get(1)?,
447 longitude: row.get(2)?,
448 accuracy: row.get(3)?,
449 timestamp: row.get(4)?,
450 independent: row.get(5)?,
451 msg_id,
452 contact_id: row.get(7)?,
453 chat_id: row.get(8)?,
454 marker,
455 };
456 Ok(loc)
457 },
458 )
459 .await?;
460 Ok(list)
461}
462
463fn is_marker(txt: &str) -> bool {
464 let mut chars = txt.chars();
465 if let Some(c) = chars.next() {
466 !c.is_whitespace() && chars.next().is_none()
467 } else {
468 false
469 }
470}
471
472pub(crate) async fn delete_expired(context: &Context, now: i64) -> Result<()> {
477 let Some(delete_device_after) = context.get_config_delete_device_after().await? else {
478 return Ok(());
479 };
480
481 let threshold_timestamp = now.saturating_sub(delete_device_after);
482 let deleted = context
483 .sql
484 .execute(
485 "DELETE FROM locations WHERE independent=0 AND timestamp < ?",
486 (threshold_timestamp,),
487 )
488 .await?
489 > 0;
490 if deleted {
491 info!(context, "Deleted {deleted} expired locations.");
492 context.emit_location_changed(None).await?;
493 }
494 Ok(())
495}
496
497pub(crate) async fn delete_poi(context: &Context, location_id: u32) -> Result<()> {
502 context
503 .sql
504 .execute(
505 "DELETE FROM locations WHERE independent = 1 AND id=?",
506 (location_id as i32,),
507 )
508 .await?;
509 Ok(())
510}
511
512pub(crate) async fn delete_orphaned_poi(context: &Context) -> Result<()> {
514 context.sql.execute("
515 DELETE FROM locations
516 WHERE independent=1 AND id NOT IN
517 (SELECT location_id from MSGS LEFT JOIN locations
518 ON locations.id=location_id
519 WHERE location_id>0 -- This check makes the query faster by not looking for locations with ID 0 that don't exist.
520 AND msgs.chat_id != ?)", (DC_CHAT_ID_TRASH,)).await?;
521 Ok(())
522}
523
524#[expect(clippy::arithmetic_side_effects)]
526pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<Option<(String, u32)>> {
527 let mut last_added_location_id = 0;
528
529 let self_addr = context.get_primary_self_addr().await?;
530
531 let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
532 "SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
533 (chat_id,), |row| {
534 let send_begin: i64 = row.get(0)?;
535 let send_until: i64 = row.get(1)?;
536 let last_sent: i64 = row.get(2)?;
537
538 Ok((send_begin, send_until, last_sent))
539 })
540 .await?;
541
542 let now = time();
543 let mut location_count = 0;
544 let mut ret = String::new();
545 if locations_send_begin != 0 && now <= locations_send_until {
546 ret += &format!(
547 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
548 <kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"{self_addr}\">\n",
549 );
550
551 context
552 .sql
553 .query_map(
554 "SELECT id, latitude, longitude, accuracy, timestamp \
555 FROM locations WHERE from_id=? \
556 AND timestamp>=? \
557 AND (timestamp>=? OR \
558 timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \
559 AND independent=0 \
560 GROUP BY timestamp \
561 ORDER BY timestamp;",
562 (
563 ContactId::SELF,
564 locations_send_begin,
565 locations_last_sent,
566 ContactId::SELF
567 ),
568 |row| {
569 let location_id: i32 = row.get(0)?;
570 let latitude: f64 = row.get(1)?;
571 let longitude: f64 = row.get(2)?;
572 let accuracy: f64 = row.get(3)?;
573 let timestamp = get_kml_timestamp(row.get(4)?);
574
575 Ok((location_id, latitude, longitude, accuracy, timestamp))
576 },
577 |rows| {
578 for row in rows {
579 let (location_id, latitude, longitude, accuracy, timestamp) = row?;
580 ret += &format!(
581 "<Placemark>\
582 <Timestamp><when>{timestamp}</when></Timestamp>\
583 <Point><coordinates accuracy=\"{accuracy}\">{longitude},{latitude}</coordinates></Point>\
584 </Placemark>\n"
585 );
586 location_count += 1;
587 last_added_location_id = location_id as u32;
588 }
589 Ok(())
590 },
591 )
592 .await?;
593 ret += "</Document>\n</kml>";
594 }
595
596 if location_count > 0 {
597 Ok(Some((ret, last_added_location_id)))
598 } else {
599 Ok(None)
600 }
601}
602
603fn get_kml_timestamp(utc: i64) -> String {
604 chrono::DateTime::<chrono::Utc>::from_timestamp(utc, 0)
606 .unwrap()
607 .format("%Y-%m-%dT%H:%M:%SZ")
608 .to_string()
609}
610
611pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String {
613 format!(
614 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
615 <kml xmlns=\"http://www.opengis.net/kml/2.2\">\n\
616 <Document>\n\
617 <Placemark>\
618 <Timestamp><when>{}</when></Timestamp>\
619 <Point><coordinates>{},{}</coordinates></Point>\
620 </Placemark>\n\
621 </Document>\n\
622 </kml>",
623 get_kml_timestamp(timestamp),
624 longitude,
625 latitude,
626 )
627}
628
629pub async fn set_kml_sent_timestamp(
631 context: &Context,
632 chat_id: ChatId,
633 timestamp: i64,
634) -> Result<()> {
635 context
636 .sql
637 .execute(
638 "UPDATE chats SET locations_last_sent=? WHERE id=?;",
639 (timestamp, chat_id),
640 )
641 .await?;
642 Ok(())
643}
644
645pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id: u32) -> Result<()> {
647 context
648 .sql
649 .execute(
650 "UPDATE msgs SET location_id=? WHERE id=?;",
651 (location_id, msg_id),
652 )
653 .await?;
654
655 Ok(())
656}
657
658pub(crate) async fn save(
662 context: &Context,
663 chat_id: ChatId,
664 contact_id: ContactId,
665 locations: &[Location],
666 independent: bool,
667) -> Result<Option<u32>> {
668 ensure!(!chat_id.is_special(), "Invalid chat id");
669
670 let mut newest_timestamp = 0;
671 let mut newest_location_id = None;
672
673 let stmt_insert = "INSERT INTO locations\
674 (timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
675 VALUES (?,?,?,?,?,?,?);";
676
677 for location in locations {
678 let &Location {
679 timestamp,
680 latitude,
681 longitude,
682 accuracy,
683 ..
684 } = location;
685
686 context
687 .sql
688 .call_write(|conn| {
689 let mut stmt_test = conn
690 .prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
691 let mut stmt_insert = conn.prepare_cached(stmt_insert)?;
692
693 let exists = stmt_test.exists((timestamp, contact_id))?;
694
695 if independent || !exists {
696 stmt_insert.execute((
697 timestamp,
698 contact_id,
699 chat_id,
700 latitude,
701 longitude,
702 accuracy,
703 independent,
704 ))?;
705
706 if timestamp > newest_timestamp {
707 newest_timestamp = timestamp;
708 newest_location_id = Some(u32::try_from(conn.last_insert_rowid())?);
709 }
710 }
711
712 Ok(())
713 })
714 .await?;
715 }
716
717 Ok(newest_location_id)
718}
719
720pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receiver<()>) {
721 loop {
722 let next_event = match maybe_send(context).await {
723 Err(err) => {
724 warn!(context, "location::maybe_send failed: {:#}", err);
725 Some(60) }
727 Ok(next_event) => next_event,
728 };
729
730 let duration = if let Some(next_event) = next_event {
731 Duration::from_secs(next_event)
732 } else {
733 Duration::from_secs(86400)
734 };
735
736 info!(
737 context,
738 "Location loop is waiting for {} or interrupt",
739 duration_to_str(duration)
740 );
741 match timeout(duration, interrupt_receiver.recv()).await {
742 Err(_err) => {
743 info!(context, "Location loop timeout.");
744 }
745 Ok(Err(err)) => {
746 warn!(
747 context,
748 "Interrupt channel closed, location loop exits now: {err:#}."
749 );
750 return;
751 }
752 Ok(Ok(())) => {
753 info!(context, "Location loop received interrupt.");
754 }
755 }
756 }
757}
758
759#[expect(clippy::arithmetic_side_effects)]
762async fn maybe_send(context: &Context) -> Result<Option<u64>> {
763 let mut next_event: Option<u64> = None;
764
765 let now = time();
766 let rows = context
767 .sql
768 .query_map_vec(
769 "SELECT id, locations_send_begin, locations_send_until, locations_last_sent
770 FROM chats
771 WHERE locations_send_until>0",
772 [],
773 |row| {
774 let chat_id: ChatId = row.get(0)?;
775 let locations_send_begin: i64 = row.get(1)?;
776 let locations_send_until: i64 = row.get(2)?;
777 let locations_last_sent: i64 = row.get(3)?;
778 Ok((
779 chat_id,
780 locations_send_begin,
781 locations_send_until,
782 locations_last_sent,
783 ))
784 },
785 )
786 .await
787 .context("failed to query location streaming chats")?;
788
789 for (chat_id, locations_send_begin, locations_send_until, locations_last_sent) in rows {
790 if locations_send_begin > 0 && locations_send_until > now {
791 let can_send = now > locations_last_sent + 60;
792 let has_locations = context
793 .sql
794 .exists(
795 "SELECT COUNT(id) \
796 FROM locations \
797 WHERE from_id=? \
798 AND timestamp>=? \
799 AND timestamp>? \
800 AND independent=0",
801 (ContactId::SELF, locations_send_begin, locations_last_sent),
802 )
803 .await?;
804
805 next_event = next_event
806 .into_iter()
807 .chain(u64::try_from(locations_send_until - now))
808 .min();
809
810 if has_locations {
811 if can_send {
812 info!(
816 context,
817 "Chat {} has pending locations, sending them.", chat_id
818 );
819 let mut msg = Message::new(Viewtype::Text);
820 msg.hidden = true;
821 msg.param.set_cmd(SystemMessage::LocationOnly);
822 chat::send_msg(context, chat_id, &mut msg).await?;
823 } else {
824 info!(
826 context,
827 "Chat {} has pending locations, but they can't be sent yet.", chat_id
828 );
829 next_event = next_event
830 .into_iter()
831 .chain(u64::try_from(locations_last_sent + 61 - now))
832 .min();
833 }
834 } else {
835 info!(
836 context,
837 "Chat {} has location streaming enabled, but no pending locations.", chat_id
838 );
839 }
840 } else {
841 info!(
844 context,
845 "Disabling location streaming for chat {}.", chat_id
846 );
847 context
848 .sql
849 .execute(
850 "UPDATE chats \
851 SET locations_send_begin=0, locations_send_until=0 \
852 WHERE id=?",
853 (chat_id,),
854 )
855 .await
856 .context("failed to disable location streaming")?;
857
858 let stock_str = stock_str::msg_location_disabled(context);
859 chat::add_info_msg(context, chat_id, &stock_str).await?;
860 context.emit_event(EventType::ChatModified(chat_id));
861 chatlist_events::emit_chatlist_item_changed(context, chat_id);
862 }
863 }
864
865 Ok(next_event)
866}
867
868#[cfg(test)]
869mod tests {
870 use super::*;
871 use crate::config::Config;
872 use crate::message::MessageState;
873 use crate::receive_imf::receive_imf;
874 use crate::test_utils::{ExpectedEvents, TestContext, TestContextManager};
875 use crate::tools::SystemTime;
876
877 #[test]
878 fn test_kml_parse() {
879 let xml =
880 b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"user@example.org\">\n<Placemark><Timestamp><when>2019-03-06T21:09:57Z</when></Timestamp><Point><coordinates accuracy=\"32.000000\">9.423110,53.790302</coordinates></Point></Placemark>\n<PlaceMARK>\n<Timestamp><WHEN > \n\t2018-12-13T22:11:12Z\t</WHEN></Timestamp><Point><coordinates aCCuracy=\"2.500000\"> 19.423110 \t , \n 63.790302\n </coordinates></Point></PlaceMARK>\n</Document>\n</kml>";
881
882 let kml = Kml::parse(xml).expect("parsing failed");
883
884 assert!(kml.addr.is_some());
885 assert_eq!(kml.addr.as_ref().unwrap(), "user@example.org",);
886
887 let locations_ref = &kml.locations;
888 assert_eq!(locations_ref.len(), 2);
889
890 assert!(locations_ref[0].latitude > 53.6f64);
891 assert!(locations_ref[0].latitude < 53.8f64);
892 assert!(locations_ref[0].longitude > 9.3f64);
893 assert!(locations_ref[0].longitude < 9.5f64);
894 assert!(locations_ref[0].accuracy > 31.9f64);
895 assert!(locations_ref[0].accuracy < 32.1f64);
896 assert_eq!(locations_ref[0].timestamp, 1551906597);
897
898 assert!(locations_ref[1].latitude > 63.6f64);
899 assert!(locations_ref[1].latitude < 63.8f64);
900 assert!(locations_ref[1].longitude > 19.3f64);
901 assert!(locations_ref[1].longitude < 19.5f64);
902 assert!(locations_ref[1].accuracy > 2.4f64);
903 assert!(locations_ref[1].accuracy < 2.6f64);
904 assert_eq!(locations_ref[1].timestamp, 1544739072);
905 }
906
907 #[test]
908 fn test_kml_parse_error() {
909 let xml = b"<?><xmlversi\"\"\">?</document>";
910 assert!(Kml::parse(xml).is_err());
911 }
912
913 #[test]
914 fn test_get_message_kml() {
915 let timestamp = 1598490000;
916
917 let xml = get_message_kml(timestamp, 51.423723f64, 8.552556f64);
918 let kml = Kml::parse(xml.as_bytes()).expect("parsing failed");
919 let locations_ref = &kml.locations;
920 assert_eq!(locations_ref.len(), 1);
921
922 assert!(locations_ref[0].latitude >= 51.423723f64);
923 assert!(locations_ref[0].latitude < 51.423724f64);
924 assert!(locations_ref[0].longitude >= 8.552556f64);
925 assert!(locations_ref[0].longitude < 8.552557f64);
926 assert!(locations_ref[0].accuracy.abs() < f64::EPSILON);
927 assert_eq!(locations_ref[0].timestamp, timestamp);
928 }
929
930 #[test]
931 fn test_is_marker() {
932 assert!(is_marker("f"));
933 assert!(!is_marker("foo"));
934 assert!(is_marker("🏠"));
935 assert!(!is_marker(" "));
936 assert!(!is_marker("\t"));
937 }
938
939 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
941 async fn receive_location_kml() -> Result<()> {
942 let alice = TestContext::new_alice().await;
943
944 receive_imf(
945 &alice,
946 br#"Subject: Hello
947Message-ID: hello@example.net
948To: Alice <alice@example.org>
949From: Bob <bob@example.net>
950Date: Mon, 20 Dec 2021 00:00:00 +0000
951Chat-Version: 1.0
952Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
953
954Text message."#,
955 false,
956 )
957 .await?;
958 let received_msg = alice.get_last_msg().await;
959 assert_eq!(received_msg.text, "Text message.");
960
961 receive_imf(
962 &alice,
963 br#"Subject: locations
964MIME-Version: 1.0
965To: <alice@example.org>
966From: <bob@example.net>
967Date: Tue, 21 Dec 2021 00:00:00 +0000
968Chat-Version: 1.0
969Message-ID: <foobar@example.net>
970Content-Type: multipart/mixed; boundary="U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF"
971
972
973--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF
974Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
975
976
977
978--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF
979Content-Type: application/vnd.google-earth.kml+xml
980Content-Disposition: attachment; filename="location.kml"
981
982<?xml version="1.0" encoding="UTF-8"?>
983<kml xmlns="http://www.opengis.net/kml/2.2">
984<Document addr="bob@example.net">
985<Placemark><Timestamp><when>2021-11-21T00:00:00Z</when></Timestamp><Point><coordinates accuracy="1.0000000000000000">10.00000000000000,20.00000000000000</coordinates></Point></Placemark>
986</Document>
987</kml>
988
989--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF--"#,
990 false,
991 )
992 .await?;
993
994 let received_msg2 = alice.get_last_msg().await;
996 assert_eq!(received_msg2.id, received_msg.id);
997
998 let locations = get_range(&alice, None, None, 0, 0).await?;
999 assert_eq!(locations.len(), 1);
1000 Ok(())
1001 }
1002
1003 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1005 async fn receive_visible_location_kml() -> Result<()> {
1006 let alice = TestContext::new_alice().await;
1007
1008 receive_imf(
1009 &alice,
1010 br#"Subject: locations
1011MIME-Version: 1.0
1012To: <alice@example.org>
1013From: <bob@example.net>
1014Date: Tue, 21 Dec 2021 00:00:00 +0000
1015Chat-Version: 1.0
1016Message-ID: <foobar@localhost>
1017Content-Type: multipart/mixed; boundary="U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF"
1018
1019
1020--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF
1021Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
1022
1023Text message.
1024
1025
1026--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF
1027Content-Type: application/vnd.google-earth.kml+xml
1028Content-Disposition: attachment; filename="location.kml"
1029
1030<?xml version="1.0" encoding="UTF-8"?>
1031<kml xmlns="http://www.opengis.net/kml/2.2">
1032<Document addr="bob@example.net">
1033<Placemark><Timestamp><when>2021-11-21T00:00:00Z</when></Timestamp><Point><coordinates accuracy="1.0000000000000000">10.00000000000000,20.00000000000000</coordinates></Point></Placemark>
1034</Document>
1035</kml>
1036
1037--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF--"#,
1038 false,
1039 )
1040 .await?;
1041
1042 let received_msg = alice.get_last_msg().await;
1043 assert_eq!(received_msg.text, "Text message.");
1044 assert_eq!(received_msg.state, MessageState::InFresh);
1045
1046 let locations = get_range(&alice, None, None, 0, 0).await?;
1047 assert_eq!(locations.len(), 1);
1048 Ok(())
1049 }
1050
1051 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1052 async fn test_send_locations_to_chat() -> Result<()> {
1053 let alice = TestContext::new_alice().await;
1054 let bob = TestContext::new_bob().await;
1055
1056 let alice_chat = alice.create_chat(&bob).await;
1057 send_to_chat(&alice, alice_chat.id, 1000).await?;
1058 let sent = alice.pop_sent_msg().await;
1059 let msg = bob.recv_msg(&sent).await;
1060 assert_eq!(msg.text, "Location streaming enabled by alice@example.org.");
1061 let bob_chat_id = msg.chat_id;
1062
1063 assert_eq!(set(&alice, 10.0, 20.0, 1.0).await?, true);
1064
1065 let file_name = "image.png";
1067 let bytes = include_bytes!("../test-data/image/logo.png");
1068 let file = alice.get_blobdir().join(file_name);
1069 tokio::fs::write(&file, bytes).await?;
1070 let mut msg = Message::new(Viewtype::Image);
1071 msg.set_file_and_deduplicate(&alice, &file, Some("logo.png"), None)?;
1072 let sent = alice.send_msg(alice_chat.id, &mut msg).await;
1073 let alice_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?;
1074 assert_eq!(alice_msg.has_location(), false);
1075
1076 let msg = bob.recv_msg_opt(&sent).await.unwrap();
1077 assert!(msg.chat_id == bob_chat_id);
1078 assert_eq!(msg.msg_ids.len(), 1);
1079
1080 let bob_msg = Message::load_from_db(&bob, *msg.msg_ids.first().unwrap()).await?;
1081 assert_eq!(bob_msg.chat_id, bob_chat_id);
1082 assert_eq!(bob_msg.viewtype, Viewtype::Image);
1083 assert_eq!(bob_msg.has_location(), false);
1084
1085 let bob_locations = get_range(&bob, None, None, 0, 0).await?;
1086 assert_eq!(bob_locations.len(), 1);
1087
1088 Ok(())
1089 }
1090
1091 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1092 async fn test_delete_expired_locations() -> Result<()> {
1093 let mut tcm = TestContextManager::new();
1094 let alice = &tcm.alice().await;
1095 let bob = &tcm.bob().await;
1096
1097 alice
1099 .set_config(Config::DeleteDeviceAfter, Some("604800"))
1100 .await?;
1101 bob.set_config(Config::DeleteDeviceAfter, Some("86400"))
1103 .await?;
1104
1105 let alice_chat = alice.create_chat(bob).await;
1106 bob.create_chat(alice).await;
1109
1110 send_to_chat(alice, alice_chat.id, 60).await?;
1113 bob.recv_msg(&alice.pop_sent_msg().await).await;
1114
1115 assert_eq!(set(alice, 10.0, 20.0, 1.0).await?, true);
1117 assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 1);
1118
1119 SystemTime::shift(Duration::from_secs(10));
1121 delete_expired(alice, time()).await?;
1122 maybe_send(alice).await?;
1123 bob.evtracker.clear_events();
1124 bob.recv_msg_opt(&alice.pop_sent_msg().await).await;
1125 bob.evtracker
1126 .get_matching_ex(
1127 bob,
1128 ExpectedEvents {
1129 expected: |e| matches!(e, EventType::MsgsChanged { .. }),
1130 unexpected: |e| matches!(e, EventType::IncomingMsg { .. }),
1131 },
1132 )
1133 .await
1134 .unwrap();
1135 assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 1);
1136 assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 1);
1137
1138 let contact = bob.add_or_lookup_contact(alice).await;
1140 assert!(!contact.is_bot());
1141
1142 SystemTime::shift(Duration::from_secs(86400));
1144 delete_expired(alice, time()).await?;
1145 delete_expired(bob, time()).await?;
1146 assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 1);
1147 assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 0);
1148
1149 SystemTime::shift(Duration::from_secs(604800));
1151 delete_expired(alice, time()).await?;
1152 delete_expired(bob, time()).await?;
1153 assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 0);
1154 assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 0);
1155
1156 Ok(())
1157 }
1158}