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