1use std::time::Duration;
14
15use anyhow::{Context as _, Result, ensure};
16use async_channel::Receiver;
17use quick_xml::XmlVersion;
18use quick_xml::events::{BytesEnd, BytesStart, BytesText};
19use tokio::time::timeout;
20
21use crate::chat::{self, ChatId};
22use crate::constants::DC_CHAT_ID_TRASH;
23use crate::contact::ContactId;
24use crate::context::Context;
25use crate::events::EventType;
26use crate::log::warn;
27use crate::message::{Message, MsgId, Viewtype};
28use crate::mimeparser::SystemMessage;
29use crate::tools::{duration_to_str, time};
30use crate::{chatlist_events, stock_str};
31
32#[derive(Debug, Clone, Default)]
34pub struct Location {
35 pub location_id: u32,
37
38 pub latitude: f64,
40
41 pub longitude: f64,
43
44 pub accuracy: f64,
46
47 pub timestamp: i64,
49
50 pub contact_id: ContactId,
52
53 pub msg_id: u32,
55
56 pub chat_id: ChatId,
58
59 pub marker: Option<String>,
61
62 pub independent: u32,
64}
65
66impl Location {
67 pub fn new() -> Self {
69 Default::default()
70 }
71}
72
73#[derive(Debug, Clone, Default)]
78pub struct Kml {
79 pub addr: Option<String>,
81
82 pub locations: Vec<Location>,
84
85 tag: KmlTag,
87
88 pub curr: Location,
90}
91
92#[derive(Default, Debug, Clone, PartialEq, Eq)]
93enum KmlTag {
94 #[default]
95 Undefined,
96 Placemark,
97 PlacemarkTimestamp,
98 PlacemarkTimestampWhen,
99 PlacemarkPoint,
100 PlacemarkPointCoordinates,
101}
102
103impl Kml {
104 pub fn new() -> Self {
106 Default::default()
107 }
108
109 pub fn parse(to_parse: &[u8]) -> Result<Self> {
111 ensure!(to_parse.len() <= 1024 * 1024, "kml-file is too large");
112
113 let mut reader = quick_xml::Reader::from_reader(to_parse);
114 reader.config_mut().trim_text(true);
115
116 let mut kml = Kml::new();
117 kml.locations = Vec::with_capacity(100);
118
119 let mut buf = Vec::new();
120
121 loop {
122 match reader.read_event_into(&mut buf).with_context(|| {
123 format!(
124 "location parsing error at position {}",
125 reader.buffer_position()
126 )
127 })? {
128 quick_xml::events::Event::Start(ref e) => kml.starttag_cb(e, &reader),
129 quick_xml::events::Event::End(ref e) => kml.endtag_cb(e),
130 quick_xml::events::Event::Text(ref e) => kml.text_cb(e),
131 quick_xml::events::Event::Eof => break,
132 _ => (),
133 }
134 buf.clear();
135 }
136
137 Ok(kml)
138 }
139
140 fn text_cb(&mut self, event: &BytesText) {
141 if self.tag == KmlTag::PlacemarkTimestampWhen
142 || self.tag == KmlTag::PlacemarkPointCoordinates
143 {
144 let val = event
145 .xml_content(XmlVersion::Implicit1_0)
146 .unwrap_or_default();
147 let val = val.replace(['\n', '\r', '\t', ' '], "");
148
149 if self.tag == KmlTag::PlacemarkTimestampWhen && val.len() >= 19 {
150 match chrono::NaiveDateTime::parse_from_str(&val, "%Y-%m-%dT%H:%M:%SZ") {
153 Ok(res) => {
154 self.curr.timestamp = res.and_utc().timestamp();
155 let now = time();
156 if self.curr.timestamp > now {
157 self.curr.timestamp = now;
158 }
159 }
160 Err(_err) => {
161 self.curr.timestamp = time();
162 }
163 }
164 } else if self.tag == KmlTag::PlacemarkPointCoordinates {
165 let parts = val.splitn(2, ',').collect::<Vec<_>>();
166 if let [longitude, latitude] = &parts[..] {
167 self.curr.longitude = longitude.parse().unwrap_or_default();
168 self.curr.latitude = latitude.parse().unwrap_or_default();
169 }
170 }
171 }
172 }
173
174 fn endtag_cb(&mut self, event: &BytesEnd) {
175 let tag = String::from_utf8_lossy(event.name().as_ref())
176 .trim()
177 .to_lowercase();
178
179 match self.tag {
180 KmlTag::PlacemarkTimestampWhen => {
181 if tag == "when" {
182 self.tag = KmlTag::PlacemarkTimestamp
183 }
184 }
185 KmlTag::PlacemarkTimestamp => {
186 if tag == "timestamp" {
187 self.tag = KmlTag::Placemark
188 }
189 }
190 KmlTag::PlacemarkPointCoordinates => {
191 if tag == "coordinates" {
192 self.tag = KmlTag::PlacemarkPoint
193 }
194 }
195 KmlTag::PlacemarkPoint => {
196 if tag == "point" {
197 self.tag = KmlTag::Placemark
198 }
199 }
200 KmlTag::Placemark => {
201 if tag == "placemark" {
202 if 0 != self.curr.timestamp
203 && 0. != self.curr.latitude
204 && 0. != self.curr.longitude
205 {
206 self.locations
207 .push(std::mem::replace(&mut self.curr, Location::new()));
208 }
209 self.tag = KmlTag::Undefined;
210 }
211 }
212 KmlTag::Undefined => {}
213 }
214 }
215
216 fn starttag_cb<B: std::io::BufRead>(
217 &mut self,
218 event: &BytesStart,
219 reader: &quick_xml::Reader<B>,
220 ) {
221 let tag = String::from_utf8_lossy(event.name().as_ref())
222 .trim()
223 .to_lowercase();
224 if tag == "document" {
225 if let Some(addr) = event.attributes().filter_map(|a| a.ok()).find(|attr| {
226 String::from_utf8_lossy(attr.key.as_ref())
227 .trim()
228 .to_lowercase()
229 == "addr"
230 }) {
231 self.addr = addr
232 .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
233 .ok()
234 .map(|a| a.into_owned());
235 }
236 } else if tag == "placemark" {
237 self.tag = KmlTag::Placemark;
238 self.curr.timestamp = 0;
239 self.curr.latitude = 0.0;
240 self.curr.longitude = 0.0;
241 self.curr.accuracy = 0.0
242 } else if tag == "timestamp" && self.tag == KmlTag::Placemark {
243 self.tag = KmlTag::PlacemarkTimestamp;
244 } else if tag == "when" && self.tag == KmlTag::PlacemarkTimestamp {
245 self.tag = KmlTag::PlacemarkTimestampWhen;
246 } else if tag == "point" && self.tag == KmlTag::Placemark {
247 self.tag = KmlTag::PlacemarkPoint;
248 } else if tag == "coordinates" && self.tag == KmlTag::PlacemarkPoint {
249 self.tag = KmlTag::PlacemarkPointCoordinates;
250 if let Some(acc) = event.attributes().find_map(|attr| {
251 attr.ok().filter(|a| {
252 String::from_utf8_lossy(a.key.as_ref())
253 .trim()
254 .eq_ignore_ascii_case("accuracy")
255 })
256 }) {
257 let v = acc
258 .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
259 .unwrap_or_default();
260
261 self.curr.accuracy = v.trim().parse().unwrap_or_default();
262 }
263 }
264 }
265}
266
267#[expect(clippy::arithmetic_side_effects)]
269pub async fn send_to_chat(context: &Context, chat_id: ChatId, seconds: i64) -> Result<()> {
270 ensure!(seconds >= 0);
271 ensure!(!chat_id.is_special());
272 let now = time();
273 let is_sending_locations_before = is_sending_to_chat(context, 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));
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);
296 chat::add_info_msg(context, chat_id, &stock_str).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(context: &Context) -> Result<bool> {
308 context
309 .sql
310 .exists(
311 "SELECT COUNT(id) FROM chats WHERE locations_send_until>?",
312 (time(),),
313 )
314 .await
315}
316
317pub async fn is_sending_to_chat(context: &Context, chat_id: ChatId) -> Result<bool> {
319 context
320 .sql
321 .exists(
322 "SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?",
323 (chat_id, time()),
324 )
325 .await
326}
327
328async fn get_chats_with_location_streaming(context: &Context) -> Result<Vec<ChatId>> {
330 context
331 .sql
332 .query_map_vec(
333 "SELECT id FROM chats WHERE locations_send_until>?",
334 (time(),),
335 |row| {
336 let chat_id: ChatId = row.get(0)?;
337 Ok(chat_id)
338 },
339 )
340 .await
341}
342
343pub async fn stop_sending(context: &Context) -> Result<()> {
345 for chat_id in get_chats_with_location_streaming(context).await? {
346 send_to_chat(context, chat_id, 0).await?;
347 }
348 Ok(())
349}
350
351pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> Result<bool> {
353 if latitude == 0.0 && longitude == 0.0 {
354 return Ok(true);
355 }
356 let mut continue_streaming = false;
357 let now = time();
358
359 let chats = context
360 .sql
361 .query_map_vec(
362 "SELECT id FROM chats WHERE locations_send_until>?;",
363 (now,),
364 |row| {
365 let id: i32 = row.get(0)?;
366 Ok(id)
367 },
368 )
369 .await?;
370
371 let mut stored_location = false;
372 for chat_id in chats {
373 context.sql.execute(
374 "INSERT INTO locations \
375 (latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
376 (
377 latitude,
378 longitude,
379 accuracy,
380 now,
381 chat_id,
382 ContactId::SELF,
383 )).await.context("Failed to store location")?;
384 stored_location = true;
385
386 info!(context, "Stored location for chat {chat_id}.");
387 continue_streaming = true;
388 }
389 if continue_streaming {
390 context.emit_location_changed(Some(ContactId::SELF)).await?;
391 };
392 if stored_location {
393 context.scheduler.interrupt_location().await;
395 }
396
397 Ok(continue_streaming)
398}
399
400#[expect(clippy::arithmetic_side_effects)]
402pub async fn get_range(
403 context: &Context,
404 chat_id: Option<ChatId>,
405 contact_id: Option<u32>,
406 timestamp_from: i64,
407 mut timestamp_to: i64,
408) -> Result<Vec<Location>> {
409 if timestamp_to == 0 {
410 timestamp_to = time() + 10;
411 }
412
413 let (disable_chat_id, chat_id) = match chat_id {
414 Some(chat_id) => (0, chat_id),
415 None => (1, ChatId::new(0)), };
417 let (disable_contact_id, contact_id) = match contact_id {
418 Some(contact_id) => (0, contact_id),
419 None => (1, 0), };
421 let list = context
422 .sql
423 .query_map_vec(
424 "SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \
425 COALESCE(m.id, 0) AS msg_id, l.from_id, l.chat_id, COALESCE(m.txt, '') AS txt \
426 FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \
427 AND (? OR l.from_id=?) \
428 AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
429 ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
430 (
431 disable_chat_id,
432 chat_id,
433 disable_contact_id,
434 contact_id as i32,
435 timestamp_from,
436 timestamp_to,
437 ),
438 |row| {
439 let msg_id = row.get(6)?;
440 let txt: String = row.get(9)?;
441 let marker = if msg_id != 0 && is_marker(&txt) {
442 Some(txt)
443 } else {
444 None
445 };
446 let loc = Location {
447 location_id: row.get(0)?,
448 latitude: row.get(1)?,
449 longitude: row.get(2)?,
450 accuracy: row.get(3)?,
451 timestamp: row.get(4)?,
452 independent: row.get(5)?,
453 msg_id,
454 contact_id: row.get(7)?,
455 chat_id: row.get(8)?,
456 marker,
457 };
458 Ok(loc)
459 },
460 )
461 .await?;
462 Ok(list)
463}
464
465fn is_marker(txt: &str) -> bool {
466 let mut chars = txt.chars();
467 if let Some(c) = chars.next() {
468 !c.is_whitespace() && chars.next().is_none()
469 } else {
470 false
471 }
472}
473
474pub(crate) async fn delete_expired(context: &Context, now: i64) -> Result<()> {
479 let Some(delete_device_after) = context.get_config_delete_device_after().await? else {
480 return Ok(());
481 };
482
483 let threshold_timestamp = now.saturating_sub(delete_device_after);
484 let deleted = context
485 .sql
486 .execute(
487 "DELETE FROM locations WHERE independent=0 AND timestamp < ?",
488 (threshold_timestamp,),
489 )
490 .await?
491 > 0;
492 if deleted {
493 info!(context, "Deleted {deleted} expired locations.");
494 context.emit_location_changed(None).await?;
495 }
496 Ok(())
497}
498
499pub(crate) async fn delete_poi(context: &Context, location_id: u32) -> Result<()> {
504 context
505 .sql
506 .execute(
507 "DELETE FROM locations WHERE independent = 1 AND id=?",
508 (location_id as i32,),
509 )
510 .await?;
511 Ok(())
512}
513
514pub(crate) async fn delete_orphaned_poi(context: &Context) -> Result<()> {
516 context.sql.execute("
517 DELETE FROM locations
518 WHERE independent=1 AND id NOT IN
519 (SELECT location_id from MSGS LEFT JOIN locations
520 ON locations.id=location_id
521 WHERE location_id>0 -- This check makes the query faster by not looking for locations with ID 0 that don't exist.
522 AND msgs.chat_id != ?)", (DC_CHAT_ID_TRASH,)).await?;
523 Ok(())
524}
525
526pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<Option<(String, i64)>> {
528 let mut last_added_location_timestamp: Option<i64> = None;
529
530 let self_addr = context.get_primary_self_addr().await?;
531
532 let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
533 "SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
534 (chat_id,), |row| {
535 let send_begin: i64 = row.get(0)?;
536 let send_until: i64 = row.get(1)?;
537 let last_sent: i64 = row.get(2)?;
538
539 Ok((send_begin, send_until, last_sent))
540 })
541 .await?;
542
543 let now = time();
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 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 latitude: f64 = row.get(0)?;
570 let longitude: f64 = row.get(1)?;
571 let accuracy: f64 = row.get(2)?;
572 let timestamp: i64 = row.get(3)?;
573
574 Ok((latitude, longitude, accuracy, timestamp))
575 },
576 |rows| {
577 for row in rows {
578 let (latitude, longitude, accuracy, timestamp) = row?;
579 let kml_timestamp = get_kml_timestamp(timestamp);
580 ret += &format!(
581 "<Placemark>\
582 <Timestamp><when>{kml_timestamp}</when></Timestamp>\
583 <Point><coordinates accuracy=\"{accuracy}\">{longitude},{latitude}</coordinates></Point>\
584 </Placemark>\n"
585 );
586 last_added_location_timestamp = std::cmp::max(last_added_location_timestamp, Some(timestamp));
587 }
588 Ok(())
589 },
590 )
591 .await?;
592 ret += "</Document>\n</kml>";
593 }
594
595 Ok(last_added_location_timestamp.map(|ts| (ret, ts)))
596}
597
598fn get_kml_timestamp(utc: i64) -> String {
599 chrono::DateTime::<chrono::Utc>::from_timestamp(utc, 0)
601 .unwrap()
602 .format("%Y-%m-%dT%H:%M:%SZ")
603 .to_string()
604}
605
606pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String {
608 format!(
609 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
610 <kml xmlns=\"http://www.opengis.net/kml/2.2\">\n\
611 <Document>\n\
612 <Placemark>\
613 <Timestamp><when>{}</when></Timestamp>\
614 <Point><coordinates>{},{}</coordinates></Point>\
615 </Placemark>\n\
616 </Document>\n\
617 </kml>",
618 get_kml_timestamp(timestamp),
619 longitude,
620 latitude,
621 )
622}
623
624pub async fn set_kml_sent_timestamp(
626 context: &Context,
627 chat_id: ChatId,
628 timestamp: i64,
629) -> Result<()> {
630 context
631 .sql
632 .execute(
633 "UPDATE chats SET locations_last_sent=? WHERE id=?;",
634 (timestamp, chat_id),
635 )
636 .await?;
637 Ok(())
638}
639
640pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id: u32) -> Result<()> {
642 context
643 .sql
644 .execute(
645 "UPDATE msgs SET location_id=? WHERE id=?;",
646 (location_id, msg_id),
647 )
648 .await?;
649
650 Ok(())
651}
652
653pub(crate) async fn save(
657 context: &Context,
658 chat_id: ChatId,
659 contact_id: ContactId,
660 locations: &[Location],
661 independent: bool,
662) -> Result<Option<u32>> {
663 ensure!(!chat_id.is_special(), "Invalid chat id");
664
665 let mut newest_timestamp = 0;
666 let mut newest_location_id = None;
667
668 let stmt_insert = "INSERT INTO locations\
669 (timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
670 VALUES (?,?,?,?,?,?,?);";
671
672 for location in locations {
673 let &Location {
674 timestamp,
675 latitude,
676 longitude,
677 accuracy,
678 ..
679 } = location;
680
681 context
682 .sql
683 .call_write(|conn| {
684 let mut stmt_test = conn
685 .prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
686 let mut stmt_insert = conn.prepare_cached(stmt_insert)?;
687
688 let exists = stmt_test.exists((timestamp, contact_id))?;
689
690 if independent || !exists {
691 stmt_insert.execute((
692 timestamp,
693 contact_id,
694 chat_id,
695 latitude,
696 longitude,
697 accuracy,
698 independent,
699 ))?;
700
701 if timestamp > newest_timestamp {
702 newest_timestamp = timestamp;
703 newest_location_id = Some(u32::try_from(conn.last_insert_rowid())?);
704 }
705 }
706
707 Ok(())
708 })
709 .await?;
710 }
711
712 Ok(newest_location_id)
713}
714
715pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receiver<()>) {
716 loop {
717 let next_event = match maybe_send(context).await {
718 Err(err) => {
719 warn!(context, "location::maybe_send failed: {:#}", err);
720 Some(60) }
722 Ok(next_event) => next_event,
723 };
724
725 let duration = if let Some(next_event) = next_event {
726 Duration::from_secs(next_event)
727 } else {
728 Duration::from_secs(86400)
729 };
730
731 info!(
732 context,
733 "Location loop is waiting for {} or interrupt",
734 duration_to_str(duration)
735 );
736 match timeout(duration, interrupt_receiver.recv()).await {
737 Err(_err) => {
738 info!(context, "Location loop timeout.");
739 }
740 Ok(Err(err)) => {
741 warn!(
742 context,
743 "Interrupt channel closed, location loop exits now: {err:#}."
744 );
745 return;
746 }
747 Ok(Ok(())) => {
748 info!(context, "Location loop received interrupt.");
749 }
750 }
751 }
752}
753
754#[expect(clippy::arithmetic_side_effects)]
757async fn maybe_send(context: &Context) -> Result<Option<u64>> {
758 let mut next_event: Option<u64> = None;
759
760 let now = time();
761 let rows = context
762 .sql
763 .query_map_vec(
764 "SELECT id, locations_send_begin, locations_send_until, locations_last_sent
765 FROM chats
766 WHERE locations_send_until>0",
767 [],
768 |row| {
769 let chat_id: ChatId = row.get(0)?;
770 let locations_send_begin: i64 = row.get(1)?;
771 let locations_send_until: i64 = row.get(2)?;
772 let locations_last_sent: i64 = row.get(3)?;
773 Ok((
774 chat_id,
775 locations_send_begin,
776 locations_send_until,
777 locations_last_sent,
778 ))
779 },
780 )
781 .await
782 .context("failed to query location streaming chats")?;
783
784 for (chat_id, locations_send_begin, locations_send_until, locations_last_sent) in rows {
785 if locations_send_begin > 0 && locations_send_until > now {
786 let can_send = now > locations_last_sent + 60;
787 let has_locations = context
788 .sql
789 .exists(
790 "SELECT COUNT(id) \
791 FROM locations \
792 WHERE from_id=? \
793 AND timestamp>=? \
794 AND timestamp>? \
795 AND independent=0",
796 (ContactId::SELF, locations_send_begin, locations_last_sent),
797 )
798 .await?;
799
800 next_event = next_event
801 .into_iter()
802 .chain(u64::try_from(locations_send_until - now))
803 .min();
804
805 if has_locations {
806 if can_send {
807 info!(
811 context,
812 "Chat {} has pending locations, sending them.", chat_id
813 );
814 let mut msg = Message::new(Viewtype::Text);
815 msg.hidden = true;
816 msg.param.set_cmd(SystemMessage::LocationOnly);
817 chat::send_msg(context, chat_id, &mut msg).await?;
818 } else {
819 info!(
821 context,
822 "Chat {} has pending locations, but they can't be sent yet.", chat_id
823 );
824 next_event = next_event
825 .into_iter()
826 .chain(u64::try_from(locations_last_sent + 61 - now))
827 .min();
828 }
829 } else {
830 info!(
831 context,
832 "Chat {} has location streaming enabled, but no pending locations.", chat_id
833 );
834 }
835 } else {
836 info!(
839 context,
840 "Disabling location streaming for chat {}.", chat_id
841 );
842 context
843 .sql
844 .execute(
845 "UPDATE chats \
846 SET locations_send_begin=0, locations_send_until=0 \
847 WHERE id=?",
848 (chat_id,),
849 )
850 .await
851 .context("failed to disable location streaming")?;
852
853 let stock_str = stock_str::msg_location_disabled(context);
854 chat::add_info_msg(context, chat_id, &stock_str).await?;
855 context.emit_event(EventType::ChatModified(chat_id));
856 chatlist_events::emit_chatlist_item_changed(context, chat_id);
857 }
858 }
859
860 Ok(next_event)
861}
862
863#[cfg(test)]
864mod tests {
865 use super::*;
866 use crate::config::Config;
867 use crate::message::MessageState;
868 use crate::receive_imf::receive_imf;
869 use crate::test_utils;
870 use crate::test_utils::{ExpectedEvents, TestContext, TestContextManager};
871 use crate::tools::SystemTime;
872
873 #[test]
874 fn test_kml_parse() {
875 let xml =
876 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>";
877
878 let kml = Kml::parse(xml).expect("parsing failed");
879
880 assert!(kml.addr.is_some());
881 assert_eq!(kml.addr.as_ref().unwrap(), "user@example.org",);
882
883 let locations_ref = &kml.locations;
884 assert_eq!(locations_ref.len(), 2);
885
886 assert!(locations_ref[0].latitude > 53.6f64);
887 assert!(locations_ref[0].latitude < 53.8f64);
888 assert!(locations_ref[0].longitude > 9.3f64);
889 assert!(locations_ref[0].longitude < 9.5f64);
890 assert!(locations_ref[0].accuracy > 31.9f64);
891 assert!(locations_ref[0].accuracy < 32.1f64);
892 assert_eq!(locations_ref[0].timestamp, 1551906597);
893
894 assert!(locations_ref[1].latitude > 63.6f64);
895 assert!(locations_ref[1].latitude < 63.8f64);
896 assert!(locations_ref[1].longitude > 19.3f64);
897 assert!(locations_ref[1].longitude < 19.5f64);
898 assert!(locations_ref[1].accuracy > 2.4f64);
899 assert!(locations_ref[1].accuracy < 2.6f64);
900 assert_eq!(locations_ref[1].timestamp, 1544739072);
901 }
902
903 #[test]
904 fn test_kml_parse_error() {
905 let xml = b"<?><xmlversi\"\"\">?</document>";
906 assert!(Kml::parse(xml).is_err());
907 }
908
909 #[test]
910 fn test_get_message_kml() {
911 let timestamp = 1598490000;
912
913 let xml = get_message_kml(timestamp, 51.423723f64, 8.552556f64);
914 let kml = Kml::parse(xml.as_bytes()).expect("parsing failed");
915 let locations_ref = &kml.locations;
916 assert_eq!(locations_ref.len(), 1);
917
918 assert!(locations_ref[0].latitude >= 51.423723f64);
919 assert!(locations_ref[0].latitude < 51.423724f64);
920 assert!(locations_ref[0].longitude >= 8.552556f64);
921 assert!(locations_ref[0].longitude < 8.552557f64);
922 assert!(locations_ref[0].accuracy.abs() < f64::EPSILON);
923 assert_eq!(locations_ref[0].timestamp, timestamp);
924 }
925
926 #[test]
927 fn test_is_marker() {
928 assert!(is_marker("f"));
929 assert!(!is_marker("foo"));
930 assert!(is_marker("🏠"));
931 assert!(!is_marker(" "));
932 assert!(!is_marker("\t"));
933 }
934
935 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
937 async fn receive_location_kml() -> Result<()> {
938 let mut tcm = TestContextManager::new();
939 let alice = &tcm.alice().await;
940 let bob = &tcm.bob().await;
941
942 let encrypted_message = test_utils::encrypt_raw_message(
943 bob,
944 &[alice],
945 br#"Subject: Hello
946Message-ID: <hello@example.net>
947To: Alice <alice@example.org>
948From: Bob <bob@example.net>
949Date: Mon, 20 Dec 2021 00:00:00 +0000
950Chat-Version: 1.0
951Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
952
953Text message."#,
954 )
955 .await?;
956 receive_imf(alice, encrypted_message.as_bytes(), false).await?;
957 let received_msg = alice.get_last_msg().await;
958 assert_eq!(received_msg.text, "Text message.");
959
960 let encrypted_message = test_utils::encrypt_raw_message(
961 bob,
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--"#).await?;
990 receive_imf(alice, encrypted_message.as_bytes(), false).await?;
991
992 let received_msg2 = alice.get_last_msg().await;
994 assert_eq!(received_msg2.id, received_msg.id);
995
996 let locations = get_range(alice, None, None, 0, 0).await?;
997 assert_eq!(locations.len(), 1);
998 Ok(())
999 }
1000
1001 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1003 async fn receive_visible_location_kml() -> Result<()> {
1004 let mut tcm = TestContextManager::new();
1005 let alice = &tcm.alice().await;
1006 let bob = &tcm.bob().await;
1007
1008 let encrypted_message = test_utils::encrypt_raw_message(
1009 bob,
1010 &[alice],
1011 br#"Subject: locations
1012MIME-Version: 1.0
1013To: <alice@example.org>
1014From: <bob@example.net>
1015Date: Tue, 21 Dec 2021 00:00:00 +0000
1016Chat-Version: 1.0
1017Message-ID: <foobar@localhost>
1018Content-Type: multipart/mixed; boundary="U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF"
1019
1020
1021--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF
1022Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
1023
1024Text message.
1025
1026
1027--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF
1028Content-Type: application/vnd.google-earth.kml+xml
1029Content-Disposition: attachment; filename="location.kml"
1030
1031<?xml version="1.0" encoding="UTF-8"?>
1032<kml xmlns="http://www.opengis.net/kml/2.2">
1033<Document addr="bob@example.net">
1034<Placemark><Timestamp><when>2021-11-21T00:00:00Z</when></Timestamp><Point><coordinates accuracy="1.0000000000000000">10.00000000000000,20.00000000000000</coordinates></Point></Placemark>
1035</Document>
1036</kml>
1037
1038--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF--"#).await?;
1039
1040 receive_imf(alice, encrypted_message.as_bytes(), false).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
1159 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1160 async fn test_last_sent_location_timestamp() -> Result<()> {
1161 let mut tcm = TestContextManager::new();
1162 let alice = &tcm.alice().await;
1163 let bob = &tcm.bob().await;
1164
1165 let alice_chat = alice.create_chat(bob).await;
1166
1167 send_to_chat(alice, alice_chat.id, 600).await?;
1168 bob.recv_msg(&alice.pop_sent_msg().await).await;
1169
1170 assert_eq!(set(alice, 10.0, 20.0, 1.0).await?, true);
1171
1172 SystemTime::shift(Duration::from_secs(60));
1173
1174 maybe_send(alice).await?;
1175 bob.recv_msg_opt(&alice.pop_sent_msg().await).await;
1176
1177 let alice_locations = get_range(alice, None, None, 0, 0).await?;
1178 assert_eq!(alice_locations.len(), 1);
1179 assert_eq!(get_range(bob, None, None, 0, 0).await?.len(), 1);
1180
1181 let last_sent = alice
1182 .sql
1183 .query_row(
1184 "SELECT locations_last_sent FROM chats WHERE id = ?",
1185 (alice_chat.id,),
1186 |row| {
1187 let last_sent: i64 = row.get(0)?;
1188
1189 Ok(last_sent)
1190 },
1191 )
1192 .await?;
1193
1194 assert_eq!(alice_locations[0].timestamp, last_sent);
1195
1196 Ok(())
1197 }
1198}