//! Location handling use bitflags::bitflags; use quick_xml; use quick_xml::events::{BytesEnd, BytesStart, BytesText}; use crate::chat; use crate::config::Config; use crate::constants::*; use crate::context::*; use crate::dc_mimeparser::SystemMessage; use crate::dc_tools::*; use crate::error::Error; use crate::events::Event; use crate::job::*; use crate::message::{Message, MsgId}; use crate::param::*; use crate::sql; use crate::stock::StockMessage; /// Location record #[derive(Debug, Clone, Default)] pub struct Location { pub location_id: u32, pub latitude: f64, pub longitude: f64, pub accuracy: f64, pub timestamp: i64, pub contact_id: u32, pub msg_id: u32, pub chat_id: u32, pub marker: Option, pub independent: u32, } impl Location { pub fn new() -> Self { Default::default() } } #[derive(Debug, Clone, Default)] pub struct Kml { pub addr: Option, pub locations: Vec, tag: KmlTag, pub curr: Location, } bitflags! { #[derive(Default)] struct KmlTag: i32 { const UNDEFINED = 0x00; const PLACEMARK = 0x01; const TIMESTAMP = 0x02; const WHEN = 0x04; const POINT = 0x08; const COORDINATES = 0x10; } } impl Kml { pub fn new() -> Self { Default::default() } pub fn parse(context: &Context, content: &[u8]) -> Result { ensure!(content.len() <= 1024 * 1024, "kml-file is too large"); let to_parse = String::from_utf8_lossy(content); let mut reader = quick_xml::Reader::from_str(&to_parse); reader.trim_text(true); let mut kml = Kml::new(); kml.locations = Vec::with_capacity(100); let mut buf = Vec::new(); loop { match reader.read_event(&mut buf) { Ok(quick_xml::events::Event::Start(ref e)) => kml.starttag_cb(e, &reader), Ok(quick_xml::events::Event::End(ref e)) => kml.endtag_cb(e), Ok(quick_xml::events::Event::Text(ref e)) => kml.text_cb(e, &reader), Err(e) => { error!( context, "Location parsing: Error at position {}: {:?}", reader.buffer_position(), e ); } Ok(quick_xml::events::Event::Eof) => break, _ => (), } buf.clear(); } Ok(kml) } fn text_cb(&mut self, event: &BytesText, reader: &quick_xml::Reader) { if self.tag.contains(KmlTag::WHEN) || self.tag.contains(KmlTag::COORDINATES) { let val = event.unescape_and_decode(reader).unwrap_or_default(); let val = val .replace("\n", "") .replace("\r", "") .replace("\t", "") .replace(" ", ""); if self.tag.contains(KmlTag::WHEN) && val.len() >= 19 { // YYYY-MM-DDTHH:MM:SSZ // 0 4 7 10 13 16 19 match chrono::NaiveDateTime::parse_from_str(&val, "%Y-%m-%dT%H:%M:%SZ") { Ok(res) => { self.curr.timestamp = res.timestamp(); if self.curr.timestamp > time() { self.curr.timestamp = time(); } } Err(_err) => { self.curr.timestamp = time(); } } } else if self.tag.contains(KmlTag::COORDINATES) { let parts = val.splitn(2, ',').collect::>(); if parts.len() == 2 { self.curr.longitude = parts[0].parse().unwrap_or_default(); self.curr.latitude = parts[1].parse().unwrap_or_default(); } } } } fn endtag_cb(&mut self, event: &BytesEnd) { let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); if tag == "placemark" { if self.tag.contains(KmlTag::PLACEMARK) && 0 != self.curr.timestamp && 0. != self.curr.latitude && 0. != self.curr.longitude { self.locations .push(std::mem::replace(&mut self.curr, Location::new())); } self.tag = KmlTag::UNDEFINED; }; } fn starttag_cb( &mut self, event: &BytesStart, reader: &quick_xml::Reader, ) { let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); if tag == "document" { if let Some(addr) = event.attributes().find(|attr| { attr.as_ref() .map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "addr") .unwrap_or_default() }) { self.addr = addr.unwrap().unescape_and_decode_value(reader).ok(); } } else if tag == "placemark" { self.tag = KmlTag::PLACEMARK; self.curr.timestamp = 0; self.curr.latitude = 0.0; self.curr.longitude = 0.0; self.curr.accuracy = 0.0 } else if tag == "timestamp" && self.tag.contains(KmlTag::PLACEMARK) { self.tag = KmlTag::PLACEMARK | KmlTag::TIMESTAMP } else if tag == "when" && self.tag.contains(KmlTag::TIMESTAMP) { self.tag = KmlTag::PLACEMARK | KmlTag::TIMESTAMP | KmlTag::WHEN } else if tag == "point" && self.tag.contains(KmlTag::PLACEMARK) { self.tag = KmlTag::PLACEMARK | KmlTag::POINT } else if tag == "coordinates" && self.tag.contains(KmlTag::POINT) { self.tag = KmlTag::PLACEMARK | KmlTag::POINT | KmlTag::COORDINATES; if let Some(acc) = event.attributes().find(|attr| { attr.as_ref() .map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "accuracy") .unwrap_or_default() }) { let v = acc .unwrap() .unescape_and_decode_value(reader) .unwrap_or_default(); self.curr.accuracy = v.trim().parse().unwrap_or_default(); } } } } // location streaming pub fn send_locations_to_chat(context: &Context, chat_id: u32, seconds: i64) { let now = time(); if !(seconds < 0 || chat_id <= DC_CHAT_ID_LAST_SPECIAL) { let is_sending_locations_before = is_sending_locations_to_chat(context, chat_id); if sql::execute( context, &context.sql, "UPDATE chats \ SET locations_send_begin=?, \ locations_send_until=? \ WHERE id=?", params![ if 0 != seconds { now } else { 0 }, if 0 != seconds { now + seconds } else { 0 }, chat_id as i32, ], ) .is_ok() { if 0 != seconds && !is_sending_locations_before { let mut msg = Message::new(Viewtype::Text); msg.text = Some(context.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0)); msg.param.set_cmd(SystemMessage::LocationStreamingEnabled); chat::send_msg(context, chat_id, &mut msg).unwrap_or_default(); } else if 0 == seconds && is_sending_locations_before { let stock_str = context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0); chat::add_info_msg(context, chat_id, stock_str); } context.call_cb(Event::ChatModified(chat_id)); if 0 != seconds { schedule_MAYBE_SEND_LOCATIONS(context, false); job_add( context, Action::MaybeSendLocationsEnded, chat_id as libc::c_int, Params::new(), seconds + 1, ); } } } } #[allow(non_snake_case)] fn schedule_MAYBE_SEND_LOCATIONS(context: &Context, force_schedule: bool) { if force_schedule || !job_action_exists(context, Action::MaybeSendLocations) { job_add(context, Action::MaybeSendLocations, 0, Params::new(), 60); }; } pub fn is_sending_locations_to_chat(context: &Context, chat_id: u32) -> bool { context .sql .exists( "SELECT id FROM chats WHERE (? OR id=?) AND locations_send_until>?;", params![if chat_id == 0 { 1 } else { 0 }, chat_id as i32, time()], ) .unwrap_or_default() } pub fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool { if latitude == 0.0 && longitude == 0.0 { return true; } let mut continue_streaming = false; if let Ok(chats) = context.sql.query_map( "SELECT id FROM chats WHERE locations_send_until>?;", params![time()], |row| row.get::<_, i32>(0), |chats| chats.collect::, _>>().map_err(Into::into), ) { for chat_id in chats { if let Err(err) = context.sql.execute( "INSERT INTO locations \ (latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);", params![ latitude, longitude, accuracy, time(), chat_id, DC_CONTACT_ID_SELF, ] ) { warn!(context, "failed to store location {:?}", err); } else { continue_streaming = true; } } if continue_streaming { context.call_cb(Event::LocationChanged(Some(DC_CONTACT_ID_SELF))); }; schedule_MAYBE_SEND_LOCATIONS(context, false); } continue_streaming } pub fn get_range( context: &Context, chat_id: u32, contact_id: u32, timestamp_from: i64, mut timestamp_to: i64, ) -> Vec { if timestamp_to == 0 { timestamp_to = time() + 10; } context .sql .query_map( "SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \ COALESCE(m.id, 0) AS msg_id, l.from_id, l.chat_id, COALESCE(m.txt, '') AS txt \ FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \ AND (? OR l.from_id=?) \ AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \ ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;", params![ if chat_id == 0 { 1 } else { 0 }, chat_id as i32, if contact_id == 0 { 1 } else { 0 }, contact_id as i32, timestamp_from, timestamp_to, ], |row| { let msg_id = row.get(6)?; let txt: String = row.get(9)?; let marker = if msg_id != 0 && is_marker(&txt) { Some(txt) } else { None }; let loc = Location { location_id: row.get(0)?, latitude: row.get(1)?, longitude: row.get(2)?, accuracy: row.get(3)?, timestamp: row.get(4)?, independent: row.get(5)?, msg_id, contact_id: row.get(7)?, chat_id: row.get(8)?, marker, }; Ok(loc) }, |locations| { let mut ret = Vec::new(); for location in locations { ret.push(location?); } Ok(ret) }, ) .unwrap_or_default() } fn is_marker(txt: &str) -> bool { txt.len() == 1 && !txt.starts_with(' ') } pub fn delete_all(context: &Context) -> Result<(), Error> { sql::execute(context, &context.sql, "DELETE FROM locations;", params![])?; context.call_cb(Event::LocationChanged(None)); Ok(()) } pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error> { let mut last_added_location_id = 0; let self_addr = context .get_config(Config::ConfiguredAddr) .unwrap_or_default(); let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row( "SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;", params![chat_id as i32], |row| { let send_begin: i64 = row.get(0)?; let send_until: i64 = row.get(1)?; let last_sent: i64 = row.get(2)?; Ok((send_begin, send_until, last_sent)) })?; let now = time(); let mut location_count = 0; let mut ret = String::new(); if locations_send_begin != 0 && now <= locations_send_until { ret += &format!( "\n\n\n", self_addr, ); context.sql.query_map( "SELECT id, latitude, longitude, accuracy, timestamp \ FROM locations WHERE from_id=? \ AND timestamp>=? \ AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \ AND independent=0 \ GROUP BY timestamp \ ORDER BY timestamp;", params![DC_CONTACT_ID_SELF, locations_send_begin, locations_last_sent, DC_CONTACT_ID_SELF], |row| { let location_id: i32 = row.get(0)?; let latitude: f64 = row.get(1)?; let longitude: f64 = row.get(2)?; let accuracy: f64 = row.get(3)?; let timestamp = get_kml_timestamp(row.get(4)?); Ok((location_id, latitude, longitude, accuracy, timestamp)) }, |rows| { for row in rows { let (location_id, latitude, longitude, accuracy, timestamp) = row?; ret += &format!( "{}{},{}\n", timestamp, accuracy, longitude, latitude ); location_count += 1; last_added_location_id = location_id as u32; } Ok(()) } )?; ret += "\n"; } ensure!(location_count > 0, "No locations processed"); Ok((ret, last_added_location_id)) } fn get_kml_timestamp(utc: i64) -> String { // Returns a string formatted as YYYY-MM-DDTHH:MM:SSZ. The trailing `Z` indicates UTC. chrono::NaiveDateTime::from_timestamp(utc, 0) .format("%Y-%m-%dT%H:%M:%SZ") .to_string() } pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String { format!( "\n\ \n\ \n\ \ {}\ {:.2},{:.2}\ \n\ \n\ ", get_kml_timestamp(timestamp), longitude, latitude, ) } pub fn set_kml_sent_timestamp( context: &Context, chat_id: u32, timestamp: i64, ) -> Result<(), Error> { sql::execute( context, &context.sql, "UPDATE chats SET locations_last_sent=? WHERE id=?;", params![timestamp, chat_id as i32], )?; Ok(()) } pub fn set_msg_location_id( context: &Context, msg_id: MsgId, location_id: u32, ) -> Result<(), Error> { sql::execute( context, &context.sql, "UPDATE msgs SET location_id=? WHERE id=?;", params![location_id, msg_id], )?; Ok(()) } pub fn save( context: &Context, chat_id: u32, contact_id: u32, locations: &[Location], independent: bool, ) -> Result { ensure!(chat_id > DC_CHAT_ID_LAST_SPECIAL, "Invalid chat id"); context.sql.prepare2( "SELECT id FROM locations WHERE timestamp=? AND from_id=?", "INSERT INTO locations\ (timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \ VALUES (?,?,?,?,?,?,?);", |mut stmt_test, mut stmt_insert, conn| { let mut newest_timestamp = 0; let mut newest_location_id = 0; for location in locations { let exists = stmt_test.exists(params![location.timestamp, contact_id as i32])?; if independent || !exists { stmt_insert.execute(params![ location.timestamp, contact_id as i32, chat_id as i32, location.latitude, location.longitude, location.accuracy, independent, ])?; if location.timestamp > newest_timestamp { newest_timestamp = location.timestamp; newest_location_id = sql::get_rowid2_with_conn( context, conn, "locations", "timestamp", location.timestamp, "from_id", contact_id as i32, ); } } } Ok(newest_location_id) }, ) } #[allow(non_snake_case)] pub fn job_do_DC_JOB_MAYBE_SEND_LOCATIONS(context: &Context, _job: &Job) { let now = time(); let mut continue_streaming = false; info!( context, " ----------------- MAYBE_SEND_LOCATIONS -------------- ", ); if let Ok(rows) = context.sql.query_map( "SELECT id, locations_send_begin, locations_last_sent \ FROM chats \ WHERE locations_send_until>?;", params![now], |row| { let chat_id: i32 = row.get(0)?; let locations_send_begin: i64 = row.get(1)?; let locations_last_sent: i64 = row.get(2)?; continue_streaming = true; // be a bit tolerant as the timer may not align exactly with time(NULL) if now - locations_last_sent < (60 - 3) { Ok(None) } else { Ok(Some((chat_id, locations_send_begin, locations_last_sent))) } }, |rows| { rows.filter_map(|v| v.transpose()) .collect::, _>>() .map_err(Into::into) }, ) { let msgs = context .sql .prepare( "SELECT id \ FROM locations \ WHERE from_id=? \ AND timestamp>=? \ AND timestamp>? \ AND independent=0 \ ORDER BY timestamp;", |mut stmt_locations, _| { let msgs = rows .into_iter() .filter_map(|(chat_id, locations_send_begin, locations_last_sent)| { if !stmt_locations .exists(params![ DC_CONTACT_ID_SELF, locations_send_begin, locations_last_sent, ]) .unwrap_or_default() { // if there is no new location, there's nothing to send. // however, maybe we want to bypass this test eg. 15 minutes None } else { // pending locations are attached automatically to every message, // so also to this empty text message. // DC_CMD_LOCATION is only needed to create a nicer subject. // // for optimisation and to avoid flooding the sending queue, // we could sending these messages only if we're really online. // the easiest way to determine this, is to check for an empty message queue. // (might not be 100%, however, as positions are sent combined later // and dc_set_location() is typically called periodically, this is ok) let mut msg = Message::new(Viewtype::Text); msg.hidden = true; msg.param.set_cmd(SystemMessage::LocationOnly); Some((chat_id, msg)) } }) .collect::>(); Ok(msgs) }, ) .unwrap_or_default(); // TODO: Better error handling for (chat_id, mut msg) in msgs.into_iter() { // TODO: better error handling chat::send_msg(context, chat_id as u32, &mut msg).unwrap_or_default(); } } if continue_streaming { schedule_MAYBE_SEND_LOCATIONS(context, true); } } #[allow(non_snake_case)] pub fn job_do_DC_JOB_MAYBE_SEND_LOC_ENDED(context: &Context, job: &mut Job) { // this function is called when location-streaming _might_ have ended for a chat. // the function checks, if location-streaming is really ended; // if so, a device-message is added if not yet done. let chat_id = job.foreign_id; if let Ok((send_begin, send_until)) = context.sql.query_row( "SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?", params![chat_id as i32], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)), ) { if !(send_begin != 0 && time() <= send_until) { // still streaming - // may happen as several calls to dc_send_locations_to_chat() // do not un-schedule pending DC_MAYBE_SEND_LOC_ENDED jobs if !(send_begin == 0 && send_until == 0) { // not streaming, device-message already sent if context.sql.execute( "UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?", params![chat_id as i32], ).is_ok() { let stock_str = context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0); chat::add_info_msg(context, chat_id, stock_str); context.call_cb(Event::ChatModified(chat_id)); } } } } } #[cfg(test)] mod tests { use super::*; use crate::test_utils::dummy_context; #[test] fn test_kml_parse() { let context = dummy_context(); let xml = b"\n\n\n2019-03-06T21:09:57Z9.423110,53.790302\n\n \n\t2018-12-13T22:11:12Z\t 19.423110 \t , \n 63.790302\n \n\n"; let kml = Kml::parse(&context.ctx, xml).expect("parsing failed"); assert!(kml.addr.is_some()); assert_eq!(kml.addr.as_ref().unwrap(), "user@example.org",); let locations_ref = &kml.locations; assert_eq!(locations_ref.len(), 2); assert!(locations_ref[0].latitude > 53.6f64); assert!(locations_ref[0].latitude < 53.8f64); assert!(locations_ref[0].longitude > 9.3f64); assert!(locations_ref[0].longitude < 9.5f64); assert!(locations_ref[0].accuracy > 31.9f64); assert!(locations_ref[0].accuracy < 32.1f64); assert_eq!(locations_ref[0].timestamp, 1551906597); assert!(locations_ref[1].latitude > 63.6f64); assert!(locations_ref[1].latitude < 63.8f64); assert!(locations_ref[1].longitude > 19.3f64); assert!(locations_ref[1].longitude < 19.5f64); assert!(locations_ref[1].accuracy > 2.4f64); assert!(locations_ref[1].accuracy < 2.6f64); assert_eq!(locations_ref[1].timestamp, 1544739072); } }