diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index 8037c9afd..c37562140 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -22,6 +22,9 @@ use deltachat::context::get_info; use deltachat::ephemeral::Timer; use deltachat::imex; use deltachat::location; +use deltachat::location::is_sending_locations; +use deltachat::location::is_sending_locations_to_chat; +use deltachat::location::send_locations_to_chat; use deltachat::message::{ self, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipt_count, get_msg_read_receipts, markseen_msgs, Message, MessageState, MsgId, Viewtype, @@ -2115,6 +2118,18 @@ impl CommandApi { // locations // --------------------------------------------- + /// Sets current location. + /// + /// Returns true if location streaming is currently + /// enabled and locations should be updated. + async fn set_location(&self, latitude: f64, longitude: f64, accuracy: f64) -> Result { + self.accounts + .read() + .await + .set_location(latitude, longitude, accuracy) + .await + } + async fn get_locations( &self, account_id: u32, @@ -2137,6 +2152,39 @@ impl CommandApi { Ok(locations.into_iter().map(|l| l.into()).collect()) } + /// Enables location streaming in chat identified by `chat_id` for `seconds` seconds. + /// + /// Pass 0 as the number of seconds to disable location streaming in the chat. + async fn send_locations_to_chat( + &self, + account_id: u32, + chat_id: u32, + seconds: i64, + ) -> Result<()> { + let ctx = self.get_context(account_id).await?; + let chat_id = ChatId::new(chat_id); + send_locations_to_chat(&ctx, chat_id, seconds).await?; + Ok(()) + } + + /// Returns whether any chat is sending locations. + async fn is_sending_locations(&self, account_id: u32) -> Result { + let ctx = self.get_context(account_id).await?; + is_sending_locations(&ctx).await + } + + /// Returns whether `chat_id` is sending locations. + async fn is_sending_locations_to_chat(&self, account_id: u32, chat_id: u32) -> Result { + let ctx = self.get_context(account_id).await?; + let chat_id = ChatId::new(chat_id); + is_sending_locations_to_chat(&ctx, chat_id).await + } + + /// Stops sending locations to all chats. + async fn stop_sending_locations(&self) -> Result<()> { + self.accounts.read().await.stop_sending_locations().await + } + // --------------------------------------------- // webxdc // --------------------------------------------- diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py index 55fddd971..dd6722298 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -495,3 +495,7 @@ class Account: """Return ICE servers for WebRTC configuration.""" ice_servers_json = self._rpc.ice_servers(self.id) return json.loads(ice_servers_json) + + def is_sending_locations(self) -> bool: + """Return True if sending locations to any chat.""" + return self._rpc.is_sending_locations(self.id) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py index a4dae751a..13ed20177 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py @@ -277,6 +277,16 @@ class Chat: """Remove profile image of this chat.""" self._rpc.set_chat_profile_image(self.account.id, self.id, None) + def send_locations(self, seconds) -> None: + """Enable location streaming in the chat for the given number of seconds. + + Pass 0 to disable location streaming.""" + self._rpc.send_locations_to_chat(self.account.id, self.id, seconds) + + def is_sending_locations(self) -> bool: + """Return True if sending locations to this chat.""" + return self._rpc.is_sending_locations_to_chat(self.account.id, self.id) + def get_locations( self, contact: Optional[Contact] = None, diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py index 3d23eac46..8bde4c586 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py @@ -59,3 +59,11 @@ class DeltaChat: def set_translations(self, translations: dict[str, str]) -> None: """Set stock translation strings.""" self.rpc.set_stock_strings(translations) + + def set_location(self, latitude, longitude, accuracy) -> bool: + """Set location, return True if location streaming should continue.""" + return self.rpc.set_location(latitude, longitude, accuracy) + + def stop_sending_locations(self) -> None: + """Stop sending locations to all chats.""" + return self.rpc.stop_sending_locations() diff --git a/deltachat-rpc-client/tests/test_location.py b/deltachat-rpc-client/tests/test_location.py new file mode 100644 index 000000000..da57dc396 --- /dev/null +++ b/deltachat-rpc-client/tests/test_location.py @@ -0,0 +1,32 @@ +def test_set_location(dc, acfactory) -> None: + # Try setting location without any accounts. + assert not dc.set_location(1.0, 2.0, 0.1) + + # Create one account that does not stream, + # set location. + acfactory.new_configured_account() + assert not dc.set_location(3.0, 4.0, 0.1) + + +def test_send_locations_to_chat(dc, acfactory): + alice, bob = acfactory.get_online_accounts(2) + + assert not alice.is_sending_locations() + alice_chat_bob = alice.create_chat(bob) + assert not alice_chat_bob.is_sending_locations() + + # Test starting and stopping location streaming in a chat. + alice_chat_bob.send_locations(3600) + assert alice.is_sending_locations() + assert alice_chat_bob.is_sending_locations() + alice_chat_bob.send_locations(0) + assert not alice.is_sending_locations() + assert not alice_chat_bob.is_sending_locations() + + # Test stop_sending_locations() for all accounts and chats. + alice_chat_bob.send_locations(3600) + assert alice.is_sending_locations() + assert alice_chat_bob.is_sending_locations() + dc.stop_sending_locations() + assert not alice.is_sending_locations() + assert not alice_chat_bob.is_sending_locations() diff --git a/src/accounts.rs b/src/accounts.rs index de8f776d8..8b28d6a97 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -22,6 +22,8 @@ use tokio::time::{Duration, sleep}; use crate::context::{Context, ContextBuilder}; use crate::events::{Event, EventEmitter, EventType, Events}; +use crate::location; +use crate::location::stop_sending_locations; use crate::log::warn; use crate::push::PushSubscriber; use crate::stock_str::StockStrings; @@ -536,6 +538,27 @@ impl Accounts { self.push_subscriber.set_device_token(token).await; Ok(()) } + + /// Sets location for all accounts. + /// + /// Returns true if location should be still be streamed. + pub async fn set_location(&self, latitude: f64, longitude: f64, accuracy: f64) -> Result { + let mut continue_streaming = false; + for account in self.accounts.values() { + if location::set(account, latitude, longitude, accuracy).await? { + continue_streaming = true; + } + } + Ok(continue_streaming) + } + + /// Stops sending locations to all chats. + pub async fn stop_sending_locations(&self) -> Result<()> { + for account in self.accounts.values() { + stop_sending_locations(account).await?; + } + Ok(()) + } } /// Configuration file name. diff --git a/src/location.rs b/src/location.rs index b162ced1a..27964c939 100644 --- a/src/location.rs +++ b/src/location.rs @@ -327,6 +327,29 @@ pub async fn is_sending_locations_to_chat(context: &Context, chat_id: ChatId) -> .await } +/// Returns a list of chats in which location streaming is enabled. +async fn get_chats_with_location_streaming(context: &Context) -> Result> { + context + .sql + .query_map_vec( + "SELECT id FROM chats WHERE locations_send_until>?", + (time(),), + |row| { + let chat_id: ChatId = row.get(0)?; + Ok(chat_id) + }, + ) + .await +} + +/// Stop sending locations in all chats. +pub async fn stop_sending_locations(context: &Context) -> Result<()> { + for chat_id in get_chats_with_location_streaming(context).await? { + send_locations_to_chat(context, chat_id, 0).await?; + } + Ok(()) +} + /// Sets current location of the user device. pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> Result { if latitude == 0.0 && longitude == 0.0 {