diff --git a/Cargo.lock b/Cargo.lock index 189c64ea4..10a2d8490 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -463,6 +463,7 @@ version = "1.0.0-alpha.4" dependencies = [ "backtrace 0.3.34 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "cc 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", "charset 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index d5204145c..825081cd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ itertools = "0.8.0" image-meta = "0.1.0" quick-xml = "0.15.0" escaper = "0.1.0" +bitflags = "1.1.0" [dev-dependencies] tempfile = "3.0" diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 24ec368d5..a5eb672bc 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -1043,7 +1043,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat( assert!(!context.is_null()); let context = &*context; - dc_location::dc_send_locations_to_chat(context, chat_id, seconds as i64) + location::send_locations_to_chat(context, chat_id, seconds as i64) } #[no_mangle] @@ -1054,7 +1054,7 @@ pub unsafe extern "C" fn dc_is_sending_locations_to_chat( assert!(!context.is_null()); let context = &*context; - dc_location::dc_is_sending_locations_to_chat(context, chat_id) as libc::c_int + location::is_sending_locations_to_chat(context, chat_id) as libc::c_int } #[no_mangle] @@ -1067,7 +1067,7 @@ pub unsafe extern "C" fn dc_set_location( assert!(!context.is_null()); let context = &*context; - dc_location::dc_set_location(context, latitude, longitude, accuracy) + location::set(context, latitude, longitude, accuracy) } #[no_mangle] @@ -1081,7 +1081,7 @@ pub unsafe extern "C" fn dc_get_locations( assert!(!context.is_null()); let context = &*context; - let res = dc_location::dc_get_locations( + let res = location::get_range( context, chat_id, contact_id, @@ -1096,7 +1096,7 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) { assert!(!context.is_null()); let context = &*context; - dc_location::dc_delete_all_locations(context); + location::delete_all(context).log_err(context, "Failed to delete locations"); } // dc_array_t diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index cbb06289b..170870881 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -9,11 +9,11 @@ use deltachat::constants::*; use deltachat::contact::*; use deltachat::context::*; use deltachat::dc_imex::*; -use deltachat::dc_location::*; use deltachat::dc_receive_imf::*; use deltachat::dc_tools::*; use deltachat::error::Error; use deltachat::job::*; +use deltachat::location; use deltachat::lot::LotState; use deltachat::message::*; use deltachat::peerstate::*; @@ -652,7 +652,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E ); } } - if dc_is_sending_locations_to_chat(context, 0 as uint32_t) { + if location::is_sending_locations_to_chat(context, 0 as uint32_t) { info!(context, 0, "Location streaming enabled."); } println!("{} chats", cnt); @@ -778,14 +778,17 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E println!( "{} contacts\nLocation streaming: {}", contacts.len(), - dc_is_sending_locations_to_chat(context, sel_chat.as_ref().unwrap().get_id()), + location::is_sending_locations_to_chat( + context, + sel_chat.as_ref().unwrap().get_id() + ), ); } "getlocations" => { ensure!(sel_chat.is_some(), "No chat selected."); let contact_id = arg1.parse().unwrap_or_default(); - let locations = dc_get_locations( + let locations = location::get_range( context, sel_chat.as_ref().unwrap().get_id(), contact_id, @@ -819,7 +822,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E ensure!(!arg1.is_empty(), "No timeout given."); let seconds = arg1.parse()?; - dc_send_locations_to_chat(context, sel_chat.as_ref().unwrap().get_id(), seconds); + location::send_locations_to_chat(context, sel_chat.as_ref().unwrap().get_id(), seconds); println!( "Locations will be sent to Chat#{} for {} seconds. Use 'setlocation ' to play around.", sel_chat.as_ref().unwrap().get_id(), @@ -834,7 +837,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E let latitude = arg1.parse()?; let longitude = arg2.parse()?; - let continue_streaming = dc_set_location(context, latitude, longitude, 0.); + let continue_streaming = location::set(context, latitude, longitude, 0.); if 0 != continue_streaming { println!("Success, streaming should be continued."); } else { @@ -842,7 +845,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E } } "dellocations" => { - dc_delete_all_locations(context); + location::delete_all(context)?; } "send" => { ensure!(sel_chat.is_some(), "No chat selected."); diff --git a/src/dc_array.rs b/src/dc_array.rs index a764b6cf6..c3e2dcbe0 100644 --- a/src/dc_array.rs +++ b/src/dc_array.rs @@ -1,11 +1,11 @@ -use crate::dc_location::dc_location; +use crate::location::Location; use crate::types::*; /* * the structure behind dc_array_t */ #[derive(Clone)] #[allow(non_camel_case_types)] pub enum dc_array_t { - Locations(Vec), + Locations(Vec), Uint(Vec), } @@ -27,7 +27,7 @@ impl dc_array_t { } } - pub fn add_location(&mut self, location: dc_location) { + pub fn add_location(&mut self, location: Location) { if let Self::Locations(array) = self { array.push(location) } else { @@ -42,7 +42,7 @@ impl dc_array_t { } } - pub fn get_location(&self, index: usize) -> &dc_location { + pub fn get_location(&self, index: usize) -> &Location { if let Self::Locations(array) = self { &array[index] } else { @@ -108,8 +108,8 @@ impl From> for dc_array_t { } } -impl From> for dc_array_t { - fn from(array: Vec) -> Self { +impl From> for dc_array_t { + fn from(array: Vec) -> Self { dc_array_t::Locations(array) } } diff --git a/src/dc_location.rs b/src/dc_location.rs deleted file mode 100644 index fd1198feb..000000000 --- a/src/dc_location.rs +++ /dev/null @@ -1,776 +0,0 @@ -use quick_xml; -use quick_xml::events::{BytesEnd, BytesStart, BytesText}; - -use crate::chat; -use crate::constants::Event; -use crate::constants::*; -use crate::context::*; -use crate::dc_tools::*; -use crate::job::*; -use crate::message::*; -use crate::param::*; -use crate::sql; -use crate::stock::StockMessage; -use crate::types::*; -use crate::x::*; - -// location handling -#[derive(Clone, Default)] -#[allow(non_camel_case_types)] -pub struct dc_location { - pub location_id: uint32_t, - pub latitude: libc::c_double, - pub longitude: libc::c_double, - pub accuracy: libc::c_double, - pub timestamp: i64, - pub contact_id: uint32_t, - pub msg_id: uint32_t, - pub chat_id: uint32_t, - pub marker: Option, - pub independent: uint32_t, -} - -impl dc_location { - pub fn new() -> Self { - dc_location { - location_id: 0, - latitude: 0.0, - longitude: 0.0, - accuracy: 0.0, - timestamp: 0, - contact_id: 0, - msg_id: 0, - chat_id: 0, - marker: None, - independent: 0, - } - } -} - -#[derive(Clone)] -#[allow(non_camel_case_types)] -pub struct dc_kml_t { - pub addr: *mut libc::c_char, - pub locations: Option>, - pub tag: libc::c_int, - pub curr: dc_location, -} - -impl dc_kml_t { - pub fn new() -> Self { - dc_kml_t { - addr: std::ptr::null_mut(), - locations: None, - tag: 0, - curr: dc_location::new(), - } - } -} - -// location streaming -pub unsafe fn dc_send_locations_to_chat(context: &Context, chat_id: uint32_t, seconds: i64) { - let now = time(); - let mut msg: Message; - let is_sending_locations_before: bool; - if !(seconds < 0 || chat_id <= 9i32 as libc::c_uint) { - is_sending_locations_before = dc_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 { - msg = dc_msg_new(context, Viewtype::Text); - msg.text = - Some(context.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0)); - msg.param.set_int(Param::Cmd, 8); - chat::send_msg(context, chat_id, &mut msg).unwrap(); - } else if 0 == seconds && is_sending_locations_before { - let stock_str = - context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0); - chat::add_device_msg(context, chat_id, stock_str); - } - context.call_cb( - Event::CHAT_MODIFIED, - chat_id as uintptr_t, - 0i32 as uintptr_t, - ); - if 0 != seconds { - schedule_MAYBE_SEND_LOCATIONS(context, 0i32); - job_add( - context, - Action::MaybeSendLocationsEnded, - chat_id as libc::c_int, - Params::new(), - seconds + 1, - ); - } - } - } -} - -/******************************************************************************* - * job to send locations out to all chats that want them - ******************************************************************************/ -#[allow(non_snake_case)] -unsafe fn schedule_MAYBE_SEND_LOCATIONS(context: &Context, flags: libc::c_int) { - if 0 != flags & 0x1 || !job_action_exists(context, Action::MaybeSendLocations) { - job_add(context, Action::MaybeSendLocations, 0, Params::new(), 60); - }; -} - -pub fn dc_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 dc_set_location( - context: &Context, - latitude: libc::c_double, - longitude: libc::c_double, - accuracy: libc::c_double, -) -> libc::c_int { - if latitude == 0.0 && longitude == 0.0 { - return 1; - } - - context.sql.query_map( - "SELECT id FROM chats WHERE locations_send_until>?;", - params![time()], |row| row.get::<_, i32>(0), - |chats| { - let mut continue_streaming = false; - - for chat in chats { - let chat_id = chat?; - context.sql.execute( - "INSERT INTO locations \ - (latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);", - params![ - latitude, - longitude, - accuracy, - time(), - chat_id, - 1, - ] - )?; - continue_streaming = true; - } - if continue_streaming { - context.call_cb(Event::LOCATION_CHANGED, 1, 0); - }; - unsafe { schedule_MAYBE_SEND_LOCATIONS(context, 0) }; - Ok(continue_streaming as libc::c_int) - } - ).unwrap_or_default() -} - -pub fn dc_get_locations( - context: &Context, - chat_id: uint32_t, - contact_id: uint32_t, - 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, \ - m.id, l.from_id, l.chat_id, m.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, m.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 = dc_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.chars().next().unwrap() != ' ' -} - -pub fn dc_delete_all_locations(context: &Context) -> bool { - if sql::execute(context, &context.sql, "DELETE FROM locations;", params![]).is_err() { - return false; - } - context.call_cb(Event::LOCATION_CHANGED, 0, 0); - true -} - -pub fn dc_get_location_kml( - context: &Context, - chat_id: uint32_t, - last_added_location_id: *mut uint32_t, -) -> *mut libc::c_char { - let mut success: libc::c_int = 0; - let now = time(); - let mut location_count: libc::c_int = 0; - let mut ret = String::new(); - - let self_addr = context - .sql - .get_config(context, "configured_addr") - .unwrap_or_default(); - - if let Ok((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)) - } - ) { - 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![1, locations_send_begin, locations_last_sent, 1], - |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 = unsafe { 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\x00", - as_str(timestamp), - accuracy, - longitude, - latitude - ); - location_count += 1; - if !last_added_location_id.is_null() { - unsafe { *last_added_location_id = location_id as u32 }; - } - unsafe { free(timestamp as *mut libc::c_void) }; - } - Ok(()) - } - ).unwrap(); // TODO: better error handling - } - } - - if location_count > 0 { - ret += "\n"; - success = 1; - } - - if 0 != success { - unsafe { ret.strdup() } - } else { - std::ptr::null_mut() - } -} - -/******************************************************************************* - * create kml-files - ******************************************************************************/ -unsafe fn get_kml_timestamp(utc: i64) -> *mut libc::c_char { - // Returns a string formatted as YYYY-MM-DDTHH:MM:SSZ. The trailing `Z` indicates UTC. - let res = chrono::NaiveDateTime::from_timestamp(utc, 0) - .format("%Y-%m-%dT%H:%M:%SZ") - .to_string(); - res.strdup() -} - -pub unsafe fn dc_get_message_kml( - timestamp: i64, - latitude: libc::c_double, - longitude: libc::c_double, -) -> *mut libc::c_char { - let timestamp_str = get_kml_timestamp(timestamp); - let latitude_str = dc_ftoa(latitude); - let longitude_str = dc_ftoa(longitude); - - let ret = dc_mprintf( - b"\n\ - \n\ - \n\ - \ - %s\ - %s,%s\ - \n\ - \n\ - \x00" as *const u8 as *const libc::c_char, - timestamp_str, - longitude_str, // reverse order! - latitude_str, - ); - - free(latitude_str as *mut libc::c_void); - free(longitude_str as *mut libc::c_void); - free(timestamp_str as *mut libc::c_void); - - ret -} - -pub fn dc_set_kml_sent_timestamp(context: &Context, chat_id: u32, timestamp: i64) -> bool { - sql::execute( - context, - &context.sql, - "UPDATE chats SET locations_last_sent=? WHERE id=?;", - params![timestamp, chat_id as i32], - ) - .is_ok() -} - -pub fn dc_set_msg_location_id(context: &Context, msg_id: u32, location_id: u32) -> bool { - sql::execute( - context, - &context.sql, - "UPDATE msgs SET location_id=? WHERE id=?;", - params![location_id, msg_id as i32], - ) - .is_ok() -} - -pub unsafe fn dc_save_locations( - context: &Context, - chat_id: u32, - contact_id: u32, - locations_opt: &Option>, - independent: libc::c_int, -) -> u32 { - if chat_id <= 9 || locations_opt.is_none() { - return 0; - } - - let locations = locations_opt.as_ref().unwrap(); - 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 0 != 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) - }, - ) - .unwrap_or_default() -} - -pub unsafe fn dc_kml_parse( - context: &Context, - content: *const libc::c_char, - content_bytes: size_t, -) -> dc_kml_t { - let mut kml = dc_kml_t::new(); - - if content_bytes > (1 * 1024 * 1024) { - warn!( - context, - 0, "A kml-files with {} bytes is larger than reasonably expected.", content_bytes, - ); - return kml; - } - - let content_null = dc_null_terminate(content, content_bytes as libc::c_int); - if !content_null.is_null() { - let mut reader = quick_xml::Reader::from_str(as_str(content_null)); - reader.trim_text(true); - - kml.locations = Some(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, &mut kml, &reader), - Ok(quick_xml::events::Event::End(ref e)) => kml_endtag_cb(e, &mut kml), - Ok(quick_xml::events::Event::Text(ref e)) => kml_text_cb(e, &mut kml, &reader), - Err(e) => { - error!( - context, - 0, - "Location parsing: Error at position {}: {:?}", - reader.buffer_position(), - e - ); - } - Ok(quick_xml::events::Event::Eof) => break, - _ => (), - } - buf.clear(); - } - } - - free(content_null.cast()); - - kml -} - -fn kml_text_cb( - event: &BytesText, - kml: &mut dc_kml_t, - reader: &quick_xml::Reader, -) { - if 0 != kml.tag & (0x4 | 0x10) { - let val = event.unescape_and_decode(reader).unwrap_or_default(); - - let val = val - .replace("\n", "") - .replace("\r", "") - .replace("\t", "") - .replace(" ", ""); - - if 0 != kml.tag & 0x4 && 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) => { - kml.curr.timestamp = res.timestamp(); - if kml.curr.timestamp > time() { - kml.curr.timestamp = time(); - } - } - Err(_err) => { - kml.curr.timestamp = time(); - } - } - } else if 0 != kml.tag & 0x10 { - let parts = val.splitn(2, ',').collect::>(); - if parts.len() == 2 { - kml.curr.longitude = parts[0].parse().unwrap_or_default(); - kml.curr.latitude = parts[1].parse().unwrap_or_default(); - } - } - } -} - -fn kml_endtag_cb(event: &BytesEnd, kml: &mut dc_kml_t) { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); - - if tag == "placemark" { - if 0 != kml.tag & 0x1 - && 0 != kml.curr.timestamp - && 0. != kml.curr.latitude - && 0. != kml.curr.longitude - { - if let Some(ref mut locations) = kml.locations { - locations.push(std::mem::replace(&mut kml.curr, dc_location::new())); - } - } - kml.tag = 0 - }; -} - -fn kml_starttag_cb( - event: &BytesStart, - kml: &mut dc_kml_t, - 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() - }) { - kml.addr = unsafe { - addr.unwrap() - .unescape_and_decode_value(reader) - .unwrap_or_default() - .strdup() - }; - } - } else if tag == "placemark" { - kml.tag = 0x1; - kml.curr.timestamp = 0; - kml.curr.latitude = 0 as libc::c_double; - kml.curr.longitude = 0.0f64; - kml.curr.accuracy = 0.0f64 - } else if tag == "timestamp" && 0 != kml.tag & 0x1 { - kml.tag = 0x1 | 0x2 - } else if tag == "when" && 0 != kml.tag & 0x2 { - kml.tag = 0x1 | 0x2 | 0x4 - } else if tag == "point" && 0 != kml.tag & 0x1 { - kml.tag = 0x1 | 0x8 - } else if tag == "coordinates" && 0 != kml.tag & 0x8 { - kml.tag = 0x1 | 0x8 | 0x10; - 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(); - - kml.curr.accuracy = v.trim().parse().unwrap_or_default(); - } - } -} - -pub unsafe fn dc_kml_unref(kml: &mut dc_kml_t) { - free(kml.addr as *mut libc::c_void); -} - -#[allow(non_snake_case)] -pub unsafe fn dc_job_do_DC_JOB_MAYBE_SEND_LOCATIONS(context: &Context, _job: &Job) { - let now = time(); - let mut continue_streaming: libc::c_int = 1; - info!( - context, - 0, " ----------------- MAYBE_SEND_LOCATIONS -------------- ", - ); - - 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 = 1; - - // 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| { - context.sql.prepare( - "SELECT id \ - FROM locations \ - WHERE from_id=? \ - AND timestamp>=? \ - AND timestamp>? \ - AND independent=0 \ - ORDER BY timestamp;", - |mut stmt_locations, _| { - for (chat_id, locations_send_begin, locations_last_sent) in - rows.filter_map(|r| match r { - Ok(Some(v)) => Some(v), - _ => None, - }) - { - // TODO: do I need to reset? - if !stmt_locations - .exists(params![1, 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 - continue; - } - // 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 = dc_msg_new(context, Viewtype::Text); - msg.hidden = true; - msg.param.set_int(Param::Cmd, 9); - // TODO: handle cleanup on error - chat::send_msg(context, chat_id as u32, &mut msg).unwrap(); - } - Ok(()) - }, - ) - }, - ) - .unwrap(); // TODO: Better error handling - - if 0 != continue_streaming { - schedule_MAYBE_SEND_LOCATIONS(context, 0x1); - } -} - -#[allow(non_snake_case)] -pub unsafe fn dc_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_device_msg(context, chat_id, stock_str); - context.call_cb( - Event::CHAT_MODIFIED, - chat_id as usize, - 0, - ); - } - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::dummy_context; - - #[test] - fn test_dc_kml_parse() { - unsafe { - 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\x00" - as *const u8 as *const libc::c_char; - - let mut kml = dc_kml_parse(&context.ctx, xml, strlen(xml)); - - assert!(!kml.addr.is_null()); - assert_eq!(as_str(kml.addr as *const libc::c_char), "user@example.org",); - - let locations_ref = &kml.locations.as_ref().unwrap(); - 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); - - dc_kml_unref(&mut kml); - } - } -} diff --git a/src/dc_mimefactory.rs b/src/dc_mimefactory.rs index b844040b2..0b5622548 100644 --- a/src/dc_mimefactory.rs +++ b/src/dc_mimefactory.rs @@ -16,10 +16,10 @@ use crate::constants::*; use crate::contact::*; use crate::context::{dc_get_version_str, Context}; use crate::dc_e2ee::*; -use crate::dc_location::*; use crate::dc_strencode::*; use crate::dc_tools::*; use crate::error::Error; +use crate::location; use crate::message::*; use crate::param::*; use crate::stock::StockMessage; @@ -883,45 +883,47 @@ pub unsafe fn dc_mimefactory_render(factory: &mut dc_mimefactory_t) -> libc::c_i .param .get_float(Param::SetLongitude) .unwrap_or_default(); - let kml_file = - dc_get_message_kml(factory.msg.timestamp_sort, latitude, longitude); - if !kml_file.is_null() { + let kml_file = location::get_message_kml( + factory.msg.timestamp_sort, + latitude, + longitude, + ); + let content_type = mailmime_content_new_with_str( + b"application/vnd.google-earth.kml+xml\x00" as *const u8 + as *const libc::c_char, + ); + let mime_fields = mailmime_fields_new_filename( + MAILMIME_DISPOSITION_TYPE_ATTACHMENT as libc::c_int, + dc_strdup(b"message.kml\x00" as *const u8 as *const libc::c_char), + MAILMIME_MECHANISM_8BIT as libc::c_int, + ); + let kml_mime_part = mailmime_new_empty(content_type, mime_fields); + mailmime_set_body_text(kml_mime_part, kml_file.strdup(), kml_file.len()); + mailmime_smart_add_part(message, kml_mime_part); + } + + if location::is_sending_locations_to_chat( + factory.msg.context, + factory.msg.chat_id, + ) { + if let Ok((kml_file, last_added_location_id)) = + location::get_kml(factory.msg.context, factory.msg.chat_id) + { let content_type = mailmime_content_new_with_str( b"application/vnd.google-earth.kml+xml\x00" as *const u8 as *const libc::c_char, ); let mime_fields = mailmime_fields_new_filename( - MAILMIME_DISPOSITION_TYPE_ATTACHMENT as libc::c_int, - dc_strdup(b"message.kml\x00" as *const u8 as *const libc::c_char), - MAILMIME_MECHANISM_8BIT as libc::c_int, - ); - let kml_mime_part = mailmime_new_empty(content_type, mime_fields); - mailmime_set_body_text(kml_mime_part, kml_file, strlen(kml_file)); - - mailmime_smart_add_part(message, kml_mime_part); - } - } - - if dc_is_sending_locations_to_chat(factory.msg.context, factory.msg.chat_id) { - let mut last_added_location_id: uint32_t = 0 as uint32_t; - let kml_file: *mut libc::c_char = dc_get_location_kml( - factory.msg.context, - factory.msg.chat_id, - &mut last_added_location_id, - ); - if !kml_file.is_null() { - let content_type: *mut mailmime_content = mailmime_content_new_with_str( - b"application/vnd.google-earth.kml+xml\x00" as *const u8 - as *const libc::c_char, - ); - let mime_fields: *mut mailmime_fields = mailmime_fields_new_filename( MAILMIME_DISPOSITION_TYPE_ATTACHMENT as libc::c_int, dc_strdup(b"location.kml\x00" as *const u8 as *const libc::c_char), MAILMIME_MECHANISM_8BIT as libc::c_int, ); - let kml_mime_part: *mut mailmime = - mailmime_new_empty(content_type, mime_fields); - mailmime_set_body_text(kml_mime_part, kml_file, strlen(kml_file)); + let kml_mime_part = mailmime_new_empty(content_type, mime_fields); + mailmime_set_body_text( + kml_mime_part, + kml_file.strdup(), + kml_file.len(), + ); mailmime_smart_add_part(message, kml_mime_part); if !factory.msg.param.exists(Param::SetLatitude) { // otherwise, the independent location is already filed diff --git a/src/dc_mimeparser.rs b/src/dc_mimeparser.rs index 118c61258..8a208a5b3 100644 --- a/src/dc_mimeparser.rs +++ b/src/dc_mimeparser.rs @@ -14,10 +14,10 @@ use mmime::other::*; use crate::contact::*; use crate::context::Context; use crate::dc_e2ee::*; -use crate::dc_location::*; use crate::dc_simplify::*; use crate::dc_strencode::*; use crate::dc_tools::*; +use crate::location; use crate::param::*; use crate::stock::StockMessage; use crate::types::*; @@ -57,8 +57,8 @@ pub struct dc_mimeparser_t<'a> { pub context: &'a Context, pub reports: Vec<*mut mailmime>, pub is_system_message: libc::c_int, - pub location_kml: Option, - pub message_kml: Option, + pub location_kml: Option, + pub message_kml: Option, } // deprecated: flag to switch generation of compound messages on and off. @@ -112,14 +112,7 @@ unsafe fn dc_mimeparser_empty(mimeparser: &mut dc_mimeparser_t) { mimeparser.decrypting_failed = 0i32; dc_e2ee_thanks(&mut mimeparser.e2ee_helper); - if let Some(location_kml) = mimeparser.location_kml.as_mut() { - dc_kml_unref(location_kml); - } mimeparser.location_kml = None; - - if let Some(message_kml) = mimeparser.message_kml.as_mut() { - dc_kml_unref(message_kml); - } mimeparser.message_kml = None; } @@ -1300,11 +1293,13 @@ unsafe fn dc_mimeparser_add_single_part_if_known( 4, ) == 0i32 { - mimeparser.location_kml = Some(dc_kml_parse( - mimeparser.context, - decoded_data, - decoded_data_bytes, - )); + if !decoded_data.is_null() && decoded_data_bytes > 0 { + let d = + dc_null_terminate(decoded_data, decoded_data_bytes as i32); + mimeparser.location_kml = + location::Kml::parse(mimeparser.context, as_str(d)).ok(); + free(d.cast()); + } } else if strncmp( desired_filename, b"message\x00" as *const u8 as *const libc::c_char, @@ -1318,11 +1313,13 @@ unsafe fn dc_mimeparser_add_single_part_if_known( 4, ) == 0i32 { - mimeparser.message_kml = Some(dc_kml_parse( - mimeparser.context, - decoded_data, - decoded_data_bytes, - )); + if !decoded_data.is_null() && decoded_data_bytes > 0 { + let d = + dc_null_terminate(decoded_data, decoded_data_bytes as i32); + mimeparser.message_kml = + location::Kml::parse(mimeparser.context, as_str(d)).ok(); + free(d.cast()); + } } else { dc_replace_bad_utf8_chars(desired_filename); do_add_single_file_part( diff --git a/src/dc_receive_imf.rs b/src/dc_receive_imf.rs index 1b834de02..d3f5cc7cd 100644 --- a/src/dc_receive_imf.rs +++ b/src/dc_receive_imf.rs @@ -14,7 +14,6 @@ use crate::chat::{self, Chat}; use crate::constants::*; use crate::contact::*; use crate::context::Context; -use crate::dc_location::*; use crate::dc_mimeparser::*; use crate::dc_move::*; use crate::dc_securejoin::*; @@ -22,6 +21,7 @@ use crate::dc_strencode::*; use crate::dc_tools::*; use crate::error::Result; use crate::job::*; +use crate::location; use crate::message::*; use crate::param::*; use crate::peerstate::*; @@ -938,36 +938,34 @@ unsafe fn save_locations( let mut send_event = false; if !mime_parser.message_kml.is_none() && chat_id > DC_CHAT_ID_LAST_SPECIAL as libc::c_uint { - let newest_location_id: uint32_t = dc_save_locations( - context, - chat_id, - from_id, - &mime_parser.message_kml.as_ref().unwrap().locations, - 1, - ); + let locations = &mime_parser.message_kml.as_ref().unwrap().locations; + let newest_location_id = + location::save(context, chat_id, from_id, locations, 1).unwrap_or_default(); if 0 != newest_location_id && 0 == hidden { - dc_set_msg_location_id(context, insert_msg_id, newest_location_id); - location_id_written = true; - send_event = true; + if location::set_msg_location_id(context, insert_msg_id, newest_location_id).is_ok() { + location_id_written = true; + send_event = true; + } } } if !mime_parser.location_kml.is_none() && chat_id > DC_CHAT_ID_LAST_SPECIAL as libc::c_uint { - if !mime_parser.location_kml.as_ref().unwrap().addr.is_null() { + if let Some(ref addr) = mime_parser.location_kml.as_ref().unwrap().addr { if let Ok(contact) = Contact::get_by_id(context, from_id) { if !contact.get_addr().is_empty() - && contact.get_addr().to_lowercase() - == as_str(mime_parser.location_kml.as_ref().unwrap().addr).to_lowercase() + && contact.get_addr().to_lowercase() == addr.to_lowercase() { - let newest_location_id = dc_save_locations( - context, - chat_id, - from_id, - &mime_parser.location_kml.as_ref().unwrap().locations, - 0, - ); + let locations = &mime_parser.location_kml.as_ref().unwrap().locations; + let newest_location_id = + location::save(context, chat_id, from_id, locations, 0).unwrap_or_default(); if newest_location_id != 0 && hidden == 0 && !location_id_written { - dc_set_msg_location_id(context, insert_msg_id, newest_location_id); + if let Err(err) = location::set_msg_location_id( + context, + insert_msg_id, + newest_location_id, + ) { + error!(context, 0, "Failed to set msg_location_id: {:?}", err); + } } send_event = true; } diff --git a/src/job.rs b/src/job.rs index 0bb313af1..0b42c5945 100644 --- a/src/job.rs +++ b/src/job.rs @@ -10,11 +10,11 @@ use crate::configure::*; use crate::constants::*; use crate::context::Context; use crate::dc_imex::*; -use crate::dc_location::*; use crate::dc_loginparam::*; use crate::dc_mimefactory::*; use crate::dc_tools::*; use crate::imap::*; +use crate::location; use crate::message::*; use crate::param::*; use crate::sql; @@ -739,13 +739,19 @@ pub unsafe fn job_send_msg(context: &Context, msg_id: uint32_t) -> libc::c_int { chat::set_gossiped_timestamp(context, mimefactory.msg.chat_id, time()); } if 0 != mimefactory.out_last_added_location_id { - dc_set_kml_sent_timestamp(context, mimefactory.msg.chat_id, time()); + if let Err(err) = + location::set_kml_sent_timestamp(context, mimefactory.msg.chat_id, time()) + { + error!(context, 0, "Failed to set kml sent_timestamp: {:?}", err); + } if !mimefactory.msg.hidden { - dc_set_msg_location_id( + if let Err(err) = location::set_msg_location_id( context, mimefactory.msg.id, mimefactory.out_last_added_location_id, - ); + ) { + error!(context, 0, "Failed to set msg_location_id: {:?}", err); + } } } if 0 != mimefactory.out_encrypted @@ -875,12 +881,12 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) { Action::SendMdn => job.do_DC_JOB_SEND(context), Action::ConfigureImap => unsafe { dc_job_do_DC_JOB_CONFIGURE_IMAP(context, &job) }, Action::ImexImap => unsafe { dc_job_do_DC_JOB_IMEX_IMAP(context, &job) }, - Action::MaybeSendLocations => unsafe { - dc_job_do_DC_JOB_MAYBE_SEND_LOCATIONS(context, &job) - }, - Action::MaybeSendLocationsEnded => unsafe { - dc_job_do_DC_JOB_MAYBE_SEND_LOC_ENDED(context, &mut job) - }, + Action::MaybeSendLocations => { + location::job_do_DC_JOB_MAYBE_SEND_LOCATIONS(context, &job) + } + Action::MaybeSendLocationsEnded => { + location::job_do_DC_JOB_MAYBE_SEND_LOC_ENDED(context, &mut job) + } Action::Housekeeping => sql::housekeeping(context), Action::SendMdnOld => {} Action::SendMsgToSmtpOld => {} diff --git a/src/lib.rs b/src/lib.rs index ee0766149..cfb4ca51c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,7 @@ pub mod job; mod job_thread; pub mod key; pub mod keyring; +pub mod location; pub mod lot; pub mod message; pub mod oauth2; @@ -52,7 +53,6 @@ pub mod dc_array; mod dc_dehtml; mod dc_e2ee; pub mod dc_imex; -pub mod dc_location; mod dc_loginparam; mod dc_mimefactory; pub mod dc_mimeparser; diff --git a/src/location.rs b/src/location.rs new file mode 100644 index 000000000..66444d948 --- /dev/null +++ b/src/location.rs @@ -0,0 +1,696 @@ +use bitflags::bitflags; +use quick_xml; +use quick_xml::events::{BytesEnd, BytesStart, BytesText}; + +use crate::chat; +use crate::constants::Event; +use crate::constants::*; +use crate::context::*; +use crate::dc_tools::*; +use crate::error::Error; +use crate::job::*; +use crate::message::*; +use crate::param::*; +use crate::sql; +use crate::stock::StockMessage; +use crate::types::*; + +// location handling +#[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: impl AsRef) -> Result { + ensure!( + content.as_ref().len() <= (1 * 1024 * 1024), + "A kml-files with {} bytes is larger than reasonably expected.", + content.as_ref().len() + ); + + let mut reader = quick_xml::Reader::from_str(content.as_ref()); + 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, + 0, + "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(); + let mut msg: Message; + let is_sending_locations_before: bool; + if !(seconds < 0 || chat_id <= 9i32 as libc::c_uint) { + 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 { + msg = dc_msg_new(context, Viewtype::Text); + msg.text = + Some(context.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0)); + msg.param.set_int(Param::Cmd, 8); + unsafe { chat::send_msg(context, chat_id, &mut msg).unwrap() }; + } else if 0 == seconds && is_sending_locations_before { + let stock_str = + context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0); + chat::add_device_msg(context, chat_id, stock_str); + } + context.call_cb( + Event::CHAT_MODIFIED, + chat_id as uintptr_t, + 0i32 as uintptr_t, + ); + if 0 != seconds { + schedule_MAYBE_SEND_LOCATIONS(context, 0i32); + 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, flags: i32) { + if 0 != flags & 0x1 || !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) -> libc::c_int { + if latitude == 0.0 && longitude == 0.0 { + return 1; + } + + context.sql.query_map( + "SELECT id FROM chats WHERE locations_send_until>?;", + params![time()], |row| row.get::<_, i32>(0), + |chats| { + let mut continue_streaming = false; + + for chat in chats { + let chat_id = chat?; + context.sql.execute( + "INSERT INTO locations \ + (latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);", + params![ + latitude, + longitude, + accuracy, + time(), + chat_id, + 1, + ] + )?; + continue_streaming = true; + } + if continue_streaming { + context.call_cb(Event::LOCATION_CHANGED, 1, 0); + }; + schedule_MAYBE_SEND_LOCATIONS(context, 0); + Ok(continue_streaming as libc::c_int) + } + ).unwrap_or_default() +} + +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, \ + m.id, l.from_id, l.chat_id, m.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, m.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.chars().next().unwrap() != ' ' +} + +pub fn delete_all(context: &Context) -> Result<(), Error> { + sql::execute(context, &context.sql, "DELETE FROM locations;", params![])?; + context.call_cb(Event::LOCATION_CHANGED, 0, 0); + Ok(()) +} + +pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error> { + let now = time(); + let mut location_count = 0; + let mut ret = String::new(); + let mut last_added_location_id = 0; + + let self_addr = context + .sql + .get_config(context, "configured_addr") + .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)) + })?; + + 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![1, locations_send_begin, locations_last_sent, 1], + |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\x00", + timestamp, + accuracy, + longitude, + latitude + ); + location_count += 1; + last_added_location_id = location_id as u32; + } + Ok(()) + } + )?; + } + + ensure!(location_count > 0, "No locations processed"); + ret += "\n"; + + 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: u32, location_id: u32) -> Result<(), Error> { + sql::execute( + context, + &context.sql, + "UPDATE msgs SET location_id=? WHERE id=?;", + params![location_id, msg_id as i32], + )?; + + Ok(()) +} + +pub fn save( + context: &Context, + chat_id: u32, + contact_id: u32, + locations: &[Location], + independent: i32, +) -> Result { + ensure!(chat_id > 9, "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 0 != 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: libc::c_int = 1; + info!( + context, + 0, " ----------------- MAYBE_SEND_LOCATIONS -------------- ", + ); + + 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 = 1; + + // 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| { + context.sql.prepare( + "SELECT id \ + FROM locations \ + WHERE from_id=? \ + AND timestamp>=? \ + AND timestamp>? \ + AND independent=0 \ + ORDER BY timestamp;", + |mut stmt_locations, _| { + for (chat_id, locations_send_begin, locations_last_sent) in + rows.filter_map(|r| match r { + Ok(Some(v)) => Some(v), + _ => None, + }) + { + // TODO: do I need to reset? + if !stmt_locations + .exists(params![1, 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 + continue; + } + // 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 = dc_msg_new(context, Viewtype::Text); + msg.hidden = true; + msg.param.set_int(Param::Cmd, 9); + // TODO: handle cleanup on error + unsafe { chat::send_msg(context, chat_id as u32, &mut msg).unwrap() }; + } + Ok(()) + }, + ) + }, + ) + .unwrap(); // TODO: Better error handling + + if 0 != continue_streaming { + schedule_MAYBE_SEND_LOCATIONS(context, 0x1); + } +} + +#[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_device_msg(context, chat_id, stock_str); + context.call_cb( + Event::CHAT_MODIFIED, + chat_id as usize, + 0, + ); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::dummy_context; + + #[test] + fn test_kml_parse() { + let context = dummy_context(); + + let xml = + "\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); + } +}