mirror of
https://github.com/chatmail/core.git
synced 2026-10-17 18:33:01 +03:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea1a962992 | ||
|
|
d8bc3769a5 | ||
|
|
a73fbf7232 | ||
|
|
b6b2f453a0 | ||
|
|
aa14015919 | ||
|
|
7551c84c4f | ||
|
|
434e53e922 | ||
|
|
b5d238f7f4 | ||
|
|
e5c9fea52d | ||
|
|
cd15a0e966 | ||
|
|
895c723d4e | ||
|
|
c7176d6bc8 | ||
|
|
f85f088d65 | ||
|
|
3e6d1d5789 |
@@ -3,6 +3,20 @@
|
||||
## Unreleased
|
||||
|
||||
### API-Changes
|
||||
- jsonrpc: add `Context` class you can get via `getContext` or `getContextEvents`, it is a convinience wrapper that has all account specific functions with the accountId pre-set.
|
||||
|
||||
### Changes
|
||||
|
||||
### Fixes
|
||||
- `dc_search_msgs()` returns unaccepted requests #3694
|
||||
|
||||
|
||||
## 1.98.0
|
||||
|
||||
### 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
|
||||
|
||||
Generated
+24
-20
@@ -895,7 +895,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.97.0"
|
||||
version = "1.98.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -954,7 +954,7 @@ dependencies = [
|
||||
"strum_macros",
|
||||
"tagger",
|
||||
"tempfile",
|
||||
"textwrap 0.15.1",
|
||||
"textwrap 0.16.0",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
@@ -967,7 +967,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.97.0"
|
||||
version = "1.98.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
@@ -996,7 +996,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.97.0"
|
||||
version = "1.98.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",
|
||||
@@ -1643,6 +1643,9 @@ name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
@@ -2431,9 +2434,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"
|
||||
@@ -3411,9 +3414,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.15.1"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
|
||||
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
"unicode-linebreak",
|
||||
@@ -3660,9 +3663,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 +3677,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",
|
||||
]
|
||||
|
||||
@@ -3783,10 +3786,11 @@ checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f"
|
||||
checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
|
||||
dependencies = [
|
||||
"hashbrown 0.12.3",
|
||||
"regex",
|
||||
]
|
||||
|
||||
@@ -3807,9 +3811,9 @@ checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.9"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
|
||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.97.0"
|
||||
version = "1.98.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"
|
||||
@@ -74,7 +74,7 @@ fast-socks5 = "0.8"
|
||||
humansize = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
tagger = "4.3.3"
|
||||
textwrap = "0.15.1"
|
||||
textwrap = "0.16.0"
|
||||
async-channel = "1.6.1"
|
||||
futures-lite = "1.12.0"
|
||||
tokio-stream = { version = "0.1.10", features = ["fs"] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.97.0"
|
||||
version = "1.98.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
+91
-11
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 _)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.97.0"
|
||||
version = "1.98.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
@@ -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;
|
||||
@@ -1466,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
|
||||
@@ -1692,3 +1710,89 @@ async fn get_config(
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn stuff() {
|
||||
let root_namespace = Some("T");
|
||||
let methods: Vec<yerpc::typescript::Method> = raw_ts_binding_methods();
|
||||
let account_methods: Vec<String> = methods
|
||||
.into_iter()
|
||||
.filter_map(|m| {
|
||||
// No idea how to check the type, to lazy to find out
|
||||
// if let Some((name, _)) = m.args.get(0) {
|
||||
// println!("{}", name);
|
||||
// }
|
||||
|
||||
//println!("-> {}", m.to_string(root_namespace));
|
||||
if m.to_string(root_namespace).contains("accountId: T.U32") {
|
||||
//println!("old: {}", m.to_string(root_namespace));
|
||||
|
||||
let args2 = Vec::from_iter(m.args[1..].iter().map(|m| m.to_owned()));
|
||||
|
||||
let m2 = yerpc::typescript::Method::new(
|
||||
&m.ts_name,
|
||||
&m.rpc_name,
|
||||
args2,
|
||||
m.output,
|
||||
m.is_notification,
|
||||
m.is_positional,
|
||||
m.docs.as_deref(),
|
||||
);
|
||||
|
||||
let args_string = if m2.args.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!(
|
||||
", {}",
|
||||
m2.args
|
||||
.iter()
|
||||
.map(|(name, _)| name.as_ref())
|
||||
.collect::<Vec<&str>>()
|
||||
.join(", ")
|
||||
)
|
||||
};
|
||||
|
||||
let replacement_line = format!(
|
||||
" return this.controller.rpc.{}(this.accountId{})",
|
||||
m.ts_name, args_string
|
||||
);
|
||||
|
||||
// println!("{}", m2.to_string(root_namespace));
|
||||
let m_string = m2.to_string(root_namespace);
|
||||
let new_string: String = m_string
|
||||
.split('\n')
|
||||
.map(|line| {
|
||||
if line.contains("this._transport") {
|
||||
&replacement_line
|
||||
} else {
|
||||
line
|
||||
}
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
|
||||
// println!("new: {}", new_string);
|
||||
|
||||
Some(new_string)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let head = r#"import * as T from "./types.js"
|
||||
import { RawClient } from "./client.js"
|
||||
import { TinyEmitter } from "tiny-emitter";
|
||||
export class Context<T> extends TinyEmitter<T> {
|
||||
constructor(
|
||||
private controller: {rpc: RawClient},
|
||||
readonly accountId: T.U32
|
||||
){
|
||||
super()
|
||||
}"#;
|
||||
|
||||
let context_methods = format!("{}\n{}\n}}\n", head, account_methods.join(""));
|
||||
|
||||
std::fs::write("typescript/generated/context_methods.ts", context_methods).unwrap();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ async function run() {
|
||||
|
||||
const client = new DeltaChat("ws://localhost:20808/ws");
|
||||
|
||||
(window as any).dc = client;
|
||||
(window as any).client = client.rpc;
|
||||
|
||||
client.on("ALL", (accountId, event) => {
|
||||
|
||||
@@ -878,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>;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -0,0 +1,855 @@
|
||||
import * as T from "./types.js"
|
||||
import { RawClient } from "./client.js"
|
||||
import { TinyEmitter } from "tiny-emitter";
|
||||
export class Context<T> extends TinyEmitter<T> {
|
||||
constructor(
|
||||
private controller: {rpc: RawClient},
|
||||
readonly accountId: T.U32
|
||||
){
|
||||
super()
|
||||
}
|
||||
|
||||
public removeAccount(): Promise<null> {
|
||||
return this.controller.rpc.removeAccount(this.accountId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get top-level info for an account.
|
||||
*/
|
||||
public getAccountInfo(): Promise<T.Account> {
|
||||
return this.controller.rpc.getAccountInfo(this.accountId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the combined filesize of an account in bytes
|
||||
*/
|
||||
public getAccountFileSize(): Promise<T.U64> {
|
||||
return this.controller.rpc.getAccountFileSize(this.accountId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public getProviderInfo(email: string): Promise<(T.ProviderInfo|null)> {
|
||||
return this.controller.rpc.getProviderInfo(this.accountId, email)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the context is already configured.
|
||||
*/
|
||||
public isConfigured(): Promise<boolean> {
|
||||
return this.controller.rpc.isConfigured(this.accountId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system info for an account.
|
||||
*/
|
||||
public getInfo(): Promise<Record<string,string>> {
|
||||
return this.controller.rpc.getInfo(this.accountId)
|
||||
}
|
||||
|
||||
|
||||
public setConfig(key: string, value: (string|null)): Promise<null> {
|
||||
return this.controller.rpc.setConfig(this.accountId, key, value)
|
||||
}
|
||||
|
||||
|
||||
public batchSetConfig(config: Record<string,(string|null)>): Promise<null> {
|
||||
return this.controller.rpc.batchSetConfig(this.accountId, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
*/
|
||||
public setConfigFromQr(qrContent: string): Promise<null> {
|
||||
return this.controller.rpc.setConfigFromQr(this.accountId, qrContent)
|
||||
}
|
||||
|
||||
|
||||
public checkQr(qrContent: string): Promise<T.Qr> {
|
||||
return this.controller.rpc.checkQr(this.accountId, qrContent)
|
||||
}
|
||||
|
||||
|
||||
public getConfig(key: string): Promise<(string|null)> {
|
||||
return this.controller.rpc.getConfig(this.accountId, key)
|
||||
}
|
||||
|
||||
|
||||
public batchGetConfig(keys: (string)[]): Promise<Record<string,(string|null)>> {
|
||||
return this.controller.rpc.batchGetConfig(this.accountId, keys)
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures this account with the currently set parameters.
|
||||
* Setup the credential config before calling this.
|
||||
*/
|
||||
public configure(): Promise<null> {
|
||||
return this.controller.rpc.configure(this.accountId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal an ongoing process to stop.
|
||||
*/
|
||||
public stopOngoingProcess(): Promise<null> {
|
||||
return this.controller.rpc.stopOngoingProcess(this.accountId)
|
||||
}
|
||||
|
||||
|
||||
public exportSelfKeys(path: string, passphrase: (string|null)): Promise<null> {
|
||||
return this.controller.rpc.exportSelfKeys(this.accountId, path, passphrase)
|
||||
}
|
||||
|
||||
|
||||
public importSelfKeys(path: string, passphrase: (string|null)): Promise<null> {
|
||||
return this.controller.rpc.importSelfKeys(this.accountId, path, passphrase)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()`.
|
||||
*/
|
||||
public getFreshMsgs(): Promise<(T.U32)[]> {
|
||||
return this.controller.rpc.getFreshMsgs(this.accountId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public getFreshMsgCnt(chatId: T.U32): Promise<T.Usize> {
|
||||
return this.controller.rpc.getFreshMsgCnt(this.accountId, chatId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public estimateAutoDeletionCount(fromServer: boolean, seconds: T.I64): Promise<T.Usize> {
|
||||
return this.controller.rpc.estimateAutoDeletionCount(this.accountId, fromServer, seconds)
|
||||
}
|
||||
|
||||
|
||||
public autocryptInitiateKeyTransfer(): Promise<string> {
|
||||
return this.controller.rpc.autocryptInitiateKeyTransfer(this.accountId)
|
||||
}
|
||||
|
||||
|
||||
public autocryptContinueKeyTransfer(messageId: T.U32, setupCode: string): Promise<null> {
|
||||
return this.controller.rpc.autocryptContinueKeyTransfer(this.accountId, messageId, setupCode)
|
||||
}
|
||||
|
||||
|
||||
public getChatlistEntries(listFlags: (T.U32|null), queryString: (string|null), queryContactId: (T.U32|null)): Promise<(T.ChatListEntry)[]> {
|
||||
return this.controller.rpc.getChatlistEntries(this.accountId, listFlags, queryString, queryContactId)
|
||||
}
|
||||
|
||||
|
||||
public getChatlistItemsByEntries(entries: (T.ChatListEntry)[]): Promise<Record<T.U32,T.ChatListItemFetchResult>> {
|
||||
return this.controller.rpc.getChatlistItemsByEntries(this.accountId, entries)
|
||||
}
|
||||
|
||||
|
||||
public chatlistGetFullChatById(chatId: T.U32): Promise<T.FullChat> {
|
||||
return this.controller.rpc.chatlistGetFullChatById(this.accountId, chatId)
|
||||
}
|
||||
|
||||
/**
|
||||
* get basic info about a chat,
|
||||
* use chatlist_get_full_chat_by_id() instead if you need more information
|
||||
*/
|
||||
public getBasicChatInfo(chatId: T.U32): Promise<T.BasicChat> {
|
||||
return this.controller.rpc.getBasicChatInfo(this.accountId, chatId)
|
||||
}
|
||||
|
||||
|
||||
public acceptChat(chatId: T.U32): Promise<null> {
|
||||
return this.controller.rpc.acceptChat(this.accountId, chatId)
|
||||
}
|
||||
|
||||
|
||||
public blockChat(chatId: T.U32): Promise<null> {
|
||||
return this.controller.rpc.blockChat(this.accountId, chatId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
*/
|
||||
public deleteChat(chatId: T.U32): Promise<null> {
|
||||
return this.controller.rpc.deleteChat(this.accountId, chatId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public getChatEncryptionInfo(chatId: T.U32): Promise<string> {
|
||||
return this.controller.rpc.getChatEncryptionInfo(this.accountId, chatId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get QR code (text and SVG) that will offer an Setup-Contact or Verified-Group invitation.
|
||||
* The QR code is compatible to the OPENPGP4FPR format
|
||||
* so that a basic fingerprint comparison also works e.g. with OpenKeychain.
|
||||
*
|
||||
* The scanning device will pass the scanned content to `checkQr()` then;
|
||||
* if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
|
||||
* an out-of-band-verification can be joined using `secure_join()`
|
||||
*
|
||||
* chat_id: If set to a group-chat-id,
|
||||
* the Verified-Group-Invite protocol is offered in the QR code;
|
||||
* works for protected groups as well as for normal groups.
|
||||
* If not set, the Setup-Contact protocol is offered in the QR code.
|
||||
* See https://countermitm.readthedocs.io/en/latest/new.html
|
||||
* for details about both protocols.
|
||||
*
|
||||
* return format: `[code, svg]`
|
||||
*/
|
||||
public getChatSecurejoinQrCodeSvg(chatId: (T.U32|null)): Promise<[string,string]> {
|
||||
return this.controller.rpc.getChatSecurejoinQrCodeSvg(this.accountId, chatId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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://countermitm.readthedocs.io/en/latest/new.html
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
public secureJoin(qr: string): Promise<T.U32> {
|
||||
return this.controller.rpc.secureJoin(this.accountId, qr)
|
||||
}
|
||||
|
||||
|
||||
public leaveGroup(chatId: T.U32): Promise<null> {
|
||||
return this.controller.rpc.leaveGroup(this.accountId, chatId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public removeContactFromChat(chatId: T.U32, contactId: T.U32): Promise<null> {
|
||||
return this.controller.rpc.removeContactFromChat(this.accountId, chatId, contactId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public addContactToChat(chatId: T.U32, contactId: T.U32): Promise<null> {
|
||||
return this.controller.rpc.addContactToChat(this.accountId, chatId, contactId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
public getChatContacts(chatId: T.U32): Promise<(T.U32)[]> {
|
||||
return this.controller.rpc.getChatContacts(this.accountId, chatId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public createGroupChat(name: string, protect: boolean): Promise<T.U32> {
|
||||
return this.controller.rpc.createGroupChat(this.accountId, name, protect)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new broadcast list.
|
||||
*
|
||||
* Broadcast lists are similar to groups on the sending device,
|
||||
* however, recipients get the messages in normal one-to-one chats
|
||||
* and will not be aware of other members.
|
||||
*
|
||||
* Replies to broadcasts go only to the sender
|
||||
* and not to all broadcast recipients.
|
||||
* Moreover, replies will not appear in the broadcast list
|
||||
* but in the one-to-one chat with the person answering.
|
||||
*
|
||||
* The name and the image of the broadcast list is set automatically
|
||||
* and is visible to the sender only.
|
||||
* Not asking for these data allows more focused creation
|
||||
* and we bypass the question who will get which data.
|
||||
* Also, many users will have at most one broadcast list
|
||||
* so, a generic name and image is sufficient at the first place.
|
||||
*
|
||||
* Later on, however, the name can be changed using dc_set_chat_name().
|
||||
* The image cannot be changed to have a unique, recognizable icon in the chat lists.
|
||||
* All in all, this is also what other messengers are doing here.
|
||||
*/
|
||||
public createBroadcastList(): Promise<T.U32> {
|
||||
return this.controller.rpc.createBroadcastList(this.accountId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public setChatName(chatId: T.U32, newName: string): Promise<null> {
|
||||
return this.controller.rpc.setChatName(this.accountId, chatId, newName)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
public setChatProfileImage(chatId: T.U32, imagePath: (string|null)): Promise<null> {
|
||||
return this.controller.rpc.setChatProfileImage(this.accountId, chatId, imagePath)
|
||||
}
|
||||
|
||||
|
||||
public setChatVisibility(chatId: T.U32, visibility: T.ChatVisibility): Promise<null> {
|
||||
return this.controller.rpc.setChatVisibility(this.accountId, chatId, visibility)
|
||||
}
|
||||
|
||||
|
||||
public setChatEphemeralTimer(chatId: T.U32, timer: T.U32): Promise<null> {
|
||||
return this.controller.rpc.setChatEphemeralTimer(this.accountId, chatId, timer)
|
||||
}
|
||||
|
||||
|
||||
public getChatEphemeralTimer(chatId: T.U32): Promise<T.U32> {
|
||||
return this.controller.rpc.getChatEphemeralTimer(this.accountId, chatId)
|
||||
}
|
||||
|
||||
|
||||
public addDeviceMessage(label: string, text: string): Promise<T.U32> {
|
||||
return this.controller.rpc.addDeviceMessage(this.accountId, label, text)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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().
|
||||
*/
|
||||
public marknoticedChat(chatId: T.U32): Promise<null> {
|
||||
return this.controller.rpc.marknoticedChat(this.accountId, chatId)
|
||||
}
|
||||
|
||||
|
||||
public getFirstUnreadMessageOfChat(chatId: T.U32): Promise<(T.U32|null)> {
|
||||
return this.controller.rpc.getFirstUnreadMessageOfChat(this.accountId, chatId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public setChatMuteDuration(chatId: T.U32, duration: T.MuteDuration): Promise<null> {
|
||||
return this.controller.rpc.setChatMuteDuration(this.accountId, chatId, duration)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public isChatMuted(chatId: T.U32): Promise<boolean> {
|
||||
return this.controller.rpc.isChatMuted(this.accountId, chatId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
|
||||
*/
|
||||
public markseenMsgs(msgIds: (T.U32)[]): Promise<null> {
|
||||
return this.controller.rpc.markseenMsgs(this.accountId, msgIds)
|
||||
}
|
||||
|
||||
|
||||
public getMessageIds(chatId: T.U32, flags: T.U32): Promise<(T.U32)[]> {
|
||||
return this.controller.rpc.getMessageIds(this.accountId, chatId, flags)
|
||||
}
|
||||
|
||||
|
||||
public getMessageListItems(chatId: T.U32, flags: T.U32): Promise<(T.MessageListItem)[]> {
|
||||
return this.controller.rpc.getMessageListItems(this.accountId, chatId, flags)
|
||||
}
|
||||
|
||||
|
||||
public messageGetMessage(messageId: T.U32): Promise<T.Message> {
|
||||
return this.controller.rpc.messageGetMessage(this.accountId, messageId)
|
||||
}
|
||||
|
||||
|
||||
public getMessageHtml(messageId: T.U32): Promise<(string|null)> {
|
||||
return this.controller.rpc.getMessageHtml(this.accountId, messageId)
|
||||
}
|
||||
|
||||
|
||||
public messageGetMessages(messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
|
||||
return this.controller.rpc.messageGetMessages(this.accountId, messageIds)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch info desktop needs for creating a notification for a message
|
||||
*/
|
||||
public messageGetNotificationInfo(messageId: T.U32): Promise<T.MessageNotificationInfo> {
|
||||
return this.controller.rpc.messageGetNotificationInfo(this.accountId, messageId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete messages. The messages are deleted on the current device and
|
||||
* on the IMAP server.
|
||||
*/
|
||||
public deleteMessages(messageIds: (T.U32)[]): Promise<null> {
|
||||
return this.controller.rpc.deleteMessages(this.accountId, messageIds)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
public getMessageInfo(messageId: T.U32): Promise<string> {
|
||||
return this.controller.rpc.getMessageInfo(this.accountId, messageId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public downloadFullMessage(messageId: T.U32): Promise<null> {
|
||||
return this.controller.rpc.downloadFullMessage(this.accountId, messageId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search messages containing the given query string.
|
||||
* Searching can be done globally (chat_id=0) or in a specified chat only (chat_id set).
|
||||
*
|
||||
* Global chat results are typically displayed using dc_msg_get_summary(), chat
|
||||
* search results may just hilite the corresponding messages and present a
|
||||
* prev/next button.
|
||||
*
|
||||
* For global search, result is limited to 1000 messages,
|
||||
* this allows incremental search done fast.
|
||||
* So, when getting exactly 1000 results, the result may be truncated;
|
||||
* the UIs may display sth. as "1000+ messages found" in this case.
|
||||
* Chat search (if a chat_id is set) is not limited.
|
||||
*/
|
||||
public searchMessages(query: string, chatId: (T.U32|null)): Promise<(T.U32)[]> {
|
||||
return this.controller.rpc.searchMessages(this.accountId, query, chatId)
|
||||
}
|
||||
|
||||
|
||||
public messageIdsToSearchResults(messageIds: (T.U32)[]): Promise<Record<T.U32,T.MessageSearchResult>> {
|
||||
return this.controller.rpc.messageIdsToSearchResults(this.accountId, messageIds)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single contact options by ID.
|
||||
*/
|
||||
public contactsGetContact(contactId: T.U32): Promise<T.Contact> {
|
||||
return this.controller.rpc.contactsGetContact(this.accountId, contactId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single contact as a result of an explicit user action.
|
||||
*
|
||||
* Returns contact id of the created or existing contact
|
||||
*/
|
||||
public contactsCreateContact(email: string, name: (string|null)): Promise<T.U32> {
|
||||
return this.controller.rpc.contactsCreateContact(this.accountId, email, name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns contact id of the created or existing DM chat with that contact
|
||||
*/
|
||||
public contactsCreateChatByContactId(contactId: T.U32): Promise<T.U32> {
|
||||
return this.controller.rpc.contactsCreateChatByContactId(this.accountId, contactId)
|
||||
}
|
||||
|
||||
|
||||
public contactsBlock(contactId: T.U32): Promise<null> {
|
||||
return this.controller.rpc.contactsBlock(this.accountId, contactId)
|
||||
}
|
||||
|
||||
|
||||
public contactsUnblock(contactId: T.U32): Promise<null> {
|
||||
return this.controller.rpc.contactsUnblock(this.accountId, contactId)
|
||||
}
|
||||
|
||||
|
||||
public contactsGetBlocked(): Promise<(T.Contact)[]> {
|
||||
return this.controller.rpc.contactsGetBlocked(this.accountId)
|
||||
}
|
||||
|
||||
|
||||
public contactsGetContactIds(listFlags: T.U32, query: (string|null)): Promise<(T.U32)[]> {
|
||||
return this.controller.rpc.contactsGetContactIds(this.accountId, listFlags, query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of contacts.
|
||||
* (formerly called getContacts2 in desktop)
|
||||
*/
|
||||
public contactsGetContacts(listFlags: T.U32, query: (string|null)): Promise<(T.Contact)[]> {
|
||||
return this.controller.rpc.contactsGetContacts(this.accountId, listFlags, query)
|
||||
}
|
||||
|
||||
|
||||
public contactsGetContactsByIds(ids: (T.U32)[]): Promise<Record<T.U32,T.Contact>> {
|
||||
return this.controller.rpc.contactsGetContactsByIds(this.accountId, ids)
|
||||
}
|
||||
|
||||
|
||||
public deleteContact(contactId: T.U32): Promise<boolean> {
|
||||
return this.controller.rpc.deleteContact(this.accountId, contactId)
|
||||
}
|
||||
|
||||
|
||||
public changeContactName(contactId: T.U32, name: string): Promise<null> {
|
||||
return this.controller.rpc.changeContactName(this.accountId, contactId, name)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public getContactEncryptionInfo(contactId: T.U32): Promise<string> {
|
||||
return this.controller.rpc.getContactEncryptionInfo(this.accountId, contactId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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().
|
||||
*/
|
||||
public lookupContactIdByAddr(addr: string): Promise<(T.U32|null)> {
|
||||
return this.controller.rpc.lookupContactIdByAddr(this.accountId, addr)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public chatGetMedia(chatId: (T.U32|null), messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<(T.U32)[]> {
|
||||
return this.controller.rpc.chatGetMedia(this.accountId, chatId, messageType, orMessageType2, orMessageType3)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search next/previous message based on a given message and a list of types.
|
||||
* Typically used to implement the "next" and "previous" buttons
|
||||
* in a gallery or in a media player.
|
||||
*
|
||||
* one combined call for getting chat::get_next_media for both directions
|
||||
* the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
|
||||
*/
|
||||
public chatGetNeighboringMedia(msgId: T.U32, messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<[(T.U32|null),(T.U32|null)]> {
|
||||
return this.controller.rpc.chatGetNeighboringMedia(this.accountId, msgId, messageType, orMessageType2, orMessageType3)
|
||||
}
|
||||
|
||||
|
||||
public exportBackup(destination: string, passphrase: (string|null)): Promise<null> {
|
||||
return this.controller.rpc.exportBackup(this.accountId, destination, passphrase)
|
||||
}
|
||||
|
||||
|
||||
public importBackup(path: string, passphrase: (string|null)): Promise<null> {
|
||||
return this.controller.rpc.importBackup(this.accountId, path, passphrase)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public getConnectivity(): Promise<T.U32> {
|
||||
return this.controller.rpc.getConnectivity(this.accountId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public getConnectivityHtml(): Promise<string> {
|
||||
return this.controller.rpc.getConnectivityHtml(this.accountId)
|
||||
}
|
||||
|
||||
|
||||
public getLocations(chatId: (T.U32|null), contactId: (T.U32|null), timestampBegin: T.I64, timestampEnd: T.I64): Promise<(T.Location)[]> {
|
||||
return this.controller.rpc.getLocations(this.accountId, chatId, contactId, timestampBegin, timestampEnd)
|
||||
}
|
||||
|
||||
|
||||
public webxdcSendStatusUpdate(instanceMsgId: T.U32, updateStr: string, description: string): Promise<null> {
|
||||
return this.controller.rpc.webxdcSendStatusUpdate(this.accountId, instanceMsgId, updateStr, description)
|
||||
}
|
||||
|
||||
|
||||
public webxdcGetStatusUpdates(instanceMsgId: T.U32, lastKnownSerial: T.U32): Promise<string> {
|
||||
return this.controller.rpc.webxdcGetStatusUpdates(this.accountId, instanceMsgId, lastKnownSerial)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get info from a webxdc message
|
||||
*/
|
||||
public messageGetWebxdcInfo(instanceMsgId: T.U32): Promise<T.WebxdcMessageInfo> {
|
||||
return this.controller.rpc.messageGetWebxdcInfo(this.accountId, instanceMsgId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public forwardMessages(messageIds: (T.U32)[], chatId: T.U32): Promise<null> {
|
||||
return this.controller.rpc.forwardMessages(this.accountId, messageIds, chatId)
|
||||
}
|
||||
|
||||
|
||||
public sendSticker(chatId: T.U32, stickerPath: string): Promise<T.U32> {
|
||||
return this.controller.rpc.sendSticker(this.accountId, chatId, stickerPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(messageId: T.U32, reaction: (string)[]): Promise<T.U32> {
|
||||
return this.controller.rpc.sendReaction(this.accountId, messageId, reaction)
|
||||
}
|
||||
|
||||
|
||||
public removeDraft(chatId: T.U32): Promise<null> {
|
||||
return this.controller.rpc.removeDraft(this.accountId, chatId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get draft for a chat, if any.
|
||||
*/
|
||||
public getDraft(chatId: T.U32): Promise<(T.Message|null)> {
|
||||
return this.controller.rpc.getDraft(this.accountId, chatId)
|
||||
}
|
||||
|
||||
|
||||
public sendVideochatInvitation(chatId: T.U32): Promise<T.U32> {
|
||||
return this.controller.rpc.sendVideochatInvitation(this.accountId, chatId)
|
||||
}
|
||||
|
||||
|
||||
public miscGetStickerFolder(): Promise<string> {
|
||||
return this.controller.rpc.miscGetStickerFolder(this.accountId)
|
||||
}
|
||||
|
||||
/**
|
||||
* for desktop, get stickers from stickers folder,
|
||||
* grouped by the folder they are in.
|
||||
*/
|
||||
public miscGetStickers(): Promise<Record<string,(string)[]>> {
|
||||
return this.controller.rpc.miscGetStickers(this.accountId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the messageid of the sent message
|
||||
*/
|
||||
public miscSendTextMessage(text: string, chatId: T.U32): Promise<T.U32> {
|
||||
return this.controller.rpc.miscSendTextMessage(this.accountId, text, chatId)
|
||||
}
|
||||
|
||||
|
||||
public miscSendMsg(chatId: T.U32, text: (string|null), file: (string|null), location: ([T.F64,T.F64]|null), quotedMessageId: (T.U32|null)): Promise<[T.U32,T.Message]> {
|
||||
return this.controller.rpc.miscSendMsg(this.accountId, chatId, text, file, location, quotedMessageId)
|
||||
}
|
||||
|
||||
|
||||
public miscSetDraft(chatId: T.U32, text: (string|null), file: (string|null), quotedMessageId: (T.U32|null)): Promise<null> {
|
||||
return this.controller.rpc.miscSetDraft(this.accountId, chatId, text, file, quotedMessageId)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -77,6 +77,10 @@ export type Event=(({
|
||||
* `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.
|
||||
|
||||
@@ -1,67 +1,6 @@
|
||||
// AUTO-GENERATED by typescript-type-def
|
||||
|
||||
export type U32=number;
|
||||
export type Account=(({"type":"Configured";}&{"id":U32;"displayName":(string|null);"addr":(string|null);"profileImage":(string|null);"color":string;})|({"type":"Unconfigured";}&{"id":U32;}));
|
||||
export type U64=number;
|
||||
export type ProviderInfo={"beforeLoginHint":string;"overviewPage":string;"status":U32;};
|
||||
export type Qr=(({"type":"askVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"askVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"fprOk";}&{"contact_id":U32;})|({"type":"fprMismatch";}&{"contact_id":(U32|null);})|({"type":"fprWithoutAddr";}&{"fingerprint":string;})|({"type":"account";}&{"domain":string;})|({"type":"webrtcInstance";}&{"domain":string;"instance_pattern":string;})|({"type":"addr";}&{"contact_id":U32;"draft":(string|null);})|({"type":"url";}&{"url":string;})|({"type":"text";}&{"text":string;})|({"type":"withdrawVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"withdrawVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"login";}&{"address":string;}));
|
||||
export type Usize=number;
|
||||
export type I64=number;
|
||||
export type ChatListEntry=[U32,U32];
|
||||
export type ChatListItemFetchResult=(({"type":"ChatListItem";}&{"id":U32;"name":string;"avatarPath":(string|null);"color":string;"lastUpdated":(I64|null);"summaryText1":string;"summaryText2":string;"summaryStatus":U32;"isProtected":boolean;"isGroup":boolean;"freshMessageCounter":Usize;"isSelfTalk":boolean;"isDeviceTalk":boolean;"isSendingLocation":boolean;"isSelfInGroup":boolean;"isArchived":boolean;"isPinned":boolean;"isMuted":boolean;"isContactRequest":boolean;
|
||||
/**
|
||||
* true when chat is a broadcastlist
|
||||
*/
|
||||
"isBroadcast":boolean;
|
||||
/**
|
||||
* contact id if this is a dm chat (for view profile entry in context menu)
|
||||
*/
|
||||
"dmChatContact":(U32|null);"wasSeenRecently":boolean;})|{"type":"ArchiveLink";}|({"type":"Error";}&{"id":U32;"error":string;}));
|
||||
export type Contact={"address":string;"color":string;"authName":string;"status":string;"displayName":string;"id":U32;"name":string;"profileImage":(string|null);"nameAndAddr":string;"isBlocked":boolean;"isVerified":boolean;
|
||||
/**
|
||||
* the contact's last seen timestamp
|
||||
*/
|
||||
"lastSeen":I64;"wasSeenRecently":boolean;};
|
||||
export type FullChat={"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"contacts":(Contact)[];"contactIds":(U32)[];"color":string;"freshMessageCounter":Usize;"isContactRequest":boolean;"isDeviceChat":boolean;"selfInGroup":boolean;"isMuted":boolean;"ephemeralTimer":U32;"canSend":boolean;"wasSeenRecently":boolean;"mailingListAddress":string;};
|
||||
|
||||
/**
|
||||
* cheaper version of fullchat, omits:
|
||||
* - contacts
|
||||
* - contact_ids
|
||||
* - fresh_message_counter
|
||||
* - ephemeral_timer
|
||||
* - self_in_group
|
||||
* - was_seen_recently
|
||||
* - can_send
|
||||
*
|
||||
* used when you only need the basic metadata of a chat like type, name, profile picture
|
||||
*/
|
||||
export type BasicChat=
|
||||
/**
|
||||
* cheaper version of fullchat, omits:
|
||||
* - contacts
|
||||
* - contact_ids
|
||||
* - fresh_message_counter
|
||||
* - ephemeral_timer
|
||||
* - self_in_group
|
||||
* - was_seen_recently
|
||||
* - can_send
|
||||
*
|
||||
* used when you only need the basic metadata of a chat like type, name, profile picture
|
||||
*/
|
||||
{"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"color":string;"isContactRequest":boolean;"isDeviceChat":boolean;"isMuted":boolean;};
|
||||
export type ChatVisibility=("Normal"|"Archived"|"Pinned");
|
||||
export type MuteDuration=("NotMuted"|"Forever"|{"Until":I64;});
|
||||
export type MessageListItem=(({"kind":"message";}&{"msg_id":U32;})|({
|
||||
/**
|
||||
* Day marker, separating messages that correspond to different
|
||||
* days according to local time.
|
||||
*/
|
||||
"kind":"dayMarker";}&{
|
||||
/**
|
||||
* Marker timestamp, for day markers, in unix milliseconds
|
||||
*/
|
||||
"timestamp":I64;}));
|
||||
export type Viewtype=("Unknown"|
|
||||
/**
|
||||
* Text message.
|
||||
@@ -108,7 +47,14 @@ export type Viewtype=("Unknown"|
|
||||
*/
|
||||
"Webxdc");
|
||||
export type MessageQuote=(({"kind":"JustText";}&{"text":string;})|({"kind":"WithMessage";}&{"text":string;"messageId":U32;"authorDisplayName":string;"authorDisplayColor":string;"overrideSenderName":(string|null);"image":(string|null);"isForwarded":boolean;"viewType":Viewtype;}));
|
||||
export type I64=number;
|
||||
export type I32=number;
|
||||
export type Contact={"address":string;"color":string;"authName":string;"status":string;"displayName":string;"id":U32;"name":string;"profileImage":(string|null);"nameAndAddr":string;"isBlocked":boolean;"isVerified":boolean;
|
||||
/**
|
||||
* the contact's last seen timestamp
|
||||
*/
|
||||
"lastSeen":I64;"wasSeenRecently":boolean;};
|
||||
export type U64=number;
|
||||
export type WebxdcMessageInfo={
|
||||
/**
|
||||
* The name of the app.
|
||||
@@ -147,7 +93,65 @@ 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 Account=(({"type":"Configured";}&{"id":U32;"displayName":(string|null);"addr":(string|null);"profileImage":(string|null);"color":string;})|({"type":"Unconfigured";}&{"id":U32;}));
|
||||
|
||||
/**
|
||||
* cheaper version of fullchat, omits:
|
||||
* - contacts
|
||||
* - contact_ids
|
||||
* - fresh_message_counter
|
||||
* - ephemeral_timer
|
||||
* - self_in_group
|
||||
* - was_seen_recently
|
||||
* - can_send
|
||||
*
|
||||
* used when you only need the basic metadata of a chat like type, name, profile picture
|
||||
*/
|
||||
export type BasicChat=
|
||||
/**
|
||||
* cheaper version of fullchat, omits:
|
||||
* - contacts
|
||||
* - contact_ids
|
||||
* - fresh_message_counter
|
||||
* - ephemeral_timer
|
||||
* - self_in_group
|
||||
* - was_seen_recently
|
||||
* - can_send
|
||||
*
|
||||
* used when you only need the basic metadata of a chat like type, name, profile picture
|
||||
*/
|
||||
{"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"color":string;"isContactRequest":boolean;"isDeviceChat":boolean;"isMuted":boolean;};
|
||||
export type Usize=number;
|
||||
export type FullChat={"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"contacts":(Contact)[];"contactIds":(U32)[];"color":string;"freshMessageCounter":Usize;"isContactRequest":boolean;"isDeviceChat":boolean;"selfInGroup":boolean;"isMuted":boolean;"ephemeralTimer":U32;"canSend":boolean;"wasSeenRecently":boolean;"mailingListAddress":string;};
|
||||
export type ChatListItemFetchResult=(({"type":"ChatListItem";}&{"id":U32;"name":string;"avatarPath":(string|null);"color":string;"lastUpdated":(I64|null);"summaryText1":string;"summaryText2":string;"summaryStatus":U32;"isProtected":boolean;"isGroup":boolean;"freshMessageCounter":Usize;"isSelfTalk":boolean;"isDeviceTalk":boolean;"isSendingLocation":boolean;"isSelfInGroup":boolean;"isArchived":boolean;"isPinned":boolean;"isMuted":boolean;"isContactRequest":boolean;
|
||||
/**
|
||||
* true when chat is a broadcastlist
|
||||
*/
|
||||
"isBroadcast":boolean;
|
||||
/**
|
||||
* contact id if this is a dm chat (for view profile entry in context menu)
|
||||
*/
|
||||
"dmChatContact":(U32|null);"wasSeenRecently":boolean;})|{"type":"ArchiveLink";}|({"type":"Error";}&{"id":U32;"error":string;}));
|
||||
export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
|
||||
export type ChatVisibility=("Normal"|"Archived"|"Pinned");
|
||||
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
|
||||
@@ -157,7 +161,20 @@ export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"imag
|
||||
* also known as summary_text2
|
||||
*/
|
||||
"summaryText":string;};
|
||||
export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
|
||||
export type MuteDuration=("NotMuted"|"Forever"|{"Until":I64;});
|
||||
export type F64=number;
|
||||
export type ProviderInfo={"beforeLoginHint":string;"overviewPage":string;"status":U32;};
|
||||
export type Qr=(({"type":"askVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"askVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"fprOk";}&{"contact_id":U32;})|({"type":"fprMismatch";}&{"contact_id":(U32|null);})|({"type":"fprWithoutAddr";}&{"fingerprint":string;})|({"type":"account";}&{"domain":string;})|({"type":"webrtcInstance";}&{"domain":string;"instance_pattern":string;})|({"type":"addr";}&{"contact_id":U32;"draft":(string|null);})|({"type":"url";}&{"url":string;})|({"type":"text";}&{"text":string;})|({"type":"withdrawVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"withdrawVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"login";}&{"address":string;}));
|
||||
export type ChatListEntry=[U32,U32];
|
||||
export type MessageListItem=(({"kind":"message";}&{"msg_id":U32;})|({
|
||||
/**
|
||||
* Day marker, separating messages that correspond to different
|
||||
* days according to local time.
|
||||
*/
|
||||
"kind":"dayMarker";}&{
|
||||
/**
|
||||
* Marker timestamp, for day markers, in unix milliseconds
|
||||
*/
|
||||
"timestamp":I64;}));
|
||||
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,(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,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];
|
||||
export type __AllTyps=[null,[(U32|null),(U32|null)],[string,string],[U32,Message],Account,Record<string,string>,BasicChat,Contact,FullChat,Record<string,(string|null)>,Record<string,(string)[]>,Record<U32,ChatListItemFetchResult>,Record<U32,Contact>,Record<U32,Message>,Record<U32,MessageSearchResult>,Record<U32,string>,ChatVisibility,MessageNotificationInfo,Message,Viewtype,MuteDuration,([F64,F64]|null),(Message|null),(Viewtype|null),(ProviderInfo|null),(string|null),(U32|null),Qr,string,(Account)[],(ChatListEntry)[],(Contact)[],(MessageListItem)[],(Location)[],(string)[],(U32)[],WebxdcMessageInfo,boolean,I64,U32,U64,Usize];
|
||||
|
||||
@@ -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",
|
||||
@@ -36,6 +36,7 @@
|
||||
"example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
|
||||
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
|
||||
"example:start": "http-server .",
|
||||
"extract-constants": "node ./scripts/generate-constants.js",
|
||||
"generate-bindings": "cargo test",
|
||||
"prettier:check": "prettier --check **.ts",
|
||||
"prettier:fix": "prettier --write **.ts",
|
||||
@@ -47,5 +48,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.97.0"
|
||||
"version": "1.98.0"
|
||||
}
|
||||
@@ -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`
|
||||
);
|
||||
@@ -2,6 +2,7 @@ import * as T from "../generated/types.js";
|
||||
import * as RPC from "../generated/jsonrpc.js";
|
||||
import { RawClient } from "../generated/client.js";
|
||||
import { Event } from "../generated/events.js";
|
||||
import { Context } from "../generated/context_methods.js";
|
||||
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
|
||||
import { TinyEmitter } from "tiny-emitter";
|
||||
|
||||
@@ -28,14 +29,14 @@ type ContextEvents = { ALL: (event: Event) => void } & {
|
||||
};
|
||||
|
||||
export type DcEvent = Event;
|
||||
export type DcEventType<T extends Event["type"]> = Extract<Event, { type: T }>
|
||||
export type DcEventType<T extends Event["type"]> = Extract<Event, { type: T }>;
|
||||
|
||||
export class BaseDeltaChat<
|
||||
Transport extends BaseTransport<any>
|
||||
> extends TinyEmitter<Events> {
|
||||
rpc: RawClient;
|
||||
account?: T.Account;
|
||||
private contextEmitters: TinyEmitter<ContextEvents>[] = [];
|
||||
private contextEmitters: Context<ContextEvents>[] = [];
|
||||
constructor(public transport: Transport) {
|
||||
super();
|
||||
this.rpc = new RawClient(this.transport);
|
||||
@@ -62,10 +63,14 @@ export class BaseDeltaChat<
|
||||
}
|
||||
|
||||
getContextEvents(account_id: number) {
|
||||
return this.getContext(account_id);
|
||||
}
|
||||
|
||||
getContext(account_id: number) {
|
||||
if (this.contextEmitters[account_id]) {
|
||||
return this.contextEmitters[account_id];
|
||||
} else {
|
||||
this.contextEmitters[account_id] = new TinyEmitter();
|
||||
this.contextEmitters[account_id] = new Context(this, account_id);
|
||||
return this.contextEmitters[account_id];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
+15
-1
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
+1
-1
@@ -60,5 +60,5 @@
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.97.0"
|
||||
"version": "1.98.0"
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
+3
-1
@@ -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 {
|
||||
|
||||
+56
-1
@@ -650,7 +650,7 @@ impl Context {
|
||||
ON m.chat_id=c.id
|
||||
WHERE m.chat_id>9
|
||||
AND m.hidden=0
|
||||
AND c.blocked=0
|
||||
AND c.blocked!=1
|
||||
AND ct.blocked=0
|
||||
AND m.txt LIKE ?
|
||||
ORDER BY m.id DESC LIMIT 1000",
|
||||
@@ -703,6 +703,8 @@ mod tests {
|
||||
use crate::chat::{
|
||||
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::ContactId;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::receive_imf::receive_imf;
|
||||
@@ -1038,6 +1040,59 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_unaccepted_requests() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: BobBar <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg1234@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Tue, 25 Oct 2022 13:37:00 +0000\n\
|
||||
\n\
|
||||
hello bob, foobar test!\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chat_id = t.get_last_msg().await.get_chat_id();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_type(), Chattype::Single);
|
||||
assert!(chat.is_contact_request());
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
|
||||
|
||||
chat_id.block(&t).await?;
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
0
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 0);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 0);
|
||||
|
||||
let contact_ids = get_chat_contacts(&t, chat_id).await?;
|
||||
Contact::unblock(&t, *contact_ids.first().unwrap()).await?;
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_limit_search_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
+1
-37
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -103,6 +103,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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+9
-1
@@ -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;
|
||||
|
||||
+67
-1
@@ -551,7 +551,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);
|
||||
}
|
||||
@@ -913,6 +916,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,
|
||||
@@ -946,6 +950,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,
|
||||
@@ -1644,6 +1672,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
|
||||
@@ -3329,4 +3360,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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
+63
-26
@@ -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?;
|
||||
|
||||
+7
-7
@@ -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,
|
||||
{
|
||||
|
||||
@@ -596,6 +596,19 @@ 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?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user