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