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::log::{info, 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.unescape().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
265pub async fn send_locations_to_chat(
267 context: &Context,
268 chat_id: ChatId,
269 seconds: i64,
270) -> Result<()> {
271 ensure!(seconds >= 0);
272 ensure!(!chat_id.is_special());
273 let now = time();
274 let is_sending_locations_before = is_sending_locations_to_chat(context, Some(chat_id)).await?;
275 context
276 .sql
277 .execute(
278 "UPDATE chats \
279 SET locations_send_begin=?, \
280 locations_send_until=? \
281 WHERE id=?",
282 (
283 if 0 != seconds { now } else { 0 },
284 if 0 != seconds { now + seconds } else { 0 },
285 chat_id,
286 ),
287 )
288 .await?;
289 if 0 != seconds && !is_sending_locations_before {
290 let mut msg = Message::new_text(stock_str::msg_location_enabled(context).await);
291 msg.param.set_cmd(SystemMessage::LocationStreamingEnabled);
292 chat::send_msg(context, chat_id, &mut msg)
293 .await
294 .unwrap_or_default();
295 } else if 0 == seconds && is_sending_locations_before {
296 let stock_str = stock_str::msg_location_disabled(context).await;
297 chat::add_info_msg(context, chat_id, &stock_str, now).await?;
298 }
299 context.emit_event(EventType::ChatModified(chat_id));
300 chatlist_events::emit_chatlist_item_changed(context, chat_id);
301 if 0 != seconds {
302 context.scheduler.interrupt_location().await;
303 }
304 Ok(())
305}
306
307pub async fn is_sending_locations_to_chat(
312 context: &Context,
313 chat_id: Option<ChatId>,
314) -> Result<bool> {
315 let exists = match chat_id {
316 Some(chat_id) => {
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 None => {
326 context
327 .sql
328 .exists(
329 "SELECT COUNT(id) FROM chats WHERE locations_send_until>?;",
330 (time(),),
331 )
332 .await?
333 }
334 };
335 Ok(exists)
336}
337
338pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> Result<bool> {
340 if latitude == 0.0 && longitude == 0.0 {
341 return Ok(true);
342 }
343 let mut continue_streaming = false;
344 let now = time();
345
346 let chats = context
347 .sql
348 .query_map(
349 "SELECT id FROM chats WHERE locations_send_until>?;",
350 (now,),
351 |row| row.get::<_, i32>(0),
352 |chats| {
353 chats
354 .collect::<std::result::Result<Vec<_>, _>>()
355 .map_err(Into::into)
356 },
357 )
358 .await?;
359
360 let mut stored_location = false;
361 for chat_id in chats {
362 context.sql.execute(
363 "INSERT INTO locations \
364 (latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
365 (
366 latitude,
367 longitude,
368 accuracy,
369 now,
370 chat_id,
371 ContactId::SELF,
372 )).await.context("Failed to store location")?;
373 stored_location = true;
374
375 info!(context, "Stored location for chat {chat_id}.");
376 continue_streaming = true;
377 }
378 if continue_streaming {
379 context.emit_location_changed(Some(ContactId::SELF)).await?;
380 };
381 if stored_location {
382 context.scheduler.interrupt_location().await;
384 }
385
386 Ok(continue_streaming)
387}
388
389pub async fn get_range(
391 context: &Context,
392 chat_id: Option<ChatId>,
393 contact_id: Option<u32>,
394 timestamp_from: i64,
395 mut timestamp_to: i64,
396) -> Result<Vec<Location>> {
397 if timestamp_to == 0 {
398 timestamp_to = time() + 10;
399 }
400
401 let (disable_chat_id, chat_id) = match chat_id {
402 Some(chat_id) => (0, chat_id),
403 None => (1, ChatId::new(0)), };
405 let (disable_contact_id, contact_id) = match contact_id {
406 Some(contact_id) => (0, contact_id),
407 None => (1, 0), };
409 let list = context
410 .sql
411 .query_map(
412 "SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \
413 COALESCE(m.id, 0) AS msg_id, l.from_id, l.chat_id, COALESCE(m.txt, '') AS txt \
414 FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \
415 AND (? OR l.from_id=?) \
416 AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
417 ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
418 (
419 disable_chat_id,
420 chat_id,
421 disable_contact_id,
422 contact_id as i32,
423 timestamp_from,
424 timestamp_to,
425 ),
426 |row| {
427 let msg_id = row.get(6)?;
428 let txt: String = row.get(9)?;
429 let marker = if msg_id != 0 && is_marker(&txt) {
430 Some(txt)
431 } else {
432 None
433 };
434 let loc = Location {
435 location_id: row.get(0)?,
436 latitude: row.get(1)?,
437 longitude: row.get(2)?,
438 accuracy: row.get(3)?,
439 timestamp: row.get(4)?,
440 independent: row.get(5)?,
441 msg_id,
442 contact_id: row.get(7)?,
443 chat_id: row.get(8)?,
444 marker,
445 };
446 Ok(loc)
447 },
448 |locations| {
449 let mut ret = Vec::new();
450
451 for location in locations {
452 ret.push(location?);
453 }
454 Ok(ret)
455 },
456 )
457 .await?;
458 Ok(list)
459}
460
461fn is_marker(txt: &str) -> bool {
462 let mut chars = txt.chars();
463 if let Some(c) = chars.next() {
464 !c.is_whitespace() && chars.next().is_none()
465 } else {
466 false
467 }
468}
469
470pub async fn delete_all(context: &Context) -> Result<()> {
472 context.sql.execute("DELETE FROM locations;", ()).await?;
473 context.emit_location_changed(None).await?;
474 Ok(())
475}
476
477pub(crate) async fn delete_expired(context: &Context, now: i64) -> Result<()> {
482 let Some(delete_device_after) = context.get_config_delete_device_after().await? else {
483 return Ok(());
484 };
485
486 let threshold_timestamp = now.saturating_sub(delete_device_after);
487 let deleted = context
488 .sql
489 .execute(
490 "DELETE FROM locations WHERE independent=0 AND timestamp < ?",
491 (threshold_timestamp,),
492 )
493 .await?
494 > 0;
495 if deleted {
496 info!(context, "Deleted {deleted} expired locations.");
497 context.emit_location_changed(None).await?;
498 }
499 Ok(())
500}
501
502pub(crate) async fn delete_poi_location(context: &Context, location_id: u32) -> Result<()> {
507 context
508 .sql
509 .execute(
510 "DELETE FROM locations WHERE independent = 1 AND id=?",
511 (location_id as i32,),
512 )
513 .await?;
514 Ok(())
515}
516
517pub(crate) async fn delete_orphaned_poi_locations(context: &Context) -> Result<()> {
519 context.sql.execute("
520 DELETE FROM locations
521 WHERE independent=1 AND id NOT IN
522 (SELECT location_id from MSGS LEFT JOIN locations
523 ON locations.id=location_id
524 WHERE location_id>0 -- This check makes the query faster by not looking for locations with ID 0 that don't exist.
525 AND msgs.chat_id != ?)", (DC_CHAT_ID_TRASH,)).await?;
526 Ok(())
527}
528
529pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<Option<(String, u32)>> {
531 let mut last_added_location_id = 0;
532
533 let self_addr = context.get_primary_self_addr().await?;
534
535 let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
536 "SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
537 (chat_id,), |row| {
538 let send_begin: i64 = row.get(0)?;
539 let send_until: i64 = row.get(1)?;
540 let last_sent: i64 = row.get(2)?;
541
542 Ok((send_begin, send_until, last_sent))
543 })
544 .await?;
545
546 let now = time();
547 let mut location_count = 0;
548 let mut ret = String::new();
549 if locations_send_begin != 0 && now <= locations_send_until {
550 ret += &format!(
551 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
552 <kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"{self_addr}\">\n",
553 );
554
555 context
556 .sql
557 .query_map(
558 "SELECT id, latitude, longitude, accuracy, timestamp \
559 FROM locations WHERE from_id=? \
560 AND timestamp>=? \
561 AND (timestamp>=? OR \
562 timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \
563 AND independent=0 \
564 GROUP BY timestamp \
565 ORDER BY timestamp;",
566 (
567 ContactId::SELF,
568 locations_send_begin,
569 locations_last_sent,
570 ContactId::SELF
571 ),
572 |row| {
573 let location_id: i32 = row.get(0)?;
574 let latitude: f64 = row.get(1)?;
575 let longitude: f64 = row.get(2)?;
576 let accuracy: f64 = row.get(3)?;
577 let timestamp = get_kml_timestamp(row.get(4)?);
578
579 Ok((location_id, latitude, longitude, accuracy, timestamp))
580 },
581 |rows| {
582 for row in rows {
583 let (location_id, latitude, longitude, accuracy, timestamp) = row?;
584 ret += &format!(
585 "<Placemark>\
586 <Timestamp><when>{timestamp}</when></Timestamp>\
587 <Point><coordinates accuracy=\"{accuracy}\">{longitude},{latitude}</coordinates></Point>\
588 </Placemark>\n"
589 );
590 location_count += 1;
591 last_added_location_id = location_id as u32;
592 }
593 Ok(())
594 },
595 )
596 .await?;
597 ret += "</Document>\n</kml>";
598 }
599
600 if location_count > 0 {
601 Ok(Some((ret, last_added_location_id)))
602 } else {
603 Ok(None)
604 }
605}
606
607fn get_kml_timestamp(utc: i64) -> String {
608 chrono::DateTime::<chrono::Utc>::from_timestamp(utc, 0)
610 .unwrap()
611 .format("%Y-%m-%dT%H:%M:%SZ")
612 .to_string()
613}
614
615pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String {
617 format!(
618 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
619 <kml xmlns=\"http://www.opengis.net/kml/2.2\">\n\
620 <Document>\n\
621 <Placemark>\
622 <Timestamp><when>{}</when></Timestamp>\
623 <Point><coordinates>{},{}</coordinates></Point>\
624 </Placemark>\n\
625 </Document>\n\
626 </kml>",
627 get_kml_timestamp(timestamp),
628 longitude,
629 latitude,
630 )
631}
632
633pub async fn set_kml_sent_timestamp(
635 context: &Context,
636 chat_id: ChatId,
637 timestamp: i64,
638) -> Result<()> {
639 context
640 .sql
641 .execute(
642 "UPDATE chats SET locations_last_sent=? WHERE id=?;",
643 (timestamp, chat_id),
644 )
645 .await?;
646 Ok(())
647}
648
649pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id: u32) -> Result<()> {
651 context
652 .sql
653 .execute(
654 "UPDATE msgs SET location_id=? WHERE id=?;",
655 (location_id, msg_id),
656 )
657 .await?;
658
659 Ok(())
660}
661
662pub(crate) async fn save(
666 context: &Context,
667 chat_id: ChatId,
668 contact_id: ContactId,
669 locations: &[Location],
670 independent: bool,
671) -> Result<Option<u32>> {
672 ensure!(!chat_id.is_special(), "Invalid chat id");
673
674 let mut newest_timestamp = 0;
675 let mut newest_location_id = None;
676
677 let stmt_insert = "INSERT INTO locations\
678 (timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
679 VALUES (?,?,?,?,?,?,?);";
680
681 for location in locations {
682 let &Location {
683 timestamp,
684 latitude,
685 longitude,
686 accuracy,
687 ..
688 } = location;
689
690 context
691 .sql
692 .call_write(|conn| {
693 let mut stmt_test = conn
694 .prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
695 let mut stmt_insert = conn.prepare_cached(stmt_insert)?;
696
697 let exists = stmt_test.exists((timestamp, contact_id))?;
698
699 if independent || !exists {
700 stmt_insert.execute((
701 timestamp,
702 contact_id,
703 chat_id,
704 latitude,
705 longitude,
706 accuracy,
707 independent,
708 ))?;
709
710 if timestamp > newest_timestamp {
711 newest_timestamp = timestamp;
712 newest_location_id = Some(u32::try_from(conn.last_insert_rowid())?);
713 }
714 }
715
716 Ok(())
717 })
718 .await?;
719 }
720
721 Ok(newest_location_id)
722}
723
724pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receiver<()>) {
725 loop {
726 let next_event = match maybe_send_locations(context).await {
727 Err(err) => {
728 warn!(context, "maybe_send_locations failed: {:#}", err);
729 Some(60) }
731 Ok(next_event) => next_event,
732 };
733
734 let duration = if let Some(next_event) = next_event {
735 Duration::from_secs(next_event)
736 } else {
737 Duration::from_secs(86400)
738 };
739
740 info!(
741 context,
742 "Location loop is waiting for {} or interrupt",
743 duration_to_str(duration)
744 );
745 match timeout(duration, interrupt_receiver.recv()).await {
746 Err(_err) => {
747 info!(context, "Location loop timeout.");
748 }
749 Ok(Err(err)) => {
750 warn!(
751 context,
752 "Interrupt channel closed, location loop exits now: {err:#}."
753 );
754 return;
755 }
756 Ok(Ok(())) => {
757 info!(context, "Location loop received interrupt.");
758 }
759 }
760 }
761}
762
763async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
766 let mut next_event: Option<u64> = None;
767
768 let now = time();
769 let rows = context
770 .sql
771 .query_map(
772 "SELECT id, locations_send_begin, locations_send_until, locations_last_sent
773 FROM chats
774 WHERE locations_send_until>0",
775 [],
776 |row| {
777 let chat_id: ChatId = row.get(0)?;
778 let locations_send_begin: i64 = row.get(1)?;
779 let locations_send_until: i64 = row.get(2)?;
780 let locations_last_sent: i64 = row.get(3)?;
781 Ok((
782 chat_id,
783 locations_send_begin,
784 locations_send_until,
785 locations_last_sent,
786 ))
787 },
788 |rows| {
789 rows.collect::<std::result::Result<Vec<_>, _>>()
790 .map_err(Into::into)
791 },
792 )
793 .await
794 .context("failed to query location streaming chats")?;
795
796 for (chat_id, locations_send_begin, locations_send_until, locations_last_sent) in rows {
797 if locations_send_begin > 0 && locations_send_until > now {
798 let can_send = now > locations_last_sent + 60;
799 let has_locations = context
800 .sql
801 .exists(
802 "SELECT COUNT(id) \
803 FROM locations \
804 WHERE from_id=? \
805 AND timestamp>=? \
806 AND timestamp>? \
807 AND independent=0",
808 (ContactId::SELF, locations_send_begin, locations_last_sent),
809 )
810 .await?;
811
812 next_event = next_event
813 .into_iter()
814 .chain(u64::try_from(locations_send_until - now))
815 .min();
816
817 if has_locations {
818 if can_send {
819 info!(
823 context,
824 "Chat {} has pending locations, sending them.", chat_id
825 );
826 let mut msg = Message::new(Viewtype::Text);
827 msg.hidden = true;
828 msg.param.set_cmd(SystemMessage::LocationOnly);
829 chat::send_msg(context, chat_id, &mut msg).await?;
830 } else {
831 info!(
833 context,
834 "Chat {} has pending locations, but they can't be sent yet.", chat_id
835 );
836 next_event = next_event
837 .into_iter()
838 .chain(u64::try_from(locations_last_sent + 61 - now))
839 .min();
840 }
841 } else {
842 info!(
843 context,
844 "Chat {} has location streaming enabled, but no pending locations.", chat_id
845 );
846 }
847 } else {
848 info!(
851 context,
852 "Disabling location streaming for chat {}.", chat_id
853 );
854 context
855 .sql
856 .execute(
857 "UPDATE chats \
858 SET locations_send_begin=0, locations_send_until=0 \
859 WHERE id=?",
860 (chat_id,),
861 )
862 .await
863 .context("failed to disable location streaming")?;
864
865 let stock_str = stock_str::msg_location_disabled(context).await;
866 chat::add_info_msg(context, chat_id, &stock_str, now).await?;
867 context.emit_event(EventType::ChatModified(chat_id));
868 chatlist_events::emit_chatlist_item_changed(context, chat_id);
869 }
870 }
871
872 Ok(next_event)
873}
874
875#[cfg(test)]
876mod tests {
877 use super::*;
878 use crate::config::Config;
879 use crate::message::MessageState;
880 use crate::receive_imf::receive_imf;
881 use crate::test_utils::{TestContext, TestContextManager};
882 use crate::tools::SystemTime;
883
884 #[test]
885 fn test_kml_parse() {
886 let xml =
887 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>";
888
889 let kml = Kml::parse(xml).expect("parsing failed");
890
891 assert!(kml.addr.is_some());
892 assert_eq!(kml.addr.as_ref().unwrap(), "user@example.org",);
893
894 let locations_ref = &kml.locations;
895 assert_eq!(locations_ref.len(), 2);
896
897 assert!(locations_ref[0].latitude > 53.6f64);
898 assert!(locations_ref[0].latitude < 53.8f64);
899 assert!(locations_ref[0].longitude > 9.3f64);
900 assert!(locations_ref[0].longitude < 9.5f64);
901 assert!(locations_ref[0].accuracy > 31.9f64);
902 assert!(locations_ref[0].accuracy < 32.1f64);
903 assert_eq!(locations_ref[0].timestamp, 1551906597);
904
905 assert!(locations_ref[1].latitude > 63.6f64);
906 assert!(locations_ref[1].latitude < 63.8f64);
907 assert!(locations_ref[1].longitude > 19.3f64);
908 assert!(locations_ref[1].longitude < 19.5f64);
909 assert!(locations_ref[1].accuracy > 2.4f64);
910 assert!(locations_ref[1].accuracy < 2.6f64);
911 assert_eq!(locations_ref[1].timestamp, 1544739072);
912 }
913
914 #[test]
915 fn test_kml_parse_error() {
916 let xml = b"<?><xmlversi\"\"\">?</document>";
917 assert!(Kml::parse(xml).is_err());
918 }
919
920 #[test]
921 fn test_get_message_kml() {
922 let timestamp = 1598490000;
923
924 let xml = get_message_kml(timestamp, 51.423723f64, 8.552556f64);
925 let kml = Kml::parse(xml.as_bytes()).expect("parsing failed");
926 let locations_ref = &kml.locations;
927 assert_eq!(locations_ref.len(), 1);
928
929 assert!(locations_ref[0].latitude >= 51.423723f64);
930 assert!(locations_ref[0].latitude < 51.423724f64);
931 assert!(locations_ref[0].longitude >= 8.552556f64);
932 assert!(locations_ref[0].longitude < 8.552557f64);
933 assert!(locations_ref[0].accuracy.abs() < f64::EPSILON);
934 assert_eq!(locations_ref[0].timestamp, timestamp);
935 }
936
937 #[test]
938 fn test_is_marker() {
939 assert!(is_marker("f"));
940 assert!(!is_marker("foo"));
941 assert!(is_marker("🏠"));
942 assert!(!is_marker(" "));
943 assert!(!is_marker("\t"));
944 }
945
946 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
948 async fn receive_location_kml() -> Result<()> {
949 let alice = TestContext::new_alice().await;
950
951 receive_imf(
952 &alice,
953 br#"Subject: Hello
954Message-ID: hello@example.net
955To: Alice <alice@example.org>
956From: Bob <bob@example.net>
957Date: Mon, 20 Dec 2021 00:00:00 +0000
958Chat-Version: 1.0
959Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
960
961Text message."#,
962 false,
963 )
964 .await?;
965 let received_msg = alice.get_last_msg().await;
966 assert_eq!(received_msg.text, "Text message.");
967
968 receive_imf(
969 &alice,
970 br#"Subject: locations
971MIME-Version: 1.0
972To: <alice@example.org>
973From: <bob@example.net>
974Date: Tue, 21 Dec 2021 00:00:00 +0000
975Chat-Version: 1.0
976Message-ID: <foobar@example.net>
977Content-Type: multipart/mixed; boundary="U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF"
978
979
980--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF
981Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
982
983
984
985--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF
986Content-Type: application/vnd.google-earth.kml+xml
987Content-Disposition: attachment; filename="location.kml"
988
989<?xml version="1.0" encoding="UTF-8"?>
990<kml xmlns="http://www.opengis.net/kml/2.2">
991<Document addr="bob@example.net">
992<Placemark><Timestamp><when>2021-11-21T00:00:00Z</when></Timestamp><Point><coordinates accuracy="1.0000000000000000">10.00000000000000,20.00000000000000</coordinates></Point></Placemark>
993</Document>
994</kml>
995
996--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF--"#,
997 false,
998 )
999 .await?;
1000
1001 let received_msg2 = alice.get_last_msg().await;
1003 assert_eq!(received_msg2.id, received_msg.id);
1004
1005 let locations = get_range(&alice, None, None, 0, 0).await?;
1006 assert_eq!(locations.len(), 1);
1007 Ok(())
1008 }
1009
1010 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1012 async fn receive_visible_location_kml() -> Result<()> {
1013 let alice = TestContext::new_alice().await;
1014
1015 receive_imf(
1016 &alice,
1017 br#"Subject: locations
1018MIME-Version: 1.0
1019To: <alice@example.org>
1020From: <bob@example.net>
1021Date: Tue, 21 Dec 2021 00:00:00 +0000
1022Chat-Version: 1.0
1023Message-ID: <foobar@localhost>
1024Content-Type: multipart/mixed; boundary="U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF"
1025
1026
1027--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF
1028Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
1029
1030Text message.
1031
1032
1033--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF
1034Content-Type: application/vnd.google-earth.kml+xml
1035Content-Disposition: attachment; filename="location.kml"
1036
1037<?xml version="1.0" encoding="UTF-8"?>
1038<kml xmlns="http://www.opengis.net/kml/2.2">
1039<Document addr="bob@example.net">
1040<Placemark><Timestamp><when>2021-11-21T00:00:00Z</when></Timestamp><Point><coordinates accuracy="1.0000000000000000">10.00000000000000,20.00000000000000</coordinates></Point></Placemark>
1041</Document>
1042</kml>
1043
1044--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF--"#,
1045 false,
1046 )
1047 .await?;
1048
1049 let received_msg = alice.get_last_msg().await;
1050 assert_eq!(received_msg.text, "Text message.");
1051 assert_eq!(received_msg.state, MessageState::InFresh);
1052
1053 let locations = get_range(&alice, None, None, 0, 0).await?;
1054 assert_eq!(locations.len(), 1);
1055 Ok(())
1056 }
1057
1058 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1059 async fn test_send_locations_to_chat() -> Result<()> {
1060 let alice = TestContext::new_alice().await;
1061 let bob = TestContext::new_bob().await;
1062
1063 let alice_chat = alice.create_chat(&bob).await;
1064 send_locations_to_chat(&alice, alice_chat.id, 1000).await?;
1065 let sent = alice.pop_sent_msg().await;
1066 let msg = bob.recv_msg(&sent).await;
1067 assert_eq!(msg.text, "Location streaming enabled by alice@example.org.");
1068 let bob_chat_id = msg.chat_id;
1069
1070 assert_eq!(set(&alice, 10.0, 20.0, 1.0).await?, true);
1071
1072 let file_name = "image.png";
1074 let bytes = include_bytes!("../test-data/image/logo.png");
1075 let file = alice.get_blobdir().join(file_name);
1076 tokio::fs::write(&file, bytes).await?;
1077 let mut msg = Message::new(Viewtype::Image);
1078 msg.set_file_and_deduplicate(&alice, &file, Some("logo.png"), None)?;
1079 let sent = alice.send_msg(alice_chat.id, &mut msg).await;
1080 let alice_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?;
1081 assert_eq!(alice_msg.has_location(), false);
1082
1083 let msg = bob.recv_msg_opt(&sent).await.unwrap();
1084 assert!(msg.chat_id == bob_chat_id);
1085 assert_eq!(msg.msg_ids.len(), 1);
1086
1087 let bob_msg = Message::load_from_db(&bob, *msg.msg_ids.first().unwrap()).await?;
1088 assert_eq!(bob_msg.chat_id, bob_chat_id);
1089 assert_eq!(bob_msg.viewtype, Viewtype::Image);
1090 assert_eq!(bob_msg.has_location(), false);
1091
1092 let bob_locations = get_range(&bob, None, None, 0, 0).await?;
1093 assert_eq!(bob_locations.len(), 1);
1094
1095 Ok(())
1096 }
1097
1098 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1099 async fn test_delete_expired_locations() -> Result<()> {
1100 let mut tcm = TestContextManager::new();
1101 let alice = &tcm.alice().await;
1102 let bob = &tcm.bob().await;
1103
1104 alice
1106 .set_config(Config::DeleteDeviceAfter, Some("604800"))
1107 .await?;
1108 bob.set_config(Config::DeleteDeviceAfter, Some("86400"))
1110 .await?;
1111
1112 let alice_chat = alice.create_chat(bob).await;
1113
1114 send_locations_to_chat(alice, alice_chat.id, 60).await?;
1117 bob.recv_msg(&alice.pop_sent_msg().await).await;
1118
1119 assert_eq!(set(alice, 10.0, 20.0, 1.0).await?, true);
1121 assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 1);
1122
1123 SystemTime::shift(Duration::from_secs(10));
1125 delete_expired(alice, time()).await?;
1126 maybe_send_locations(alice).await?;
1127 bob.recv_msg_opt(&alice.pop_sent_msg().await).await;
1128 assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 1);
1129 assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 1);
1130
1131 let contact = bob.add_or_lookup_contact(alice).await;
1133 assert!(!contact.is_bot());
1134
1135 SystemTime::shift(Duration::from_secs(86400));
1137 delete_expired(alice, time()).await?;
1138 delete_expired(bob, time()).await?;
1139 assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 1);
1140 assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 0);
1141
1142 SystemTime::shift(Duration::from_secs(604800));
1144 delete_expired(alice, time()).await?;
1145 delete_expired(bob, time()).await?;
1146 assert_eq!(get_range(alice, None, None, 0, 0).await?.len(), 0);
1147 assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 0);
1148
1149 Ok(())
1150 }
1151}