Skip to main content

deltachat/
location.rs

1//! Location handling.
2//!
3//! Delta Chat handles two kind of locations.
4//!
5//! There are two kinds of locations:
6//! - Independent locations, also known as Points of Interest (POI).
7//! - Path locations.
8//!
9//! Locations are sent as KML attachments.
10//! Independent locations are sent in `message.kml` attachments
11//! and path locations are sent in `location.kml` attachments.
12
13use 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/// Location record.
33#[derive(Debug, Clone, Default)]
34pub struct Location {
35    /// Row ID of the location.
36    pub location_id: u32,
37
38    /// Location latitude.
39    pub latitude: f64,
40
41    /// Location longitude.
42    pub longitude: f64,
43
44    /// Nonstandard `accuracy` attribute of the `coordinates` tag.
45    pub accuracy: f64,
46
47    /// Location timestamp in seconds.
48    pub timestamp: i64,
49
50    /// Contact ID.
51    pub contact_id: ContactId,
52
53    /// Message ID.
54    pub msg_id: u32,
55
56    /// Chat ID.
57    pub chat_id: ChatId,
58
59    /// A marker string, such as an emoji, to be displayed on top of the location.
60    pub marker: Option<String>,
61
62    /// Whether location is independent, i.e. not part of the path.
63    pub independent: u32,
64}
65
66impl Location {
67    /// Creates a new empty location.
68    pub fn new() -> Self {
69        Default::default()
70    }
71}
72
73/// KML document.
74///
75/// See <https://www.ogc.org/standards/kml/> for the standard and
76/// <https://developers.google.com/kml> for documentation.
77#[derive(Debug, Clone, Default)]
78pub struct Kml {
79    /// Nonstandard `addr` attribute of the `Document` tag storing the user email address.
80    pub addr: Option<String>,
81
82    /// Placemarks.
83    pub locations: Vec<Location>,
84
85    /// Currently parsed XML tag.
86    tag: KmlTag,
87
88    /// Currently parsed placemark.
89    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    /// Creates a new empty KML document.
105    pub fn new() -> Self {
106        Default::default()
107    }
108
109    /// Parses a KML document.
110    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                // YYYY-MM-DDTHH:MM:SSZ
151                // 0   4  7  10 13 16 19
152                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/// Enables location streaming in chat identified by `chat_id` for `seconds` seconds.
268#[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
306/// Returns whether any chat is sending locations.
307pub 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
317/// Returns whether `chat_id` is sending locations.
318pub 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
328/// Returns a list of chats in which location streaming is enabled.
329async 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
343/// Stop sending locations in all chats.
344pub 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
351/// Sets current location of the user device.
352pub 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        // Interrupt location loop so it may send a location-only message.
394        context.scheduler.interrupt_location().await;
395    }
396
397    Ok(continue_streaming)
398}
399
400/// Searches for locations in the given time range, optionally filtering by chat and contact IDs.
401#[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)), // this ChatId is unused
416    };
417    let (disable_contact_id, contact_id) = match contact_id {
418        Some(contact_id) => (0, contact_id),
419        None => (1, 0), // this contact_id is unused
420    };
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
474/// Deletes expired locations.
475///
476/// Only path locations are deleted.
477/// POIs should be deleted when corresponding message is deleted.
478pub(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
499/// Deletes location if it is an independent location.
500///
501/// This function is used when a message is deleted
502/// that has a corresponding `location_id`.
503pub(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
514/// Deletes POI locations that don't have corresponding message anymore.
515pub(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
526/// Returns `location.kml` contents and the largest location timestamp, if any.
527pub 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    // Returns a string formatted as YYYY-MM-DDTHH:MM:SSZ. The trailing `Z` indicates UTC.
600    chrono::DateTime::<chrono::Utc>::from_timestamp(utc, 0)
601        .unwrap()
602        .format("%Y-%m-%dT%H:%M:%SZ")
603        .to_string()
604}
605
606/// Returns a KML document containing a single location with the given timestamp and coordinates.
607pub 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
624/// Sets the timestamp of the last time location was sent in the chat.
625pub 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
640/// Sets the location of the message.
641pub 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
653/// Saves given locations to the database.
654///
655/// Returns the database row ID of the location with the highest timestamp.
656pub(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) // Retry one minute later.
721            }
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/// Returns number of seconds until the next time location streaming for some chat ends
755/// automatically.
756#[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                    // Send location-only message.
808                    // Pending locations are attached automatically to every message,
809                    // so also to this empty text message.
810                    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                    // Wait until pending locations can be sent.
820                    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            // Location streaming was either explicitly disabled (locations_send_begin = 0) or
837            // locations_send_until is in the past.
838            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    /// Tests that location.kml is hidden.
936    #[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        // Received location message is not visible, last message stays the same.
993        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    /// Tests that `location.kml` is not hidden and not seen if it contains a message.
1002    #[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        // Send image without text.
1066        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 enables deletion of messages from device after 1 week.
1098        alice
1099            .set_config(Config::DeleteDeviceAfter, Some("604800"))
1100            .await?;
1101        // Bob enables deletion of messages from device after 1 day.
1102        bob.set_config(Config::DeleteDeviceAfter, Some("86400"))
1103            .await?;
1104
1105        let alice_chat = alice.create_chat(bob).await;
1106        // Bob needs the chat accepted so that "normal" messages from Alice trigger `IncomingMsg`.
1107        // Location-only messages still must trigger `MsgsChanged`.
1108        bob.create_chat(alice).await;
1109
1110        // Alice enables location streaming.
1111        // Bob receives a message saying that Alice enabled location streaming.
1112        send_to_chat(alice, alice_chat.id, 60).await?;
1113        bob.recv_msg(&alice.pop_sent_msg().await).await;
1114
1115        // Alice gets new location from GPS.
1116        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        // 10 seconds later location sending stream manages to send location.
1120        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        // Location-only messages are "auto-generated", but they mustn't make the contact a bot.
1139        let contact = bob.add_or_lookup_contact(alice).await;
1140        assert!(!contact.is_bot());
1141
1142        // Day later Bob removes location.
1143        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        // Week late Alice removes location.
1150        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}