Merge remote-tracking branch 'origin/master' into hoc/migitate-from-forgery

This commit is contained in:
Hocuri
2022-10-24 18:02:26 +02:00
45 changed files with 1718 additions and 351 deletions

View File

@@ -52,6 +52,7 @@ jobs:
npm install --verbose
- name: Test
timeout-minutes: 10
if: runner.os != 'Windows'
run: |
cd node
@@ -59,6 +60,7 @@ jobs:
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
- name: Run tests on Windows, except lint
timeout-minutes: 10
if: runner.os == 'Windows'
run: |
cd node

View File

@@ -2,6 +2,19 @@
## Unreleased
### API-Changes
- jsonrpc: typescript client: export constants under `C` enum, similar to how its exported from `deltachat-node` #3681
- added reactions support #3644
- jsonrpc: reactions: added reactions to `Message` type and the `sendReaction()` method #3686
### Changes
- simplify `UPSERT` queries #3676
### Fixes
## 1.97.0
### API-Changes
- jsonrpc: add function: #3641, #3645, #3653
- `getChatContacts()`
@@ -33,6 +46,8 @@
- `stopIo()`
- `exportBackup()`
- `importBackup()`
- `getMessageHtml()` #3671
- `miscGetStickerFolder` and `miscGetStickers` #3672
- breaking: jsonrpc: remove function `messageListGetMessageIds()`, it is replaced by `getMessageIds()` and `getMessageListItems()` the latter returns a new `MessageListItem` type, which is the now prefered way of using the message list.
- jsonrpc: add type: #3641, #3645
- `MessageSearchResult`

26
Cargo.lock generated
View File

@@ -895,7 +895,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.96.0"
version = "1.97.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -967,7 +967,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.96.0"
version = "1.97.0"
dependencies = [
"anyhow",
"async-channel",
@@ -996,7 +996,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.96.0"
version = "1.97.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1263,9 +1263,9 @@ checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
[[package]]
name = "enum-as-inner"
version = "0.4.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73"
checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116"
dependencies = [
"heck",
"proc-macro2",
@@ -2431,9 +2431,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pgp"
@@ -3660,9 +3660,9 @@ dependencies = [
[[package]]
name = "trust-dns-proto"
version = "0.21.2"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c31f240f59877c3d4bb3b3ea0ec5a6a0cff07323580ff8c7a605cd7d08b255d"
checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26"
dependencies = [
"async-trait",
"cfg-if",
@@ -3674,32 +3674,32 @@ dependencies = [
"idna",
"ipnet",
"lazy_static",
"log",
"rand 0.8.5",
"smallvec",
"thiserror",
"tinyvec",
"tokio",
"tracing",
"url",
]
[[package]]
name = "trust-dns-resolver"
version = "0.21.2"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4ba72c2ea84515690c9fcef4c6c660bb9df3036ed1051686de84605b74fd558"
checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe"
dependencies = [
"cfg-if",
"futures-util",
"ipconfig",
"lazy_static",
"log",
"lru-cache",
"parking_lot",
"resolv-conf",
"smallvec",
"thiserror",
"tokio",
"tracing",
"trust-dns-proto",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.96.0"
version = "1.97.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
license = "MPL-2.0"
@@ -23,7 +23,7 @@ anyhow = "1"
async-imap = { git = "https://github.com/async-email/async-imap", branch = "master", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.4", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.5", default-features = false, features = ["smtp-transport", "socks5", "runtime-tokio"] }
trust-dns-resolver = "0.21"
trust-dns-resolver = "0.22"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
backtrace = "0.3"
@@ -47,7 +47,7 @@ num_cpus = "1.13"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.15.0"
percent-encoding = "2.0"
percent-encoding = "2.2"
pgp = { version = "0.8", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
quick-xml = "0.23"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.96.0"
version = "1.97.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"

View File

@@ -11,16 +11,17 @@ extern "C" {
#endif
typedef struct _dc_context dc_context_t;
typedef struct _dc_accounts dc_accounts_t;
typedef struct _dc_array dc_array_t;
typedef struct _dc_chatlist dc_chatlist_t;
typedef struct _dc_chat dc_chat_t;
typedef struct _dc_msg dc_msg_t;
typedef struct _dc_contact dc_contact_t;
typedef struct _dc_lot dc_lot_t;
typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_context dc_context_t;
typedef struct _dc_accounts dc_accounts_t;
typedef struct _dc_array dc_array_t;
typedef struct _dc_chatlist dc_chatlist_t;
typedef struct _dc_chat dc_chat_t;
typedef struct _dc_msg dc_msg_t;
typedef struct _dc_reactions dc_reactions_t;
typedef struct _dc_contact dc_contact_t;
typedef struct _dc_lot dc_lot_t;
typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
@@ -991,6 +992,34 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
/**
* 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.
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id ID of the message you react to.
* @param reaction A string consisting of emojis separated by spaces.
* @return The ID of the message sent out or 0 for errors.
*/
uint32_t dc_send_reaction (dc_context_t* context, uint32_t msg_id, char *reaction);
/**
* Get a structure with reactions to the message.
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id The message ID to get reactions for.
* @return A structure with all reactions to the message.
*/
dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
/**
* A webxdc instance sends a status update to its other members.
*
@@ -4882,7 +4911,49 @@ uint32_t dc_lot_get_id (const dc_lot_t* lot);
* @param lot The lot object.
* @return The timestamp as defined by the creator of the object. 0 if there is not timestamp or on errors.
*/
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* @class dc_reactions_t
*
* An object representing all reactions for a single message.
*/
/**
* Returns array of contacts which reacted to the given message.
*
* @memberof dc_reactions_t
* @param reactions The object containing message reactions.
* @return array of contact IDs. Use dc_array_get_cnt() to get array length and
* dc_array_get_id() to get the IDs. Should be freed using `dc_array_unref()` after usage.
*/
dc_array_t* dc_reactions_get_contacts(dc_reactions_t* reactions);
/**
* Returns a string containing space-separated reactions of a single contact.
*
* @memberof dc_reactions_t
* @param reactions The object containing message reactions.
* @param contact_id ID of the contact.
* @return Space-separated list of emoji sequences, which could be empty.
* Returned string should not be modified and should be freed
* with dc_str_unref() after usage.
*/
char* dc_reactions_get_by_contact_id(dc_reactions_t* reactions, uint32_t contact_id);
/**
* Frees an object containing message reactions.
*
* Reactions objects are created by dc_get_msg_reactions().
*
* @memberof dc_reactions_t
* @param reactions The object to free.
* If NULL is given, nothing is done.
*/
void dc_reactions_unref (dc_reactions_t* reactions);
/**
@@ -5533,6 +5604,15 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_MSGS_CHANGED 2000
/**
* Message reactions changed.
*
* @param data1 (int) chat_id ID of the chat affected by the changes.
* @param data2 (int) msg_id ID of the message for which reactions were changed.
*/
#define DC_EVENT_REACTIONS_CHANGED 2001
/**
* There is a fresh message. Typically, the user will show an notification
* when receiving this message.

View File

@@ -1,5 +1,6 @@
use crate::chat::ChatItem;
use crate::constants::DC_MSG_ID_DAYMARKER;
use crate::contact::ContactId;
use crate::location::Location;
use crate::message::MsgId;
@@ -7,6 +8,7 @@ use crate::message::MsgId;
#[derive(Debug, Clone)]
pub enum dc_array_t {
MsgIds(Vec<MsgId>),
ContactIds(Vec<ContactId>),
Chat(Vec<ChatItem>),
Locations(Vec<Location>),
Uint(Vec<u32>),
@@ -16,6 +18,7 @@ impl dc_array_t {
pub(crate) fn get_id(&self, index: usize) -> u32 {
match self {
Self::MsgIds(array) => array[index].to_u32(),
Self::ContactIds(array) => array[index].to_u32(),
Self::Chat(array) => match array[index] {
ChatItem::Message { msg_id } => msg_id.to_u32(),
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
@@ -28,6 +31,7 @@ impl dc_array_t {
pub(crate) fn get_timestamp(&self, index: usize) -> Option<i64> {
match self {
Self::MsgIds(_) => None,
Self::ContactIds(_) => None,
Self::Chat(array) => array.get(index).and_then(|item| match item {
ChatItem::Message { .. } => None,
ChatItem::DayMarker { timestamp } => Some(*timestamp),
@@ -40,6 +44,7 @@ impl dc_array_t {
pub(crate) fn get_marker(&self, index: usize) -> Option<&str> {
match self {
Self::MsgIds(_) => None,
Self::ContactIds(_) => None,
Self::Chat(_) => None,
Self::Locations(array) => array
.get(index)
@@ -60,6 +65,7 @@ impl dc_array_t {
pub(crate) fn len(&self) -> usize {
match self {
Self::MsgIds(array) => array.len(),
Self::ContactIds(array) => array.len(),
Self::Chat(array) => array.len(),
Self::Locations(array) => array.len(),
Self::Uint(array) => array.len(),
@@ -83,6 +89,12 @@ impl From<Vec<MsgId>> for dc_array_t {
}
}
impl From<Vec<ContactId>> for dc_array_t {
fn from(array: Vec<ContactId>) -> Self {
dc_array_t::ContactIds(array)
}
}
impl From<Vec<ChatItem>> for dc_array_t {
fn from(array: Vec<ChatItem>) -> Self {
dc_array_t::Chat(array)

View File

@@ -37,6 +37,7 @@ use deltachat::context::Context;
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
use deltachat::stock_str::StockMessage;
use deltachat::stock_str::StockStrings;
use deltachat::webxdc::StatusUpdateSerial;
@@ -66,6 +67,8 @@ use deltachat::chatlist::Chatlist;
/// Struct representing the deltachat context.
pub type dc_context_t = Context;
pub type dc_reactions_t = Reactions;
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
fn block_on<T>(fut: T) -> T::Output
@@ -498,6 +501,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::Error(_) => 400,
EventType::ErrorSelfNotInGroup(_) => 410,
EventType::MsgsChanged { .. } => 2000,
EventType::ReactionsChanged { .. } => 2001,
EventType::IncomingMsg { .. } => 2005,
EventType::MsgsNoticed { .. } => 2008,
EventType::MsgDelivered { .. } => 2010,
@@ -542,6 +546,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::SelfavatarChanged
| EventType::ErrorSelfNotInGroup(_) => 0,
EventType::MsgsChanged { chat_id, .. }
| EventType::ReactionsChanged { chat_id, .. }
| EventType::IncomingMsg { chat_id, .. }
| EventType::MsgsNoticed(chat_id)
| EventType::MsgDelivered { chat_id, .. }
@@ -598,6 +603,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::SelfavatarChanged => 0,
EventType::ChatModified(_) => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
| EventType::MsgDelivered { msg_id, .. }
| EventType::MsgFailed { msg_id, .. }
@@ -637,6 +643,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
data2.into_raw()
}
EventType::MsgsChanged { .. }
| EventType::ReactionsChanged { .. }
| EventType::IncomingMsg { .. }
| EventType::MsgsNoticed(_)
| EventType::MsgDelivered { .. }
@@ -948,6 +955,48 @@ pub unsafe extern "C" fn dc_send_videochat_invitation(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_reaction(
context: *mut dc_context_t,
msg_id: u32,
reaction: *const libc::c_char,
) -> u32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_send_reaction()");
return 0;
}
let ctx = &*context;
block_on(async move {
send_reaction(ctx, MsgId::new(msg_id), &to_string_lossy(reaction))
.await
.map(|msg_id| msg_id.to_u32())
.unwrap_or_log_default(ctx, "Failed to send reaction")
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_msg_reactions(
context: *mut dc_context_t,
msg_id: u32,
) -> *mut dc_reactions_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_msg_reactions()");
return ptr::null_mut();
}
let ctx = &*context;
let reactions = if let Ok(reactions) = block_on(get_msg_reactions(ctx, MsgId::new(msg_id)))
.log_err(ctx, "failed dc_get_msg_reactions() call")
{
reactions
} else {
return ptr::null_mut();
};
Box::into_raw(Box::new(reactions))
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_webxdc_status_update(
context: *mut dc_context_t,
@@ -3988,6 +4037,45 @@ pub unsafe extern "C" fn dc_lot_get_timestamp(lot: *mut dc_lot_t) -> i64 {
lot.get_timestamp()
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_get_contacts(
reactions: *mut dc_reactions_t,
) -> *mut dc_array::dc_array_t {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_get_contacts()");
return ptr::null_mut();
}
let reactions = &*reactions;
let array: dc_array_t = reactions.contacts().into();
Box::into_raw(Box::new(array))
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_get_by_contact_id(
reactions: *mut dc_reactions_t,
contact_id: u32,
) -> *mut libc::c_char {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_get_by_contact_id()");
return ptr::null_mut();
}
let reactions = &*reactions;
reactions.get(ContactId::new(contact_id)).as_str().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_unref(reactions: *mut dc_reactions_t) {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_unref()");
return;
}
drop(Box::from_raw(reactions));
}
#[no_mangle]
pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
libc::free(s as *mut _)
@@ -4515,7 +4603,7 @@ mod jsonrpc {
return;
}
(*jsonrpc_instance).event_thread.abort();
Box::from_raw(jsonrpc_instance);
drop(Box::from_raw(jsonrpc_instance));
}
#[no_mangle]

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.96.0"
version = "1.97.0"
description = "DeltaChat JSON-RPC API"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"

View File

@@ -102,6 +102,14 @@ pub enum JSONRPCEventType {
msg_id: u32,
},
/// Reactions for the message changed.
#[serde(rename_all = "camelCase")]
ReactionsChanged {
chat_id: u32,
msg_id: u32,
contact_id: u32,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
@@ -284,6 +292,15 @@ impl From<EventType> for JSONRPCEventType {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
} => ReactionsChanged {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
contact_id: contact_id.to_u32(),
},
EventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),

View File

@@ -17,6 +17,7 @@ use deltachat::{
provider::get_provider_info,
qr,
qr_code_generator::get_securejoin_qr_svg,
reaction::send_reaction,
securejoin,
stock_str::StockMessage,
webxdc::StatusUpdateSerial,
@@ -24,7 +25,7 @@ use deltachat::{
use std::collections::BTreeMap;
use std::sync::Arc;
use std::{collections::HashMap, str::FromStr};
use tokio::sync::RwLock;
use tokio::{fs, sync::RwLock};
use walkdir::WalkDir;
use yerpc::rpc;
@@ -34,7 +35,7 @@ pub mod events;
pub mod types;
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
use crate::api::types::QrObject;
use crate::api::types::qr::QrObject;
use types::account::Account;
use types::chat::FullChat;
@@ -915,6 +916,11 @@ impl CommandApi {
MessageObject::from_message_id(&ctx, message_id).await
}
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
}
async fn message_get_messages(
&self,
account_id: u32,
@@ -1461,6 +1467,23 @@ impl CommandApi {
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())
}
// ---------------------------------------------
// functions for the composer
// the composer is the message input field
@@ -1495,6 +1518,63 @@ impl CommandApi {
// 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")
}
/// for desktop, get stickers from stickers folder,
/// grouped by the 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,

View File

@@ -8,6 +8,7 @@ use deltachat::download;
use deltachat::message::Message;
use deltachat::message::MsgId;
use deltachat::message::Viewtype;
use deltachat::reaction::get_msg_reactions;
use num_traits::cast::ToPrimitive;
use serde::Deserialize;
use serde::Serialize;
@@ -15,6 +16,7 @@ use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
use super::contact::ContactObject;
use super::reactions::JSONRPCReactions;
use super::webxdc::WebxdcMessageInfo;
#[derive(Serialize, TypeDef)]
@@ -64,6 +66,8 @@ pub struct MessageObject {
webxdc_info: Option<WebxdcMessageInfo>,
download_state: DownloadState,
reactions: Option<JSONRPCReactions>,
}
#[derive(Serialize, TypeDef)]
@@ -139,6 +143,13 @@ impl MessageObject {
None
};
let reactions = get_msg_reactions(context, msg_id).await?;
let reactions = if reactions.is_empty() {
None
} else {
Some(reactions.into())
};
Ok(MessageObject {
id: msg_id.to_u32(),
chat_id: message.get_chat_id().to_u32(),
@@ -193,6 +204,8 @@ impl MessageObject {
webxdc_info,
download_state,
reactions,
})
}
}

View File

@@ -1,7 +1,3 @@
use deltachat::qr::Qr;
use serde::Serialize;
use typescript_type_def::TypeDef;
pub mod account;
pub mod chat;
pub mod chat_list;
@@ -9,6 +5,8 @@ pub mod contact;
pub mod location;
pub mod message;
pub mod provider_info;
pub mod qr;
pub mod reactions;
pub mod webxdc;
pub fn color_int_to_hex_string(color: u32) -> String {
@@ -22,213 +20,3 @@ fn maybe_empty_string_to_option(string: String) -> Option<String> {
Some(string)
}
}
#[derive(Serialize, TypeDef)]
#[serde(rename = "Qr", rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum QrObject {
AskVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
AskVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
FprOk {
contact_id: u32,
},
FprMismatch {
contact_id: Option<u32>,
},
FprWithoutAddr {
fingerprint: String,
},
Account {
domain: String,
},
WebrtcInstance {
domain: String,
instance_pattern: String,
},
Addr {
contact_id: u32,
draft: Option<String>,
},
Url {
url: String,
},
Text {
text: String,
},
WithdrawVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
WithdrawVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
ReviveVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
ReviveVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
Login {
address: String,
},
}
impl From<Qr> for QrObject {
fn from(qr: Qr) -> Self {
match qr {
Qr::AskVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::AskVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::AskVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::AskVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::FprOk { contact_id } => {
let contact_id = contact_id.to_u32();
QrObject::FprOk { contact_id }
}
Qr::FprMismatch { contact_id } => {
let contact_id = contact_id.map(|contact_id| contact_id.to_u32());
QrObject::FprMismatch { contact_id }
}
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
Qr::Account { domain } => QrObject::Account { domain },
Qr::WebrtcInstance {
domain,
instance_pattern,
} => QrObject::WebrtcInstance {
domain,
instance_pattern,
},
Qr::Addr { contact_id, draft } => {
let contact_id = contact_id.to_u32();
QrObject::Addr { contact_id, draft }
}
Qr::Url { url } => QrObject::Url { url },
Qr::Text { text } => QrObject::Text { text },
Qr::WithdrawVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::WithdrawVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::WithdrawVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::WithdrawVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::ReviveVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::ReviveVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::ReviveVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::ReviveVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::Login { address, .. } => QrObject::Login { address },
}
}
}

View File

@@ -0,0 +1,213 @@
use deltachat::qr::Qr;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef)]
#[serde(rename = "Qr", rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum QrObject {
AskVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
AskVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
FprOk {
contact_id: u32,
},
FprMismatch {
contact_id: Option<u32>,
},
FprWithoutAddr {
fingerprint: String,
},
Account {
domain: String,
},
WebrtcInstance {
domain: String,
instance_pattern: String,
},
Addr {
contact_id: u32,
draft: Option<String>,
},
Url {
url: String,
},
Text {
text: String,
},
WithdrawVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
WithdrawVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
ReviveVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
ReviveVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
Login {
address: String,
},
}
impl From<Qr> for QrObject {
fn from(qr: Qr) -> Self {
match qr {
Qr::AskVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::AskVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::AskVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::AskVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::FprOk { contact_id } => {
let contact_id = contact_id.to_u32();
QrObject::FprOk { contact_id }
}
Qr::FprMismatch { contact_id } => {
let contact_id = contact_id.map(|contact_id| contact_id.to_u32());
QrObject::FprMismatch { contact_id }
}
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
Qr::Account { domain } => QrObject::Account { domain },
Qr::WebrtcInstance {
domain,
instance_pattern,
} => QrObject::WebrtcInstance {
domain,
instance_pattern,
},
Qr::Addr { contact_id, draft } => {
let contact_id = contact_id.to_u32();
QrObject::Addr { contact_id, draft }
}
Qr::Url { url } => QrObject::Url { url },
Qr::Text { text } => QrObject::Text { text },
Qr::WithdrawVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::WithdrawVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::WithdrawVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::WithdrawVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::ReviveVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::ReviveVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::ReviveVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::ReviveVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::Login { address, .. } => QrObject::Login { address },
}
}
}

View File

@@ -0,0 +1,47 @@
use std::collections::BTreeMap;
use deltachat::reaction::Reactions;
use serde::Serialize;
use typescript_type_def::TypeDef;
/// Structure representing all reactions to a particular message.
#[derive(Serialize, TypeDef)]
#[serde(rename = "Reactions", rename_all = "camelCase")]
pub struct JSONRPCReactions {
/// Map from a contact to it's reaction to message.
reactions_by_contact: BTreeMap<u32, Vec<String>>,
/// Unique reactions and their count
reactions: BTreeMap<String, u32>,
}
impl From<Reactions> for JSONRPCReactions {
fn from(reactions: Reactions) -> Self {
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
let mut unique_reactions: BTreeMap<String, u32> = BTreeMap::new();
for contact_id in reactions.contacts() {
let reaction = reactions.get(contact_id);
if reaction.is_empty() {
continue;
}
let emojis: Vec<String> = reaction
.emojis()
.into_iter()
.map(|emoji| emoji.to_owned())
.collect();
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
for emoji in emojis {
if let Some(x) = unique_reactions.get_mut(&emoji) {
*x += 1;
} else {
unique_reactions.insert(emoji, 1);
}
}
}
JSONRPCReactions {
reactions_by_contact,
reactions: unique_reactions,
}
}
}

View File

@@ -598,6 +598,11 @@ export class RawClient {
}
public getMessageHtml(accountId: T.U32, messageId: T.U32): Promise<(string|null)> {
return (this._transport.request('get_message_html', [accountId, messageId] as RPC.Params)) as Promise<(string|null)>;
}
public messageGetMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
return (this._transport.request('message_get_messages', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.Message>>;
}
@@ -873,6 +878,18 @@ export class RawClient {
return (this._transport.request('send_sticker', [accountId, chatId, stickerPath] as RPC.Params)) as Promise<T.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.
*/
public sendReaction(accountId: T.U32, messageId: T.U32, reaction: (string)[]): Promise<T.U32> {
return (this._transport.request('send_reaction', [accountId, messageId, reaction] as RPC.Params)) as Promise<T.U32>;
}
public removeDraft(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('remove_draft', [accountId, chatId] as RPC.Params)) as Promise<null>;
@@ -890,6 +907,19 @@ export class RawClient {
return (this._transport.request('send_videochat_invitation', [accountId, chatId] as RPC.Params)) as Promise<T.U32>;
}
public miscGetStickerFolder(accountId: T.U32): Promise<string> {
return (this._transport.request('misc_get_sticker_folder', [accountId] as RPC.Params)) as Promise<string>;
}
/**
* for desktop, get stickers from stickers folder,
* grouped by the folder they are in.
*/
public miscGetStickers(accountId: T.U32): Promise<Record<string,(string)[]>> {
return (this._transport.request('misc_get_stickers', [accountId] as RPC.Params)) as Promise<Record<string,(string)[]>>;
}
/**
* Returns the messageid of the sent message
*/

View File

@@ -0,0 +1,201 @@
// Generated!
export enum C {
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
DC_CERTCK_AUTO = 0,
DC_CERTCK_STRICT = 1,
DC_CHAT_ID_ALLDONE_HINT = 7,
DC_CHAT_ID_ARCHIVED_LINK = 6,
DC_CHAT_ID_LAST_SPECIAL = 9,
DC_CHAT_ID_TRASH = 3,
DC_CHAT_TYPE_BROADCAST = 160,
DC_CHAT_TYPE_GROUP = 120,
DC_CHAT_TYPE_MAILINGLIST = 140,
DC_CHAT_TYPE_SINGLE = 100,
DC_CHAT_TYPE_UNDEFINED = 0,
DC_CONNECTIVITY_CONNECTED = 4000,
DC_CONNECTIVITY_CONNECTING = 2000,
DC_CONNECTIVITY_NOT_CONNECTED = 1000,
DC_CONNECTIVITY_WORKING = 3000,
DC_CONTACT_ID_DEVICE = 5,
DC_CONTACT_ID_INFO = 2,
DC_CONTACT_ID_LAST_SPECIAL = 9,
DC_CONTACT_ID_SELF = 1,
DC_GCL_ADD_ALLDONE_HINT = 4,
DC_GCL_ADD_SELF = 2,
DC_GCL_ARCHIVED_ONLY = 1,
DC_GCL_FOR_FORWARDING = 8,
DC_GCL_NO_SPECIALS = 2,
DC_GCL_VERIFIED_ONLY = 1,
DC_GCM_ADDDAYMARKER = 1,
DC_GCM_INFO_ONLY = 2,
DC_INFO_PROTECTION_DISABLED = 12,
DC_INFO_PROTECTION_ENABLED = 11,
DC_KEY_GEN_DEFAULT = 0,
DC_KEY_GEN_ED25519 = 2,
DC_KEY_GEN_RSA2048 = 1,
DC_LP_AUTH_NORMAL = 4,
DC_LP_AUTH_OAUTH2 = 2,
DC_MEDIA_QUALITY_BALANCED = 0,
DC_MEDIA_QUALITY_WORSE = 1,
DC_MSG_ID_DAYMARKER = 9,
DC_MSG_ID_LAST_SPECIAL = 9,
DC_MSG_ID_MARKER1 = 1,
DC_PROVIDER_STATUS_BROKEN = 3,
DC_PROVIDER_STATUS_OK = 1,
DC_PROVIDER_STATUS_PREPARATION = 2,
DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1,
DC_SHOW_EMAILS_ALL = 2,
DC_SHOW_EMAILS_OFF = 0,
DC_SOCKET_AUTO = 0,
DC_SOCKET_PLAIN = 3,
DC_SOCKET_SSL = 1,
DC_SOCKET_STARTTLS = 2,
DC_STATE_IN_FRESH = 10,
DC_STATE_IN_NOTICED = 13,
DC_STATE_IN_SEEN = 16,
DC_STATE_OUT_DELIVERED = 26,
DC_STATE_OUT_DRAFT = 19,
DC_STATE_OUT_FAILED = 24,
DC_STATE_OUT_MDN_RCVD = 28,
DC_STATE_OUT_PENDING = 20,
DC_STATE_OUT_PREPARING = 18,
DC_STATE_UNDEFINED = 0,
DC_STR_AC_SETUP_MSG_BODY = 43,
DC_STR_AC_SETUP_MSG_SUBJECT = 42,
DC_STR_ADD_MEMBER_BY_OTHER = 129,
DC_STR_ADD_MEMBER_BY_YOU = 128,
DC_STR_AEAP_ADDR_CHANGED = 122,
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
DC_STR_ARCHIVEDCHATS = 40,
DC_STR_AUDIO = 11,
DC_STR_BAD_TIME_MSG_BODY = 85,
DC_STR_BROADCAST_LIST = 115,
DC_STR_CANNOT_LOGIN = 60,
DC_STR_CANTDECRYPT_MSG_BODY = 29,
DC_STR_CONFIGURATION_FAILED = 84,
DC_STR_CONNECTED = 107,
DC_STR_CONNTECTING = 108,
DC_STR_CONTACT_NOT_VERIFIED = 36,
DC_STR_CONTACT_SETUP_CHANGED = 37,
DC_STR_CONTACT_VERIFIED = 35,
DC_STR_DEVICE_MESSAGES = 68,
DC_STR_DEVICE_MESSAGES_HINT = 70,
DC_STR_DOWNLOAD_AVAILABILITY = 100,
DC_STR_DRAFT = 3,
DC_STR_E2E_AVAILABLE = 25,
DC_STR_E2E_PREFERRED = 34,
DC_STR_ENCRYPTEDMSG = 24,
DC_STR_ENCR_NONE = 28,
DC_STR_ENCR_TRANSP = 27,
DC_STR_EPHEMERAL_DAY = 79,
DC_STR_EPHEMERAL_DAYS = 95,
DC_STR_EPHEMERAL_DISABLED = 75,
DC_STR_EPHEMERAL_FOUR_WEEKS = 81,
DC_STR_EPHEMERAL_HOUR = 78,
DC_STR_EPHEMERAL_HOURS = 94,
DC_STR_EPHEMERAL_MINUTE = 77,
DC_STR_EPHEMERAL_MINUTES = 93,
DC_STR_EPHEMERAL_SECONDS = 76,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER = 147,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU = 146,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER = 145,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU = 144,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER = 143,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU = 142,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER = 149,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU = 148,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER = 155,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU = 154,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER = 139,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU = 138,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER = 153,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU = 152,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER = 151,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU = 150,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER = 141,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU = 140,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER = 157,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU = 156,
DC_STR_EPHEMERAL_WEEK = 80,
DC_STR_EPHEMERAL_WEEKS = 96,
DC_STR_ERROR = 112,
DC_STR_ERROR_NO_NETWORK = 87,
DC_STR_FAILED_SENDING_TO = 74,
DC_STR_FILE = 12,
DC_STR_FINGERPRINTS = 30,
DC_STR_FORWARDED = 97,
DC_STR_GIF = 23,
DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER = 127,
DC_STR_GROUP_IMAGE_CHANGED_BY_YOU = 126,
DC_STR_GROUP_IMAGE_DELETED_BY_OTHER = 135,
DC_STR_GROUP_IMAGE_DELETED_BY_YOU = 134,
DC_STR_GROUP_LEFT_BY_OTHER = 133,
DC_STR_GROUP_LEFT_BY_YOU = 132,
DC_STR_GROUP_NAME_CHANGED_BY_OTHER = 125,
DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124,
DC_STR_IMAGE = 9,
DC_STR_INCOMING_MESSAGES = 103,
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
DC_STR_LOCATION = 66,
DC_STR_LOCATION_ENABLED_BY_OTHER = 137,
DC_STR_LOCATION_ENABLED_BY_YOU = 136,
DC_STR_MESSAGES = 114,
DC_STR_MSGACTIONBYME = 63,
DC_STR_MSGACTIONBYUSER = 62,
DC_STR_MSGADDMEMBER = 17,
DC_STR_MSGDELMEMBER = 18,
DC_STR_MSGGROUPLEFT = 19,
DC_STR_MSGGRPIMGCHANGED = 16,
DC_STR_MSGGRPIMGDELETED = 33,
DC_STR_MSGGRPNAME = 15,
DC_STR_MSGLOCATIONDISABLED = 65,
DC_STR_MSGLOCATIONENABLED = 64,
DC_STR_NOMESSAGES = 1,
DC_STR_NOT_CONNECTED = 121,
DC_STR_NOT_SUPPORTED_BY_PROVIDER = 113,
DC_STR_ONE_MOMENT = 106,
DC_STR_OUTGOING_MESSAGES = 104,
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
DC_STR_PART_OF_TOTAL_USED = 116,
DC_STR_PROTECTION_DISABLED = 89,
DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
DC_STR_PROTECTION_ENABLED = 88,
DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
DC_STR_REMOVE_MEMBER_BY_YOU = 130,
DC_STR_REPLY_NOUN = 90,
DC_STR_SAVED_MESSAGES = 69,
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
DC_STR_SECURE_JOIN_REPLIES = 118,
DC_STR_SECURE_JOIN_STARTED = 117,
DC_STR_SELF = 2,
DC_STR_SELF_DELETED_MSG_BODY = 91,
DC_STR_SENDING = 110,
DC_STR_SERVER_TURNED_OFF = 92,
DC_STR_SETUP_CONTACT_QR_DESC = 119,
DC_STR_STICKER = 67,
DC_STR_STORAGE_ON_DOMAIN = 105,
DC_STR_SUBJECT_FOR_NEW_CONTACT = 73,
DC_STR_SYNC_MSG_BODY = 102,
DC_STR_SYNC_MSG_SUBJECT = 101,
DC_STR_UNKNOWN_SENDER_FOR_CHAT = 72,
DC_STR_UPDATE_REMINDER_MSG_BODY = 86,
DC_STR_UPDATING = 109,
DC_STR_VIDEO = 10,
DC_STR_VIDEOCHAT_INVITATION = 82,
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
DC_STR_VOICEMESSAGE = 7,
DC_STR_WELCOME_MESSAGE = 71,
DC_TEXT1_DRAFT = 1,
DC_TEXT1_SELF = 3,
DC_TEXT1_USERNAME = 2,
DC_VIDEOCHATTYPE_BASICWEBRTC = 1,
DC_VIDEOCHATTYPE_JITSI = 2,
DC_VIDEOCHATTYPE_UNKNOWN = 0,
}

View File

@@ -52,7 +52,7 @@ export type Event=(({
* should not be disturbed by a dialog or so. Instead, use a bubble or so.
*
* However, for ongoing processes (eg. configure())
* or for functions that are expected to fail (eg. dc_continue_key_transfer())
* or for functions that are expected to fail (eg. autocryptContinueKeyTransfer())
* it might be better to delay showing these events until the function has really
* failed (returned false). It should be sufficient to report only the *last* error
* in a messasge box then.
@@ -61,9 +61,9 @@ export type Event=(({
/**
* An action cannot be performed because the user is not in the group.
* Reported eg. after a call to
* dc_set_chat_name(), dc_set_chat_profile_image(),
* dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
* dc_send_text_msg() or another sending function.
* setChatName(), setChatProfileImage(),
* addContactToChat(), removeContactFromChat(),
* and messages sending functions.
*/
"type":"ErrorSelfNotInGroup";}&{"msg":string;})|({
/**
@@ -73,10 +73,14 @@ export type Event=(({
* - Chats created, deleted or archived
* - A draft has been set
*
* `chat_id` is set if only a single chat is affected by the changes, otherwise 0.
* `msg_id` is set if only a single message is affected by the changes, otherwise 0.
* `chatId` is set if only a single chat is affected by the changes, otherwise 0.
* `msgId` is set if only a single message is affected by the changes, otherwise 0.
*/
"type":"MsgsChanged";}&{"chatId":U32;"msgId":U32;})|({
/**
* Reactions for the message changed.
*/
"type":"ReactionsChanged";}&{"chatId":U32;"msgId":U32;"contactId":U32;})|({
/**
* There is a fresh message. Typically, the user will show an notification
* when receiving this message.
@@ -91,24 +95,24 @@ export type Event=(({
"type":"MsgsNoticed";}&{"chatId":U32;})|({
/**
* A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
* DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
* DC_STATE_OUT_DELIVERED, see `Message.state`.
*/
"type":"MsgDelivered";}&{"chatId":U32;"msgId":U32;})|({
/**
* A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
* DC_STATE_OUT_FAILED, see dc_msg_get_state().
* DC_STATE_OUT_FAILED, see `Message.state`.
*/
"type":"MsgFailed";}&{"chatId":U32;"msgId":U32;})|({
/**
* A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
* DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
* DC_STATE_OUT_MDN_RCVD, see `Message.state`.
*/
"type":"MsgRead";}&{"chatId":U32;"msgId":U32;})|({
/**
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
* Or the verify state of a chat has changed.
* See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
* and dc_remove_contact_from_chat().
* See setChatName(), setChatProfileImage(), addContactToChat()
* and removeContactFromChat().
*
* This event does not include ephemeral timer modification, which
* is a separate event.
@@ -129,7 +133,7 @@ export type Event=(({
*
* @param data1 (u32) contact_id of the contact for which the location has changed.
* If the locations of several contacts have been changed,
* eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
* this parameter is set to `None`.
*/
"type":"LocationChanged";}&{"contactId":(U32|null);})|({
/**
@@ -168,7 +172,7 @@ export type Event=(({
* (Alice, the person who shows the QR code).
*
* These events are typically sent after a joiner has scanned the QR code
* generated by dc_get_securejoin_qr().
* generated by getChatSecurejoinQrCodeSvg().
*
* @param data1 (int) ID of the contact that wants to join.
* @param data2 (int) Progress as:
@@ -181,7 +185,7 @@ export type Event=(({
/**
* Progress information of a secure-join handshake from the view of the joiner
* (Bob, the person who scans the QR code).
* The events are typically sent while dc_join_securejoin(), which
* The events are typically sent while secureJoin(), which
* may take some time, is executed.
* @param data1 (int) ID of the inviting contact.
* @param data2 (int) Progress as:
@@ -192,8 +196,8 @@ export type Event=(({
/**
* The connectivity to the server changed.
* This means that you should refresh the connectivity view
* and possibly the connectivtiy HTML; see dc_get_connectivity() and
* dc_get_connectivity_html() for details.
* and possibly the connectivtiy HTML; see getConnectivity() and
* getConnectivityHtml() for details.
*/
"type":"ConnectivityChanged";}|{"type":"SelfavatarChanged";}|({"type":"WebxdcStatusUpdate";}&{"msgId":U32;"statusUpdateSerial":U32;})|({
/**

View File

@@ -147,7 +147,24 @@ export type WebxdcMessageInfo={
*/
"internetAccess":boolean;};
export type DownloadState=("Done"|"Available"|"Failure"|"InProgress");
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;};
/**
* Structure representing all reactions to a particular message.
*/
export type Reactions=
/**
* Structure representing all reactions to a particular message.
*/
{
/**
* Map from a contact to it's reaction to message.
*/
"reactionsByContact":Record<U32,(string)[]>;
/**
* Unique reactions and their count
*/
"reactions":Record<string,U32>;};
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;"reactions":(Reactions|null);};
export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"image":(string|null);"imageMimeType":(string|null);"chatName":string;"chatProfileImage":(string|null);
/**
* also known as summary_text1
@@ -160,4 +177,4 @@ export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"imag
export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
export type F64=number;
export type Location={"locationId":U32;"isIndependent":boolean;"latitude":F64;"longitude":F64;"accuracy":F64;"timestamp":I64;"contactId":U32;"msgId":U32;"chatId":U32;"marker":(string|null);};
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,U32,(string|null),U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,(string)[],U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,Record<string,(string)[]>,U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];

View File

@@ -28,7 +28,7 @@
"main": "dist/deltachat.js",
"name": "@deltachat/jsonrpc-client",
"scripts": {
"build": "run-s generate-bindings build:tsc build:bundle",
"build": "run-s generate-bindings extract-constants build:tsc build:bundle",
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
"build:tsc": "tsc",
"docs": "typedoc --out docs deltachat.ts",
@@ -37,6 +37,7 @@
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
"example:start": "http-server .",
"generate-bindings": "cargo test",
"extract-constants": "node ./scripts/generate-constants.js",
"prettier:check": "prettier --check **.ts",
"prettier:fix": "prettier --write **.ts",
"test": "run-s test:prepare test:run-coverage test:report-coverage",
@@ -47,5 +48,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.96.0"
"version": "1.97.0"
}

View File

@@ -0,0 +1,53 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from "fs";
import { resolve } from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const data = [];
const header = resolve(__dirname, "../../../deltachat-ffi/deltachat.h");
console.log("Generating constants...");
const header_data = readFileSync(header, "UTF-8");
const regex = /^#define\s+(\w+)\s+(\w+)/gm;
let match;
while (null != (match = regex.exec(header_data))) {
const key = match[1];
const value = parseInt(match[2]);
if (!isNaN(value)) {
data.push({ key, value });
}
}
const constants = data
.filter(
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
)
.sort((lhs, rhs) => {
if (lhs.key < rhs.key) return -1;
else if (lhs.key > rhs.key) return 1;
return 0;
})
.filter(({ key }) => {
// filter out what we don't need it
return !(
key.startsWith("DC_EVENT_") ||
key.startsWith("DC_IMEX_") ||
key.startsWith("DC_CHAT_VISIBILITY") ||
key.startsWith("DC_DOWNLOAD") ||
(key.startsWith("DC_MSG") && !key.startsWith("DC_MSG_ID")) ||
key.startsWith("DC_QR_")
);
})
.map((row) => {
return ` ${row.key}: ${row.value}`;
})
.join(",\n");
writeFileSync(
resolve(__dirname, "../generated/constants.ts"),
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`
);

View File

@@ -28,6 +28,7 @@ type ContextEvents = { ALL: (event: Event) => void } & {
};
export type DcEvent = Event;
export type DcEventType<T extends Event["type"]> = Extract<Event, { type: T }>
export class BaseDeltaChat<
Transport extends BaseTransport<any>

View File

@@ -4,3 +4,4 @@ export * from "../generated/events.js";
export { RawClient } from "../generated/client.js";
export * from "./client.js";
export * as yerpc from "yerpc";
export { C } from "../generated/constants.js";

View File

@@ -20,6 +20,7 @@ use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::reaction::send_reaction;
use deltachat::receive_imf::*;
use deltachat::sql;
use deltachat::tools::*;
@@ -407,6 +408,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
resend <msg-id>\n\
markseen <msg-id>\n\
delmsg <msg-id>\n\
react <msg-id> [<reaction>]\n\
===========================Contact commands==\n\
listcontacts [<query>]\n\
listverified [<query>]\n\
@@ -1121,6 +1123,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ids[0] = MsgId::new(arg1.parse()?);
message::delete_msgs(&context, &ids).await?;
}
"react" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let msg_id = MsgId::new(arg1.parse()?);
let reaction = arg2;
send_reaction(&context, msg_id, reaction).await?;
}
"listcontacts" | "contacts" | "listverified" => {
let contacts = Contact::get_all(
&context,

View File

@@ -72,6 +72,19 @@ fn receive_event(event: EventType) {
))
);
}
EventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
} => {
info!(
"{}",
yellow.paint(format!(
"Received REACTIONS_CHANGED(chat_id={}, msg_id={}, contact_id={})",
chat_id, msg_id, contact_id
))
);
}
EventType::ContactsChanged(_) => {
info!("{}", yellow.paint("Received CONTACTS_CHANGED()"));
}
@@ -208,7 +221,7 @@ const CHAT_COMMANDS: [&str; 36] = [
"accept",
"blockchat",
];
const MESSAGE_COMMANDS: [&str; 8] = [
const MESSAGE_COMMANDS: [&str; 9] = [
"listmsgs",
"msginfo",
"listfresh",
@@ -217,6 +230,7 @@ const MESSAGE_COMMANDS: [&str; 8] = [
"markseen",
"delmsg",
"download",
"react",
];
const CONTACT_COMMANDS: [&str; 9] = [
"listcontacts",

View File

@@ -50,6 +50,7 @@ module.exports = {
DC_EVENT_MSG_FAILED: 2012,
DC_EVENT_MSG_READ: 2015,
DC_EVENT_NEW_BLOB_FILE: 150,
DC_EVENT_REACTIONS_CHANGED: 2001,
DC_EVENT_SECUREJOIN_INVITER_PROGRESS: 2060,
DC_EVENT_SECUREJOIN_JOINER_PROGRESS: 2061,
DC_EVENT_SELFAVATAR_CHANGED: 2110,

View File

@@ -14,6 +14,7 @@ module.exports = {
400: 'DC_EVENT_ERROR',
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2005: 'DC_EVENT_INCOMING_MSG',
2008: 'DC_EVENT_MSGS_NOTICED',
2010: 'DC_EVENT_MSG_DELIVERED',

View File

@@ -50,6 +50,7 @@ export enum C {
DC_EVENT_MSG_FAILED = 2012,
DC_EVENT_MSG_READ = 2015,
DC_EVENT_NEW_BLOB_FILE = 150,
DC_EVENT_REACTIONS_CHANGED = 2001,
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060,
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061,
DC_EVENT_SELFAVATAR_CHANGED = 2110,
@@ -282,6 +283,7 @@ export const EventId2EventName: { [key: number]: string } = {
400: 'DC_EVENT_ERROR',
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2005: 'DC_EVENT_INCOMING_MSG',
2008: 'DC_EVENT_MSGS_NOTICED',
2010: 'DC_EVENT_MSG_DELIVERED',

View File

@@ -89,7 +89,11 @@ describe('JSON RPC', function () {
const { dc } = DeltaChat.newTemporary()
let promise_resolve
const promise = new Promise((res, _rej) => {
promise_resolve = res
promise_resolve = (response) => {
// ignore events
const answer = JSON.parse(response)
if (answer['method'] !== 'event') res(answer)
}
})
dc.startJsonRpcHandler(promise_resolve)
dc.jsonRpcRequest(
@@ -106,7 +110,7 @@ describe('JSON RPC', function () {
id: 2,
result: [1],
},
JSON.parse(await promise)
await promise
)
dc.close()
})

View File

@@ -57,8 +57,8 @@
"prebuildify": "cd node && prebuildify -t 16.13.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
"test": "npm run test:lint && npm run test:mocha",
"test:lint": "npm run lint",
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail"
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.96.0"
"version": "1.97.0"
}

10
spec.md
View File

@@ -450,6 +450,16 @@ This allows the receiver to show the time without knowing the file format.
Chat-Duration: 10000
# Reactions
Messengers MAY implement [RFC 9078](https://tools.ietf.org/html/rfc9078) reactions.
Received reaction should be interpreted as overwriting all previous reactions
received from the same contact.
This semantics is compatible to [XEP-0444](https://xmpp.org/extensions/xep-0444.html).
As an extension to RFC 9078, it is allowed to send empty reaction message,
in which case all previously sent reactions are retracted.
# Miscellaneous
Messengers SHOULD use the header `In-Reply-To` as usual.

View File

@@ -34,7 +34,9 @@ const SEEN_RECENTLY_SECONDS: i64 = 600;
///
/// Some contact IDs are reserved to identify special contacts. This
/// type can represent both the special as well as normal contacts.
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(
Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
pub struct ContactId(u32);
impl ContactId {

View File

@@ -11,7 +11,7 @@ use crate::imap::{Imap, ImapActionResult};
use crate::job::{self, Action, Job, Status};
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, Part};
use crate::param::{Param, Params};
use crate::param::Params;
use crate::tools::time;
use crate::{job_try, stock_str, EventType};
use std::cmp::max;
@@ -69,42 +69,6 @@ impl Context {
Ok(Some(max(MIN_DOWNLOAD_LIMIT, download_limit as u32)))
}
}
// Merges the two messages to `placeholder_msg_id`;
// `full_msg_id` is no longer used afterwards.
pub(crate) async fn merge_messages(
&self,
full_msg_id: MsgId,
placeholder_msg_id: MsgId,
) -> Result<()> {
let placeholder = Message::load_from_db(self, placeholder_msg_id).await?;
self.sql
.transaction(move |transaction| {
transaction
.execute("DELETE FROM msgs WHERE id=?;", paramsv![placeholder_msg_id])?;
transaction.execute(
"UPDATE msgs SET id=? WHERE id=?",
paramsv![placeholder_msg_id, full_msg_id],
)?;
Ok(())
})
.await?;
let mut full = Message::load_from_db(self, placeholder_msg_id).await?;
for key in [
Param::WebxdcSummary,
Param::WebxdcSummaryTimestamp,
Param::WebxdcDocument,
Param::WebxdcDocumentTimestamp,
] {
if let Some(value) = placeholder.param.get(key) {
full.param.set(key, value);
}
}
full.update_param(self).await?;
Ok(())
}
}
impl MsgId {

View File

@@ -173,6 +173,13 @@ pub enum EventType {
msg_id: MsgId,
},
/// Reactions for the message changed.
ReactionsChanged {
chat_id: ChatId,
msg_id: MsgId,
contact_id: ContactId,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///

View File

@@ -2157,8 +2157,8 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
.sql
.execute(
"INSERT INTO imap_sync (folder, uid_next) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET uid_next=? WHERE folder=?;",
paramsv![folder, uid_next, uid_next, folder],
ON CONFLICT(folder) DO UPDATE SET uid_next=excluded.uid_next",
paramsv![folder, uid_next],
)
.await?;
Ok(())
@@ -2189,8 +2189,8 @@ pub(crate) async fn set_uidvalidity(
.sql
.execute(
"INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET uidvalidity=? WHERE folder=?;",
paramsv![folder, uidvalidity, uidvalidity, folder],
ON CONFLICT(folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
paramsv![folder, uidvalidity],
)
.await?;
Ok(())
@@ -2212,8 +2212,8 @@ pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) ->
.sql
.execute(
"INSERT INTO imap_sync (folder, modseq) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET modseq=? WHERE folder=?;",
paramsv![folder, modseq, modseq, folder],
ON CONFLICT(folder) DO UPDATE SET modseq=excluded.modseq",
paramsv![folder, modseq],
)
.await?;
Ok(())

View File

@@ -104,6 +104,7 @@ pub mod receive_imf;
pub mod tools;
pub mod accounts;
pub mod reaction;
/// if set imap/incoming and smtp/outgoing MIME messages will be printed
pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";

View File

@@ -22,6 +22,7 @@ use crate::imap::markseen_on_imap_table;
use crate::mimeparser::{parse_message_id, DeliveryReport, SystemMessage};
use crate::param::{Param, Params};
use crate::pgp::split_armored_data;
use crate::reaction::get_msg_reactions;
use crate::scheduler::InterruptInfo;
use crate::sql;
use crate::stock_str;
@@ -751,6 +752,11 @@ impl Message {
self.param.set_int(Param::Duration, duration);
}
/// Marks the message as reaction.
pub(crate) fn set_reaction(&mut self) {
self.param.set_int(Param::Reaction, 1);
}
pub async fn latefiling_mediasize(
&mut self,
context: &Context,
@@ -1082,6 +1088,11 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
ret += "\n";
let reactions = get_msg_reactions(context, msg_id).await?;
if !reactions.is_empty() {
ret += &format!("Reactions: {}\n", reactions);
}
if let Some(error) = msg.error.as_ref() {
ret += &format!("Error: {}", error);
}

View File

@@ -183,7 +183,10 @@ impl<'a> MimeFactory<'a> {
)
.await?;
if !msg.is_system_message() && context.get_config_bool(Config::MdnsEnabled).await? {
if !msg.is_system_message()
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
&& context.get_config_bool(Config::MdnsEnabled).await?
{
req_mdn = true;
}
}
@@ -1122,6 +1125,11 @@ impl<'a> MimeFactory<'a> {
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
))
.body(message_text);
if self.msg.param.get_int(Param::Reaction).unwrap_or_default() != 0 {
main_part = main_part.header(("Content-Disposition", "reaction"));
}
let mut parts = Vec::new();
// add HTML-part, this is needed only if a HTML-message from a non-delta-client is forwarded;

View File

@@ -557,7 +557,10 @@ impl MimeMessage {
}
if prepend_subject && !subject.is_empty() {
let part_with_text = self.parts.iter_mut().find(|part| !part.msg.is_empty());
let part_with_text = self
.parts
.iter_mut()
.find(|part| !part.msg.is_empty() && !part.is_reaction);
if let Some(mut part) = part_with_text {
part.msg = format!("{} {}", subject, part.msg);
}
@@ -919,6 +922,7 @@ impl MimeMessage {
Ok(any_part_added)
}
/// Returns true if any part was added, false otherwise.
async fn add_single_part_if_known(
&mut self,
context: &Context,
@@ -952,6 +956,30 @@ impl MimeMessage {
warn!(context, "Missing attachment");
return Ok(false);
}
mime::TEXT
if mail.get_content_disposition().disposition
== DispositionType::Extension("reaction".to_string()) =>
{
// Reaction.
let decoded_data = match mail.get_body() {
Ok(decoded_data) => decoded_data,
Err(err) => {
warn!(context, "Invalid body parsed {:?}", err);
// Note that it's not always an error - might be no data
return Ok(false);
}
};
let part = Part {
typ: Viewtype::Text,
mimetype: Some(mime_type),
msg: decoded_data,
is_reaction: true,
..Default::default()
};
self.do_add_single_part(part);
return Ok(true);
}
mime::TEXT | mime::HTML => {
let decoded_data = match mail.get_body() {
Ok(decoded_data) => decoded_data,
@@ -1650,6 +1678,9 @@ pub struct Part {
/// note that multipart/related may contain further multipart nestings
/// and all of them needs to be marked with `is_related`.
pub(crate) is_related: bool,
/// Part is an RFC 9078 reaction.
pub(crate) is_reaction: bool,
}
/// return mimetype and viewtype for a parsed mail
@@ -3335,4 +3366,39 @@ Message.
Ok(())
}
/// Tests parsing of MIME message containing RFC 9078 reaction.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
let mime_message = MimeMessage::from_bytes(
&alice,
"To: alice@example.org\n\
From: bob@example.net\n\
Date: Today, 29 February 2021 00:00:10 -800\n\
Message-ID: 56789@example.net\n\
In-Reply-To: 12345@example.org\n\
Subject: Meeting\n\
Mime-Version: 1.0 (1.0)\n\
Content-Type: text/plain; charset=utf-8\n\
Content-Disposition: reaction\n\
\n\
\u{1F44D}"
.as_bytes(),
)
.await?;
assert_eq!(mime_message.parts.len(), 1);
assert_eq!(mime_message.parts[0].is_reaction, true);
assert_eq!(
mime_message
.get_header(HeaderDef::InReplyTo)
.and_then(|msgid| parse_message_id(msgid).ok())
.unwrap(),
"12345@example.org"
);
Ok(())
}
}

View File

@@ -59,6 +59,9 @@ pub enum Param {
/// For Messages
WantsMdn = b'r',
/// For Messages: the message is a reaction.
Reaction = b'x',
/// For Messages: a message with Auto-Submitted header ("bot").
Bot = b'b',

551
src/reaction.rs Normal file
View File

@@ -0,0 +1,551 @@
//! # Reactions.
//!
//! Reactions are short messages consisting of emojis sent in reply to
//! messages. Unlike normal messages which are added to the end of the chat,
//! reactions are supposed to be displayed near the original messages.
//!
//! RFC 9078 specifies how reactions are transmitted in MIME messages.
//!
//! Reaction update semantics is not well-defined in RFC 9078, so
//! Delta Chat uses the same semantics as in
//! [XEP-0444](https://xmpp.org/extensions/xep-0444.html) section
//! "3.2 Updating reactions to a message". Received reactions override
//! all previously received reactions from the same user and it is
//! possible to remove all reactions by sending an empty string as a reaction,
//! even though RFC 9078 requires at least one emoji to be sent.
use std::collections::BTreeMap;
use std::fmt;
use anyhow::Result;
use crate::chat::{send_msg, ChatId};
use crate::contact::ContactId;
use crate::context::Context;
use crate::events::EventType;
use crate::message::{rfc724_mid_exists, Message, MsgId, Viewtype};
/// A single reaction consisting of multiple emoji sequences.
///
/// It is guaranteed to have all emojis sorted and deduplicated inside.
#[derive(Debug, Default, Clone)]
pub struct Reaction {
/// Canonical represntation of reaction as a string of space-separated emojis.
reaction: String,
}
// We implement From<&str> instead of std::str::FromStr, because
// FromStr requires error type and reaction parsing never returns an
// error.
impl From<&str> for Reaction {
/// Parses a string containing a reaction.
///
/// Reaction string is separated by spaces or tabs (`WSP` in ABNF),
/// but this function accepts any ASCII whitespace, so even a CRLF at
/// the end of string is acceptable.
///
/// Any short enough string is accepted as a reaction to avoid the
/// complexity of validating emoji sequences as required by RFC
/// 9078. On the sender side UI is responsible to provide only
/// valid emoji sequences via reaction picker. On the receiver
/// side, abuse of the possibility to use arbitrary strings as
/// reactions is not different from other kinds of spam attacks
/// such as sending large numbers of large messages, and should be
/// dealt with the same way, e.g. by blocking the user.
fn from(reaction: &str) -> Self {
let mut emojis: Vec<&str> = reaction
.split_ascii_whitespace()
.filter(|&emoji| emoji.len() < 30)
.collect();
emojis.sort();
emojis.dedup();
let reaction = emojis.join(" ");
Self { reaction }
}
}
impl Reaction {
/// Returns true if reaction contains no emojis.
pub fn is_empty(&self) -> bool {
self.reaction.is_empty()
}
/// Returns a vector of emojis composing a reaction.
pub fn emojis(&self) -> Vec<&str> {
self.reaction.split(' ').collect()
}
/// Returns space-separated string of emojis
pub fn as_str(&self) -> &str {
&self.reaction
}
/// Appends emojis from another reaction to this reaction.
pub fn add(&self, other: Self) -> Self {
let mut emojis: Vec<&str> = self.emojis();
emojis.append(&mut other.emojis());
emojis.sort();
emojis.dedup();
let reaction = emojis.join(" ");
Self { reaction }
}
}
/// Structure representing all reactions to a particular message.
#[derive(Debug)]
pub struct Reactions {
/// Map from a contact to its reaction to message.
reactions: BTreeMap<ContactId, Reaction>,
}
impl Reactions {
/// Returns vector of contacts that reacted to the message.
pub fn contacts(&self) -> Vec<ContactId> {
self.reactions.keys().copied().collect()
}
/// Returns reaction of a given contact to message.
///
/// If contact did not react to message or removed the reaction,
/// this method returns an empty reaction.
pub fn get(&self, contact_id: ContactId) -> Reaction {
self.reactions.get(&contact_id).cloned().unwrap_or_default()
}
/// Returns true if the message has no reactions.
pub fn is_empty(&self) -> bool {
self.reactions.is_empty()
}
}
impl fmt::Display for Reactions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut emoji_frequencies: BTreeMap<String, usize> = BTreeMap::new();
for reaction in self.reactions.values() {
for emoji in reaction.emojis() {
emoji_frequencies
.entry(emoji.to_string())
.and_modify(|x| *x += 1)
.or_insert(1);
}
}
let mut first = true;
for (emoji, frequency) in emoji_frequencies {
if !first {
write!(f, " ")?;
}
first = false;
write!(f, "{}{}", emoji, frequency)?;
}
Ok(())
}
}
async fn set_msg_id_reaction(
context: &Context,
msg_id: MsgId,
chat_id: ChatId,
contact_id: ContactId,
reaction: Reaction,
) -> Result<()> {
if reaction.is_empty() {
// Simply remove the record instead of setting it to empty string.
context
.sql
.execute(
"DELETE FROM reactions
WHERE msg_id = ?1
AND contact_id = ?2",
paramsv![msg_id, contact_id],
)
.await?;
} else {
context
.sql
.execute(
"INSERT INTO reactions (msg_id, contact_id, reaction)
VALUES (?1, ?2, ?3)
ON CONFLICT(msg_id, contact_id)
DO UPDATE SET reaction=excluded.reaction",
paramsv![msg_id, contact_id, reaction.as_str()],
)
.await?;
}
context.emit_event(EventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
});
Ok(())
}
/// Sends a reaction to message `msg_id`, overriding previously sent reactions.
///
/// `reaction` is a string consisting of space-separated emoji. Use
/// empty string to retract a reaction.
pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) -> Result<MsgId> {
let msg = Message::load_from_db(context, msg_id).await?;
let chat_id = msg.chat_id;
let reaction: Reaction = reaction.into();
let mut reaction_msg = Message::new(Viewtype::Text);
reaction_msg.text = Some(reaction.as_str().to_string());
reaction_msg.set_reaction();
reaction_msg.in_reply_to = Some(msg.rfc724_mid);
reaction_msg.hidden = true;
// Send messsage first.
let reaction_msg_id = send_msg(context, chat_id, &mut reaction_msg).await?;
// Only set reaction if we successfully sent the message.
set_msg_id_reaction(context, msg_id, msg.chat_id, ContactId::SELF, reaction).await?;
Ok(reaction_msg_id)
}
/// Adds given reaction to message `msg_id` and sends an update.
///
/// This can be used to implement advanced clients that allow reacting
/// with multiple emojis. For a simple messenger UI, you probably want
/// to use [`send_reaction()`] instead so reacting with a new emoji
/// removes previous emoji at the same time.
pub async fn add_reaction(context: &Context, msg_id: MsgId, reaction: &str) -> Result<MsgId> {
let self_reaction = get_self_reaction(context, msg_id).await?;
let reaction = self_reaction.add(Reaction::from(reaction));
send_reaction(context, msg_id, reaction.as_str()).await
}
/// Updates reaction of `contact_id` on the message with `in_reply_to`
/// Message-ID. If no such message is found in the database, reaction
/// is ignored.
///
/// `reaction` is a space-separated string of emojis. It can be empty
/// if contact wants to remove all reactions.
pub(crate) async fn set_msg_reaction(
context: &Context,
in_reply_to: &str,
chat_id: ChatId,
contact_id: ContactId,
reaction: Reaction,
) -> Result<()> {
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
set_msg_id_reaction(context, msg_id, chat_id, contact_id, reaction).await
} else {
info!(
context,
"Can't assign reaction to unknown message with Message-ID {}", in_reply_to
);
Ok(())
}
}
/// Get our own reaction for a given message.
async fn get_self_reaction(context: &Context, msg_id: MsgId) -> Result<Reaction> {
let reaction_str: Option<String> = context
.sql
.query_get_value(
"SELECT reaction
FROM reactions
WHERE msg_id=? AND contact_id=?",
paramsv![msg_id, ContactId::SELF],
)
.await?;
Ok(reaction_str
.as_deref()
.map(Reaction::from)
.unwrap_or_default())
}
/// Returns a structure containing all reactions to the message.
pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result<Reactions> {
let reactions = context
.sql
.query_map(
"SELECT contact_id, reaction FROM reactions WHERE msg_id=?",
paramsv![msg_id],
|row| {
let contact_id: ContactId = row.get(0)?;
let reaction: String = row.get(1)?;
Ok((contact_id, reaction))
},
|rows| {
let mut reactions = Vec::new();
for row in rows {
let (contact_id, reaction) = row?;
reactions.push((contact_id, Reaction::from(reaction.as_str())));
}
Ok(reactions)
},
)
.await?
.into_iter()
.collect();
Ok(Reactions { reactions })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::get_chat_msgs;
use crate::config::Config;
use crate::constants::DC_CHAT_ID_TRASH;
use crate::contact::{Contact, Origin};
use crate::download::DownloadState;
use crate::message::MessageState;
use crate::receive_imf::{receive_imf, receive_imf_inner};
use crate::test_utils::TestContext;
#[test]
fn test_parse_reaction() {
// Check that basic set of emojis from RFC 9078 is supported.
assert_eq!(Reaction::from("👍").emojis(), vec!["👍"]);
assert_eq!(Reaction::from("👎").emojis(), vec!["👎"]);
assert_eq!(Reaction::from("😀").emojis(), vec!["😀"]);
assert_eq!(Reaction::from("").emojis(), vec![""]);
assert_eq!(Reaction::from("😢").emojis(), vec!["😢"]);
// Empty string can be used to remove all reactions.
assert!(Reaction::from("").is_empty());
// Short strings can be used as emojis, could be used to add
// support for custom emojis via emoji shortcodes.
assert_eq!(Reaction::from(":deltacat:").emojis(), vec![":deltacat:"]);
// Check that long strings are not valid emojis.
assert!(
Reaction::from(":foobarbazquuxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:").is_empty()
);
// Multiple reactions separated by spaces or tabs are supported.
assert_eq!(Reaction::from("👍 ❤").emojis(), vec!["", "👍"]);
assert_eq!(Reaction::from("👍\t").emojis(), vec!["", "👍"]);
// Invalid emojis are removed, but valid emojis are retained.
assert_eq!(
Reaction::from("👍\t:foo: ❤").emojis(),
vec![":foo:", "", "👍"]
);
assert_eq!(Reaction::from("👍\t:foo: ❤").as_str(), ":foo: ❤ 👍");
// Duplicates are removed.
assert_eq!(Reaction::from("👍 👍").emojis(), vec!["👍"]);
}
#[test]
fn test_add_reaction() {
let reaction1 = Reaction::from("👍 😀");
let reaction2 = Reaction::from("");
let reaction_sum = reaction1.add(reaction2);
assert_eq!(reaction_sum.emojis(), vec!["", "👍", "😀"]);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_receive_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
alice.set_config(Config::ShowEmails, Some("2")).await?;
// Alice receives BCC-self copy of a message sent to Bob.
receive_imf(
&alice,
"To: bob@example.net\n\
From: alice@example.org\n\
Date: Today, 29 February 2021 00:00:00 -800\n\
Message-ID: 12345@example.org\n\
Subject: Meeting\n\
\n\
Can we chat at 1pm pacific, today?"
.as_bytes(),
false,
)
.await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.state, MessageState::OutDelivered);
let reactions = get_msg_reactions(&alice, msg.id).await?;
let contacts = reactions.contacts();
assert_eq!(contacts.len(), 0);
let bob_id = Contact::add_or_lookup(&alice, "", "bob@example.net", Origin::ManuallyCreated)
.await?
.0;
let bob_reaction = reactions.get(bob_id);
assert!(bob_reaction.is_empty()); // Bob has not reacted to message yet.
// Alice receives reaction to her message from Bob.
receive_imf(
&alice,
"To: alice@example.org\n\
From: bob@example.net\n\
Date: Today, 29 February 2021 00:00:10 -800\n\
Message-ID: 56789@example.net\n\
In-Reply-To: 12345@example.org\n\
Subject: Meeting\n\
Mime-Version: 1.0 (1.0)\n\
Content-Type: text/plain; charset=utf-8\n\
Content-Disposition: reaction\n\
\n\
\u{1F44D}"
.as_bytes(),
false,
)
.await?;
let reactions = get_msg_reactions(&alice, msg.id).await?;
assert_eq!(reactions.to_string(), "👍1");
let contacts = reactions.contacts();
assert_eq!(contacts.len(), 1);
assert_eq!(contacts.get(0), Some(&bob_id));
let bob_reaction = reactions.get(bob_id);
assert_eq!(bob_reaction.is_empty(), false);
assert_eq!(bob_reaction.emojis(), vec!["👍"]);
assert_eq!(bob_reaction.as_str(), "👍");
Ok(())
}
async fn expect_reactions_changed_event(
t: &TestContext,
expected_chat_id: ChatId,
expected_msg_id: MsgId,
expected_contact_id: ContactId,
) -> Result<()> {
let event = t
.evtracker
.get_matching(|evt| matches!(evt, EventType::ReactionsChanged { .. }))
.await;
match event {
EventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
} => {
assert_eq!(chat_id, expected_chat_id);
assert_eq!(msg_id, expected_msg_id);
assert_eq!(contact_id, expected_contact_id);
}
_ => unreachable!(),
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_alice = alice.create_chat(&bob).await;
let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
let bob_msg = bob.recv_msg(&alice_msg).await;
assert_eq!(get_chat_msgs(&alice, chat_alice.id, 0).await?.len(), 1);
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id, 0).await?.len(), 1);
let alice_msg2 = alice.send_text(chat_alice.id, "Hi again!").await;
bob.recv_msg(&alice_msg2).await;
assert_eq!(get_chat_msgs(&alice, chat_alice.id, 0).await?.len(), 2);
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id, 0).await?.len(), 2);
bob_msg.chat_id.accept(&bob).await?;
send_reaction(&bob, bob_msg.id, "👍").await.unwrap();
expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id, 0).await?.len(), 2);
let bob_reaction_msg = bob.pop_sent_msg().await;
let alice_reaction_msg = alice.recv_msg_opt(&bob_reaction_msg).await.unwrap();
assert_eq!(alice_reaction_msg.chat_id, DC_CHAT_ID_TRASH);
assert_eq!(get_chat_msgs(&alice, chat_alice.id, 0).await?.len(), 2);
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
assert_eq!(reactions.to_string(), "👍1");
let contacts = reactions.contacts();
assert_eq!(contacts.len(), 1);
let bob_id = contacts.get(0).unwrap();
let bob_reaction = reactions.get(*bob_id);
assert_eq!(bob_reaction.is_empty(), false);
assert_eq!(bob_reaction.emojis(), vec!["👍"]);
assert_eq!(bob_reaction.as_str(), "👍");
expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id)
.await?;
// Alice reacts to own message.
send_reaction(&alice, alice_msg.sender_msg_id, "👍 😀")
.await
.unwrap();
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
assert_eq!(reactions.to_string(), "👍2 😀1");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_partial_download_and_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
alice
.create_chat_with_contact("Bob", "bob@example.net")
.await;
let msg_header = "From: Bob <bob@example.net>\n\
To: Alice <alice@example.org>\n\
Chat-Version: 1.0\n\
Subject: subject\n\
Message-ID: <first@example.org>\n\
Date: Sun, 14 Nov 2021 00:10:00 +0000\
Content-Type: text/plain";
let msg_full = format!("{}\n\n100k text...", msg_header);
// Alice downloads message from Bob partially.
let alice_received_message = receive_imf_inner(
&alice,
"first@example.org",
msg_header.as_bytes(),
false,
Some(100000),
false,
)
.await?
.unwrap();
let alice_msg_id = *alice_received_message.msg_ids.get(0).unwrap();
// Bob downloads own message on the other device.
let bob_received_message = receive_imf(&bob, msg_full.as_bytes(), false)
.await?
.unwrap();
let bob_msg_id = *bob_received_message.msg_ids.get(0).unwrap();
// Bob reacts to own message.
send_reaction(&bob, bob_msg_id, "👍").await.unwrap();
let bob_reaction_msg = bob.pop_sent_msg().await;
// Alice receives a reaction.
alice.recv_msg_opt(&bob_reaction_msg).await.unwrap();
let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
assert_eq!(reactions.to_string(), "👍1");
let msg = Message::load_from_db(&alice, alice_msg_id).await?;
assert_eq!(msg.download_state(), DownloadState::Available);
// Alice downloads full message.
receive_imf_inner(
&alice,
"first@example.org",
msg_full.as_bytes(),
false,
None,
false,
)
.await?;
// Check that reaction is still on the message after full download.
let msg = Message::load_from_db(&alice, alice_msg_id).await?;
assert_eq!(msg.download_state(), DownloadState::Done);
let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
assert_eq!(reactions.to_string(), "👍1");
Ok(())
}
}

View File

@@ -33,6 +33,7 @@ use crate::mimeparser::{
};
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
use crate::reaction::{set_msg_reaction, Reaction};
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
use crate::sql;
use crate::stock_str;
@@ -404,7 +405,7 @@ async fn add_parts(
from_id: ContactId,
seen: bool,
is_partial_download: Option<u32>,
replace_msg_id: Option<MsgId>,
mut replace_msg_id: Option<MsgId>,
fetching_existing_messages: bool,
prevent_rename: bool,
) -> Result<ReceivedMsg> {
@@ -430,8 +431,9 @@ async fn add_parts(
};
// incoming non-chat messages may be discarded
let location_kml_is = mime_parser.location_kml.is_some();
let is_location_kml = mime_parser.location_kml.is_some();
let is_mdn = !mime_parser.mdn_reports.is_empty();
let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction);
let show_emails =
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
@@ -450,7 +452,7 @@ async fn add_parts(
ShowEmails::All => allow_creation = !is_mdn,
}
} else {
allow_creation = !is_mdn;
allow_creation = !is_mdn && !is_reaction;
}
// check if the message introduces a new chat:
@@ -689,7 +691,8 @@ async fn add_parts(
state = if seen
|| fetching_existing_messages
|| is_mdn
|| location_kml_is
|| is_reaction
|| is_location_kml
|| securejoin_seen
|| chat_id_blocked == Blocked::Yes
{
@@ -841,14 +844,15 @@ async fn add_parts(
}
}
if is_mdn {
chat_id = Some(DC_CHAT_ID_TRASH);
}
let chat_id = chat_id.unwrap_or_else(|| {
info!(context, "No chat id for message (TRASH)");
let orig_chat_id = chat_id;
let chat_id = if is_mdn || is_reaction {
DC_CHAT_ID_TRASH
});
} else {
chat_id.unwrap_or_else(|| {
info!(context, "No chat id for message (TRASH)");
DC_CHAT_ID_TRASH
})
};
// Extract ephemeral timer from the message or use the existing timer if the message is not fully downloaded.
let mut ephemeral_timer = if is_partial_download.is_some() {
@@ -1053,11 +1057,41 @@ async fn add_parts(
let conn = context.sql.get_conn().await?;
for part in &mime_parser.parts {
if part.is_reaction {
set_msg_reaction(
context,
&mime_in_reply_to,
orig_chat_id.unwrap_or_default(),
from_id,
Reaction::from(part.msg.as_str()),
)
.await?;
}
let mut param = part.param.clone();
if is_system_message != SystemMessage::Unknown {
param.set_int(Param::Cmd, is_system_message as i32);
}
if let Some(replace_msg_id) = replace_msg_id {
let placeholder = Message::load_from_db(context, replace_msg_id).await?;
for key in [
Param::WebxdcSummary,
Param::WebxdcSummaryTimestamp,
Param::WebxdcDocument,
Param::WebxdcDocumentTimestamp,
] {
if let Some(value) = placeholder.param.get(key) {
param.set(key, value);
}
}
}
let mut txt_raw = "".to_string();
let mut stmt = conn.prepare_cached(
r#"
INSERT INTO msgs
(
id,
rfc724_mid, chat_id,
from_id, to_id, timestamp, timestamp_sent,
timestamp_rcvd, type, state, msgrmsg,
@@ -1067,13 +1101,22 @@ INSERT INTO msgs
ephemeral_timestamp, download_state, hop_info
)
VALUES (
?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?
);
)
ON CONFLICT (id) DO UPDATE
SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
from_id=excluded.from_id, to_id=excluded.to_id, timestamp=excluded.timestamp, timestamp_sent=excluded.timestamp_sent,
timestamp_rcvd=excluded.timestamp_rcvd, type=excluded.type, state=excluded.state, msgrmsg=excluded.msgrmsg,
txt=excluded.txt, subject=excluded.subject, txt_raw=excluded.txt_raw, param=excluded.param,
bytes=excluded.bytes, mime_headers=excluded.mime_headers, mime_in_reply_to=excluded.mime_in_reply_to,
mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
ephemeral_timestamp=excluded.ephemeral_timestamp, download_state=excluded.download_state, hop_info=excluded.hop_info
"#,
)?;
@@ -1095,11 +1138,6 @@ INSERT INTO msgs
txt_raw = format!("{}\n\n{}", subject, msg_raw);
}
let mut param = part.param.clone();
if is_system_message != SystemMessage::Unknown {
param.set_int(Param::Cmd, is_system_message as i32);
}
let ephemeral_timestamp = if in_fresh {
0
} else {
@@ -1113,9 +1151,10 @@ INSERT INTO msgs
// If you change which information is skipped if the message is trashed,
// also change `MsgId::trash()` and `delete_expired_messages()`
let trash = chat_id.is_trash() || (location_kml_is && msg.is_empty());
let trash = chat_id.is_trash() || (is_location_kml && msg.is_empty());
stmt.execute(paramsv![
replace_msg_id,
rfc724_mid,
if trash { DC_CHAT_ID_TRASH } else { chat_id },
if trash { ContactId::UNDEFINED } else { from_id },
@@ -1154,6 +1193,10 @@ INSERT INTO msgs
},
mime_parser.hop_info
])?;
// We only replace placeholder with a first part,
// afterwards insert additional parts.
replace_msg_id = None;
let row_id = conn.last_insert_rowid();
drop(stmt);
@@ -1162,14 +1205,8 @@ INSERT INTO msgs
drop(conn);
if let Some(replace_msg_id) = replace_msg_id {
if let Some(created_msg_id) = created_db_entries.pop() {
context
.merge_messages(created_msg_id, replace_msg_id)
.await?;
created_db_entries.push(replace_msg_id);
} else {
replace_msg_id.delete_from_db(context).await?;
}
// "Replace" placeholder with a message that has no parts.
replace_msg_id.delete_from_db(context).await?;
}
chat_id.unarchive_if_not_muted(context).await?;

View File

@@ -396,7 +396,7 @@ impl Sql {
}
/// Used for executing `SELECT COUNT` statements only. Returns the resulting count.
pub async fn count(&self, query: &str, params: impl rusqlite::Params) -> anyhow::Result<usize> {
pub async fn count(&self, query: &str, params: impl rusqlite::Params) -> Result<usize> {
let count: isize = self.query_row(query, params, |row| row.get(0)).await?;
Ok(usize::try_from(count)?)
}
@@ -429,10 +429,10 @@ impl Sql {
///
/// If the function returns an error, the transaction will be rolled back. If it does not return an
/// error, the transaction will be committed.
pub async fn transaction<G, H>(&self, callback: G) -> anyhow::Result<H>
pub async fn transaction<G, H>(&self, callback: G) -> Result<H>
where
H: Send + 'static,
G: Send + 'static + FnOnce(&mut rusqlite::Transaction<'_>) -> anyhow::Result<H>,
G: Send + 'static + FnOnce(&mut rusqlite::Transaction<'_>) -> Result<H>,
{
let mut conn = self.get_conn().await?;
tokio::task::block_in_place(move || {
@@ -453,7 +453,7 @@ impl Sql {
}
/// Query the database if the requested table already exists.
pub async fn table_exists(&self, name: &str) -> anyhow::Result<bool> {
pub async fn table_exists(&self, name: &str) -> Result<bool> {
let conn = self.get_conn().await?;
tokio::task::block_in_place(move || {
let mut exists = false;
@@ -468,7 +468,7 @@ impl Sql {
}
/// Check if a column exists in a given table.
pub async fn col_exists(&self, table_name: &str, col_name: &str) -> anyhow::Result<bool> {
pub async fn col_exists(&self, table_name: &str, col_name: &str) -> Result<bool> {
let conn = self.get_conn().await?;
tokio::task::block_in_place(move || {
let mut exists = false;
@@ -492,7 +492,7 @@ impl Sql {
sql: &str,
params: impl rusqlite::Params,
f: F,
) -> anyhow::Result<Option<T>>
) -> Result<Option<T>>
where
F: FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
{
@@ -516,7 +516,7 @@ impl Sql {
&self,
query: &str,
params: impl rusqlite::Params,
) -> anyhow::Result<Option<T>>
) -> Result<Option<T>>
where
T: rusqlite::types::FromSql,
{

View File

@@ -597,9 +597,22 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
.await?;
}
if dbversion < 92 {
sql.execute_migration(
r#"CREATE TABLE reactions (
msg_id INTEGER NOT NULL, -- id of the message reacted to
contact_id INTEGER NOT NULL, -- id of the contact reacting to the message
reaction TEXT DEFAULT '' NOT NULL, -- a sequence of emojis separated by spaces
PRIMARY KEY(msg_id, contact_id),
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE -- delete reactions when message is deleted
FOREIGN KEY(contact_id) REFERENCES contacts(id) ON DELETE CASCADE -- delete reactions when contact is deleted
)"#,
92
).await?;
}
if dbversion < 93 {
sql.execute_migration(
"CREATE TABLE sending_domains(domain TEXT PRIMARY KEY, dkim_works INTEGER DEFAULT 0);",
92,
93,
)
.await?;
}

View File

@@ -8,6 +8,7 @@ Transport | IMAP v4 ([RFC 3501](https://tools.ietf.org/ht
Proxy | SOCKS5 ([RFC 1928](https://tools.ietf.org/html/rfc1928))
Embedded media | MIME Document Series ([RFC 2045](https://tools.ietf.org/html/rfc2045), [RFC 2046](https://tools.ietf.org/html/rfc2046)), Content-Disposition Header ([RFC 2183](https://tools.ietf.org/html/rfc2183)), Multipart/Related ([RFC 2387](https://tools.ietf.org/html/rfc2387))
Text and Quote encoding | Fixed, Flowed ([RFC 3676](https://tools.ietf.org/html/rfc3676))
Reactions | Reaction: Indicating Summary Reaction to a Message [RFC 9078](https://datatracker.ietf.org/doc/rfc9078/)
Filename encoding | Encoded Words ([RFC 2047](https://tools.ietf.org/html/rfc2047)), Encoded Word Extensions ([RFC 2231](https://tools.ietf.org/html/rfc2231))
Identify server folders | IMAP LIST Extension ([RFC 6154](https://tools.ietf.org/html/rfc6154))
Push | IMAP IDLE ([RFC 2177](https://tools.ietf.org/html/rfc2177))