mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 21:46:35 +03:00
2273 lines
84 KiB
Rust
2273 lines
84 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::path::Path;
|
|
use std::str;
|
|
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::location;
|
|
use deltachat::message::get_msg_read_receipts;
|
|
use deltachat::message::{
|
|
self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
|
};
|
|
use deltachat::peer_channels::{
|
|
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
|
|
};
|
|
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 deltachat::{imex, info};
|
|
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, VcardContact};
|
|
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
|
|
}
|
|
|
|
/// Imports/migrated an existing account from a database path into this account manager.
|
|
/// Returns the ID of new account.
|
|
async fn migrate_account(&self, path_to_db: String) -> Result<u32> {
|
|
self.accounts
|
|
.write()
|
|
.await
|
|
.migrate_account(std::path::PathBuf::from(path_to_db))
|
|
.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<()> {
|
|
let future = {
|
|
let lock = self.accounts.read().await;
|
|
lock.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
|
|
};
|
|
// At this point account manager is not locked anymore.
|
|
future.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 proxy_enabled = ctx
|
|
.get_config_bool(deltachat::config::Config::ProxyEnabled)
|
|
.await?;
|
|
|
|
let provider_info =
|
|
get_provider_info(&ctx, email.split('@').last().unwrap_or(""), proxy_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 get_blob_dir(&self, account_id: u32) -> Result<Option<String>> {
|
|
let ctx = self.get_context(account_id).await?;
|
|
Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned()))
|
|
}
|
|
|
|
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 that will offer a [SecureJoin](https://securejoin.delta.chat/) invitation.
|
|
///
|
|
/// If `chat_id` is a group chat ID, SecureJoin QR code for the group is returned.
|
|
/// If `chat_id` is unset, setup contact QR code is returned.
|
|
async fn get_chat_securejoin_qr_code(
|
|
&self,
|
|
account_id: u32,
|
|
chat_id: Option<u32>,
|
|
) -> Result<String> {
|
|
let ctx = self.get_context(account_id).await?;
|
|
let chat = chat_id.map(ChatId::new);
|
|
let qr = securejoin::get_securejoin_qr(&ctx, chat).await?;
|
|
Ok(qr)
|
|
}
|
|
|
|
/// Get QR code (text and SVG) that will offer a 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);
|
|
let qr = securejoin::get_securejoin_qr(&ctx, chat).await?;
|
|
let svg = get_securejoin_qr_svg(&ctx, chat).await?;
|
|
Ok((qr, svg))
|
|
}
|
|
|
|
/// 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);
|
|
let message_object = MessageObject::from_msg_id(&ctx, msg_id)
|
|
.await
|
|
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))?
|
|
.with_context(|| format!("Message {msg_id} does not exist for account {account_id}"))?;
|
|
Ok(message_object)
|
|
}
|
|
|
|
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(Some(message)) => MessageLoadResult::Message(message),
|
|
Ok(None) => MessageLoadResult::LoadingError {
|
|
error: "Message does not exist".to_string(),
|
|
},
|
|
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(())
|
|
}
|
|
|
|
/// Resets contact encryption.
|
|
async fn reset_contact_encryption(&self, account_id: u32, contact_id: u32) -> Result<()> {
|
|
let ctx = self.get_context(account_id).await?;
|
|
let contact_id = ContactId::new(contact_id);
|
|
|
|
contact_id.reset_encryption(&ctx).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()))
|
|
}
|
|
|
|
/// Parses a vCard file located at the given path. Returns contacts in their original order.
|
|
async fn parse_vcard(&self, path: String) -> Result<Vec<VcardContact>> {
|
|
let vcard = fs::read(Path::new(&path)).await?;
|
|
let vcard = str::from_utf8(&vcard)?;
|
|
Ok(deltachat_contact_tools::parse_vcard(vcard)
|
|
.into_iter()
|
|
.map(|c| c.into())
|
|
.collect())
|
|
}
|
|
|
|
/// Imports contacts from a vCard file located at the given path.
|
|
///
|
|
/// Returns the ids of created/modified contacts in the order they appear in the vCard.
|
|
async fn import_vcard(&self, account_id: u32, path: String) -> Result<Vec<u32>> {
|
|
let ctx = self.get_context(account_id).await?;
|
|
let vcard = tokio::fs::read(Path::new(&path)).await?;
|
|
let vcard = str::from_utf8(&vcard)?;
|
|
Ok(deltachat::contact::import_vcard(&ctx, vcard)
|
|
.await?
|
|
.into_iter()
|
|
.map(|c| c.to_u32())
|
|
.collect())
|
|
}
|
|
|
|
/// Returns a vCard containing contacts with the given ids.
|
|
async fn make_vcard(&self, account_id: u32, contacts: Vec<u32>) -> Result<String> {
|
|
let ctx = self.get_context(account_id).await?;
|
|
let contacts: Vec<_> = contacts.iter().map(|&c| ContactId::new(c)).collect();
|
|
deltachat::contact::make_vcard(&ctx, &contacts).await
|
|
}
|
|
|
|
/// Sets vCard containing the given contacts to the message draft.
|
|
async fn set_draft_vcard(
|
|
&self,
|
|
account_id: u32,
|
|
msg_id: u32,
|
|
contacts: Vec<u32>,
|
|
) -> Result<()> {
|
|
let ctx = self.get_context(account_id).await?;
|
|
let contacts: Vec<_> = contacts.iter().map(|&c| ContactId::new(c)).collect();
|
|
let mut msg = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
|
msg.make_vcard(&ctx, &contacts).await?;
|
|
msg.get_chat_id().set_draft(&ctx, Some(&mut msg)).await
|
|
}
|
|
|
|
// ---------------------------------------------
|
|
// 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())
|
|
}
|
|
|
|
// ---------------------------------------------
|
|
// 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 60 seconds to avoid deadlocks.
|
|
async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
|
|
let qr = tokio::time::timeout(
|
|
Duration::from_secs(60),
|
|
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 60 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(60),
|
|
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 send_webxdc_realtime_data(
|
|
&self,
|
|
account_id: u32,
|
|
instance_msg_id: u32,
|
|
data: Vec<u8>,
|
|
) -> Result<()> {
|
|
let ctx = self.get_context(account_id).await?;
|
|
send_webxdc_realtime_data(&ctx, MsgId::new(instance_msg_id), data).await
|
|
}
|
|
|
|
async fn send_webxdc_realtime_advertisement(
|
|
&self,
|
|
account_id: u32,
|
|
instance_msg_id: u32,
|
|
) -> Result<()> {
|
|
let ctx = self.get_context(account_id).await?;
|
|
let fut = send_webxdc_realtime_advertisement(&ctx, MsgId::new(instance_msg_id)).await?;
|
|
if let Some(fut) = fut {
|
|
tokio::spawn(async move {
|
|
fut.await.ok();
|
|
info!(ctx, "send_webxdc_realtime_advertisement done")
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn leave_webxdc_realtime(&self, account_id: u32, instance_message_id: u32) -> Result<()> {
|
|
let ctx = self.get_context(account_id).await?;
|
|
leave_webxdc_realtime(&ctx, MsgId::new(instance_message_id)).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))
|
|
}
|
|
|
|
/// Sets Webxdc file as integration.
|
|
/// `file` is the .xdc to use as Webxdc integration.
|
|
async fn set_webxdc_integration(&self, account_id: u32, file_path: String) -> Result<()> {
|
|
let ctx = self.get_context(account_id).await?;
|
|
ctx.set_webxdc_integration(&file_path).await
|
|
}
|
|
|
|
/// Returns Webxdc instance used for optional integrations.
|
|
/// UI can open the Webxdc as usual.
|
|
/// Returns `None` if there is no integration; the caller can add one using `set_webxdc_integration` then.
|
|
/// `integrate_for` is the chat to get the integration for.
|
|
async fn init_webxdc_integration(
|
|
&self,
|
|
account_id: u32,
|
|
chat_id: Option<u32>,
|
|
) -> Result<Option<u32>> {
|
|
let ctx = self.get_context(account_id).await?;
|
|
Ok(ctx
|
|
.init_webxdc_integration(chat_id.map(ChatId::new))
|
|
.await?
|
|
.map(|msg_id| msg_id.to_u32()))
|
|
}
|
|
|
|
/// 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
|
|
.context("Failed to create message")?;
|
|
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
|
|
.await
|
|
.context("Failed to send created message")?
|
|
.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(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_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?
|
|
.context("Just sent message does not exist")?;
|
|
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
|
|
}
|
|
}
|