mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 09:26:29 +03:00
Merge remote-tracking branch 'origin/master' into hoc/migitate-from-forgery
This commit is contained in:
2
.github/workflows/node-tests.yml
vendored
2
.github/workflows/node-tests.yml
vendored
@@ -52,6 +52,7 @@ jobs:
|
|||||||
npm install --verbose
|
npm install --verbose
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
|
timeout-minutes: 10
|
||||||
if: runner.os != 'Windows'
|
if: runner.os != 'Windows'
|
||||||
run: |
|
run: |
|
||||||
cd node
|
cd node
|
||||||
@@ -59,6 +60,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||||
- name: Run tests on Windows, except lint
|
- name: Run tests on Windows, except lint
|
||||||
|
timeout-minutes: 10
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
run: |
|
run: |
|
||||||
cd node
|
cd node
|
||||||
|
|||||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -2,6 +2,19 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### API-Changes
|
||||||
|
- jsonrpc: typescript client: export constants under `C` enum, similar to how its exported from `deltachat-node` #3681
|
||||||
|
- added reactions support #3644
|
||||||
|
- jsonrpc: reactions: added reactions to `Message` type and the `sendReaction()` method #3686
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- simplify `UPSERT` queries #3676
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
|
||||||
|
## 1.97.0
|
||||||
|
|
||||||
### API-Changes
|
### API-Changes
|
||||||
- jsonrpc: add function: #3641, #3645, #3653
|
- jsonrpc: add function: #3641, #3645, #3653
|
||||||
- `getChatContacts()`
|
- `getChatContacts()`
|
||||||
@@ -33,6 +46,8 @@
|
|||||||
- `stopIo()`
|
- `stopIo()`
|
||||||
- `exportBackup()`
|
- `exportBackup()`
|
||||||
- `importBackup()`
|
- `importBackup()`
|
||||||
|
- `getMessageHtml()` #3671
|
||||||
|
- `miscGetStickerFolder` and `miscGetStickers` #3672
|
||||||
- breaking: jsonrpc: remove function `messageListGetMessageIds()`, it is replaced by `getMessageIds()` and `getMessageListItems()` the latter returns a new `MessageListItem` type, which is the now prefered way of using the message list.
|
- breaking: jsonrpc: remove function `messageListGetMessageIds()`, it is replaced by `getMessageIds()` and `getMessageListItems()` the latter returns a new `MessageListItem` type, which is the now prefered way of using the message list.
|
||||||
- jsonrpc: add type: #3641, #3645
|
- jsonrpc: add type: #3641, #3645
|
||||||
- `MessageSearchResult`
|
- `MessageSearchResult`
|
||||||
|
|||||||
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -895,7 +895,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "1.96.0"
|
version = "1.97.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ansi_term",
|
"ansi_term",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -967,7 +967,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deltachat-jsonrpc"
|
name = "deltachat-jsonrpc"
|
||||||
version = "1.96.0"
|
version = "1.97.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-channel",
|
"async-channel",
|
||||||
@@ -996,7 +996,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deltachat_ffi"
|
name = "deltachat_ffi"
|
||||||
version = "1.96.0"
|
version = "1.97.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"deltachat",
|
"deltachat",
|
||||||
@@ -1263,9 +1263,9 @@ checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enum-as-inner"
|
name = "enum-as-inner"
|
||||||
version = "0.4.0"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73"
|
checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2431,9 +2431,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
|
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pgp"
|
name = "pgp"
|
||||||
@@ -3660,9 +3660,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "trust-dns-proto"
|
name = "trust-dns-proto"
|
||||||
version = "0.21.2"
|
version = "0.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c31f240f59877c3d4bb3b3ea0ec5a6a0cff07323580ff8c7a605cd7d08b255d"
|
checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
@@ -3674,32 +3674,32 @@ dependencies = [
|
|||||||
"idna",
|
"idna",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tinyvec",
|
"tinyvec",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "trust-dns-resolver"
|
name = "trust-dns-resolver"
|
||||||
version = "0.21.2"
|
version = "0.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e4ba72c2ea84515690c9fcef4c6c660bb9df3036ed1051686de84605b74fd558"
|
checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"ipconfig",
|
"ipconfig",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
|
||||||
"lru-cache",
|
"lru-cache",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"resolv-conf",
|
"resolv-conf",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tracing",
|
||||||
"trust-dns-proto",
|
"trust-dns-proto",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "1.96.0"
|
version = "1.97.0"
|
||||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MPL-2.0"
|
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-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-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"] }
|
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 = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||||
backtrace = "0.3"
|
backtrace = "0.3"
|
||||||
@@ -47,7 +47,7 @@ num_cpus = "1.13"
|
|||||||
num-derive = "0.3"
|
num-derive = "0.3"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
once_cell = "1.15.0"
|
once_cell = "1.15.0"
|
||||||
percent-encoding = "2.0"
|
percent-encoding = "2.2"
|
||||||
pgp = { version = "0.8", default-features = false }
|
pgp = { version = "0.8", default-features = false }
|
||||||
pretty_env_logger = { version = "0.4", optional = true }
|
pretty_env_logger = { version = "0.4", optional = true }
|
||||||
quick-xml = "0.23"
|
quick-xml = "0.23"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat_ffi"
|
name = "deltachat_ffi"
|
||||||
version = "1.96.0"
|
version = "1.97.0"
|
||||||
description = "Deltachat FFI"
|
description = "Deltachat FFI"
|
||||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|||||||
@@ -11,16 +11,17 @@ extern "C" {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
typedef struct _dc_context dc_context_t;
|
typedef struct _dc_context dc_context_t;
|
||||||
typedef struct _dc_accounts dc_accounts_t;
|
typedef struct _dc_accounts dc_accounts_t;
|
||||||
typedef struct _dc_array dc_array_t;
|
typedef struct _dc_array dc_array_t;
|
||||||
typedef struct _dc_chatlist dc_chatlist_t;
|
typedef struct _dc_chatlist dc_chatlist_t;
|
||||||
typedef struct _dc_chat dc_chat_t;
|
typedef struct _dc_chat dc_chat_t;
|
||||||
typedef struct _dc_msg dc_msg_t;
|
typedef struct _dc_msg dc_msg_t;
|
||||||
typedef struct _dc_contact dc_contact_t;
|
typedef struct _dc_reactions dc_reactions_t;
|
||||||
typedef struct _dc_lot dc_lot_t;
|
typedef struct _dc_contact dc_contact_t;
|
||||||
typedef struct _dc_provider dc_provider_t;
|
typedef struct _dc_lot dc_lot_t;
|
||||||
typedef struct _dc_event dc_event_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_event_emitter dc_event_emitter_t;
|
||||||
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_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);
|
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.
|
* 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.
|
* @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.
|
* @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
|
#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
|
* There is a fresh message. Typically, the user will show an notification
|
||||||
* when receiving this message.
|
* when receiving this message.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::chat::ChatItem;
|
use crate::chat::ChatItem;
|
||||||
use crate::constants::DC_MSG_ID_DAYMARKER;
|
use crate::constants::DC_MSG_ID_DAYMARKER;
|
||||||
|
use crate::contact::ContactId;
|
||||||
use crate::location::Location;
|
use crate::location::Location;
|
||||||
use crate::message::MsgId;
|
use crate::message::MsgId;
|
||||||
|
|
||||||
@@ -7,6 +8,7 @@ use crate::message::MsgId;
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum dc_array_t {
|
pub enum dc_array_t {
|
||||||
MsgIds(Vec<MsgId>),
|
MsgIds(Vec<MsgId>),
|
||||||
|
ContactIds(Vec<ContactId>),
|
||||||
Chat(Vec<ChatItem>),
|
Chat(Vec<ChatItem>),
|
||||||
Locations(Vec<Location>),
|
Locations(Vec<Location>),
|
||||||
Uint(Vec<u32>),
|
Uint(Vec<u32>),
|
||||||
@@ -16,6 +18,7 @@ impl dc_array_t {
|
|||||||
pub(crate) fn get_id(&self, index: usize) -> u32 {
|
pub(crate) fn get_id(&self, index: usize) -> u32 {
|
||||||
match self {
|
match self {
|
||||||
Self::MsgIds(array) => array[index].to_u32(),
|
Self::MsgIds(array) => array[index].to_u32(),
|
||||||
|
Self::ContactIds(array) => array[index].to_u32(),
|
||||||
Self::Chat(array) => match array[index] {
|
Self::Chat(array) => match array[index] {
|
||||||
ChatItem::Message { msg_id } => msg_id.to_u32(),
|
ChatItem::Message { msg_id } => msg_id.to_u32(),
|
||||||
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
|
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
|
||||||
@@ -28,6 +31,7 @@ impl dc_array_t {
|
|||||||
pub(crate) fn get_timestamp(&self, index: usize) -> Option<i64> {
|
pub(crate) fn get_timestamp(&self, index: usize) -> Option<i64> {
|
||||||
match self {
|
match self {
|
||||||
Self::MsgIds(_) => None,
|
Self::MsgIds(_) => None,
|
||||||
|
Self::ContactIds(_) => None,
|
||||||
Self::Chat(array) => array.get(index).and_then(|item| match item {
|
Self::Chat(array) => array.get(index).and_then(|item| match item {
|
||||||
ChatItem::Message { .. } => None,
|
ChatItem::Message { .. } => None,
|
||||||
ChatItem::DayMarker { timestamp } => Some(*timestamp),
|
ChatItem::DayMarker { timestamp } => Some(*timestamp),
|
||||||
@@ -40,6 +44,7 @@ impl dc_array_t {
|
|||||||
pub(crate) fn get_marker(&self, index: usize) -> Option<&str> {
|
pub(crate) fn get_marker(&self, index: usize) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
Self::MsgIds(_) => None,
|
Self::MsgIds(_) => None,
|
||||||
|
Self::ContactIds(_) => None,
|
||||||
Self::Chat(_) => None,
|
Self::Chat(_) => None,
|
||||||
Self::Locations(array) => array
|
Self::Locations(array) => array
|
||||||
.get(index)
|
.get(index)
|
||||||
@@ -60,6 +65,7 @@ impl dc_array_t {
|
|||||||
pub(crate) fn len(&self) -> usize {
|
pub(crate) fn len(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Self::MsgIds(array) => array.len(),
|
Self::MsgIds(array) => array.len(),
|
||||||
|
Self::ContactIds(array) => array.len(),
|
||||||
Self::Chat(array) => array.len(),
|
Self::Chat(array) => array.len(),
|
||||||
Self::Locations(array) => array.len(),
|
Self::Locations(array) => array.len(),
|
||||||
Self::Uint(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 {
|
impl From<Vec<ChatItem>> for dc_array_t {
|
||||||
fn from(array: Vec<ChatItem>) -> Self {
|
fn from(array: Vec<ChatItem>) -> Self {
|
||||||
dc_array_t::Chat(array)
|
dc_array_t::Chat(array)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ use deltachat::context::Context;
|
|||||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||||
use deltachat::key::DcKey;
|
use deltachat::key::DcKey;
|
||||||
use deltachat::message::MsgId;
|
use deltachat::message::MsgId;
|
||||||
|
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
|
||||||
use deltachat::stock_str::StockMessage;
|
use deltachat::stock_str::StockMessage;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
use deltachat::webxdc::StatusUpdateSerial;
|
use deltachat::webxdc::StatusUpdateSerial;
|
||||||
@@ -66,6 +67,8 @@ use deltachat::chatlist::Chatlist;
|
|||||||
/// Struct representing the deltachat context.
|
/// Struct representing the deltachat context.
|
||||||
pub type dc_context_t = 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"));
|
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
|
||||||
|
|
||||||
fn block_on<T>(fut: T) -> T::Output
|
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::Error(_) => 400,
|
||||||
EventType::ErrorSelfNotInGroup(_) => 410,
|
EventType::ErrorSelfNotInGroup(_) => 410,
|
||||||
EventType::MsgsChanged { .. } => 2000,
|
EventType::MsgsChanged { .. } => 2000,
|
||||||
|
EventType::ReactionsChanged { .. } => 2001,
|
||||||
EventType::IncomingMsg { .. } => 2005,
|
EventType::IncomingMsg { .. } => 2005,
|
||||||
EventType::MsgsNoticed { .. } => 2008,
|
EventType::MsgsNoticed { .. } => 2008,
|
||||||
EventType::MsgDelivered { .. } => 2010,
|
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::SelfavatarChanged
|
||||||
| EventType::ErrorSelfNotInGroup(_) => 0,
|
| EventType::ErrorSelfNotInGroup(_) => 0,
|
||||||
EventType::MsgsChanged { chat_id, .. }
|
EventType::MsgsChanged { chat_id, .. }
|
||||||
|
| EventType::ReactionsChanged { chat_id, .. }
|
||||||
| EventType::IncomingMsg { chat_id, .. }
|
| EventType::IncomingMsg { chat_id, .. }
|
||||||
| EventType::MsgsNoticed(chat_id)
|
| EventType::MsgsNoticed(chat_id)
|
||||||
| EventType::MsgDelivered { 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::SelfavatarChanged => 0,
|
||||||
EventType::ChatModified(_) => 0,
|
EventType::ChatModified(_) => 0,
|
||||||
EventType::MsgsChanged { msg_id, .. }
|
EventType::MsgsChanged { msg_id, .. }
|
||||||
|
| EventType::ReactionsChanged { msg_id, .. }
|
||||||
| EventType::IncomingMsg { msg_id, .. }
|
| EventType::IncomingMsg { msg_id, .. }
|
||||||
| EventType::MsgDelivered { msg_id, .. }
|
| EventType::MsgDelivered { msg_id, .. }
|
||||||
| EventType::MsgFailed { 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()
|
data2.into_raw()
|
||||||
}
|
}
|
||||||
EventType::MsgsChanged { .. }
|
EventType::MsgsChanged { .. }
|
||||||
|
| EventType::ReactionsChanged { .. }
|
||||||
| EventType::IncomingMsg { .. }
|
| EventType::IncomingMsg { .. }
|
||||||
| EventType::MsgsNoticed(_)
|
| EventType::MsgsNoticed(_)
|
||||||
| EventType::MsgDelivered { .. }
|
| 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]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
||||||
context: *mut dc_context_t,
|
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()
|
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]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
|
pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
|
||||||
libc::free(s as *mut _)
|
libc::free(s as *mut _)
|
||||||
@@ -4515,7 +4603,7 @@ mod jsonrpc {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(*jsonrpc_instance).event_thread.abort();
|
(*jsonrpc_instance).event_thread.abort();
|
||||||
Box::from_raw(jsonrpc_instance);
|
drop(Box::from_raw(jsonrpc_instance));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-jsonrpc"
|
name = "deltachat-jsonrpc"
|
||||||
version = "1.96.0"
|
version = "1.97.0"
|
||||||
description = "DeltaChat JSON-RPC API"
|
description = "DeltaChat JSON-RPC API"
|
||||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -102,6 +102,14 @@ pub enum JSONRPCEventType {
|
|||||||
msg_id: u32,
|
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
|
/// There is a fresh message. Typically, the user will show an notification
|
||||||
/// when receiving this message.
|
/// when receiving this message.
|
||||||
///
|
///
|
||||||
@@ -284,6 +292,15 @@ impl From<EventType> for JSONRPCEventType {
|
|||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
msg_id: msg_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 {
|
EventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use deltachat::{
|
|||||||
provider::get_provider_info,
|
provider::get_provider_info,
|
||||||
qr,
|
qr,
|
||||||
qr_code_generator::get_securejoin_qr_svg,
|
qr_code_generator::get_securejoin_qr_svg,
|
||||||
|
reaction::send_reaction,
|
||||||
securejoin,
|
securejoin,
|
||||||
stock_str::StockMessage,
|
stock_str::StockMessage,
|
||||||
webxdc::StatusUpdateSerial,
|
webxdc::StatusUpdateSerial,
|
||||||
@@ -24,7 +25,7 @@ use deltachat::{
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{collections::HashMap, str::FromStr};
|
use std::{collections::HashMap, str::FromStr};
|
||||||
use tokio::sync::RwLock;
|
use tokio::{fs, sync::RwLock};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
use yerpc::rpc;
|
use yerpc::rpc;
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ pub mod events;
|
|||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
|
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::account::Account;
|
||||||
use types::chat::FullChat;
|
use types::chat::FullChat;
|
||||||
@@ -915,6 +916,11 @@ impl CommandApi {
|
|||||||
MessageObject::from_message_id(&ctx, message_id).await
|
MessageObject::from_message_id(&ctx, message_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
MsgId::new(message_id).get_html(&ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn message_get_messages(
|
async fn message_get_messages(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -1461,6 +1467,23 @@ impl CommandApi {
|
|||||||
Ok(message_id.to_u32())
|
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
|
// functions for the composer
|
||||||
// the composer is the message input field
|
// the composer is the message input field
|
||||||
@@ -1495,6 +1518,63 @@ impl CommandApi {
|
|||||||
// that might get removed later again
|
// that might get removed later again
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
|
|
||||||
|
async fn misc_get_sticker_folder(&self, account_id: u32) -> Result<String> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let account_folder = ctx
|
||||||
|
.get_dbfile()
|
||||||
|
.parent()
|
||||||
|
.context("account folder not found")?;
|
||||||
|
let sticker_folder_path = account_folder.join("./stickers");
|
||||||
|
fs::create_dir_all(&sticker_folder_path).await?;
|
||||||
|
sticker_folder_path
|
||||||
|
.to_str()
|
||||||
|
.map(|s| s.to_owned())
|
||||||
|
.context("path conversion to string failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// for desktop, get stickers from stickers folder,
|
||||||
|
/// grouped by the folder they are in.
|
||||||
|
async fn misc_get_stickers(&self, account_id: u32) -> Result<HashMap<String, Vec<String>>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let account_folder = ctx
|
||||||
|
.get_dbfile()
|
||||||
|
.parent()
|
||||||
|
.context("account folder not found")?;
|
||||||
|
let sticker_folder_path = account_folder.join("./stickers");
|
||||||
|
fs::create_dir_all(&sticker_folder_path).await?;
|
||||||
|
let mut result = HashMap::new();
|
||||||
|
|
||||||
|
let mut packs = tokio::fs::read_dir(sticker_folder_path).await?;
|
||||||
|
while let Some(entry) = packs.next_entry().await? {
|
||||||
|
if !entry.file_type().await?.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let pack_name = entry.file_name().into_string().unwrap_or_default();
|
||||||
|
let mut stickers = tokio::fs::read_dir(entry.path()).await?;
|
||||||
|
let mut sticker_paths = Vec::new();
|
||||||
|
while let Some(sticker_entry) = stickers.next_entry().await? {
|
||||||
|
if !sticker_entry.file_type().await?.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let sticker_name = sticker_entry.file_name().into_string().unwrap_or_default();
|
||||||
|
if sticker_name.ends_with(".png") || sticker_name.ends_with(".webp") {
|
||||||
|
sticker_paths.push(
|
||||||
|
sticker_entry
|
||||||
|
.path()
|
||||||
|
.to_str()
|
||||||
|
.map(|s| s.to_owned())
|
||||||
|
.context("path conversion to string failed")?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !sticker_paths.is_empty() {
|
||||||
|
result.insert(pack_name, sticker_paths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the messageid of the sent message
|
/// Returns the messageid of the sent message
|
||||||
async fn misc_send_text_message(
|
async fn misc_send_text_message(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use deltachat::download;
|
|||||||
use deltachat::message::Message;
|
use deltachat::message::Message;
|
||||||
use deltachat::message::MsgId;
|
use deltachat::message::MsgId;
|
||||||
use deltachat::message::Viewtype;
|
use deltachat::message::Viewtype;
|
||||||
|
use deltachat::reaction::get_msg_reactions;
|
||||||
use num_traits::cast::ToPrimitive;
|
use num_traits::cast::ToPrimitive;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -15,6 +16,7 @@ use typescript_type_def::TypeDef;
|
|||||||
|
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
use super::contact::ContactObject;
|
use super::contact::ContactObject;
|
||||||
|
use super::reactions::JSONRPCReactions;
|
||||||
use super::webxdc::WebxdcMessageInfo;
|
use super::webxdc::WebxdcMessageInfo;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef)]
|
||||||
@@ -64,6 +66,8 @@ pub struct MessageObject {
|
|||||||
webxdc_info: Option<WebxdcMessageInfo>,
|
webxdc_info: Option<WebxdcMessageInfo>,
|
||||||
|
|
||||||
download_state: DownloadState,
|
download_state: DownloadState,
|
||||||
|
|
||||||
|
reactions: Option<JSONRPCReactions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef)]
|
||||||
@@ -139,6 +143,13 @@ impl MessageObject {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let reactions = get_msg_reactions(context, msg_id).await?;
|
||||||
|
let reactions = if reactions.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(reactions.into())
|
||||||
|
};
|
||||||
|
|
||||||
Ok(MessageObject {
|
Ok(MessageObject {
|
||||||
id: msg_id.to_u32(),
|
id: msg_id.to_u32(),
|
||||||
chat_id: message.get_chat_id().to_u32(),
|
chat_id: message.get_chat_id().to_u32(),
|
||||||
@@ -193,6 +204,8 @@ impl MessageObject {
|
|||||||
webxdc_info,
|
webxdc_info,
|
||||||
|
|
||||||
download_state,
|
download_state,
|
||||||
|
|
||||||
|
reactions,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
use deltachat::qr::Qr;
|
|
||||||
use serde::Serialize;
|
|
||||||
use typescript_type_def::TypeDef;
|
|
||||||
|
|
||||||
pub mod account;
|
pub mod account;
|
||||||
pub mod chat;
|
pub mod chat;
|
||||||
pub mod chat_list;
|
pub mod chat_list;
|
||||||
@@ -9,6 +5,8 @@ pub mod contact;
|
|||||||
pub mod location;
|
pub mod location;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod provider_info;
|
pub mod provider_info;
|
||||||
|
pub mod qr;
|
||||||
|
pub mod reactions;
|
||||||
pub mod webxdc;
|
pub mod webxdc;
|
||||||
|
|
||||||
pub fn color_int_to_hex_string(color: u32) -> String {
|
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)
|
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 },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
213
deltachat-jsonrpc/src/api/types/qr.rs
Normal file
213
deltachat-jsonrpc/src/api/types/qr.rs
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
use deltachat::qr::Qr;
|
||||||
|
use serde::Serialize;
|
||||||
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
|
#[derive(Serialize, TypeDef)]
|
||||||
|
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum QrObject {
|
||||||
|
AskVerifyContact {
|
||||||
|
contact_id: u32,
|
||||||
|
fingerprint: String,
|
||||||
|
invitenumber: String,
|
||||||
|
authcode: String,
|
||||||
|
},
|
||||||
|
AskVerifyGroup {
|
||||||
|
grpname: String,
|
||||||
|
grpid: String,
|
||||||
|
contact_id: u32,
|
||||||
|
fingerprint: String,
|
||||||
|
invitenumber: String,
|
||||||
|
authcode: String,
|
||||||
|
},
|
||||||
|
FprOk {
|
||||||
|
contact_id: u32,
|
||||||
|
},
|
||||||
|
FprMismatch {
|
||||||
|
contact_id: Option<u32>,
|
||||||
|
},
|
||||||
|
FprWithoutAddr {
|
||||||
|
fingerprint: String,
|
||||||
|
},
|
||||||
|
Account {
|
||||||
|
domain: String,
|
||||||
|
},
|
||||||
|
WebrtcInstance {
|
||||||
|
domain: String,
|
||||||
|
instance_pattern: String,
|
||||||
|
},
|
||||||
|
Addr {
|
||||||
|
contact_id: u32,
|
||||||
|
draft: Option<String>,
|
||||||
|
},
|
||||||
|
Url {
|
||||||
|
url: String,
|
||||||
|
},
|
||||||
|
Text {
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
WithdrawVerifyContact {
|
||||||
|
contact_id: u32,
|
||||||
|
fingerprint: String,
|
||||||
|
invitenumber: String,
|
||||||
|
authcode: String,
|
||||||
|
},
|
||||||
|
WithdrawVerifyGroup {
|
||||||
|
grpname: String,
|
||||||
|
grpid: String,
|
||||||
|
contact_id: u32,
|
||||||
|
fingerprint: String,
|
||||||
|
invitenumber: String,
|
||||||
|
authcode: String,
|
||||||
|
},
|
||||||
|
ReviveVerifyContact {
|
||||||
|
contact_id: u32,
|
||||||
|
fingerprint: String,
|
||||||
|
invitenumber: String,
|
||||||
|
authcode: String,
|
||||||
|
},
|
||||||
|
ReviveVerifyGroup {
|
||||||
|
grpname: String,
|
||||||
|
grpid: String,
|
||||||
|
contact_id: u32,
|
||||||
|
fingerprint: String,
|
||||||
|
invitenumber: String,
|
||||||
|
authcode: String,
|
||||||
|
},
|
||||||
|
Login {
|
||||||
|
address: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Qr> for QrObject {
|
||||||
|
fn from(qr: Qr) -> Self {
|
||||||
|
match qr {
|
||||||
|
Qr::AskVerifyContact {
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
invitenumber,
|
||||||
|
authcode,
|
||||||
|
} => {
|
||||||
|
let contact_id = contact_id.to_u32();
|
||||||
|
let fingerprint = fingerprint.to_string();
|
||||||
|
QrObject::AskVerifyContact {
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
invitenumber,
|
||||||
|
authcode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Qr::AskVerifyGroup {
|
||||||
|
grpname,
|
||||||
|
grpid,
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
invitenumber,
|
||||||
|
authcode,
|
||||||
|
} => {
|
||||||
|
let contact_id = contact_id.to_u32();
|
||||||
|
let fingerprint = fingerprint.to_string();
|
||||||
|
QrObject::AskVerifyGroup {
|
||||||
|
grpname,
|
||||||
|
grpid,
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
invitenumber,
|
||||||
|
authcode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Qr::FprOk { contact_id } => {
|
||||||
|
let contact_id = contact_id.to_u32();
|
||||||
|
QrObject::FprOk { contact_id }
|
||||||
|
}
|
||||||
|
Qr::FprMismatch { contact_id } => {
|
||||||
|
let contact_id = contact_id.map(|contact_id| contact_id.to_u32());
|
||||||
|
QrObject::FprMismatch { contact_id }
|
||||||
|
}
|
||||||
|
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
|
||||||
|
Qr::Account { domain } => QrObject::Account { domain },
|
||||||
|
Qr::WebrtcInstance {
|
||||||
|
domain,
|
||||||
|
instance_pattern,
|
||||||
|
} => QrObject::WebrtcInstance {
|
||||||
|
domain,
|
||||||
|
instance_pattern,
|
||||||
|
},
|
||||||
|
Qr::Addr { contact_id, draft } => {
|
||||||
|
let contact_id = contact_id.to_u32();
|
||||||
|
QrObject::Addr { contact_id, draft }
|
||||||
|
}
|
||||||
|
Qr::Url { url } => QrObject::Url { url },
|
||||||
|
Qr::Text { text } => QrObject::Text { text },
|
||||||
|
Qr::WithdrawVerifyContact {
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
invitenumber,
|
||||||
|
authcode,
|
||||||
|
} => {
|
||||||
|
let contact_id = contact_id.to_u32();
|
||||||
|
let fingerprint = fingerprint.to_string();
|
||||||
|
QrObject::WithdrawVerifyContact {
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
invitenumber,
|
||||||
|
authcode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Qr::WithdrawVerifyGroup {
|
||||||
|
grpname,
|
||||||
|
grpid,
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
invitenumber,
|
||||||
|
authcode,
|
||||||
|
} => {
|
||||||
|
let contact_id = contact_id.to_u32();
|
||||||
|
let fingerprint = fingerprint.to_string();
|
||||||
|
QrObject::WithdrawVerifyGroup {
|
||||||
|
grpname,
|
||||||
|
grpid,
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
invitenumber,
|
||||||
|
authcode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Qr::ReviveVerifyContact {
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
invitenumber,
|
||||||
|
authcode,
|
||||||
|
} => {
|
||||||
|
let contact_id = contact_id.to_u32();
|
||||||
|
let fingerprint = fingerprint.to_string();
|
||||||
|
QrObject::ReviveVerifyContact {
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
invitenumber,
|
||||||
|
authcode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Qr::ReviveVerifyGroup {
|
||||||
|
grpname,
|
||||||
|
grpid,
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
invitenumber,
|
||||||
|
authcode,
|
||||||
|
} => {
|
||||||
|
let contact_id = contact_id.to_u32();
|
||||||
|
let fingerprint = fingerprint.to_string();
|
||||||
|
QrObject::ReviveVerifyGroup {
|
||||||
|
grpname,
|
||||||
|
grpid,
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
invitenumber,
|
||||||
|
authcode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Qr::Login { address, .. } => QrObject::Login { address },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
deltachat-jsonrpc/src/api/types/reactions.rs
Normal file
47
deltachat-jsonrpc/src/api/types/reactions.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use deltachat::reaction::Reactions;
|
||||||
|
use serde::Serialize;
|
||||||
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
|
/// Structure representing all reactions to a particular message.
|
||||||
|
#[derive(Serialize, TypeDef)]
|
||||||
|
#[serde(rename = "Reactions", rename_all = "camelCase")]
|
||||||
|
pub struct JSONRPCReactions {
|
||||||
|
/// Map from a contact to it's reaction to message.
|
||||||
|
reactions_by_contact: BTreeMap<u32, Vec<String>>,
|
||||||
|
/// Unique reactions and their count
|
||||||
|
reactions: BTreeMap<String, u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Reactions> for JSONRPCReactions {
|
||||||
|
fn from(reactions: Reactions) -> Self {
|
||||||
|
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
|
||||||
|
let mut unique_reactions: BTreeMap<String, u32> = BTreeMap::new();
|
||||||
|
|
||||||
|
for contact_id in reactions.contacts() {
|
||||||
|
let reaction = reactions.get(contact_id);
|
||||||
|
if reaction.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let emojis: Vec<String> = reaction
|
||||||
|
.emojis()
|
||||||
|
.into_iter()
|
||||||
|
.map(|emoji| emoji.to_owned())
|
||||||
|
.collect();
|
||||||
|
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
|
||||||
|
for emoji in emojis {
|
||||||
|
if let Some(x) = unique_reactions.get_mut(&emoji) {
|
||||||
|
*x += 1;
|
||||||
|
} else {
|
||||||
|
unique_reactions.insert(emoji, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONRPCReactions {
|
||||||
|
reactions_by_contact,
|
||||||
|
reactions: unique_reactions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -598,6 +598,11 @@ export class RawClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public getMessageHtml(accountId: T.U32, messageId: T.U32): Promise<(string|null)> {
|
||||||
|
return (this._transport.request('get_message_html', [accountId, messageId] as RPC.Params)) as Promise<(string|null)>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public messageGetMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
|
public messageGetMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
|
||||||
return (this._transport.request('message_get_messages', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.Message>>;
|
return (this._transport.request('message_get_messages', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.Message>>;
|
||||||
}
|
}
|
||||||
@@ -873,6 +878,18 @@ export class RawClient {
|
|||||||
return (this._transport.request('send_sticker', [accountId, chatId, stickerPath] as RPC.Params)) as Promise<T.U32>;
|
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> {
|
public removeDraft(accountId: T.U32, chatId: T.U32): Promise<null> {
|
||||||
return (this._transport.request('remove_draft', [accountId, chatId] as RPC.Params)) as Promise<null>;
|
return (this._transport.request('remove_draft', [accountId, chatId] as RPC.Params)) as Promise<null>;
|
||||||
@@ -890,6 +907,19 @@ export class RawClient {
|
|||||||
return (this._transport.request('send_videochat_invitation', [accountId, chatId] as RPC.Params)) as Promise<T.U32>;
|
return (this._transport.request('send_videochat_invitation', [accountId, chatId] as RPC.Params)) as Promise<T.U32>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public miscGetStickerFolder(accountId: T.U32): Promise<string> {
|
||||||
|
return (this._transport.request('misc_get_sticker_folder', [accountId] as RPC.Params)) as Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* for desktop, get stickers from stickers folder,
|
||||||
|
* grouped by the folder they are in.
|
||||||
|
*/
|
||||||
|
public miscGetStickers(accountId: T.U32): Promise<Record<string,(string)[]>> {
|
||||||
|
return (this._transport.request('misc_get_stickers', [accountId] as RPC.Params)) as Promise<Record<string,(string)[]>>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the messageid of the sent message
|
* Returns the messageid of the sent message
|
||||||
*/
|
*/
|
||||||
|
|||||||
201
deltachat-jsonrpc/typescript/generated/constants.ts
Normal file
201
deltachat-jsonrpc/typescript/generated/constants.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
// Generated!
|
||||||
|
|
||||||
|
export enum C {
|
||||||
|
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
|
||||||
|
DC_CERTCK_AUTO = 0,
|
||||||
|
DC_CERTCK_STRICT = 1,
|
||||||
|
DC_CHAT_ID_ALLDONE_HINT = 7,
|
||||||
|
DC_CHAT_ID_ARCHIVED_LINK = 6,
|
||||||
|
DC_CHAT_ID_LAST_SPECIAL = 9,
|
||||||
|
DC_CHAT_ID_TRASH = 3,
|
||||||
|
DC_CHAT_TYPE_BROADCAST = 160,
|
||||||
|
DC_CHAT_TYPE_GROUP = 120,
|
||||||
|
DC_CHAT_TYPE_MAILINGLIST = 140,
|
||||||
|
DC_CHAT_TYPE_SINGLE = 100,
|
||||||
|
DC_CHAT_TYPE_UNDEFINED = 0,
|
||||||
|
DC_CONNECTIVITY_CONNECTED = 4000,
|
||||||
|
DC_CONNECTIVITY_CONNECTING = 2000,
|
||||||
|
DC_CONNECTIVITY_NOT_CONNECTED = 1000,
|
||||||
|
DC_CONNECTIVITY_WORKING = 3000,
|
||||||
|
DC_CONTACT_ID_DEVICE = 5,
|
||||||
|
DC_CONTACT_ID_INFO = 2,
|
||||||
|
DC_CONTACT_ID_LAST_SPECIAL = 9,
|
||||||
|
DC_CONTACT_ID_SELF = 1,
|
||||||
|
DC_GCL_ADD_ALLDONE_HINT = 4,
|
||||||
|
DC_GCL_ADD_SELF = 2,
|
||||||
|
DC_GCL_ARCHIVED_ONLY = 1,
|
||||||
|
DC_GCL_FOR_FORWARDING = 8,
|
||||||
|
DC_GCL_NO_SPECIALS = 2,
|
||||||
|
DC_GCL_VERIFIED_ONLY = 1,
|
||||||
|
DC_GCM_ADDDAYMARKER = 1,
|
||||||
|
DC_GCM_INFO_ONLY = 2,
|
||||||
|
DC_INFO_PROTECTION_DISABLED = 12,
|
||||||
|
DC_INFO_PROTECTION_ENABLED = 11,
|
||||||
|
DC_KEY_GEN_DEFAULT = 0,
|
||||||
|
DC_KEY_GEN_ED25519 = 2,
|
||||||
|
DC_KEY_GEN_RSA2048 = 1,
|
||||||
|
DC_LP_AUTH_NORMAL = 4,
|
||||||
|
DC_LP_AUTH_OAUTH2 = 2,
|
||||||
|
DC_MEDIA_QUALITY_BALANCED = 0,
|
||||||
|
DC_MEDIA_QUALITY_WORSE = 1,
|
||||||
|
DC_MSG_ID_DAYMARKER = 9,
|
||||||
|
DC_MSG_ID_LAST_SPECIAL = 9,
|
||||||
|
DC_MSG_ID_MARKER1 = 1,
|
||||||
|
DC_PROVIDER_STATUS_BROKEN = 3,
|
||||||
|
DC_PROVIDER_STATUS_OK = 1,
|
||||||
|
DC_PROVIDER_STATUS_PREPARATION = 2,
|
||||||
|
DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1,
|
||||||
|
DC_SHOW_EMAILS_ALL = 2,
|
||||||
|
DC_SHOW_EMAILS_OFF = 0,
|
||||||
|
DC_SOCKET_AUTO = 0,
|
||||||
|
DC_SOCKET_PLAIN = 3,
|
||||||
|
DC_SOCKET_SSL = 1,
|
||||||
|
DC_SOCKET_STARTTLS = 2,
|
||||||
|
DC_STATE_IN_FRESH = 10,
|
||||||
|
DC_STATE_IN_NOTICED = 13,
|
||||||
|
DC_STATE_IN_SEEN = 16,
|
||||||
|
DC_STATE_OUT_DELIVERED = 26,
|
||||||
|
DC_STATE_OUT_DRAFT = 19,
|
||||||
|
DC_STATE_OUT_FAILED = 24,
|
||||||
|
DC_STATE_OUT_MDN_RCVD = 28,
|
||||||
|
DC_STATE_OUT_PENDING = 20,
|
||||||
|
DC_STATE_OUT_PREPARING = 18,
|
||||||
|
DC_STATE_UNDEFINED = 0,
|
||||||
|
DC_STR_AC_SETUP_MSG_BODY = 43,
|
||||||
|
DC_STR_AC_SETUP_MSG_SUBJECT = 42,
|
||||||
|
DC_STR_ADD_MEMBER_BY_OTHER = 129,
|
||||||
|
DC_STR_ADD_MEMBER_BY_YOU = 128,
|
||||||
|
DC_STR_AEAP_ADDR_CHANGED = 122,
|
||||||
|
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
|
||||||
|
DC_STR_ARCHIVEDCHATS = 40,
|
||||||
|
DC_STR_AUDIO = 11,
|
||||||
|
DC_STR_BAD_TIME_MSG_BODY = 85,
|
||||||
|
DC_STR_BROADCAST_LIST = 115,
|
||||||
|
DC_STR_CANNOT_LOGIN = 60,
|
||||||
|
DC_STR_CANTDECRYPT_MSG_BODY = 29,
|
||||||
|
DC_STR_CONFIGURATION_FAILED = 84,
|
||||||
|
DC_STR_CONNECTED = 107,
|
||||||
|
DC_STR_CONNTECTING = 108,
|
||||||
|
DC_STR_CONTACT_NOT_VERIFIED = 36,
|
||||||
|
DC_STR_CONTACT_SETUP_CHANGED = 37,
|
||||||
|
DC_STR_CONTACT_VERIFIED = 35,
|
||||||
|
DC_STR_DEVICE_MESSAGES = 68,
|
||||||
|
DC_STR_DEVICE_MESSAGES_HINT = 70,
|
||||||
|
DC_STR_DOWNLOAD_AVAILABILITY = 100,
|
||||||
|
DC_STR_DRAFT = 3,
|
||||||
|
DC_STR_E2E_AVAILABLE = 25,
|
||||||
|
DC_STR_E2E_PREFERRED = 34,
|
||||||
|
DC_STR_ENCRYPTEDMSG = 24,
|
||||||
|
DC_STR_ENCR_NONE = 28,
|
||||||
|
DC_STR_ENCR_TRANSP = 27,
|
||||||
|
DC_STR_EPHEMERAL_DAY = 79,
|
||||||
|
DC_STR_EPHEMERAL_DAYS = 95,
|
||||||
|
DC_STR_EPHEMERAL_DISABLED = 75,
|
||||||
|
DC_STR_EPHEMERAL_FOUR_WEEKS = 81,
|
||||||
|
DC_STR_EPHEMERAL_HOUR = 78,
|
||||||
|
DC_STR_EPHEMERAL_HOURS = 94,
|
||||||
|
DC_STR_EPHEMERAL_MINUTE = 77,
|
||||||
|
DC_STR_EPHEMERAL_MINUTES = 93,
|
||||||
|
DC_STR_EPHEMERAL_SECONDS = 76,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER = 147,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU = 146,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER = 145,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU = 144,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER = 143,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU = 142,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER = 149,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU = 148,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER = 155,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU = 154,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER = 139,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU = 138,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER = 153,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU = 152,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER = 151,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU = 150,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER = 141,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU = 140,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER = 157,
|
||||||
|
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU = 156,
|
||||||
|
DC_STR_EPHEMERAL_WEEK = 80,
|
||||||
|
DC_STR_EPHEMERAL_WEEKS = 96,
|
||||||
|
DC_STR_ERROR = 112,
|
||||||
|
DC_STR_ERROR_NO_NETWORK = 87,
|
||||||
|
DC_STR_FAILED_SENDING_TO = 74,
|
||||||
|
DC_STR_FILE = 12,
|
||||||
|
DC_STR_FINGERPRINTS = 30,
|
||||||
|
DC_STR_FORWARDED = 97,
|
||||||
|
DC_STR_GIF = 23,
|
||||||
|
DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER = 127,
|
||||||
|
DC_STR_GROUP_IMAGE_CHANGED_BY_YOU = 126,
|
||||||
|
DC_STR_GROUP_IMAGE_DELETED_BY_OTHER = 135,
|
||||||
|
DC_STR_GROUP_IMAGE_DELETED_BY_YOU = 134,
|
||||||
|
DC_STR_GROUP_LEFT_BY_OTHER = 133,
|
||||||
|
DC_STR_GROUP_LEFT_BY_YOU = 132,
|
||||||
|
DC_STR_GROUP_NAME_CHANGED_BY_OTHER = 125,
|
||||||
|
DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124,
|
||||||
|
DC_STR_IMAGE = 9,
|
||||||
|
DC_STR_INCOMING_MESSAGES = 103,
|
||||||
|
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
|
||||||
|
DC_STR_LOCATION = 66,
|
||||||
|
DC_STR_LOCATION_ENABLED_BY_OTHER = 137,
|
||||||
|
DC_STR_LOCATION_ENABLED_BY_YOU = 136,
|
||||||
|
DC_STR_MESSAGES = 114,
|
||||||
|
DC_STR_MSGACTIONBYME = 63,
|
||||||
|
DC_STR_MSGACTIONBYUSER = 62,
|
||||||
|
DC_STR_MSGADDMEMBER = 17,
|
||||||
|
DC_STR_MSGDELMEMBER = 18,
|
||||||
|
DC_STR_MSGGROUPLEFT = 19,
|
||||||
|
DC_STR_MSGGRPIMGCHANGED = 16,
|
||||||
|
DC_STR_MSGGRPIMGDELETED = 33,
|
||||||
|
DC_STR_MSGGRPNAME = 15,
|
||||||
|
DC_STR_MSGLOCATIONDISABLED = 65,
|
||||||
|
DC_STR_MSGLOCATIONENABLED = 64,
|
||||||
|
DC_STR_NOMESSAGES = 1,
|
||||||
|
DC_STR_NOT_CONNECTED = 121,
|
||||||
|
DC_STR_NOT_SUPPORTED_BY_PROVIDER = 113,
|
||||||
|
DC_STR_ONE_MOMENT = 106,
|
||||||
|
DC_STR_OUTGOING_MESSAGES = 104,
|
||||||
|
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
|
||||||
|
DC_STR_PART_OF_TOTAL_USED = 116,
|
||||||
|
DC_STR_PROTECTION_DISABLED = 89,
|
||||||
|
DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
|
||||||
|
DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
|
||||||
|
DC_STR_PROTECTION_ENABLED = 88,
|
||||||
|
DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
|
||||||
|
DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
|
||||||
|
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
|
||||||
|
DC_STR_READRCPT = 31,
|
||||||
|
DC_STR_READRCPT_MAILBODY = 32,
|
||||||
|
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
|
||||||
|
DC_STR_REMOVE_MEMBER_BY_YOU = 130,
|
||||||
|
DC_STR_REPLY_NOUN = 90,
|
||||||
|
DC_STR_SAVED_MESSAGES = 69,
|
||||||
|
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
|
||||||
|
DC_STR_SECURE_JOIN_REPLIES = 118,
|
||||||
|
DC_STR_SECURE_JOIN_STARTED = 117,
|
||||||
|
DC_STR_SELF = 2,
|
||||||
|
DC_STR_SELF_DELETED_MSG_BODY = 91,
|
||||||
|
DC_STR_SENDING = 110,
|
||||||
|
DC_STR_SERVER_TURNED_OFF = 92,
|
||||||
|
DC_STR_SETUP_CONTACT_QR_DESC = 119,
|
||||||
|
DC_STR_STICKER = 67,
|
||||||
|
DC_STR_STORAGE_ON_DOMAIN = 105,
|
||||||
|
DC_STR_SUBJECT_FOR_NEW_CONTACT = 73,
|
||||||
|
DC_STR_SYNC_MSG_BODY = 102,
|
||||||
|
DC_STR_SYNC_MSG_SUBJECT = 101,
|
||||||
|
DC_STR_UNKNOWN_SENDER_FOR_CHAT = 72,
|
||||||
|
DC_STR_UPDATE_REMINDER_MSG_BODY = 86,
|
||||||
|
DC_STR_UPDATING = 109,
|
||||||
|
DC_STR_VIDEO = 10,
|
||||||
|
DC_STR_VIDEOCHAT_INVITATION = 82,
|
||||||
|
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
|
||||||
|
DC_STR_VOICEMESSAGE = 7,
|
||||||
|
DC_STR_WELCOME_MESSAGE = 71,
|
||||||
|
DC_TEXT1_DRAFT = 1,
|
||||||
|
DC_TEXT1_SELF = 3,
|
||||||
|
DC_TEXT1_USERNAME = 2,
|
||||||
|
DC_VIDEOCHATTYPE_BASICWEBRTC = 1,
|
||||||
|
DC_VIDEOCHATTYPE_JITSI = 2,
|
||||||
|
DC_VIDEOCHATTYPE_UNKNOWN = 0,
|
||||||
|
}
|
||||||
@@ -52,7 +52,7 @@ export type Event=(({
|
|||||||
* should not be disturbed by a dialog or so. Instead, use a bubble or so.
|
* should not be disturbed by a dialog or so. Instead, use a bubble or so.
|
||||||
*
|
*
|
||||||
* However, for ongoing processes (eg. configure())
|
* However, for ongoing processes (eg. configure())
|
||||||
* or for functions that are expected to fail (eg. dc_continue_key_transfer())
|
* or for functions that are expected to fail (eg. autocryptContinueKeyTransfer())
|
||||||
* it might be better to delay showing these events until the function has really
|
* it might be better to delay showing these events until the function has really
|
||||||
* failed (returned false). It should be sufficient to report only the *last* error
|
* failed (returned false). It should be sufficient to report only the *last* error
|
||||||
* in a messasge box then.
|
* in a messasge box then.
|
||||||
@@ -61,9 +61,9 @@ export type Event=(({
|
|||||||
/**
|
/**
|
||||||
* An action cannot be performed because the user is not in the group.
|
* An action cannot be performed because the user is not in the group.
|
||||||
* Reported eg. after a call to
|
* Reported eg. after a call to
|
||||||
* dc_set_chat_name(), dc_set_chat_profile_image(),
|
* setChatName(), setChatProfileImage(),
|
||||||
* dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
|
* addContactToChat(), removeContactFromChat(),
|
||||||
* dc_send_text_msg() or another sending function.
|
* and messages sending functions.
|
||||||
*/
|
*/
|
||||||
"type":"ErrorSelfNotInGroup";}&{"msg":string;})|({
|
"type":"ErrorSelfNotInGroup";}&{"msg":string;})|({
|
||||||
/**
|
/**
|
||||||
@@ -73,10 +73,14 @@ export type Event=(({
|
|||||||
* - Chats created, deleted or archived
|
* - Chats created, deleted or archived
|
||||||
* - A draft has been set
|
* - A draft has been set
|
||||||
*
|
*
|
||||||
* `chat_id` is set if only a single chat is affected by the changes, otherwise 0.
|
* `chatId` is set if only a single chat is affected by the changes, otherwise 0.
|
||||||
* `msg_id` is set if only a single message is affected by the changes, otherwise 0.
|
* `msgId` is set if only a single message is affected by the changes, otherwise 0.
|
||||||
*/
|
*/
|
||||||
"type":"MsgsChanged";}&{"chatId":U32;"msgId":U32;})|({
|
"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
|
* There is a fresh message. Typically, the user will show an notification
|
||||||
* when receiving this message.
|
* when receiving this message.
|
||||||
@@ -91,24 +95,24 @@ export type Event=(({
|
|||||||
"type":"MsgsNoticed";}&{"chatId":U32;})|({
|
"type":"MsgsNoticed";}&{"chatId":U32;})|({
|
||||||
/**
|
/**
|
||||||
* A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
* A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
||||||
* DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
|
* DC_STATE_OUT_DELIVERED, see `Message.state`.
|
||||||
*/
|
*/
|
||||||
"type":"MsgDelivered";}&{"chatId":U32;"msgId":U32;})|({
|
"type":"MsgDelivered";}&{"chatId":U32;"msgId":U32;})|({
|
||||||
/**
|
/**
|
||||||
* A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
|
* A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
|
||||||
* DC_STATE_OUT_FAILED, see dc_msg_get_state().
|
* DC_STATE_OUT_FAILED, see `Message.state`.
|
||||||
*/
|
*/
|
||||||
"type":"MsgFailed";}&{"chatId":U32;"msgId":U32;})|({
|
"type":"MsgFailed";}&{"chatId":U32;"msgId":U32;})|({
|
||||||
/**
|
/**
|
||||||
* A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
* A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
||||||
* DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
|
* DC_STATE_OUT_MDN_RCVD, see `Message.state`.
|
||||||
*/
|
*/
|
||||||
"type":"MsgRead";}&{"chatId":U32;"msgId":U32;})|({
|
"type":"MsgRead";}&{"chatId":U32;"msgId":U32;})|({
|
||||||
/**
|
/**
|
||||||
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||||
* Or the verify state of a chat has changed.
|
* Or the verify state of a chat has changed.
|
||||||
* See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
|
* See setChatName(), setChatProfileImage(), addContactToChat()
|
||||||
* and dc_remove_contact_from_chat().
|
* and removeContactFromChat().
|
||||||
*
|
*
|
||||||
* This event does not include ephemeral timer modification, which
|
* This event does not include ephemeral timer modification, which
|
||||||
* is a separate event.
|
* is a separate event.
|
||||||
@@ -129,7 +133,7 @@ export type Event=(({
|
|||||||
*
|
*
|
||||||
* @param data1 (u32) contact_id of the contact for which the location has changed.
|
* @param data1 (u32) contact_id of the contact for which the location has changed.
|
||||||
* If the locations of several contacts have been changed,
|
* If the locations of several contacts have been changed,
|
||||||
* eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
|
* this parameter is set to `None`.
|
||||||
*/
|
*/
|
||||||
"type":"LocationChanged";}&{"contactId":(U32|null);})|({
|
"type":"LocationChanged";}&{"contactId":(U32|null);})|({
|
||||||
/**
|
/**
|
||||||
@@ -168,7 +172,7 @@ export type Event=(({
|
|||||||
* (Alice, the person who shows the QR code).
|
* (Alice, the person who shows the QR code).
|
||||||
*
|
*
|
||||||
* These events are typically sent after a joiner has scanned the QR code
|
* These events are typically sent after a joiner has scanned the QR code
|
||||||
* generated by dc_get_securejoin_qr().
|
* generated by getChatSecurejoinQrCodeSvg().
|
||||||
*
|
*
|
||||||
* @param data1 (int) ID of the contact that wants to join.
|
* @param data1 (int) ID of the contact that wants to join.
|
||||||
* @param data2 (int) Progress as:
|
* @param data2 (int) Progress as:
|
||||||
@@ -181,7 +185,7 @@ export type Event=(({
|
|||||||
/**
|
/**
|
||||||
* Progress information of a secure-join handshake from the view of the joiner
|
* Progress information of a secure-join handshake from the view of the joiner
|
||||||
* (Bob, the person who scans the QR code).
|
* (Bob, the person who scans the QR code).
|
||||||
* The events are typically sent while dc_join_securejoin(), which
|
* The events are typically sent while secureJoin(), which
|
||||||
* may take some time, is executed.
|
* may take some time, is executed.
|
||||||
* @param data1 (int) ID of the inviting contact.
|
* @param data1 (int) ID of the inviting contact.
|
||||||
* @param data2 (int) Progress as:
|
* @param data2 (int) Progress as:
|
||||||
@@ -192,8 +196,8 @@ export type Event=(({
|
|||||||
/**
|
/**
|
||||||
* The connectivity to the server changed.
|
* The connectivity to the server changed.
|
||||||
* This means that you should refresh the connectivity view
|
* This means that you should refresh the connectivity view
|
||||||
* and possibly the connectivtiy HTML; see dc_get_connectivity() and
|
* and possibly the connectivtiy HTML; see getConnectivity() and
|
||||||
* dc_get_connectivity_html() for details.
|
* getConnectivityHtml() for details.
|
||||||
*/
|
*/
|
||||||
"type":"ConnectivityChanged";}|{"type":"SelfavatarChanged";}|({"type":"WebxdcStatusUpdate";}&{"msgId":U32;"statusUpdateSerial":U32;})|({
|
"type":"ConnectivityChanged";}|{"type":"SelfavatarChanged";}|({"type":"WebxdcStatusUpdate";}&{"msgId":U32;"statusUpdateSerial":U32;})|({
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -147,7 +147,24 @@ export type WebxdcMessageInfo={
|
|||||||
*/
|
*/
|
||||||
"internetAccess":boolean;};
|
"internetAccess":boolean;};
|
||||||
export type DownloadState=("Done"|"Available"|"Failure"|"InProgress");
|
export type DownloadState=("Done"|"Available"|"Failure"|"InProgress");
|
||||||
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;};
|
|
||||||
|
/**
|
||||||
|
* Structure representing all reactions to a particular message.
|
||||||
|
*/
|
||||||
|
export type Reactions=
|
||||||
|
/**
|
||||||
|
* Structure representing all reactions to a particular message.
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Map from a contact to it's reaction to message.
|
||||||
|
*/
|
||||||
|
"reactionsByContact":Record<U32,(string)[]>;
|
||||||
|
/**
|
||||||
|
* Unique reactions and their count
|
||||||
|
*/
|
||||||
|
"reactions":Record<string,U32>;};
|
||||||
|
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;"reactions":(Reactions|null);};
|
||||||
export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"image":(string|null);"imageMimeType":(string|null);"chatName":string;"chatProfileImage":(string|null);
|
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
|
* also known as summary_text1
|
||||||
@@ -160,4 +177,4 @@ export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"imag
|
|||||||
export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
|
export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
|
||||||
export type F64=number;
|
export type F64=number;
|
||||||
export type Location={"locationId":U32;"isIndependent":boolean;"latitude":F64;"longitude":F64;"accuracy":F64;"timestamp":I64;"contactId":U32;"msgId":U32;"chatId":U32;"marker":(string|null);};
|
export type Location={"locationId":U32;"isIndependent":boolean;"latitude":F64;"longitude":F64;"accuracy":F64;"timestamp":I64;"contactId":U32;"msgId":U32;"chatId":U32;"marker":(string|null);};
|
||||||
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];
|
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,U32,(string|null),U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,(string)[],U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,Record<string,(string)[]>,U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"main": "dist/deltachat.js",
|
"main": "dist/deltachat.js",
|
||||||
"name": "@deltachat/jsonrpc-client",
|
"name": "@deltachat/jsonrpc-client",
|
||||||
"scripts": {
|
"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:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
|
||||||
"build:tsc": "tsc",
|
"build:tsc": "tsc",
|
||||||
"docs": "typedoc --out docs deltachat.ts",
|
"docs": "typedoc --out docs deltachat.ts",
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
|
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
|
||||||
"example:start": "http-server .",
|
"example:start": "http-server .",
|
||||||
"generate-bindings": "cargo test",
|
"generate-bindings": "cargo test",
|
||||||
|
"extract-constants": "node ./scripts/generate-constants.js",
|
||||||
"prettier:check": "prettier --check **.ts",
|
"prettier:check": "prettier --check **.ts",
|
||||||
"prettier:fix": "prettier --write **.ts",
|
"prettier:fix": "prettier --write **.ts",
|
||||||
"test": "run-s test:prepare test:run-coverage test:report-coverage",
|
"test": "run-s test:prepare test:run-coverage test:report-coverage",
|
||||||
@@ -47,5 +48,5 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "dist/deltachat.d.ts",
|
"types": "dist/deltachat.d.ts",
|
||||||
"version": "1.96.0"
|
"version": "1.97.0"
|
||||||
}
|
}
|
||||||
53
deltachat-jsonrpc/typescript/scripts/generate-constants.js
Executable file
53
deltachat-jsonrpc/typescript/scripts/generate-constants.js
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
|
import { resolve } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { dirname } from "path";
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const data = [];
|
||||||
|
const header = resolve(__dirname, "../../../deltachat-ffi/deltachat.h");
|
||||||
|
|
||||||
|
console.log("Generating constants...");
|
||||||
|
|
||||||
|
const header_data = readFileSync(header, "UTF-8");
|
||||||
|
const regex = /^#define\s+(\w+)\s+(\w+)/gm;
|
||||||
|
let match;
|
||||||
|
while (null != (match = regex.exec(header_data))) {
|
||||||
|
const key = match[1];
|
||||||
|
const value = parseInt(match[2]);
|
||||||
|
if (!isNaN(value)) {
|
||||||
|
data.push({ key, value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const constants = data
|
||||||
|
.filter(
|
||||||
|
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
|
||||||
|
)
|
||||||
|
.sort((lhs, rhs) => {
|
||||||
|
if (lhs.key < rhs.key) return -1;
|
||||||
|
else if (lhs.key > rhs.key) return 1;
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
.filter(({ key }) => {
|
||||||
|
// filter out what we don't need it
|
||||||
|
return !(
|
||||||
|
key.startsWith("DC_EVENT_") ||
|
||||||
|
key.startsWith("DC_IMEX_") ||
|
||||||
|
key.startsWith("DC_CHAT_VISIBILITY") ||
|
||||||
|
key.startsWith("DC_DOWNLOAD") ||
|
||||||
|
(key.startsWith("DC_MSG") && !key.startsWith("DC_MSG_ID")) ||
|
||||||
|
key.startsWith("DC_QR_")
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((row) => {
|
||||||
|
return ` ${row.key}: ${row.value}`;
|
||||||
|
})
|
||||||
|
.join(",\n");
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
resolve(__dirname, "../generated/constants.ts"),
|
||||||
|
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`
|
||||||
|
);
|
||||||
@@ -28,6 +28,7 @@ type ContextEvents = { ALL: (event: Event) => void } & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type DcEvent = Event;
|
export type DcEvent = Event;
|
||||||
|
export type DcEventType<T extends Event["type"]> = Extract<Event, { type: T }>
|
||||||
|
|
||||||
export class BaseDeltaChat<
|
export class BaseDeltaChat<
|
||||||
Transport extends BaseTransport<any>
|
Transport extends BaseTransport<any>
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export * from "../generated/events.js";
|
|||||||
export { RawClient } from "../generated/client.js";
|
export { RawClient } from "../generated/client.js";
|
||||||
export * from "./client.js";
|
export * from "./client.js";
|
||||||
export * as yerpc from "yerpc";
|
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::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||||
use deltachat::peerstate::*;
|
use deltachat::peerstate::*;
|
||||||
use deltachat::qr::*;
|
use deltachat::qr::*;
|
||||||
|
use deltachat::reaction::send_reaction;
|
||||||
use deltachat::receive_imf::*;
|
use deltachat::receive_imf::*;
|
||||||
use deltachat::sql;
|
use deltachat::sql;
|
||||||
use deltachat::tools::*;
|
use deltachat::tools::*;
|
||||||
@@ -407,6 +408,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
resend <msg-id>\n\
|
resend <msg-id>\n\
|
||||||
markseen <msg-id>\n\
|
markseen <msg-id>\n\
|
||||||
delmsg <msg-id>\n\
|
delmsg <msg-id>\n\
|
||||||
|
react <msg-id> [<reaction>]\n\
|
||||||
===========================Contact commands==\n\
|
===========================Contact commands==\n\
|
||||||
listcontacts [<query>]\n\
|
listcontacts [<query>]\n\
|
||||||
listverified [<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()?);
|
ids[0] = MsgId::new(arg1.parse()?);
|
||||||
message::delete_msgs(&context, &ids).await?;
|
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" => {
|
"listcontacts" | "contacts" | "listverified" => {
|
||||||
let contacts = Contact::get_all(
|
let contacts = Contact::get_all(
|
||||||
&context,
|
&context,
|
||||||
|
|||||||
@@ -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(_) => {
|
EventType::ContactsChanged(_) => {
|
||||||
info!("{}", yellow.paint("Received CONTACTS_CHANGED()"));
|
info!("{}", yellow.paint("Received CONTACTS_CHANGED()"));
|
||||||
}
|
}
|
||||||
@@ -208,7 +221,7 @@ const CHAT_COMMANDS: [&str; 36] = [
|
|||||||
"accept",
|
"accept",
|
||||||
"blockchat",
|
"blockchat",
|
||||||
];
|
];
|
||||||
const MESSAGE_COMMANDS: [&str; 8] = [
|
const MESSAGE_COMMANDS: [&str; 9] = [
|
||||||
"listmsgs",
|
"listmsgs",
|
||||||
"msginfo",
|
"msginfo",
|
||||||
"listfresh",
|
"listfresh",
|
||||||
@@ -217,6 +230,7 @@ const MESSAGE_COMMANDS: [&str; 8] = [
|
|||||||
"markseen",
|
"markseen",
|
||||||
"delmsg",
|
"delmsg",
|
||||||
"download",
|
"download",
|
||||||
|
"react",
|
||||||
];
|
];
|
||||||
const CONTACT_COMMANDS: [&str; 9] = [
|
const CONTACT_COMMANDS: [&str; 9] = [
|
||||||
"listcontacts",
|
"listcontacts",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ module.exports = {
|
|||||||
DC_EVENT_MSG_FAILED: 2012,
|
DC_EVENT_MSG_FAILED: 2012,
|
||||||
DC_EVENT_MSG_READ: 2015,
|
DC_EVENT_MSG_READ: 2015,
|
||||||
DC_EVENT_NEW_BLOB_FILE: 150,
|
DC_EVENT_NEW_BLOB_FILE: 150,
|
||||||
|
DC_EVENT_REACTIONS_CHANGED: 2001,
|
||||||
DC_EVENT_SECUREJOIN_INVITER_PROGRESS: 2060,
|
DC_EVENT_SECUREJOIN_INVITER_PROGRESS: 2060,
|
||||||
DC_EVENT_SECUREJOIN_JOINER_PROGRESS: 2061,
|
DC_EVENT_SECUREJOIN_JOINER_PROGRESS: 2061,
|
||||||
DC_EVENT_SELFAVATAR_CHANGED: 2110,
|
DC_EVENT_SELFAVATAR_CHANGED: 2110,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ module.exports = {
|
|||||||
400: 'DC_EVENT_ERROR',
|
400: 'DC_EVENT_ERROR',
|
||||||
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
||||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||||
|
2001: 'DC_EVENT_REACTIONS_CHANGED',
|
||||||
2005: 'DC_EVENT_INCOMING_MSG',
|
2005: 'DC_EVENT_INCOMING_MSG',
|
||||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||||
2010: 'DC_EVENT_MSG_DELIVERED',
|
2010: 'DC_EVENT_MSG_DELIVERED',
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export enum C {
|
|||||||
DC_EVENT_MSG_FAILED = 2012,
|
DC_EVENT_MSG_FAILED = 2012,
|
||||||
DC_EVENT_MSG_READ = 2015,
|
DC_EVENT_MSG_READ = 2015,
|
||||||
DC_EVENT_NEW_BLOB_FILE = 150,
|
DC_EVENT_NEW_BLOB_FILE = 150,
|
||||||
|
DC_EVENT_REACTIONS_CHANGED = 2001,
|
||||||
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060,
|
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060,
|
||||||
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061,
|
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061,
|
||||||
DC_EVENT_SELFAVATAR_CHANGED = 2110,
|
DC_EVENT_SELFAVATAR_CHANGED = 2110,
|
||||||
@@ -282,6 +283,7 @@ export const EventId2EventName: { [key: number]: string } = {
|
|||||||
400: 'DC_EVENT_ERROR',
|
400: 'DC_EVENT_ERROR',
|
||||||
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
||||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||||
|
2001: 'DC_EVENT_REACTIONS_CHANGED',
|
||||||
2005: 'DC_EVENT_INCOMING_MSG',
|
2005: 'DC_EVENT_INCOMING_MSG',
|
||||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||||
2010: 'DC_EVENT_MSG_DELIVERED',
|
2010: 'DC_EVENT_MSG_DELIVERED',
|
||||||
|
|||||||
@@ -89,7 +89,11 @@ describe('JSON RPC', function () {
|
|||||||
const { dc } = DeltaChat.newTemporary()
|
const { dc } = DeltaChat.newTemporary()
|
||||||
let promise_resolve
|
let promise_resolve
|
||||||
const promise = new Promise((res, _rej) => {
|
const promise = new Promise((res, _rej) => {
|
||||||
promise_resolve = res
|
promise_resolve = (response) => {
|
||||||
|
// ignore events
|
||||||
|
const answer = JSON.parse(response)
|
||||||
|
if (answer['method'] !== 'event') res(answer)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
dc.startJsonRpcHandler(promise_resolve)
|
dc.startJsonRpcHandler(promise_resolve)
|
||||||
dc.jsonRpcRequest(
|
dc.jsonRpcRequest(
|
||||||
@@ -106,7 +110,7 @@ describe('JSON RPC', function () {
|
|||||||
id: 2,
|
id: 2,
|
||||||
result: [1],
|
result: [1],
|
||||||
},
|
},
|
||||||
JSON.parse(await promise)
|
await promise
|
||||||
)
|
)
|
||||||
dc.close()
|
dc.close()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -57,8 +57,8 @@
|
|||||||
"prebuildify": "cd node && prebuildify -t 16.13.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
|
"prebuildify": "cd node && prebuildify -t 16.13.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
|
||||||
"test": "npm run test:lint && npm run test:mocha",
|
"test": "npm run test:lint && npm run test:mocha",
|
||||||
"test:lint": "npm run lint",
|
"test:lint": "npm run lint",
|
||||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail"
|
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||||
},
|
},
|
||||||
"types": "node/dist/index.d.ts",
|
"types": "node/dist/index.d.ts",
|
||||||
"version": "1.96.0"
|
"version": "1.97.0"
|
||||||
}
|
}
|
||||||
10
spec.md
10
spec.md
@@ -450,6 +450,16 @@ This allows the receiver to show the time without knowing the file format.
|
|||||||
Chat-Duration: 10000
|
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
|
# Miscellaneous
|
||||||
|
|
||||||
Messengers SHOULD use the header `In-Reply-To` as usual.
|
Messengers SHOULD use the header `In-Reply-To` as usual.
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ const SEEN_RECENTLY_SECONDS: i64 = 600;
|
|||||||
///
|
///
|
||||||
/// Some contact IDs are reserved to identify special contacts. This
|
/// Some contact IDs are reserved to identify special contacts. This
|
||||||
/// type can represent both the special as well as normal contacts.
|
/// 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);
|
pub struct ContactId(u32);
|
||||||
|
|
||||||
impl ContactId {
|
impl ContactId {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::imap::{Imap, ImapActionResult};
|
|||||||
use crate::job::{self, Action, Job, Status};
|
use crate::job::{self, Action, Job, Status};
|
||||||
use crate::message::{Message, MsgId, Viewtype};
|
use crate::message::{Message, MsgId, Viewtype};
|
||||||
use crate::mimeparser::{MimeMessage, Part};
|
use crate::mimeparser::{MimeMessage, Part};
|
||||||
use crate::param::{Param, Params};
|
use crate::param::Params;
|
||||||
use crate::tools::time;
|
use crate::tools::time;
|
||||||
use crate::{job_try, stock_str, EventType};
|
use crate::{job_try, stock_str, EventType};
|
||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
@@ -69,42 +69,6 @@ impl Context {
|
|||||||
Ok(Some(max(MIN_DOWNLOAD_LIMIT, download_limit as u32)))
|
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 {
|
impl MsgId {
|
||||||
|
|||||||
@@ -173,6 +173,13 @@ pub enum EventType {
|
|||||||
msg_id: MsgId,
|
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
|
/// There is a fresh message. Typically, the user will show an notification
|
||||||
/// when receiving this message.
|
/// when receiving this message.
|
||||||
///
|
///
|
||||||
|
|||||||
12
src/imap.rs
12
src/imap.rs
@@ -2157,8 +2157,8 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
|
|||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
"INSERT INTO imap_sync (folder, uid_next) VALUES (?,?)
|
"INSERT INTO imap_sync (folder, uid_next) VALUES (?,?)
|
||||||
ON CONFLICT(folder) DO UPDATE SET uid_next=? WHERE folder=?;",
|
ON CONFLICT(folder) DO UPDATE SET uid_next=excluded.uid_next",
|
||||||
paramsv![folder, uid_next, uid_next, folder],
|
paramsv![folder, uid_next],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -2189,8 +2189,8 @@ pub(crate) async fn set_uidvalidity(
|
|||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
"INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?)
|
"INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?)
|
||||||
ON CONFLICT(folder) DO UPDATE SET uidvalidity=? WHERE folder=?;",
|
ON CONFLICT(folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
|
||||||
paramsv![folder, uidvalidity, uidvalidity, folder],
|
paramsv![folder, uidvalidity],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -2212,8 +2212,8 @@ pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) ->
|
|||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
"INSERT INTO imap_sync (folder, modseq) VALUES (?,?)
|
"INSERT INTO imap_sync (folder, modseq) VALUES (?,?)
|
||||||
ON CONFLICT(folder) DO UPDATE SET modseq=? WHERE folder=?;",
|
ON CONFLICT(folder) DO UPDATE SET modseq=excluded.modseq",
|
||||||
paramsv![folder, modseq, modseq, folder],
|
paramsv![folder, modseq],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ pub mod receive_imf;
|
|||||||
pub mod tools;
|
pub mod tools;
|
||||||
|
|
||||||
pub mod accounts;
|
pub mod accounts;
|
||||||
|
pub mod reaction;
|
||||||
|
|
||||||
/// if set imap/incoming and smtp/outgoing MIME messages will be printed
|
/// if set imap/incoming and smtp/outgoing MIME messages will be printed
|
||||||
pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";
|
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::mimeparser::{parse_message_id, DeliveryReport, SystemMessage};
|
||||||
use crate::param::{Param, Params};
|
use crate::param::{Param, Params};
|
||||||
use crate::pgp::split_armored_data;
|
use crate::pgp::split_armored_data;
|
||||||
|
use crate::reaction::get_msg_reactions;
|
||||||
use crate::scheduler::InterruptInfo;
|
use crate::scheduler::InterruptInfo;
|
||||||
use crate::sql;
|
use crate::sql;
|
||||||
use crate::stock_str;
|
use crate::stock_str;
|
||||||
@@ -751,6 +752,11 @@ impl Message {
|
|||||||
self.param.set_int(Param::Duration, duration);
|
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(
|
pub async fn latefiling_mediasize(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
@@ -1082,6 +1088,11 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
|
|||||||
|
|
||||||
ret += "\n";
|
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() {
|
if let Some(error) = msg.error.as_ref() {
|
||||||
ret += &format!("Error: {}", error);
|
ret += &format!("Error: {}", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,10 @@ impl<'a> MimeFactory<'a> {
|
|||||||
)
|
)
|
||||||
.await?;
|
.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;
|
req_mdn = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1122,6 +1125,11 @@ impl<'a> MimeFactory<'a> {
|
|||||||
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
|
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
|
||||||
))
|
))
|
||||||
.body(message_text);
|
.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();
|
let mut parts = Vec::new();
|
||||||
|
|
||||||
// add HTML-part, this is needed only if a HTML-message from a non-delta-client is forwarded;
|
// add HTML-part, this is needed only if a HTML-message from a non-delta-client is forwarded;
|
||||||
|
|||||||
@@ -557,7 +557,10 @@ impl MimeMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if prepend_subject && !subject.is_empty() {
|
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 {
|
if let Some(mut part) = part_with_text {
|
||||||
part.msg = format!("{} – {}", subject, part.msg);
|
part.msg = format!("{} – {}", subject, part.msg);
|
||||||
}
|
}
|
||||||
@@ -919,6 +922,7 @@ impl MimeMessage {
|
|||||||
Ok(any_part_added)
|
Ok(any_part_added)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if any part was added, false otherwise.
|
||||||
async fn add_single_part_if_known(
|
async fn add_single_part_if_known(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
@@ -952,6 +956,30 @@ impl MimeMessage {
|
|||||||
warn!(context, "Missing attachment");
|
warn!(context, "Missing attachment");
|
||||||
return Ok(false);
|
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 => {
|
mime::TEXT | mime::HTML => {
|
||||||
let decoded_data = match mail.get_body() {
|
let decoded_data = match mail.get_body() {
|
||||||
Ok(decoded_data) => decoded_data,
|
Ok(decoded_data) => decoded_data,
|
||||||
@@ -1650,6 +1678,9 @@ pub struct Part {
|
|||||||
/// note that multipart/related may contain further multipart nestings
|
/// note that multipart/related may contain further multipart nestings
|
||||||
/// and all of them needs to be marked with `is_related`.
|
/// and all of them needs to be marked with `is_related`.
|
||||||
pub(crate) is_related: bool,
|
pub(crate) is_related: bool,
|
||||||
|
|
||||||
|
/// Part is an RFC 9078 reaction.
|
||||||
|
pub(crate) is_reaction: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// return mimetype and viewtype for a parsed mail
|
/// return mimetype and viewtype for a parsed mail
|
||||||
@@ -3335,4 +3366,39 @@ Message.
|
|||||||
|
|
||||||
Ok(())
|
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
|
/// For Messages
|
||||||
WantsMdn = b'r',
|
WantsMdn = b'r',
|
||||||
|
|
||||||
|
/// For Messages: the message is a reaction.
|
||||||
|
Reaction = b'x',
|
||||||
|
|
||||||
/// For Messages: a message with Auto-Submitted header ("bot").
|
/// For Messages: a message with Auto-Submitted header ("bot").
|
||||||
Bot = b'b',
|
Bot = b'b',
|
||||||
|
|
||||||
|
|||||||
551
src/reaction.rs
Normal file
551
src/reaction.rs
Normal file
@@ -0,0 +1,551 @@
|
|||||||
|
//! # Reactions.
|
||||||
|
//!
|
||||||
|
//! Reactions are short messages consisting of emojis sent in reply to
|
||||||
|
//! messages. Unlike normal messages which are added to the end of the chat,
|
||||||
|
//! reactions are supposed to be displayed near the original messages.
|
||||||
|
//!
|
||||||
|
//! RFC 9078 specifies how reactions are transmitted in MIME messages.
|
||||||
|
//!
|
||||||
|
//! Reaction update semantics is not well-defined in RFC 9078, so
|
||||||
|
//! Delta Chat uses the same semantics as in
|
||||||
|
//! [XEP-0444](https://xmpp.org/extensions/xep-0444.html) section
|
||||||
|
//! "3.2 Updating reactions to a message". Received reactions override
|
||||||
|
//! all previously received reactions from the same user and it is
|
||||||
|
//! possible to remove all reactions by sending an empty string as a reaction,
|
||||||
|
//! even though RFC 9078 requires at least one emoji to be sent.
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::chat::{send_msg, ChatId};
|
||||||
|
use crate::contact::ContactId;
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::events::EventType;
|
||||||
|
use crate::message::{rfc724_mid_exists, Message, MsgId, Viewtype};
|
||||||
|
|
||||||
|
/// A single reaction consisting of multiple emoji sequences.
|
||||||
|
///
|
||||||
|
/// It is guaranteed to have all emojis sorted and deduplicated inside.
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Reaction {
|
||||||
|
/// Canonical represntation of reaction as a string of space-separated emojis.
|
||||||
|
reaction: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We implement From<&str> instead of std::str::FromStr, because
|
||||||
|
// FromStr requires error type and reaction parsing never returns an
|
||||||
|
// error.
|
||||||
|
impl From<&str> for Reaction {
|
||||||
|
/// Parses a string containing a reaction.
|
||||||
|
///
|
||||||
|
/// Reaction string is separated by spaces or tabs (`WSP` in ABNF),
|
||||||
|
/// but this function accepts any ASCII whitespace, so even a CRLF at
|
||||||
|
/// the end of string is acceptable.
|
||||||
|
///
|
||||||
|
/// Any short enough string is accepted as a reaction to avoid the
|
||||||
|
/// complexity of validating emoji sequences as required by RFC
|
||||||
|
/// 9078. On the sender side UI is responsible to provide only
|
||||||
|
/// valid emoji sequences via reaction picker. On the receiver
|
||||||
|
/// side, abuse of the possibility to use arbitrary strings as
|
||||||
|
/// reactions is not different from other kinds of spam attacks
|
||||||
|
/// such as sending large numbers of large messages, and should be
|
||||||
|
/// dealt with the same way, e.g. by blocking the user.
|
||||||
|
fn from(reaction: &str) -> Self {
|
||||||
|
let mut emojis: Vec<&str> = reaction
|
||||||
|
.split_ascii_whitespace()
|
||||||
|
.filter(|&emoji| emoji.len() < 30)
|
||||||
|
.collect();
|
||||||
|
emojis.sort();
|
||||||
|
emojis.dedup();
|
||||||
|
let reaction = emojis.join(" ");
|
||||||
|
Self { reaction }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reaction {
|
||||||
|
/// Returns true if reaction contains no emojis.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.reaction.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a vector of emojis composing a reaction.
|
||||||
|
pub fn emojis(&self) -> Vec<&str> {
|
||||||
|
self.reaction.split(' ').collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns space-separated string of emojis
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.reaction
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appends emojis from another reaction to this reaction.
|
||||||
|
pub fn add(&self, other: Self) -> Self {
|
||||||
|
let mut emojis: Vec<&str> = self.emojis();
|
||||||
|
emojis.append(&mut other.emojis());
|
||||||
|
emojis.sort();
|
||||||
|
emojis.dedup();
|
||||||
|
let reaction = emojis.join(" ");
|
||||||
|
Self { reaction }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Structure representing all reactions to a particular message.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Reactions {
|
||||||
|
/// Map from a contact to its reaction to message.
|
||||||
|
reactions: BTreeMap<ContactId, Reaction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reactions {
|
||||||
|
/// Returns vector of contacts that reacted to the message.
|
||||||
|
pub fn contacts(&self) -> Vec<ContactId> {
|
||||||
|
self.reactions.keys().copied().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns reaction of a given contact to message.
|
||||||
|
///
|
||||||
|
/// If contact did not react to message or removed the reaction,
|
||||||
|
/// this method returns an empty reaction.
|
||||||
|
pub fn get(&self, contact_id: ContactId) -> Reaction {
|
||||||
|
self.reactions.get(&contact_id).cloned().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the message has no reactions.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.reactions.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Reactions {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let mut emoji_frequencies: BTreeMap<String, usize> = BTreeMap::new();
|
||||||
|
for reaction in self.reactions.values() {
|
||||||
|
for emoji in reaction.emojis() {
|
||||||
|
emoji_frequencies
|
||||||
|
.entry(emoji.to_string())
|
||||||
|
.and_modify(|x| *x += 1)
|
||||||
|
.or_insert(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut first = true;
|
||||||
|
for (emoji, frequency) in emoji_frequencies {
|
||||||
|
if !first {
|
||||||
|
write!(f, " ")?;
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
write!(f, "{}{}", emoji, frequency)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_msg_id_reaction(
|
||||||
|
context: &Context,
|
||||||
|
msg_id: MsgId,
|
||||||
|
chat_id: ChatId,
|
||||||
|
contact_id: ContactId,
|
||||||
|
reaction: Reaction,
|
||||||
|
) -> Result<()> {
|
||||||
|
if reaction.is_empty() {
|
||||||
|
// Simply remove the record instead of setting it to empty string.
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.execute(
|
||||||
|
"DELETE FROM reactions
|
||||||
|
WHERE msg_id = ?1
|
||||||
|
AND contact_id = ?2",
|
||||||
|
paramsv![msg_id, contact_id],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.execute(
|
||||||
|
"INSERT INTO reactions (msg_id, contact_id, reaction)
|
||||||
|
VALUES (?1, ?2, ?3)
|
||||||
|
ON CONFLICT(msg_id, contact_id)
|
||||||
|
DO UPDATE SET reaction=excluded.reaction",
|
||||||
|
paramsv![msg_id, contact_id, reaction.as_str()],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.emit_event(EventType::ReactionsChanged {
|
||||||
|
chat_id,
|
||||||
|
msg_id,
|
||||||
|
contact_id,
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends a reaction to message `msg_id`, overriding previously sent reactions.
|
||||||
|
///
|
||||||
|
/// `reaction` is a string consisting of space-separated emoji. Use
|
||||||
|
/// empty string to retract a reaction.
|
||||||
|
pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) -> Result<MsgId> {
|
||||||
|
let msg = Message::load_from_db(context, msg_id).await?;
|
||||||
|
let chat_id = msg.chat_id;
|
||||||
|
|
||||||
|
let reaction: Reaction = reaction.into();
|
||||||
|
let mut reaction_msg = Message::new(Viewtype::Text);
|
||||||
|
reaction_msg.text = Some(reaction.as_str().to_string());
|
||||||
|
reaction_msg.set_reaction();
|
||||||
|
reaction_msg.in_reply_to = Some(msg.rfc724_mid);
|
||||||
|
reaction_msg.hidden = true;
|
||||||
|
|
||||||
|
// Send messsage first.
|
||||||
|
let reaction_msg_id = send_msg(context, chat_id, &mut reaction_msg).await?;
|
||||||
|
|
||||||
|
// Only set reaction if we successfully sent the message.
|
||||||
|
set_msg_id_reaction(context, msg_id, msg.chat_id, ContactId::SELF, reaction).await?;
|
||||||
|
Ok(reaction_msg_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds given reaction to message `msg_id` and sends an update.
|
||||||
|
///
|
||||||
|
/// This can be used to implement advanced clients that allow reacting
|
||||||
|
/// with multiple emojis. For a simple messenger UI, you probably want
|
||||||
|
/// to use [`send_reaction()`] instead so reacting with a new emoji
|
||||||
|
/// removes previous emoji at the same time.
|
||||||
|
pub async fn add_reaction(context: &Context, msg_id: MsgId, reaction: &str) -> Result<MsgId> {
|
||||||
|
let self_reaction = get_self_reaction(context, msg_id).await?;
|
||||||
|
let reaction = self_reaction.add(Reaction::from(reaction));
|
||||||
|
send_reaction(context, msg_id, reaction.as_str()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates reaction of `contact_id` on the message with `in_reply_to`
|
||||||
|
/// Message-ID. If no such message is found in the database, reaction
|
||||||
|
/// is ignored.
|
||||||
|
///
|
||||||
|
/// `reaction` is a space-separated string of emojis. It can be empty
|
||||||
|
/// if contact wants to remove all reactions.
|
||||||
|
pub(crate) async fn set_msg_reaction(
|
||||||
|
context: &Context,
|
||||||
|
in_reply_to: &str,
|
||||||
|
chat_id: ChatId,
|
||||||
|
contact_id: ContactId,
|
||||||
|
reaction: Reaction,
|
||||||
|
) -> Result<()> {
|
||||||
|
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
|
||||||
|
set_msg_id_reaction(context, msg_id, chat_id, contact_id, reaction).await
|
||||||
|
} else {
|
||||||
|
info!(
|
||||||
|
context,
|
||||||
|
"Can't assign reaction to unknown message with Message-ID {}", in_reply_to
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get our own reaction for a given message.
|
||||||
|
async fn get_self_reaction(context: &Context, msg_id: MsgId) -> Result<Reaction> {
|
||||||
|
let reaction_str: Option<String> = context
|
||||||
|
.sql
|
||||||
|
.query_get_value(
|
||||||
|
"SELECT reaction
|
||||||
|
FROM reactions
|
||||||
|
WHERE msg_id=? AND contact_id=?",
|
||||||
|
paramsv![msg_id, ContactId::SELF],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(reaction_str
|
||||||
|
.as_deref()
|
||||||
|
.map(Reaction::from)
|
||||||
|
.unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a structure containing all reactions to the message.
|
||||||
|
pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result<Reactions> {
|
||||||
|
let reactions = context
|
||||||
|
.sql
|
||||||
|
.query_map(
|
||||||
|
"SELECT contact_id, reaction FROM reactions WHERE msg_id=?",
|
||||||
|
paramsv![msg_id],
|
||||||
|
|row| {
|
||||||
|
let contact_id: ContactId = row.get(0)?;
|
||||||
|
let reaction: String = row.get(1)?;
|
||||||
|
Ok((contact_id, reaction))
|
||||||
|
},
|
||||||
|
|rows| {
|
||||||
|
let mut reactions = Vec::new();
|
||||||
|
for row in rows {
|
||||||
|
let (contact_id, reaction) = row?;
|
||||||
|
reactions.push((contact_id, Reaction::from(reaction.as_str())));
|
||||||
|
}
|
||||||
|
Ok(reactions)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
Ok(Reactions { reactions })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::chat::get_chat_msgs;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::constants::DC_CHAT_ID_TRASH;
|
||||||
|
use crate::contact::{Contact, Origin};
|
||||||
|
use crate::download::DownloadState;
|
||||||
|
use crate::message::MessageState;
|
||||||
|
use crate::receive_imf::{receive_imf, receive_imf_inner};
|
||||||
|
use crate::test_utils::TestContext;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_reaction() {
|
||||||
|
// Check that basic set of emojis from RFC 9078 is supported.
|
||||||
|
assert_eq!(Reaction::from("👍").emojis(), vec!["👍"]);
|
||||||
|
assert_eq!(Reaction::from("👎").emojis(), vec!["👎"]);
|
||||||
|
assert_eq!(Reaction::from("😀").emojis(), vec!["😀"]);
|
||||||
|
assert_eq!(Reaction::from("☹").emojis(), vec!["☹"]);
|
||||||
|
assert_eq!(Reaction::from("😢").emojis(), vec!["😢"]);
|
||||||
|
|
||||||
|
// Empty string can be used to remove all reactions.
|
||||||
|
assert!(Reaction::from("").is_empty());
|
||||||
|
|
||||||
|
// Short strings can be used as emojis, could be used to add
|
||||||
|
// support for custom emojis via emoji shortcodes.
|
||||||
|
assert_eq!(Reaction::from(":deltacat:").emojis(), vec![":deltacat:"]);
|
||||||
|
|
||||||
|
// Check that long strings are not valid emojis.
|
||||||
|
assert!(
|
||||||
|
Reaction::from(":foobarbazquuxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:").is_empty()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Multiple reactions separated by spaces or tabs are supported.
|
||||||
|
assert_eq!(Reaction::from("👍 ❤").emojis(), vec!["❤", "👍"]);
|
||||||
|
assert_eq!(Reaction::from("👍\t❤").emojis(), vec!["❤", "👍"]);
|
||||||
|
|
||||||
|
// Invalid emojis are removed, but valid emojis are retained.
|
||||||
|
assert_eq!(
|
||||||
|
Reaction::from("👍\t:foo: ❤").emojis(),
|
||||||
|
vec![":foo:", "❤", "👍"]
|
||||||
|
);
|
||||||
|
assert_eq!(Reaction::from("👍\t:foo: ❤").as_str(), ":foo: ❤ 👍");
|
||||||
|
|
||||||
|
// Duplicates are removed.
|
||||||
|
assert_eq!(Reaction::from("👍 👍").emojis(), vec!["👍"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_reaction() {
|
||||||
|
let reaction1 = Reaction::from("👍 😀");
|
||||||
|
let reaction2 = Reaction::from("❤");
|
||||||
|
let reaction_sum = reaction1.add(reaction2);
|
||||||
|
|
||||||
|
assert_eq!(reaction_sum.emojis(), vec!["❤", "👍", "😀"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_receive_reaction() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
alice.set_config(Config::ShowEmails, Some("2")).await?;
|
||||||
|
|
||||||
|
// Alice receives BCC-self copy of a message sent to Bob.
|
||||||
|
receive_imf(
|
||||||
|
&alice,
|
||||||
|
"To: bob@example.net\n\
|
||||||
|
From: alice@example.org\n\
|
||||||
|
Date: Today, 29 February 2021 00:00:00 -800\n\
|
||||||
|
Message-ID: 12345@example.org\n\
|
||||||
|
Subject: Meeting\n\
|
||||||
|
\n\
|
||||||
|
Can we chat at 1pm pacific, today?"
|
||||||
|
.as_bytes(),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let msg = alice.get_last_msg().await;
|
||||||
|
assert_eq!(msg.state, MessageState::OutDelivered);
|
||||||
|
let reactions = get_msg_reactions(&alice, msg.id).await?;
|
||||||
|
let contacts = reactions.contacts();
|
||||||
|
assert_eq!(contacts.len(), 0);
|
||||||
|
|
||||||
|
let bob_id = Contact::add_or_lookup(&alice, "", "bob@example.net", Origin::ManuallyCreated)
|
||||||
|
.await?
|
||||||
|
.0;
|
||||||
|
let bob_reaction = reactions.get(bob_id);
|
||||||
|
assert!(bob_reaction.is_empty()); // Bob has not reacted to message yet.
|
||||||
|
|
||||||
|
// Alice receives reaction to her message from Bob.
|
||||||
|
receive_imf(
|
||||||
|
&alice,
|
||||||
|
"To: alice@example.org\n\
|
||||||
|
From: bob@example.net\n\
|
||||||
|
Date: Today, 29 February 2021 00:00:10 -800\n\
|
||||||
|
Message-ID: 56789@example.net\n\
|
||||||
|
In-Reply-To: 12345@example.org\n\
|
||||||
|
Subject: Meeting\n\
|
||||||
|
Mime-Version: 1.0 (1.0)\n\
|
||||||
|
Content-Type: text/plain; charset=utf-8\n\
|
||||||
|
Content-Disposition: reaction\n\
|
||||||
|
\n\
|
||||||
|
\u{1F44D}"
|
||||||
|
.as_bytes(),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let reactions = get_msg_reactions(&alice, msg.id).await?;
|
||||||
|
assert_eq!(reactions.to_string(), "👍1");
|
||||||
|
|
||||||
|
let contacts = reactions.contacts();
|
||||||
|
assert_eq!(contacts.len(), 1);
|
||||||
|
|
||||||
|
assert_eq!(contacts.get(0), Some(&bob_id));
|
||||||
|
let bob_reaction = reactions.get(bob_id);
|
||||||
|
assert_eq!(bob_reaction.is_empty(), false);
|
||||||
|
assert_eq!(bob_reaction.emojis(), vec!["👍"]);
|
||||||
|
assert_eq!(bob_reaction.as_str(), "👍");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn expect_reactions_changed_event(
|
||||||
|
t: &TestContext,
|
||||||
|
expected_chat_id: ChatId,
|
||||||
|
expected_msg_id: MsgId,
|
||||||
|
expected_contact_id: ContactId,
|
||||||
|
) -> Result<()> {
|
||||||
|
let event = t
|
||||||
|
.evtracker
|
||||||
|
.get_matching(|evt| matches!(evt, EventType::ReactionsChanged { .. }))
|
||||||
|
.await;
|
||||||
|
match event {
|
||||||
|
EventType::ReactionsChanged {
|
||||||
|
chat_id,
|
||||||
|
msg_id,
|
||||||
|
contact_id,
|
||||||
|
} => {
|
||||||
|
assert_eq!(chat_id, expected_chat_id);
|
||||||
|
assert_eq!(msg_id, expected_msg_id);
|
||||||
|
assert_eq!(contact_id, expected_contact_id);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_send_reaction() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
let bob = TestContext::new_bob().await;
|
||||||
|
|
||||||
|
let chat_alice = alice.create_chat(&bob).await;
|
||||||
|
let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
|
||||||
|
let bob_msg = bob.recv_msg(&alice_msg).await;
|
||||||
|
assert_eq!(get_chat_msgs(&alice, chat_alice.id, 0).await?.len(), 1);
|
||||||
|
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id, 0).await?.len(), 1);
|
||||||
|
|
||||||
|
let alice_msg2 = alice.send_text(chat_alice.id, "Hi again!").await;
|
||||||
|
bob.recv_msg(&alice_msg2).await;
|
||||||
|
assert_eq!(get_chat_msgs(&alice, chat_alice.id, 0).await?.len(), 2);
|
||||||
|
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id, 0).await?.len(), 2);
|
||||||
|
|
||||||
|
bob_msg.chat_id.accept(&bob).await?;
|
||||||
|
|
||||||
|
send_reaction(&bob, bob_msg.id, "👍").await.unwrap();
|
||||||
|
expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
|
||||||
|
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id, 0).await?.len(), 2);
|
||||||
|
|
||||||
|
let bob_reaction_msg = bob.pop_sent_msg().await;
|
||||||
|
let alice_reaction_msg = alice.recv_msg_opt(&bob_reaction_msg).await.unwrap();
|
||||||
|
assert_eq!(alice_reaction_msg.chat_id, DC_CHAT_ID_TRASH);
|
||||||
|
assert_eq!(get_chat_msgs(&alice, chat_alice.id, 0).await?.len(), 2);
|
||||||
|
|
||||||
|
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
|
||||||
|
assert_eq!(reactions.to_string(), "👍1");
|
||||||
|
let contacts = reactions.contacts();
|
||||||
|
assert_eq!(contacts.len(), 1);
|
||||||
|
let bob_id = contacts.get(0).unwrap();
|
||||||
|
let bob_reaction = reactions.get(*bob_id);
|
||||||
|
assert_eq!(bob_reaction.is_empty(), false);
|
||||||
|
assert_eq!(bob_reaction.emojis(), vec!["👍"]);
|
||||||
|
assert_eq!(bob_reaction.as_str(), "👍");
|
||||||
|
expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Alice reacts to own message.
|
||||||
|
send_reaction(&alice, alice_msg.sender_msg_id, "👍 😀")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
|
||||||
|
assert_eq!(reactions.to_string(), "👍2 😀1");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_partial_download_and_reaction() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
let bob = TestContext::new_bob().await;
|
||||||
|
|
||||||
|
alice
|
||||||
|
.create_chat_with_contact("Bob", "bob@example.net")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let msg_header = "From: Bob <bob@example.net>\n\
|
||||||
|
To: Alice <alice@example.org>\n\
|
||||||
|
Chat-Version: 1.0\n\
|
||||||
|
Subject: subject\n\
|
||||||
|
Message-ID: <first@example.org>\n\
|
||||||
|
Date: Sun, 14 Nov 2021 00:10:00 +0000\
|
||||||
|
Content-Type: text/plain";
|
||||||
|
let msg_full = format!("{}\n\n100k text...", msg_header);
|
||||||
|
|
||||||
|
// Alice downloads message from Bob partially.
|
||||||
|
let alice_received_message = receive_imf_inner(
|
||||||
|
&alice,
|
||||||
|
"first@example.org",
|
||||||
|
msg_header.as_bytes(),
|
||||||
|
false,
|
||||||
|
Some(100000),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.unwrap();
|
||||||
|
let alice_msg_id = *alice_received_message.msg_ids.get(0).unwrap();
|
||||||
|
|
||||||
|
// Bob downloads own message on the other device.
|
||||||
|
let bob_received_message = receive_imf(&bob, msg_full.as_bytes(), false)
|
||||||
|
.await?
|
||||||
|
.unwrap();
|
||||||
|
let bob_msg_id = *bob_received_message.msg_ids.get(0).unwrap();
|
||||||
|
|
||||||
|
// Bob reacts to own message.
|
||||||
|
send_reaction(&bob, bob_msg_id, "👍").await.unwrap();
|
||||||
|
let bob_reaction_msg = bob.pop_sent_msg().await;
|
||||||
|
|
||||||
|
// Alice receives a reaction.
|
||||||
|
alice.recv_msg_opt(&bob_reaction_msg).await.unwrap();
|
||||||
|
|
||||||
|
let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
|
||||||
|
assert_eq!(reactions.to_string(), "👍1");
|
||||||
|
let msg = Message::load_from_db(&alice, alice_msg_id).await?;
|
||||||
|
assert_eq!(msg.download_state(), DownloadState::Available);
|
||||||
|
|
||||||
|
// Alice downloads full message.
|
||||||
|
receive_imf_inner(
|
||||||
|
&alice,
|
||||||
|
"first@example.org",
|
||||||
|
msg_full.as_bytes(),
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Check that reaction is still on the message after full download.
|
||||||
|
let msg = Message::load_from_db(&alice, alice_msg_id).await?;
|
||||||
|
assert_eq!(msg.download_state(), DownloadState::Done);
|
||||||
|
let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
|
||||||
|
assert_eq!(reactions.to_string(), "👍1");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ use crate::mimeparser::{
|
|||||||
};
|
};
|
||||||
use crate::param::{Param, Params};
|
use crate::param::{Param, Params};
|
||||||
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
|
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::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
|
||||||
use crate::sql;
|
use crate::sql;
|
||||||
use crate::stock_str;
|
use crate::stock_str;
|
||||||
@@ -404,7 +405,7 @@ async fn add_parts(
|
|||||||
from_id: ContactId,
|
from_id: ContactId,
|
||||||
seen: bool,
|
seen: bool,
|
||||||
is_partial_download: Option<u32>,
|
is_partial_download: Option<u32>,
|
||||||
replace_msg_id: Option<MsgId>,
|
mut replace_msg_id: Option<MsgId>,
|
||||||
fetching_existing_messages: bool,
|
fetching_existing_messages: bool,
|
||||||
prevent_rename: bool,
|
prevent_rename: bool,
|
||||||
) -> Result<ReceivedMsg> {
|
) -> Result<ReceivedMsg> {
|
||||||
@@ -430,8 +431,9 @@ async fn add_parts(
|
|||||||
};
|
};
|
||||||
// incoming non-chat messages may be discarded
|
// 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_mdn = !mime_parser.mdn_reports.is_empty();
|
||||||
|
let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction);
|
||||||
let show_emails =
|
let show_emails =
|
||||||
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
|
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,
|
ShowEmails::All => allow_creation = !is_mdn,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
allow_creation = !is_mdn;
|
allow_creation = !is_mdn && !is_reaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the message introduces a new chat:
|
// check if the message introduces a new chat:
|
||||||
@@ -689,7 +691,8 @@ async fn add_parts(
|
|||||||
state = if seen
|
state = if seen
|
||||||
|| fetching_existing_messages
|
|| fetching_existing_messages
|
||||||
|| is_mdn
|
|| is_mdn
|
||||||
|| location_kml_is
|
|| is_reaction
|
||||||
|
|| is_location_kml
|
||||||
|| securejoin_seen
|
|| securejoin_seen
|
||||||
|| chat_id_blocked == Blocked::Yes
|
|| chat_id_blocked == Blocked::Yes
|
||||||
{
|
{
|
||||||
@@ -841,14 +844,15 @@ async fn add_parts(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_mdn {
|
let orig_chat_id = chat_id;
|
||||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
let chat_id = if is_mdn || is_reaction {
|
||||||
}
|
|
||||||
|
|
||||||
let chat_id = chat_id.unwrap_or_else(|| {
|
|
||||||
info!(context, "No chat id for message (TRASH)");
|
|
||||||
DC_CHAT_ID_TRASH
|
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.
|
// 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() {
|
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?;
|
let conn = context.sql.get_conn().await?;
|
||||||
|
|
||||||
for part in &mime_parser.parts {
|
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 txt_raw = "".to_string();
|
||||||
let mut stmt = conn.prepare_cached(
|
let mut stmt = conn.prepare_cached(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO msgs
|
INSERT INTO msgs
|
||||||
(
|
(
|
||||||
|
id,
|
||||||
rfc724_mid, chat_id,
|
rfc724_mid, chat_id,
|
||||||
from_id, to_id, timestamp, timestamp_sent,
|
from_id, to_id, timestamp, timestamp_sent,
|
||||||
timestamp_rcvd, type, state, msgrmsg,
|
timestamp_rcvd, type, state, msgrmsg,
|
||||||
@@ -1067,13 +1101,22 @@ INSERT INTO msgs
|
|||||||
ephemeral_timestamp, download_state, hop_info
|
ephemeral_timestamp, download_state, hop_info
|
||||||
)
|
)
|
||||||
VALUES (
|
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);
|
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 {
|
let ephemeral_timestamp = if in_fresh {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
@@ -1113,9 +1151,10 @@ INSERT INTO msgs
|
|||||||
|
|
||||||
// If you change which information is skipped if the message is trashed,
|
// If you change which information is skipped if the message is trashed,
|
||||||
// also change `MsgId::trash()` and `delete_expired_messages()`
|
// 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![
|
stmt.execute(paramsv![
|
||||||
|
replace_msg_id,
|
||||||
rfc724_mid,
|
rfc724_mid,
|
||||||
if trash { DC_CHAT_ID_TRASH } else { chat_id },
|
if trash { DC_CHAT_ID_TRASH } else { chat_id },
|
||||||
if trash { ContactId::UNDEFINED } else { from_id },
|
if trash { ContactId::UNDEFINED } else { from_id },
|
||||||
@@ -1154,6 +1193,10 @@ INSERT INTO msgs
|
|||||||
},
|
},
|
||||||
mime_parser.hop_info
|
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();
|
let row_id = conn.last_insert_rowid();
|
||||||
|
|
||||||
drop(stmt);
|
drop(stmt);
|
||||||
@@ -1162,14 +1205,8 @@ INSERT INTO msgs
|
|||||||
drop(conn);
|
drop(conn);
|
||||||
|
|
||||||
if let Some(replace_msg_id) = replace_msg_id {
|
if let Some(replace_msg_id) = replace_msg_id {
|
||||||
if let Some(created_msg_id) = created_db_entries.pop() {
|
// "Replace" placeholder with a message that has no parts.
|
||||||
context
|
replace_msg_id.delete_from_db(context).await?;
|
||||||
.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?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
chat_id.unarchive_if_not_muted(context).await?;
|
chat_id.unarchive_if_not_muted(context).await?;
|
||||||
|
|||||||
14
src/sql.rs
14
src/sql.rs
@@ -396,7 +396,7 @@ impl Sql {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Used for executing `SELECT COUNT` statements only. Returns the resulting count.
|
/// 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?;
|
let count: isize = self.query_row(query, params, |row| row.get(0)).await?;
|
||||||
Ok(usize::try_from(count)?)
|
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
|
/// If the function returns an error, the transaction will be rolled back. If it does not return an
|
||||||
/// error, the transaction will be committed.
|
/// 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
|
where
|
||||||
H: Send + 'static,
|
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?;
|
let mut conn = self.get_conn().await?;
|
||||||
tokio::task::block_in_place(move || {
|
tokio::task::block_in_place(move || {
|
||||||
@@ -453,7 +453,7 @@ impl Sql {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Query the database if the requested table already exists.
|
/// 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?;
|
let conn = self.get_conn().await?;
|
||||||
tokio::task::block_in_place(move || {
|
tokio::task::block_in_place(move || {
|
||||||
let mut exists = false;
|
let mut exists = false;
|
||||||
@@ -468,7 +468,7 @@ impl Sql {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a column exists in a given table.
|
/// 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?;
|
let conn = self.get_conn().await?;
|
||||||
tokio::task::block_in_place(move || {
|
tokio::task::block_in_place(move || {
|
||||||
let mut exists = false;
|
let mut exists = false;
|
||||||
@@ -492,7 +492,7 @@ impl Sql {
|
|||||||
sql: &str,
|
sql: &str,
|
||||||
params: impl rusqlite::Params,
|
params: impl rusqlite::Params,
|
||||||
f: F,
|
f: F,
|
||||||
) -> anyhow::Result<Option<T>>
|
) -> Result<Option<T>>
|
||||||
where
|
where
|
||||||
F: FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
|
F: FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
|
||||||
{
|
{
|
||||||
@@ -516,7 +516,7 @@ impl Sql {
|
|||||||
&self,
|
&self,
|
||||||
query: &str,
|
query: &str,
|
||||||
params: impl rusqlite::Params,
|
params: impl rusqlite::Params,
|
||||||
) -> anyhow::Result<Option<T>>
|
) -> Result<Option<T>>
|
||||||
where
|
where
|
||||||
T: rusqlite::types::FromSql,
|
T: rusqlite::types::FromSql,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -597,9 +597,22 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
if dbversion < 92 {
|
if dbversion < 92 {
|
||||||
|
sql.execute_migration(
|
||||||
|
r#"CREATE TABLE reactions (
|
||||||
|
msg_id INTEGER NOT NULL, -- id of the message reacted to
|
||||||
|
contact_id INTEGER NOT NULL, -- id of the contact reacting to the message
|
||||||
|
reaction TEXT DEFAULT '' NOT NULL, -- a sequence of emojis separated by spaces
|
||||||
|
PRIMARY KEY(msg_id, contact_id),
|
||||||
|
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE -- delete reactions when message is deleted
|
||||||
|
FOREIGN KEY(contact_id) REFERENCES contacts(id) ON DELETE CASCADE -- delete reactions when contact is deleted
|
||||||
|
)"#,
|
||||||
|
92
|
||||||
|
).await?;
|
||||||
|
}
|
||||||
|
if dbversion < 93 {
|
||||||
sql.execute_migration(
|
sql.execute_migration(
|
||||||
"CREATE TABLE sending_domains(domain TEXT PRIMARY KEY, dkim_works INTEGER DEFAULT 0);",
|
"CREATE TABLE sending_domains(domain TEXT PRIMARY KEY, dkim_works INTEGER DEFAULT 0);",
|
||||||
92,
|
93,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Transport | IMAP v4 ([RFC 3501](https://tools.ietf.org/ht
|
|||||||
Proxy | SOCKS5 ([RFC 1928](https://tools.ietf.org/html/rfc1928))
|
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))
|
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))
|
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))
|
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))
|
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))
|
Push | IMAP IDLE ([RFC 2177](https://tools.ietf.org/html/rfc2177))
|
||||||
|
|||||||
Reference in New Issue
Block a user