Files
chatmail-core/deltachat-jsonrpc/src/api.rs
iequidoo 56b86adf18 api: Add dc_msg_save_file() which saves file copy at the provided path (#4309)
... and fails if file already exists. The UI should open the file saving dialog, defaulting to
Downloads and original filename, when asked to save the file. After confirmation it should call
dc_msg_save_file().
2024-04-24 16:38:25 -03:00

2172 lines
79 KiB
Rust

use std::collections::BTreeMap;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, bail, ensure, Context, Result};
pub use deltachat::accounts::Accounts;
use deltachat::chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
ProtectionStatus,
};
use deltachat::chatlist::Chatlist;
use deltachat::config::Config;
use deltachat::constants::DC_MSG_ID_DAYMARKER;
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
use deltachat::context::get_info;
use deltachat::ephemeral::Timer;
use deltachat::imex;
use deltachat::location;
use deltachat::message::get_msg_read_receipts;
use deltachat::message::{
self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype,
};
use deltachat::provider::get_provider_info;
use deltachat::qr::{self, Qr};
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction};
use deltachat::securejoin;
use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::EventEmitter;
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
use walkdir::WalkDir;
use yerpc::rpc;
pub mod types;
use num_traits::FromPrimitive;
use types::account::Account;
use types::chat::FullChat;
use types::contact::ContactObject;
use types::events::Event;
use types::http::HttpResponse;
use types::message::{MessageData, MessageObject, MessageReadReceipt};
use types::provider_info::ProviderInfo;
use types::reactions::JSONRPCReactions;
use types::webxdc::WebxdcMessageInfo;
use self::types::message::{MessageInfo, MessageLoadResult};
use self::types::{
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
location::JsonrpcLocation,
message::{
JSONRPCMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
},
};
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
use crate::api::types::qr::QrObject;
#[derive(Debug)]
struct AccountState {
/// The Qr code for current [`CommandApi::provide_backup`] call.
///
/// If there is currently is a call to [`CommandApi::provide_backup`] this will be
/// `Some`, otherwise `None`.
backup_provider_qr: watch::Sender<Option<Qr>>,
}
impl Default for AccountState {
fn default() -> Self {
let tx = watch::Sender::new(None);
Self {
backup_provider_qr: tx,
}
}
}
#[derive(Clone, Debug)]
pub struct CommandApi {
pub(crate) accounts: Arc<RwLock<Accounts>>,
/// Receiver side of the event channel.
///
/// Events from it can be received by calling `get_next_event` method.
event_emitter: Arc<EventEmitter>,
states: Arc<Mutex<BTreeMap<u32, AccountState>>>,
}
impl CommandApi {
pub fn new(accounts: Accounts) -> Self {
let event_emitter = Arc::new(accounts.get_event_emitter());
CommandApi {
accounts: Arc::new(RwLock::new(accounts)),
event_emitter,
states: Arc::new(Mutex::new(BTreeMap::new())),
}
}
#[allow(dead_code)]
pub async fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
let event_emitter = Arc::new(accounts.read().await.get_event_emitter());
CommandApi {
accounts,
event_emitter,
states: Arc::new(Mutex::new(BTreeMap::new())),
}
}
async fn get_context(&self, id: u32) -> Result<deltachat::context::Context> {
let sc = self
.accounts
.read()
.await
.get_account(id)
.ok_or_else(|| anyhow!("account with id {} not found", id))?;
Ok(sc)
}
async fn with_state<F, T>(&self, id: u32, with_state: F) -> T
where
F: FnOnce(&AccountState) -> T,
{
let mut states = self.states.lock().await;
let state = states.entry(id).or_insert_with(Default::default);
with_state(state)
}
async fn inner_get_backup_qr(&self, account_id: u32) -> Result<Qr> {
let mut receiver = self
.with_state(account_id, |state| state.backup_provider_qr.subscribe())
.await;
loop {
if let Some(qr) = receiver.borrow_and_update().clone() {
return Ok(qr);
}
if receiver.changed().await.is_err() {
bail!("No backup being provided (account state dropped)");
}
}
}
}
#[rpc(all_positional, ts_outdir = "typescript/generated")]
impl CommandApi {
/// Test function.
async fn sleep(&self, delay: f64) {
tokio::time::sleep(std::time::Duration::from_secs_f64(delay)).await
}
// ---------------------------------------------
// Misc top level functions
// ---------------------------------------------
/// Checks if an email address is valid.
async fn check_email_validity(&self, email: String) -> bool {
may_be_valid_addr(&email)
}
/// Returns general system info.
async fn get_system_info(&self) -> BTreeMap<&'static str, String> {
get_info()
}
/// Get the next event.
async fn get_next_event(&self) -> Result<Event> {
self.event_emitter
.recv()
.await
.map(|event| event.into())
.context("event channel is closed")
}
// ---------------------------------------------
// Account Management
// ---------------------------------------------
async fn add_account(&self) -> Result<u32> {
self.accounts.write().await.add_account().await
}
async fn remove_account(&self, account_id: u32) -> Result<()> {
self.accounts
.write()
.await
.remove_account(account_id)
.await?;
self.states.lock().await.remove(&account_id);
Ok(())
}
async fn get_all_account_ids(&self) -> Vec<u32> {
self.accounts.read().await.get_all()
}
/// Select account id for internally selected state.
/// TODO: Likely this is deprecated as all methods take an account id now.
async fn select_account(&self, id: u32) -> Result<()> {
self.accounts.write().await.select_account(id).await
}
/// Get the selected account id of the internal state..
/// TODO: Likely this is deprecated as all methods take an account id now.
async fn get_selected_account_id(&self) -> Option<u32> {
self.accounts.read().await.get_selected_account_id()
}
/// Get a list of all configured accounts.
async fn get_all_accounts(&self) -> Result<Vec<Account>> {
let mut accounts = Vec::new();
for id in self.accounts.read().await.get_all() {
let context_option = self.accounts.read().await.get_account(id);
if let Some(ctx) = context_option {
accounts.push(Account::from_context(&ctx, id).await?)
}
}
Ok(accounts)
}
/// Starts background tasks for all accounts.
async fn start_io_for_all_accounts(&self) -> Result<()> {
self.accounts.write().await.start_io().await;
Ok(())
}
/// Stops background tasks for all accounts.
async fn stop_io_for_all_accounts(&self) -> Result<()> {
self.accounts.write().await.stop_io().await;
Ok(())
}
/// Performs a background fetch for all accounts in parallel with a timeout.
///
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout.
/// Process all events until you get this one and you can safely return to the background
/// without forgetting to create notifications caused by timing race conditions.
async fn accounts_background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
self.accounts
.write()
.await
.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
.await;
Ok(())
}
// ---------------------------------------------
// Methods that work on individual accounts
// ---------------------------------------------
/// Starts background tasks for a single account.
async fn start_io(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.start_io().await;
Ok(())
}
/// Stops background tasks for a single account.
async fn stop_io(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.stop_io().await;
Ok(())
}
/// Get top-level info for an account.
async fn get_account_info(&self, account_id: u32) -> Result<Account> {
let context_option = self.accounts.read().await.get_account(account_id);
if let Some(ctx) = context_option {
Ok(Account::from_context(&ctx, account_id).await?)
} else {
Err(anyhow!(
"account with id {} doesn't exist anymore",
account_id
))
}
}
/// Get the combined filesize of an account in bytes
async fn get_account_file_size(&self, account_id: u32) -> Result<u64> {
let ctx = self.get_context(account_id).await?;
let dbfile = ctx.get_dbfile().metadata()?.len();
let total_size = WalkDir::new(ctx.get_blobdir())
.max_depth(2)
.into_iter()
.filter_map(|entry| entry.ok())
.filter_map(|entry| entry.metadata().ok())
.filter(|metadata| metadata.is_file())
.fold(0, |acc, m| acc + m.len());
Ok(dbfile + total_size)
}
/// Returns provider for the given domain.
///
/// This function looks up domain in offline database.
///
/// For compatibility, email address can be passed to this function
/// instead of the domain.
async fn get_provider_info(
&self,
account_id: u32,
email: String,
) -> Result<Option<ProviderInfo>> {
let ctx = self.get_context(account_id).await?;
let socks5_enabled = ctx
.get_config_bool(deltachat::config::Config::Socks5Enabled)
.await?;
let provider_info =
get_provider_info(&ctx, email.split('@').last().unwrap_or(""), socks5_enabled).await;
Ok(ProviderInfo::from_dc_type(provider_info))
}
/// Checks if the context is already configured.
async fn is_configured(&self, account_id: u32) -> Result<bool> {
let ctx = self.get_context(account_id).await?;
ctx.is_configured().await
}
/// Get system info for an account.
async fn get_info(&self, account_id: u32) -> Result<BTreeMap<&'static str, String>> {
let ctx = self.get_context(account_id).await?;
ctx.get_info().await
}
async fn draft_self_report(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.draft_self_report().await?.to_u32())
}
/// Sets the given configuration key.
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
set_config(&ctx, &key, value.as_deref()).await
}
/// Updates a batch of configuration values.
async fn batch_set_config(
&self,
account_id: u32,
config: HashMap<String, Option<String>>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
for (key, value) in config.into_iter() {
set_config(&ctx, &key, value.as_deref())
.await
.with_context(|| format!("Can't set {key} to {value:?}"))?;
}
Ok(())
}
/// Set configuration values from a QR code. (technically from the URI that is stored in the qrcode)
/// Before this function is called, `checkQr()` should confirm the type of the
/// QR code is `account` or `webrtcInstance`.
///
/// Internally, the function will call dc_set_config() with the appropriate keys,
async fn set_config_from_qr(&self, account_id: u32, qr_content: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
qr::set_config_from_qr(&ctx, &qr_content).await
}
async fn check_qr(&self, account_id: u32, qr_content: String) -> Result<QrObject> {
let ctx = self.get_context(account_id).await?;
let qr = qr::check_qr(&ctx, &qr_content).await?;
let qr_object = QrObject::from(qr);
Ok(qr_object)
}
/// Returns configuration value for the given key.
async fn get_config(&self, account_id: u32, key: String) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
get_config(&ctx, &key).await
}
async fn batch_get_config(
&self,
account_id: u32,
keys: Vec<String>,
) -> Result<HashMap<String, Option<String>>> {
let ctx = self.get_context(account_id).await?;
let mut result: HashMap<String, Option<String>> = HashMap::new();
for key in keys {
result.insert(key.clone(), get_config(&ctx, &key).await?);
}
Ok(result)
}
async fn set_stock_strings(&self, strings: HashMap<u32, String>) -> Result<()> {
let accounts = self.accounts.read().await;
for (stock_id, stock_message) in strings {
if let Some(stock_id) = StockMessage::from_u32(stock_id) {
accounts
.set_stock_translation(stock_id, stock_message)
.await?;
}
}
Ok(())
}
/// Configures this account with the currently set parameters.
/// Setup the credential config before calling this.
async fn configure(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.stop_io().await;
let result = ctx.configure().await;
if result.is_err() {
if let Ok(true) = ctx.is_configured().await {
ctx.start_io().await;
}
return result;
}
ctx.start_io().await;
Ok(())
}
/// Signal an ongoing process to stop.
async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.stop_ongoing().await;
Ok(())
}
async fn export_self_keys(
&self,
account_id: u32,
path: String,
passphrase: Option<String>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
imex::imex(
&ctx,
imex::ImexMode::ExportSelfKeys,
path.as_ref(),
passphrase,
)
.await
}
async fn import_self_keys(
&self,
account_id: u32,
path: String,
passphrase: Option<String>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
imex::imex(
&ctx,
imex::ImexMode::ImportSelfKeys,
path.as_ref(),
passphrase,
)
.await
}
/// Returns the message IDs of all _fresh_ messages of any chat.
/// Typically used for implementing notification summaries
/// or badge counters e.g. on the app icon.
/// The list is already sorted and starts with the most recent fresh message.
///
/// Messages belonging to muted chats or to the contact requests are not returned;
/// these messages should not be notified
/// and also badge counters should not include these messages.
///
/// To get the number of fresh messages for a single chat, muted or not,
/// use `get_fresh_msg_cnt()`.
async fn get_fresh_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
Ok(ctx
.get_fresh_msgs()
.await?
.iter()
.map(|msg_id| msg_id.to_u32())
.collect())
}
/// Get the number of _fresh_ messages in a chat.
/// Typically used to implement a badge with a number in the chatlist.
///
/// If the specified chat is muted,
/// the UI should show the badge counter "less obtrusive",
/// e.g. using "gray" instead of "red" color.
async fn get_fresh_msg_cnt(&self, account_id: u32, chat_id: u32) -> Result<usize> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await
}
/// Gets messages to be processed by the bot and returns their IDs.
///
/// Only messages with database ID higher than `last_msg_id` config value
/// are returned. After processing the messages, the bot should
/// update `last_msg_id` by calling [`markseen_msgs`]
/// or manually updating the value to avoid getting already
/// processed messages.
///
/// [`markseen_msgs`]: Self::markseen_msgs
async fn get_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg_ids = ctx
.get_next_msgs()
.await?
.iter()
.map(|msg_id| msg_id.to_u32())
.collect();
Ok(msg_ids)
}
/// Waits for messages to be processed by the bot and returns their IDs.
///
/// This function is similar to [`get_next_msgs`],
/// but waits for internal new message notification before returning.
/// New message notification is sent when new message is added to the database,
/// on initialization, when I/O is started and when I/O is stopped.
/// This allows bots to use `wait_next_msgs` in a loop to process
/// old messages after initialization and during the bot runtime.
/// To shutdown the bot, stopping I/O can be used to interrupt
/// pending or next `wait_next_msgs` call.
///
/// [`get_next_msgs`]: Self::get_next_msgs
async fn wait_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg_ids = ctx
.wait_next_msgs()
.await?
.iter()
.map(|msg_id| msg_id.to_u32())
.collect();
Ok(msg_ids)
}
/// Estimate the number of messages that will be deleted
/// by the set_config()-options `delete_device_after` or `delete_server_after`.
/// This is typically used to show the estimated impact to the user
/// before actually enabling deletion of old messages.
async fn estimate_auto_deletion_count(
&self,
account_id: u32,
from_server: bool,
seconds: i64,
) -> Result<usize> {
let ctx = self.get_context(account_id).await?;
message::estimate_deletion_cnt(&ctx, from_server, seconds).await
}
// ---------------------------------------------
// autocrypt
// ---------------------------------------------
async fn initiate_autocrypt_key_transfer(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
deltachat::imex::initiate_key_transfer(&ctx).await
}
async fn continue_autocrypt_key_transfer(
&self,
account_id: u32,
message_id: u32,
setup_code: String,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
deltachat::imex::continue_key_transfer(&ctx, MsgId::new(message_id), &setup_code).await
}
// ---------------------------------------------
// chat list
// ---------------------------------------------
async fn get_chatlist_entries(
&self,
account_id: u32,
list_flags: Option<u32>,
query_string: Option<String>,
query_contact_id: Option<u32>,
) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let list = Chatlist::try_load(
&ctx,
list_flags.unwrap_or(0) as usize,
query_string.as_deref(),
query_contact_id.map(ContactId::new),
)
.await?;
let mut l: Vec<u32> = Vec::with_capacity(list.len());
for i in 0..list.len() {
l.push(list.get_chat_id(i)?.to_u32());
}
Ok(l)
}
/// Returns chats similar to the given one.
///
/// Experimental API, subject to change without notice.
async fn get_similar_chat_ids(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let chat_id = ChatId::new(chat_id);
let list = chat_id
.get_similar_chat_ids(&ctx)
.await?
.into_iter()
.map(|(chat_id, _metric)| chat_id.to_u32())
.collect();
Ok(list)
}
async fn get_chatlist_items_by_entries(
&self,
account_id: u32,
entries: Vec<u32>,
) -> Result<HashMap<u32, ChatListItemFetchResult>> {
let ctx = self.get_context(account_id).await?;
let mut result: HashMap<u32, ChatListItemFetchResult> =
HashMap::with_capacity(entries.len());
for &entry in entries.iter() {
result.insert(
entry,
match get_chat_list_item_by_id(&ctx, entry).await {
Ok(res) => res,
Err(err) => ChatListItemFetchResult::Error {
id: entry,
error: format!("{err:#}"),
},
},
);
}
Ok(result)
}
// ---------------------------------------------
// chat
// ---------------------------------------------
async fn get_full_chat_by_id(&self, account_id: u32, chat_id: u32) -> Result<FullChat> {
let ctx = self.get_context(account_id).await?;
FullChat::try_from_dc_chat_id(&ctx, chat_id).await
}
/// get basic info about a chat,
/// use chatlist_get_full_chat_by_id() instead if you need more information
async fn get_basic_chat_info(&self, account_id: u32, chat_id: u32) -> Result<BasicChat> {
let ctx = self.get_context(account_id).await?;
BasicChat::try_from_dc_chat_id(&ctx, chat_id).await
}
async fn accept_chat(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id).accept(&ctx).await
}
async fn block_chat(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id).block(&ctx).await
}
/// Delete a chat.
///
/// Messages are deleted from the device and the chat database entry is deleted.
/// After that, the event #DC_EVENT_MSGS_CHANGED is posted.
///
/// Things that are _not done_ implicitly:
///
/// - Messages are **not deleted from the server**.
/// - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
/// and the user may create the chat again.
/// - **Groups are not left** - this would
/// be unexpected as (1) deleting a normal chat also does not prevent new mails
/// from arriving, (2) leaving a group requires sending a message to
/// all group members - especially for groups not used for a longer time, this is
/// really unexpected when deletion results in contacting all members again,
/// (3) only leaving groups is also a valid usecase.
///
/// To leave a chat explicitly, use leave_group()
async fn delete_chat(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id).delete(&ctx).await
}
/// Get encryption info for a chat.
/// Get a multi-line encryption info, containing encryption preferences of all members.
/// Can be used to find out why messages sent to group are not encrypted.
///
/// returns Multi-line text
async fn get_chat_encryption_info(&self, account_id: u32, chat_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id).get_encryption_info(&ctx).await
}
/// Get QR code (text and SVG) that will offer an Setup-Contact or Verified-Group invitation.
/// The QR code is compatible to the OPENPGP4FPR format
/// so that a basic fingerprint comparison also works e.g. with OpenKeychain.
///
/// The scanning device will pass the scanned content to `checkQr()` then;
/// if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
/// an out-of-band-verification can be joined using `secure_join()`
///
/// chat_id: If set to a group-chat-id,
/// the Verified-Group-Invite protocol is offered in the QR code;
/// works for protected groups as well as for normal groups.
/// If not set, the Setup-Contact protocol is offered in the QR code.
/// See https://securejoin.delta.chat/ for details about both protocols.
///
/// return format: `[code, svg]`
async fn get_chat_securejoin_qr_code_svg(
&self,
account_id: u32,
chat_id: Option<u32>,
) -> Result<(String, String)> {
let ctx = self.get_context(account_id).await?;
let chat = chat_id.map(ChatId::new);
Ok((
securejoin::get_securejoin_qr(&ctx, chat).await?,
get_securejoin_qr_svg(&ctx, chat).await?,
))
}
/// Continue a Setup-Contact or Verified-Group-Invite protocol
/// started on another device with `get_chat_securejoin_qr_code_svg()`.
/// This function is typically called when `check_qr()` returns
/// type=AskVerifyContact or type=AskVerifyGroup.
///
/// The function returns immediately and the handshake runs in background,
/// sending and receiving several messages.
/// During the handshake, info messages are added to the chat,
/// showing progress, success or errors.
///
/// Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
///
/// See https://securejoin.delta.chat/ for details about both protocols.
///
/// **qr**: The text of the scanned QR code. Typically, the same string as given
/// to `check_qr()`.
///
/// **returns**: The chat ID of the joined chat, the UI may redirect to the this chat.
/// A returned chat ID does not guarantee that the chat is protected or the belonging contact is verified.
///
async fn secure_join(&self, account_id: u32, qr: String) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let chat_id = securejoin::join_securejoin(&ctx, &qr).await?;
Ok(chat_id.to_u32())
}
async fn leave_group(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
remove_contact_from_chat(&ctx, ChatId::new(chat_id), ContactId::SELF).await
}
/// Remove a member from a group.
///
/// If the group is already _promoted_ (any message was sent to the group),
/// all group members are informed by a special status message that is sent automatically by this function.
///
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
async fn remove_contact_from_chat(
&self,
account_id: u32,
chat_id: u32,
contact_id: u32,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
remove_contact_from_chat(&ctx, ChatId::new(chat_id), ContactId::new(contact_id)).await
}
/// Add a member to a group.
///
/// If the group is already _promoted_ (any message was sent to the group),
/// all group members are informed by a special status message that is sent automatically by this function.
///
/// If the group has group protection enabled, only verified contacts can be added to the group.
///
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
async fn add_contact_to_chat(
&self,
account_id: u32,
chat_id: u32,
contact_id: u32,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
add_contact_to_chat(&ctx, ChatId::new(chat_id), ContactId::new(contact_id)).await
}
/// Get the contact IDs belonging to a chat.
///
/// - for normal chats, the function always returns exactly one contact,
/// DC_CONTACT_ID_SELF is returned only for SELF-chats.
///
/// - for group chats all members are returned, DC_CONTACT_ID_SELF is returned
/// explicitly as it may happen that oneself gets removed from a still existing
/// group
///
/// - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included
///
/// - for mailing lists, the behavior is not documented currently, we will decide on that later.
/// for now, the UI should not show the list for mailing lists.
/// (we do not know all members and there is not always a global mailing list address,
/// so we could return only SELF or the known members; this is not decided yet)
async fn get_chat_contacts(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let contacts = chat::get_chat_contacts(&ctx, ChatId::new(chat_id)).await?;
Ok(contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>())
}
/// Create a new group chat.
///
/// After creation,
/// the group has one member with the ID DC_CONTACT_ID_SELF
/// and is in _unpromoted_ state.
/// This means, you can add or remove members, change the name,
/// the group image and so on without messages being sent to all group members.
///
/// This changes as soon as the first message is sent to the group members
/// and the group becomes _promoted_.
/// After that, all changes are synced with all group members
/// by sending status message.
///
/// To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of `BasicChat` or `FullChat`.
/// This may be useful if you want to show some help for just created groups.
///
/// @param protect If set to 1 the function creates group with protection initially enabled.
/// Only verified members are allowed in these groups
/// and end-to-end-encryption is always enabled.
async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let protect = match protect {
true => ProtectionStatus::Protected,
false => ProtectionStatus::Unprotected,
};
chat::create_group_chat(&ctx, protect, &name)
.await
.map(|id| id.to_u32())
}
/// Create a new broadcast list.
///
/// Broadcast lists are similar to groups on the sending device,
/// however, recipients get the messages in a read-only chat
/// and will see who the other members are.
///
/// For historical reasons, this function does not take a name directly,
/// instead you have to set the name using dc_set_chat_name()
/// after creating the broadcast list.
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::create_broadcast_list(&ctx)
.await
.map(|id| id.to_u32())
}
/// Set group name.
///
/// If the group is already _promoted_ (any message was sent to the group),
/// all group members are informed by a special status message that is sent automatically by this function.
///
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
async fn set_chat_name(&self, account_id: u32, chat_id: u32, new_name: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
chat::set_chat_name(&ctx, ChatId::new(chat_id), &new_name).await
}
/// Set group profile image.
///
/// If the group is already _promoted_ (any message was sent to the group),
/// all group members are informed by a special status message that is sent automatically by this function.
///
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
///
/// To find out the profile image of a chat, use dc_chat_get_profile_image()
///
/// @param image_path Full path of the image to use as the group image. The image will immediately be copied to the
/// `blobdir`; the original image will not be needed anymore.
/// If you pass null here, the group image is deleted (for promoted groups, all members are informed about
/// this change anyway).
async fn set_chat_profile_image(
&self,
account_id: u32,
chat_id: u32,
image_path: Option<String>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
chat::set_chat_profile_image(&ctx, ChatId::new(chat_id), &image_path.unwrap_or_default())
.await
}
async fn set_chat_visibility(
&self,
account_id: u32,
chat_id: u32,
visibility: JSONRPCChatVisibility,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id)
.set_visibility(&ctx, visibility.into_core_type())
.await
}
async fn set_chat_ephemeral_timer(
&self,
account_id: u32,
chat_id: u32,
timer: u32,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id)
.set_ephemeral_timer(&ctx, Timer::from_u32(timer))
.await
}
async fn get_chat_ephemeral_timer(&self, account_id: u32, chat_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
Ok(ChatId::new(chat_id)
.get_ephemeral_timer(&ctx)
.await?
.to_u32())
}
/// Add a message to the device-chat.
/// Device-messages usually contain update information
/// and some hints that are added during the program runs, multi-device etc.
/// The device-message may be defined by a label;
/// if a message with the same label was added or skipped before,
/// the message is not added again, even if the message was deleted in between.
/// If needed, the device-chat is created before.
///
/// Sends the `MsgsChanged` event on success.
///
/// Setting msg to None will prevent the device message with this label from being added in the future.
async fn add_device_message(
&self,
account_id: u32,
label: String,
msg: Option<MessageData>,
) -> Result<Option<u32>> {
let ctx = self.get_context(account_id).await?;
if let Some(msg) = msg {
let mut message = msg.create_message(&ctx).await?;
let message_id =
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut message)).await?;
if !message_id.is_unset() {
return Ok(Some(message_id.to_u32()));
}
} else {
deltachat::chat::add_device_msg(&ctx, Some(&label), None).await?;
}
Ok(None)
}
/// Mark all messages in a chat as _noticed_.
/// _Noticed_ messages are no longer _fresh_ and do not count as being unseen
/// but are still waiting for being marked as "seen" using markseen_msgs()
/// (IMAP/MDNs is not done for noticed messages).
///
/// Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
/// See also markseen_msgs().
async fn marknoticed_chat(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
marknoticed_chat(&ctx, ChatId::new(chat_id)).await
}
async fn get_first_unread_message_of_chat(
&self,
account_id: u32,
chat_id: u32,
) -> Result<Option<u32>> {
let ctx = self.get_context(account_id).await?;
// TODO: implement this in core with an SQL query, that will be way faster
let messages = get_chat_msgs(&ctx, ChatId::new(chat_id)).await?;
let mut first_unread_message_id = None;
for item in messages.into_iter().rev() {
if let ChatItem::Message { msg_id } = item {
match msg_id.get_state(&ctx).await? {
MessageState::InSeen => break,
MessageState::InFresh | MessageState::InNoticed => {
first_unread_message_id = Some(msg_id)
}
_ => continue,
}
}
}
Ok(first_unread_message_id.map(|id| id.to_u32()))
}
/// Set mute duration of a chat.
///
/// The UI can then call is_chat_muted() when receiving a new message
/// to decide whether it should trigger an notification.
///
/// Muted chats should not sound or vibrate
/// and should not show a visual notification in the system area.
/// Moreover, muted chats should be excluded from global badge counter
/// (get_fresh_msgs() skips muted chats therefore)
/// and the in-app, per-chat badge counter should use a less obtrusive color.
///
/// Sends out #DC_EVENT_CHAT_MODIFIED.
async fn set_chat_mute_duration(
&self,
account_id: u32,
chat_id: u32,
duration: MuteDuration,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
chat::set_muted(&ctx, ChatId::new(chat_id), duration.try_into_core_type()?).await
}
/// Check whether the chat is currently muted (can be changed by set_chat_mute_duration()).
///
/// This is available as a standalone function outside of fullchat, because it might be only needed for notification
async fn is_chat_muted(&self, account_id: u32, chat_id: u32) -> Result<bool> {
let ctx = self.get_context(account_id).await?;
Ok(Chat::load_from_db(&ctx, ChatId::new(chat_id))
.await?
.is_muted())
}
// ---------------------------------------------
// message list
// ---------------------------------------------
/// Mark messages as presented to the user.
/// Typically, UIs call this function on scrolling through the message list,
/// when the messages are presented at least for a little moment.
/// The concrete action depends on the type of the chat and on the users settings
/// (dc_msgs_presented() may be a better name therefore, but well. :)
///
/// - For normal chats, the IMAP state is updated, MDN is sent
/// (if set_config()-options `mdns_enabled` is set)
/// and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
///
/// - For contact requests, no IMAP or MDNs is done
/// and the internal state is not changed therefore.
/// See also marknoticed_chat().
///
/// Moreover, timer is started for incoming ephemeral messages.
/// This also happens for contact requests chats.
///
/// This function updates `last_msg_id` configuration value
/// to the maximum of the current value and IDs passed to this function.
/// Bots which mark messages as seen can rely on this side effect
/// to avoid updating `last_msg_id` value manually.
///
/// One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
async fn markseen_msgs(&self, account_id: u32, msg_ids: Vec<u32>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
markseen_msgs(&ctx, msg_ids.into_iter().map(MsgId::new).collect()).await
}
async fn get_message_ids(
&self,
account_id: u32,
chat_id: u32,
info_only: bool,
add_daymarker: bool,
) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg = get_chat_msgs_ex(
&ctx,
ChatId::new(chat_id),
MessageListOptions {
info_only,
add_daymarker,
},
)
.await?;
Ok(msg
.iter()
.map(|chat_item| -> u32 {
match chat_item {
deltachat::chat::ChatItem::Message { msg_id } => msg_id.to_u32(),
deltachat::chat::ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
}
})
.collect())
}
async fn get_message_list_items(
&self,
account_id: u32,
chat_id: u32,
info_only: bool,
add_daymarker: bool,
) -> Result<Vec<JSONRPCMessageListItem>> {
let ctx = self.get_context(account_id).await?;
let msg = get_chat_msgs_ex(
&ctx,
ChatId::new(chat_id),
MessageListOptions {
info_only,
add_daymarker,
},
)
.await?;
Ok(msg
.iter()
.map(|chat_item| (*chat_item).into())
.collect::<Vec<JSONRPCMessageListItem>>())
}
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
let ctx = self.get_context(account_id).await?;
let msg_id = MsgId::new(msg_id);
MessageObject::from_msg_id(&ctx, msg_id)
.await
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))
}
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
MsgId::new(message_id).get_html(&ctx).await
}
/// get multiple messages in one call,
/// if loading one message fails the error is stored in the result object in it's place.
///
/// this is the batch variant of [get_message]
async fn get_messages(
&self,
account_id: u32,
message_ids: Vec<u32>,
) -> Result<HashMap<u32, MessageLoadResult>> {
let ctx = self.get_context(account_id).await?;
let mut messages: HashMap<u32, MessageLoadResult> = HashMap::new();
for message_id in message_ids {
let message_result = MessageObject::from_msg_id(&ctx, MsgId::new(message_id)).await;
messages.insert(
message_id,
match message_result {
Ok(message) => MessageLoadResult::Message(message),
Err(error) => MessageLoadResult::LoadingError {
error: format!("{error:#}"),
},
},
);
}
Ok(messages)
}
/// Fetch info desktop needs for creating a notification for a message
async fn get_message_notification_info(
&self,
account_id: u32,
message_id: u32,
) -> Result<MessageNotificationInfo> {
let ctx = self.get_context(account_id).await?;
MessageNotificationInfo::from_msg_id(&ctx, MsgId::new(message_id)).await
}
/// Delete messages. The messages are deleted on the current device and
/// on the IMAP server.
async fn delete_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
delete_msgs(&ctx, &msgs).await
}
/// Get an informational text for a single message. The text is multiline and may
/// contain e.g. the raw text of the message.
///
/// The max. text returned is typically longer (about 100000 characters) than the
/// max. text returned by dc_msg_get_text() (about 30000 characters).
async fn get_message_info(&self, account_id: u32, message_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
MsgId::new(message_id).get_info(&ctx).await
}
/// Returns additional information for single message.
async fn get_message_info_object(
&self,
account_id: u32,
message_id: u32,
) -> Result<MessageInfo> {
let ctx = self.get_context(account_id).await?;
MessageInfo::from_msg_id(&ctx, MsgId::new(message_id)).await
}
/// Returns contacts that sent read receipts and the time of reading.
async fn get_message_read_receipts(
&self,
account_id: u32,
message_id: u32,
) -> Result<Vec<MessageReadReceipt>> {
let ctx = self.get_context(account_id).await?;
let receipts = get_msg_read_receipts(&ctx, MsgId::new(message_id))
.await?
.iter()
.map(|(contact_id, ts)| MessageReadReceipt {
contact_id: contact_id.to_u32(),
timestamp: *ts,
})
.collect();
Ok(receipts)
}
/// Asks the core to start downloading a message fully.
/// This function is typically called when the user hits the "Download" button
/// that is shown by the UI in case `download_state` is `'Available'` or `'Failure'`
///
/// On success, the @ref DC_MSG "view type of the message" may change
/// or the message may be replaced completely by one or more messages with other message IDs.
/// That may happen e.g. in cases where the message was encrypted
/// and the type could not be determined without fully downloading.
/// Downloaded content can be accessed as usual after download.
///
/// To reflect these changes a @ref DC_EVENT_MSGS_CHANGED event will be emitted.
async fn download_full_message(&self, account_id: u32, message_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
MsgId::new(message_id).download_full(&ctx).await
}
/// Search messages containing the given query string.
/// Searching can be done globally (chat_id=None) or in a specified chat only (chat_id set).
///
/// Global search results are typically displayed using dc_msg_get_summary(), chat
/// search results may just highlight the corresponding messages and present a
/// prev/next button.
///
/// For the global search, the result is limited to 1000 messages,
/// this allows an incremental search done fast.
/// So, when getting exactly 1000 messages, the result actually may be truncated;
/// the UIs may display sth. like "1000+ messages found" in this case.
/// The chat search (if chat_id is set) is not limited.
async fn search_messages(
&self,
account_id: u32,
query: String,
chat_id: Option<u32>,
) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let messages = ctx.search_msgs(chat_id.map(ChatId::new), &query).await?;
Ok(messages
.iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>())
}
async fn message_ids_to_search_results(
&self,
account_id: u32,
message_ids: Vec<u32>,
) -> Result<HashMap<u32, MessageSearchResult>> {
let ctx = self.get_context(account_id).await?;
let mut results: HashMap<u32, MessageSearchResult> =
HashMap::with_capacity(message_ids.len());
for id in message_ids {
results.insert(
id,
MessageSearchResult::from_msg_id(&ctx, MsgId::new(id)).await?,
);
}
Ok(results)
}
// ---------------------------------------------
// contact
// ---------------------------------------------
/// Get a single contact options by ID.
async fn get_contact(&self, account_id: u32, contact_id: u32) -> Result<ContactObject> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
ContactObject::try_from_dc_contact(
&ctx,
deltachat::contact::Contact::get_by_id(&ctx, contact_id).await?,
)
.await
}
/// Add a single contact as a result of an explicit user action.
///
/// Returns contact id of the created or existing contact
async fn create_contact(
&self,
account_id: u32,
email: String,
name: Option<String>,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
if !may_be_valid_addr(&email) {
bail!(anyhow!(
"provided email address is not a valid email address"
))
}
let contact_id = Contact::create(&ctx, &name.unwrap_or_default(), &email).await?;
Ok(contact_id.to_u32())
}
/// Returns contact id of the created or existing DM chat with that contact
async fn create_chat_by_contact_id(&self, account_id: u32, contact_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let contact = Contact::get_by_id(&ctx, ContactId::new(contact_id)).await?;
ChatId::create_for_contact(&ctx, contact.id)
.await
.map(|id| id.to_u32())
}
async fn block_contact(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
Contact::block(&ctx, ContactId::new(contact_id)).await
}
async fn unblock_contact(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
Contact::unblock(&ctx, ContactId::new(contact_id)).await
}
async fn get_blocked_contacts(&self, account_id: u32) -> Result<Vec<ContactObject>> {
let ctx = self.get_context(account_id).await?;
let blocked_ids = Contact::get_all_blocked(&ctx).await?;
let mut contacts: Vec<ContactObject> = Vec::with_capacity(blocked_ids.len());
for id in blocked_ids {
contacts.push(
ContactObject::try_from_dc_contact(
&ctx,
deltachat::contact::Contact::get_by_id(&ctx, id).await?,
)
.await?,
);
}
Ok(contacts)
}
async fn get_contact_ids(
&self,
account_id: u32,
list_flags: u32,
query: Option<String>,
) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let contacts = Contact::get_all(&ctx, list_flags, query.as_deref()).await?;
Ok(contacts.into_iter().map(|c| c.to_u32()).collect())
}
/// Get a list of contacts.
/// (formerly called getContacts2 in desktop)
async fn get_contacts(
&self,
account_id: u32,
list_flags: u32,
query: Option<String>,
) -> Result<Vec<ContactObject>> {
let ctx = self.get_context(account_id).await?;
let contact_ids = Contact::get_all(&ctx, list_flags, query.as_deref()).await?;
let mut contacts: Vec<ContactObject> = Vec::with_capacity(contact_ids.len());
for id in contact_ids {
contacts.push(
ContactObject::try_from_dc_contact(
&ctx,
deltachat::contact::Contact::get_by_id(&ctx, id).await?,
)
.await?,
);
}
Ok(contacts)
}
async fn get_contacts_by_ids(
&self,
account_id: u32,
ids: Vec<u32>,
) -> Result<HashMap<u32, ContactObject>> {
let ctx = self.get_context(account_id).await?;
let mut contacts = HashMap::with_capacity(ids.len());
for id in ids {
contacts.insert(
id,
ContactObject::try_from_dc_contact(
&ctx,
deltachat::contact::Contact::get_by_id(&ctx, ContactId::new(id)).await?,
)
.await?,
);
}
Ok(contacts)
}
async fn delete_contact(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
Contact::delete(&ctx, contact_id).await?;
Ok(())
}
async fn change_contact_name(
&self,
account_id: u32,
contact_id: u32,
name: String,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
let contact = Contact::get_by_id(&ctx, contact_id).await?;
let addr = contact.get_addr();
Contact::create(&ctx, &name, addr).await?;
Ok(())
}
/// Get encryption info for a contact.
/// Get a multi-line encryption info, containing your fingerprint and the
/// fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
async fn get_contact_encryption_info(
&self,
account_id: u32,
contact_id: u32,
) -> Result<String> {
let ctx = self.get_context(account_id).await?;
Contact::get_encrinfo(&ctx, ContactId::new(contact_id)).await
}
/// Check if an e-mail address belongs to a known and unblocked contact.
/// To get a list of all known and unblocked contacts, use contacts_get_contacts().
///
/// To validate an e-mail address independently of the contact database
/// use check_email_validity().
async fn lookup_contact_id_by_addr(
&self,
account_id: u32,
addr: String,
) -> Result<Option<u32>> {
let ctx = self.get_context(account_id).await?;
let contact_id = Contact::lookup_id_by_addr(&ctx, &addr, Origin::IncomingReplyTo).await?;
Ok(contact_id.map(|id| id.to_u32()))
}
// ---------------------------------------------
// chat
// ---------------------------------------------
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
///
/// If it does not exist, `None` is returned.
async fn get_chat_id_by_contact_id(
&self,
account_id: u32,
contact_id: u32,
) -> Result<Option<u32>> {
let ctx = self.get_context(account_id).await?;
let chat_id = ChatId::lookup_by_contact(&ctx, ContactId::new(contact_id)).await?;
Ok(chat_id.map(|id| id.to_u32()))
}
/// Returns all message IDs of the given types in a chat.
/// Typically used to show a gallery.
///
/// The list is already sorted and starts with the oldest message.
/// Clients should not try to re-sort the list as this would be an expensive action
/// and would result in inconsistencies between clients.
///
/// Setting `chat_id` to `None` (`null` in typescript) means get messages with media
/// from any chat of the currently used account.
async fn get_chat_media(
&self,
account_id: u32,
chat_id: Option<u32>,
message_type: MessageViewtype,
or_message_type2: Option<MessageViewtype>,
or_message_type3: Option<MessageViewtype>,
) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let chat_id = match chat_id {
None | Some(0) => None,
Some(id) => Some(ChatId::new(id)),
};
let msg_type = message_type.into();
let or_msg_type2 = or_message_type2.map_or(Viewtype::Unknown, |v| v.into());
let or_msg_type3 = or_message_type3.map_or(Viewtype::Unknown, |v| v.into());
let media = get_chat_media(&ctx, chat_id, msg_type, or_msg_type2, or_msg_type3).await?;
Ok(media.iter().map(|msg_id| msg_id.to_u32()).collect())
}
/// Search next/previous message based on a given message and a list of types.
/// Typically used to implement the "next" and "previous" buttons
/// in a gallery or in a media player.
///
/// one combined call for getting chat::get_next_media for both directions
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
///
/// Deprecated 2023-10-03, use `get_chat_media` method
/// and navigate the returned array instead.
#[allow(deprecated)]
async fn get_neighboring_chat_media(
&self,
account_id: u32,
msg_id: u32,
message_type: MessageViewtype,
or_message_type2: Option<MessageViewtype>,
or_message_type3: Option<MessageViewtype>,
) -> Result<(Option<u32>, Option<u32>)> {
let ctx = self.get_context(account_id).await?;
let msg_type: Viewtype = message_type.into();
let msg_type2: Viewtype = or_message_type2.map(|v| v.into()).unwrap_or_default();
let msg_type3: Viewtype = or_message_type3.map(|v| v.into()).unwrap_or_default();
let prev = chat::get_next_media(
&ctx,
MsgId::new(msg_id),
chat::Direction::Backward,
msg_type,
msg_type2,
msg_type3,
)
.await?
.map(|id| id.to_u32());
let next = chat::get_next_media(
&ctx,
MsgId::new(msg_id),
chat::Direction::Forward,
msg_type,
msg_type2,
msg_type3,
)
.await?
.map(|id| id.to_u32());
Ok((prev, next))
}
// ---------------------------------------------
// backup
// ---------------------------------------------
async fn export_backup(
&self,
account_id: u32,
destination: String,
passphrase: Option<String>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
imex::imex(
&ctx,
imex::ImexMode::ExportBackup,
destination.as_ref(),
passphrase,
)
.await
}
async fn import_backup(
&self,
account_id: u32,
path: String,
passphrase: Option<String>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
imex::imex(
&ctx,
imex::ImexMode::ImportBackup,
path.as_ref(),
passphrase,
)
.await
}
/// Offers a backup for remote devices to retrieve.
///
/// Can be cancelled by stopping the ongoing process. Success or failure can be tracked
/// via the `ImexProgress` event which should either reach `1000` for success or `0` for
/// failure.
///
/// This **stops IO** while it is running.
///
/// Returns once a remote device has retrieved the backup, or is cancelled.
async fn provide_backup(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let provider = imex::BackupProvider::prepare(&ctx).await?;
self.with_state(account_id, |state| {
state.backup_provider_qr.send_replace(Some(provider.qr()));
})
.await;
let res = provider.await;
self.with_state(account_id, |state| {
state.backup_provider_qr.send_replace(None);
})
.await;
res
}
/// Returns the text of the QR code for the running [`CommandApi::provide_backup`].
///
/// This QR code text can be used in [`CommandApi::get_backup`] on a second device to
/// retrieve the backup and setup this second device.
///
/// This call will block until the QR code is ready,
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
/// but will fail after 10 seconds to avoid deadlocks.
async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
let qr = tokio::time::timeout(
Duration::from_secs(10),
self.inner_get_backup_qr(account_id),
)
.await
.context("Backup provider did not start in time")?
.context("Failed to get backup QR code")?;
qr::format_backup(&qr)
}
/// Returns the rendered QR code for the running [`CommandApi::provide_backup`].
///
/// This QR code can be used in [`CommandApi::get_backup`] on a second device to
/// retrieve the backup and setup this second device.
///
/// This call will block until the QR code is ready,
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
/// but will fail after 10 seconds to avoid deadlocks.
///
/// Returns the QR code rendered as an SVG image.
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let qr = tokio::time::timeout(
Duration::from_secs(10),
self.inner_get_backup_qr(account_id),
)
.await
.context("Backup provider did not start in time")?
.context("Failed to get backup QR code")?;
generate_backup_qr(&ctx, &qr).await
}
/// Gets a backup from a remote provider.
///
/// This retrieves the backup from a remote device over the network and imports it into
/// the current device.
///
/// Can be cancelled by stopping the ongoing process.
///
/// Do not forget to call start_io on the account after a successful import,
/// otherwise it will not connect to the email server.
async fn get_backup(&self, account_id: u32, qr_text: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let qr = qr::check_qr(&ctx, &qr_text).await?;
imex::get_backup(&ctx, qr).await?;
Ok(())
}
// ---------------------------------------------
// connectivity
// ---------------------------------------------
/// Indicate that the network likely has come back.
/// or just that the network conditions might have changed
async fn maybe_network(&self) -> Result<()> {
self.accounts.read().await.maybe_network().await;
Ok(())
}
/// Get the current connectivity, i.e. whether the device is connected to the IMAP server.
/// One of:
/// - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
/// - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
/// - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
/// - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
///
/// We don't use exact values but ranges here so that we can split up
/// states into multiple states in the future.
///
/// Meant as a rough overview that can be shown
/// e.g. in the title of the main screen.
///
/// If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
async fn get_connectivity(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.get_connectivity().await as u32)
}
/// Get an overview of the current connectivity, and possibly more statistics.
/// Meant to give the user more insight about the current status than
/// the basic connectivity info returned by get_connectivity(); show this
/// e.g., if the user taps on said basic connectivity info.
///
/// If this page changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
///
/// This comes as an HTML from the core so that we can easily improve it
/// and the improvement instantly reaches all UIs.
async fn get_connectivity_html(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
ctx.get_connectivity_html().await
}
// ---------------------------------------------
// locations
// ---------------------------------------------
async fn get_locations(
&self,
account_id: u32,
chat_id: Option<u32>,
contact_id: Option<u32>,
timestamp_begin: i64,
timestamp_end: i64,
) -> Result<Vec<JsonrpcLocation>> {
let ctx = self.get_context(account_id).await?;
let locations = location::get_range(
&ctx,
chat_id.map(ChatId::new),
contact_id,
timestamp_begin,
timestamp_end,
)
.await?;
Ok(locations.into_iter().map(|l| l.into()).collect())
}
// ---------------------------------------------
// webxdc
// ---------------------------------------------
async fn send_webxdc_status_update(
&self,
account_id: u32,
instance_msg_id: u32,
update_str: String,
description: String,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.send_webxdc_status_update(MsgId::new(instance_msg_id), &update_str, &description)
.await
}
async fn get_webxdc_status_updates(
&self,
account_id: u32,
instance_msg_id: u32,
last_known_serial: u32,
) -> Result<String> {
let ctx = self.get_context(account_id).await?;
ctx.get_webxdc_status_updates(
MsgId::new(instance_msg_id),
StatusUpdateSerial::new(last_known_serial),
)
.await
}
/// Get info from a webxdc message
async fn get_webxdc_info(
&self,
account_id: u32,
instance_msg_id: u32,
) -> Result<WebxdcMessageInfo> {
let ctx = self.get_context(account_id).await?;
WebxdcMessageInfo::get_for_message(&ctx, MsgId::new(instance_msg_id)).await
}
/// Get blob encoded as base64 from a webxdc message
///
/// path is the path of the file within webxdc archive
async fn get_webxdc_blob(
&self,
account_id: u32,
instance_msg_id: u32,
path: String,
) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
let blob = message.get_webxdc_blob(&ctx, &path).await?;
use base64::{engine::general_purpose, Engine as _};
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
}
/// Makes an HTTP GET request and returns a response.
///
/// `url` is the HTTP or HTTPS URL.
async fn get_http_response(&self, account_id: u32, url: String) -> Result<HttpResponse> {
let ctx = self.get_context(account_id).await?;
let response = deltachat::net::read_url_blob(&ctx, &url).await?.into();
Ok(response)
}
/// Forward messages to another chat.
///
/// All types of messages can be forwarded,
/// however, they will be flagged as such (dc_msg_is_forwarded() is set).
///
/// Original sender, info-state and webxdc updates are not forwarded on purpose.
async fn forward_messages(
&self,
account_id: u32,
message_ids: Vec<u32>,
chat_id: u32,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let message_ids: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
}
/// Resend messages and make information available for newly added chat members.
/// Resending sends out the original message, however, recipients and webxdc-status may differ.
/// Clients that already have the original message can still ignore the resent message as
/// they have tracked the state by dedicated updates.
///
/// Some messages cannot be resent, eg. info-messages, drafts, already pending messages or messages that are not sent by SELF.
///
/// message_ids all message IDs that should be resend. All messages must belong to the same chat.
async fn resend_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let message_ids: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
chat::resend_msgs(&ctx, &message_ids).await
}
async fn send_sticker(
&self,
account_id: u32,
chat_id: u32,
sticker_path: String,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file(&sticker_path, None);
// JSON-rpc does not need heuristics to turn [Viewtype::Sticker] into [Viewtype::Image]
msg.force_sticker();
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
Ok(message_id.to_u32())
}
/// Send a reaction to message.
///
/// Reaction is a string of emojis separated by spaces. Reaction to a
/// single message can be sent multiple times. The last reaction
/// received overrides all previously received reactions. It is
/// possible to remove all reactions by sending an empty string.
async fn send_reaction(
&self,
account_id: u32,
message_id: u32,
reaction: Vec<String>,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let message_id = send_reaction(&ctx, MsgId::new(message_id), &reaction.join(" ")).await?;
Ok(message_id.to_u32())
}
/// Returns reactions to the message.
async fn get_message_reactions(
&self,
account_id: u32,
message_id: u32,
) -> Result<Option<JSONRPCReactions>> {
let ctx = self.get_context(account_id).await?;
let reactions = get_msg_reactions(&ctx, MsgId::new(message_id)).await?;
if reactions.is_empty() {
Ok(None)
} else {
Ok(Some(reactions.into()))
}
}
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut message = data.create_message(&ctx).await?;
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
.await?
.to_u32();
Ok(msg_id)
}
/// Checks if messages can be sent to a given chat.
async fn can_send(&self, account_id: u32, chat_id: u32) -> Result<bool> {
let ctx = self.get_context(account_id).await?;
let chat_id = ChatId::new(chat_id);
let chat = Chat::load_from_db(&ctx, chat_id).await?;
let can_send = chat.can_send(&ctx).await?;
Ok(can_send)
}
/// Saves a file copy at the user-provided path.
///
/// Fails if file already exists at the provided path.
async fn save_msg_file(&self, account_id: u32, msg_id: u32, path: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let message = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
message.save_file(&ctx, Path::new(&path)).await
}
// ---------------------------------------------
// functions for the composer
// the composer is the message input field
// ---------------------------------------------
async fn remove_draft(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id).set_draft(&ctx, None).await
}
/// Get draft for a chat, if any.
async fn get_draft(&self, account_id: u32, chat_id: u32) -> Result<Option<MessageObject>> {
let ctx = self.get_context(account_id).await?;
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
Ok(Some(
MessageObject::from_msg_id(&ctx, draft.get_id()).await?,
))
} else {
Ok(None)
}
}
async fn send_videochat_invitation(&self, account_id: u32, chat_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::send_videochat_invitation(&ctx, ChatId::new(chat_id))
.await
.map(|msg_id| msg_id.to_u32())
}
// ---------------------------------------------
// misc prototyping functions
// that might get removed later again
// ---------------------------------------------
async fn misc_get_sticker_folder(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let account_folder = ctx
.get_dbfile()
.parent()
.context("account folder not found")?;
let sticker_folder_path = account_folder.join("stickers");
fs::create_dir_all(&sticker_folder_path).await?;
sticker_folder_path
.to_str()
.map(|s| s.to_owned())
.context("path conversion to string failed")
}
/// Saves a sticker to a collection/folder in the account's sticker folder.
async fn misc_save_sticker(
&self,
account_id: u32,
msg_id: u32,
collection: String,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let message = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
ensure!(
message.get_viewtype() == Viewtype::Sticker,
"message {} is not a sticker",
msg_id
);
let account_folder = ctx
.get_dbfile()
.parent()
.context("account folder not found")?;
ensure!(
is_sanitized(&collection),
"illegal characters in collection name"
);
let destination_path = account_folder.join("stickers").join(collection);
fs::create_dir_all(&destination_path).await?;
let file = message.get_filename().context("no file?")?;
message
.save_file(
&ctx,
&destination_path.join(format!(
"{}.{}",
msg_id,
Path::new(&file)
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
)),
)
.await?;
Ok(())
}
/// for desktop, get stickers from stickers folder,
/// grouped by the collection/folder they are in.
async fn misc_get_stickers(&self, account_id: u32) -> Result<HashMap<String, Vec<String>>> {
let ctx = self.get_context(account_id).await?;
let account_folder = ctx
.get_dbfile()
.parent()
.context("account folder not found")?;
let sticker_folder_path = account_folder.join("stickers");
fs::create_dir_all(&sticker_folder_path).await?;
let mut result = HashMap::new();
let mut packs = tokio::fs::read_dir(sticker_folder_path).await?;
while let Some(entry) = packs.next_entry().await? {
if !entry.file_type().await?.is_dir() {
continue;
}
let pack_name = entry.file_name().into_string().unwrap_or_default();
let mut stickers = tokio::fs::read_dir(entry.path()).await?;
let mut sticker_paths = Vec::new();
while let Some(sticker_entry) = stickers.next_entry().await? {
if !sticker_entry.file_type().await?.is_file() {
continue;
}
let sticker_name = sticker_entry.file_name().into_string().unwrap_or_default();
if sticker_name.ends_with(".png") || sticker_name.ends_with(".webp") {
sticker_paths.push(
sticker_entry
.path()
.to_str()
.map(|s| s.to_owned())
.context("path conversion to string failed")?,
);
}
}
if !sticker_paths.is_empty() {
result.insert(pack_name, sticker_paths);
}
}
Ok(result)
}
/// Returns the messageid of the sent message
async fn misc_send_text_message(
&self,
account_id: u32,
chat_id: u32,
text: String,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(text);
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
Ok(message_id.to_u32())
}
// mimics the old desktop call, will get replaced with something better in the composer rewrite,
// the better version will just be sending the current draft, though there will be probably something similar with more options to this for the corner cases like setting a marker on the map
async fn misc_send_msg(
&self,
account_id: u32,
chat_id: u32,
text: Option<String>,
file: Option<String>,
location: Option<(f64, f64)>,
quoted_message_id: Option<u32>,
) -> Result<(u32, MessageObject)> {
let ctx = self.get_context(account_id).await?;
let mut message = Message::new(if file.is_some() {
Viewtype::File
} else {
Viewtype::Text
});
message.set_text(text.unwrap_or_default());
if let Some(file) = file {
message.set_file(file, None);
}
if let Some((latitude, longitude)) = location {
message.set_location(latitude, longitude);
}
if let Some(id) = quoted_message_id {
message
.set_quote(
&ctx,
Some(
&Message::load_from_db(&ctx, MsgId::new(id))
.await
.context("message to quote could not be loaded")?,
),
)
.await?;
}
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message).await?;
let message = MessageObject::from_msg_id(&ctx, msg_id).await?;
Ok((msg_id.to_u32(), message))
}
// mimics the old desktop call, will get replaced with something better in the composer rewrite,
// the better version should support:
// - changing viewtype to enable/disable compression
// - keeping same message id as long as attachment does not change for webxdc messages
async fn misc_set_draft(
&self,
account_id: u32,
chat_id: u32,
text: Option<String>,
file: Option<String>,
quoted_message_id: Option<u32>,
view_type: Option<MessageViewtype>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let mut draft = Message::new(view_type.map_or_else(
|| {
if file.is_some() {
Viewtype::File
} else {
Viewtype::Text
}
},
|v| v.into(),
));
draft.set_text(text.unwrap_or_default());
if let Some(file) = file {
draft.set_file(file, None);
}
if let Some(id) = quoted_message_id {
draft
.set_quote(
&ctx,
Some(
&Message::load_from_db(&ctx, MsgId::new(id))
.await
.context("message to quote could not be loaded")?,
),
)
.await?;
}
ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await
}
// send the chat's current set draft
async fn misc_send_draft(&self, account_id: u32, chat_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
let mut draft = draft;
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut draft)
.await?
.to_u32();
Ok(msg_id)
} else {
Err(anyhow!(
"chat with id {} doesn't have draft message",
chat_id
))
}
}
}
// Helper functions (to prevent code duplication)
async fn set_config(
ctx: &deltachat::context::Context,
key: &str,
value: Option<&str>,
) -> Result<(), anyhow::Error> {
if key.starts_with("ui.") {
ctx.set_ui_config(key, value).await?;
} else {
ctx.set_config(
Config::from_str(key).with_context(|| format!("unknown key {key:?}"))?,
value,
)
.await?;
}
Ok(())
}
async fn get_config(
ctx: &deltachat::context::Context,
key: &str,
) -> Result<Option<String>, anyhow::Error> {
if key.starts_with("ui.") {
ctx.get_ui_config(key).await
} else {
ctx.get_config(Config::from_str(key).with_context(|| format!("unknown key {key:?}"))?)
.await
}
}