Compare commits

...
Author SHA1 Message Date
Simon Laux ea1a962992 jsonrpc: add Context class you can get via getContext or getContextEvents,
it is a convenience wrapper that has all account specific functions with the accountId pre-set.
2022-10-25 23:54:05 +02:00
bjoern d8bc3769a5 allow searching for unaccepted requests (#3694)
* let search_msgs() return unaccepted requests

unaccepted chat requests are shown in the chatlist,
it should be returned by search_msgs() an by the other search functions as well.

form the view of the user, the search acts like a filter,
so there is no reason to hide things additionally.

also, the user may remember a word in a chat request,
maybe even an archived one (there is no need to accept a request before archiving)
that one wants to search later on.

* test searching for unaccepted requests

* simplyfy expression; `c.blocked!=1` is also what is used in similar statements
2022-10-25 22:22:31 +02:00
Floris Bruynooghe a73fbf7232 Update textwrap dependency
The current version is unsatisfyable if you use deltachat as a
dependency itself.
2022-10-24 22:09:41 +02:00
link2xt b6b2f453a0 Prepare 1.98.0 2022-10-24 16:09:24 +00:00
link2xt aa14015919 sql: every Result is anyhow::Result 2022-10-23 11:25:27 +00:00
Simon Laux 7551c84c4f jsonrpc: move qr/uri type to dedicated file (#3687)
#skip-changelog
2022-10-23 08:41:32 +00:00
link2xt 434e53e922 Use UPSERT to insert into msgs table
This way no temporary rows are created and it is easier to maintain
because UPDATE statement is right below the INSERT statement,
unlike `merge_messages` function which is easy to forget about.
2022-10-22 21:34:56 +00:00
link2xt b5d238f7f4 Keep reactions when downloading partially downloaded message 2022-10-22 15:02:05 +00:00
e5c9fea52d Implement reactions
Co-Authored-By: bjoern <r10s@b44t.com>
Co-Authored-By: Simon Laux <mobile.info@simonlaux.de>
2022-10-22 09:59:43 +00:00
Simon Laux cd15a0e966 jsonrpc: typescript client: export constants under C enum, similar to how its exported from deltachat-node (#3681)
* jsonrpc: typescript client: export constants
under `C` enum,
similar to how its exported from `deltachat-node`

* add pr number to changelog

* fix tests

* fix changelog entry position
2022-10-21 17:51:38 +00:00
dependabot[bot] 895c723d4e Merge pull request #3626 from deltachat/dependabot/cargo/trust-dns-resolver-0.22.0 2022-10-18 09:41:10 +00:00
dependabot[bot] c7176d6bc8 Merge pull request #3628 from deltachat/dependabot/cargo/percent-encoding-2.2.0 2022-10-18 09:29:54 +00:00
dependabot[bot] f85f088d65 cargo: bump percent-encoding from 2.1.0 to 2.2.0
Bumps [percent-encoding](https://github.com/servo/rust-url) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/percent-encoding-v2.1.0...v2.2.0)

---
updated-dependencies:
- dependency-name: percent-encoding
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-11 12:33:13 +00:00
dependabot[bot] 3e6d1d5789 cargo: bump trust-dns-resolver from 0.21.2 to 0.22.0
Bumps [trust-dns-resolver](https://github.com/bluejekyll/trust-dns) from 0.21.2 to 0.22.0.
- [Release notes](https://github.com/bluejekyll/trust-dns/releases)
- [Changelog](https://github.com/bluejekyll/trust-dns/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bluejekyll/trust-dns/compare/v0.21.2...v0.22.0)

---
updated-dependencies:
- dependency-name: trust-dns-resolver
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-01 21:02:36 +00:00
45 changed files with 2682 additions and 397 deletions
+14
View File
@@ -3,6 +3,20 @@
## Unreleased
### API-Changes
- jsonrpc: add `Context` class you can get via `getContext` or `getContextEvents`, it is a convinience wrapper that has all account specific functions with the accountId pre-set.
### Changes
### Fixes
- `dc_search_msgs()` returns unaccepted requests #3694
## 1.98.0
### API-Changes
- jsonrpc: typescript client: export constants under `C` enum, similar to how its exported from `deltachat-node` #3681
- added reactions support #3644
- jsonrpc: reactions: added reactions to `Message` type and the `sendReaction()` method #3686
### Changes
- simplify `UPSERT` queries #3676
Generated
+24 -20
View File
@@ -895,7 +895,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.97.0"
version = "1.98.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -954,7 +954,7 @@ dependencies = [
"strum_macros",
"tagger",
"tempfile",
"textwrap 0.15.1",
"textwrap 0.16.0",
"thiserror",
"tokio",
"tokio-stream",
@@ -967,7 +967,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.97.0"
version = "1.98.0"
dependencies = [
"anyhow",
"async-channel",
@@ -996,7 +996,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.97.0"
version = "1.98.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1263,9 +1263,9 @@ checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
[[package]]
name = "enum-as-inner"
version = "0.4.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73"
checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116"
dependencies = [
"heck",
"proc-macro2",
@@ -1643,6 +1643,9 @@ name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
]
[[package]]
name = "hashlink"
@@ -2431,9 +2434,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pgp"
@@ -3411,9 +3414,9 @@ dependencies = [
[[package]]
name = "textwrap"
version = "0.15.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
dependencies = [
"smawk",
"unicode-linebreak",
@@ -3660,9 +3663,9 @@ dependencies = [
[[package]]
name = "trust-dns-proto"
version = "0.21.2"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c31f240f59877c3d4bb3b3ea0ec5a6a0cff07323580ff8c7a605cd7d08b255d"
checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26"
dependencies = [
"async-trait",
"cfg-if",
@@ -3674,32 +3677,32 @@ dependencies = [
"idna",
"ipnet",
"lazy_static",
"log",
"rand 0.8.5",
"smallvec",
"thiserror",
"tinyvec",
"tokio",
"tracing",
"url",
]
[[package]]
name = "trust-dns-resolver"
version = "0.21.2"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4ba72c2ea84515690c9fcef4c6c660bb9df3036ed1051686de84605b74fd558"
checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe"
dependencies = [
"cfg-if",
"futures-util",
"ipconfig",
"lazy_static",
"log",
"lru-cache",
"parking_lot",
"resolv-conf",
"smallvec",
"thiserror",
"tokio",
"tracing",
"trust-dns-proto",
]
@@ -3783,10 +3786,11 @@ checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
[[package]]
name = "unicode-linebreak"
version = "0.1.2"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f"
checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
dependencies = [
"hashbrown 0.12.3",
"regex",
]
@@ -3807,9 +3811,9 @@ checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]]
name = "unicode-width"
version = "0.1.9"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "unicode-xid"
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.97.0"
version = "1.98.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
license = "MPL-2.0"
@@ -23,7 +23,7 @@ anyhow = "1"
async-imap = { git = "https://github.com/async-email/async-imap", branch = "master", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.4", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.5", default-features = false, features = ["smtp-transport", "socks5", "runtime-tokio"] }
trust-dns-resolver = "0.21"
trust-dns-resolver = "0.22"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
backtrace = "0.3"
@@ -47,7 +47,7 @@ num_cpus = "1.13"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.15.0"
percent-encoding = "2.0"
percent-encoding = "2.2"
pgp = { version = "0.8", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
quick-xml = "0.23"
@@ -74,7 +74,7 @@ fast-socks5 = "0.8"
humansize = "1"
qrcodegen = "1.7.0"
tagger = "4.3.3"
textwrap = "0.15.1"
textwrap = "0.16.0"
async-channel = "1.6.1"
futures-lite = "1.12.0"
tokio-stream = { version = "0.1.10", features = ["fs"] }
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.97.0"
version = "1.98.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
+91 -11
View File
@@ -11,16 +11,17 @@ extern "C" {
#endif
typedef struct _dc_context dc_context_t;
typedef struct _dc_accounts dc_accounts_t;
typedef struct _dc_array dc_array_t;
typedef struct _dc_chatlist dc_chatlist_t;
typedef struct _dc_chat dc_chat_t;
typedef struct _dc_msg dc_msg_t;
typedef struct _dc_contact dc_contact_t;
typedef struct _dc_lot dc_lot_t;
typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_context dc_context_t;
typedef struct _dc_accounts dc_accounts_t;
typedef struct _dc_array dc_array_t;
typedef struct _dc_chatlist dc_chatlist_t;
typedef struct _dc_chat dc_chat_t;
typedef struct _dc_msg dc_msg_t;
typedef struct _dc_reactions dc_reactions_t;
typedef struct _dc_contact dc_contact_t;
typedef struct _dc_lot dc_lot_t;
typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
@@ -991,6 +992,34 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
/**
* Send a reaction to message.
*
* Reaction is a string of emojis separated by spaces. Reaction to a
* single message can be sent multiple times. The last reaction
* received overrides all previously received reactions. It is
* possible to remove all reactions by sending an empty string.
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id ID of the message you react to.
* @param reaction A string consisting of emojis separated by spaces.
* @return The ID of the message sent out or 0 for errors.
*/
uint32_t dc_send_reaction (dc_context_t* context, uint32_t msg_id, char *reaction);
/**
* Get a structure with reactions to the message.
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id The message ID to get reactions for.
* @return A structure with all reactions to the message.
*/
dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
/**
* A webxdc instance sends a status update to its other members.
*
@@ -4882,7 +4911,49 @@ uint32_t dc_lot_get_id (const dc_lot_t* lot);
* @param lot The lot object.
* @return The timestamp as defined by the creator of the object. 0 if there is not timestamp or on errors.
*/
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* @class dc_reactions_t
*
* An object representing all reactions for a single message.
*/
/**
* Returns array of contacts which reacted to the given message.
*
* @memberof dc_reactions_t
* @param reactions The object containing message reactions.
* @return array of contact IDs. Use dc_array_get_cnt() to get array length and
* dc_array_get_id() to get the IDs. Should be freed using `dc_array_unref()` after usage.
*/
dc_array_t* dc_reactions_get_contacts(dc_reactions_t* reactions);
/**
* Returns a string containing space-separated reactions of a single contact.
*
* @memberof dc_reactions_t
* @param reactions The object containing message reactions.
* @param contact_id ID of the contact.
* @return Space-separated list of emoji sequences, which could be empty.
* Returned string should not be modified and should be freed
* with dc_str_unref() after usage.
*/
char* dc_reactions_get_by_contact_id(dc_reactions_t* reactions, uint32_t contact_id);
/**
* Frees an object containing message reactions.
*
* Reactions objects are created by dc_get_msg_reactions().
*
* @memberof dc_reactions_t
* @param reactions The object to free.
* If NULL is given, nothing is done.
*/
void dc_reactions_unref (dc_reactions_t* reactions);
/**
@@ -5533,6 +5604,15 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_MSGS_CHANGED 2000
/**
* Message reactions changed.
*
* @param data1 (int) chat_id ID of the chat affected by the changes.
* @param data2 (int) msg_id ID of the message for which reactions were changed.
*/
#define DC_EVENT_REACTIONS_CHANGED 2001
/**
* There is a fresh message. Typically, the user will show an notification
* when receiving this message.
+12
View File
@@ -1,5 +1,6 @@
use crate::chat::ChatItem;
use crate::constants::DC_MSG_ID_DAYMARKER;
use crate::contact::ContactId;
use crate::location::Location;
use crate::message::MsgId;
@@ -7,6 +8,7 @@ use crate::message::MsgId;
#[derive(Debug, Clone)]
pub enum dc_array_t {
MsgIds(Vec<MsgId>),
ContactIds(Vec<ContactId>),
Chat(Vec<ChatItem>),
Locations(Vec<Location>),
Uint(Vec<u32>),
@@ -16,6 +18,7 @@ impl dc_array_t {
pub(crate) fn get_id(&self, index: usize) -> u32 {
match self {
Self::MsgIds(array) => array[index].to_u32(),
Self::ContactIds(array) => array[index].to_u32(),
Self::Chat(array) => match array[index] {
ChatItem::Message { msg_id } => msg_id.to_u32(),
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
@@ -28,6 +31,7 @@ impl dc_array_t {
pub(crate) fn get_timestamp(&self, index: usize) -> Option<i64> {
match self {
Self::MsgIds(_) => None,
Self::ContactIds(_) => None,
Self::Chat(array) => array.get(index).and_then(|item| match item {
ChatItem::Message { .. } => None,
ChatItem::DayMarker { timestamp } => Some(*timestamp),
@@ -40,6 +44,7 @@ impl dc_array_t {
pub(crate) fn get_marker(&self, index: usize) -> Option<&str> {
match self {
Self::MsgIds(_) => None,
Self::ContactIds(_) => None,
Self::Chat(_) => None,
Self::Locations(array) => array
.get(index)
@@ -60,6 +65,7 @@ impl dc_array_t {
pub(crate) fn len(&self) -> usize {
match self {
Self::MsgIds(array) => array.len(),
Self::ContactIds(array) => array.len(),
Self::Chat(array) => array.len(),
Self::Locations(array) => array.len(),
Self::Uint(array) => array.len(),
@@ -83,6 +89,12 @@ impl From<Vec<MsgId>> for dc_array_t {
}
}
impl From<Vec<ContactId>> for dc_array_t {
fn from(array: Vec<ContactId>) -> Self {
dc_array_t::ContactIds(array)
}
}
impl From<Vec<ChatItem>> for dc_array_t {
fn from(array: Vec<ChatItem>) -> Self {
dc_array_t::Chat(array)
+88
View File
@@ -37,6 +37,7 @@ use deltachat::context::Context;
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
use deltachat::stock_str::StockMessage;
use deltachat::stock_str::StockStrings;
use deltachat::webxdc::StatusUpdateSerial;
@@ -66,6 +67,8 @@ use deltachat::chatlist::Chatlist;
/// Struct representing the deltachat context.
pub type dc_context_t = Context;
pub type dc_reactions_t = Reactions;
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
fn block_on<T>(fut: T) -> T::Output
@@ -498,6 +501,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::Error(_) => 400,
EventType::ErrorSelfNotInGroup(_) => 410,
EventType::MsgsChanged { .. } => 2000,
EventType::ReactionsChanged { .. } => 2001,
EventType::IncomingMsg { .. } => 2005,
EventType::MsgsNoticed { .. } => 2008,
EventType::MsgDelivered { .. } => 2010,
@@ -542,6 +546,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::SelfavatarChanged
| EventType::ErrorSelfNotInGroup(_) => 0,
EventType::MsgsChanged { chat_id, .. }
| EventType::ReactionsChanged { chat_id, .. }
| EventType::IncomingMsg { chat_id, .. }
| EventType::MsgsNoticed(chat_id)
| EventType::MsgDelivered { chat_id, .. }
@@ -598,6 +603,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::SelfavatarChanged => 0,
EventType::ChatModified(_) => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
| EventType::MsgDelivered { msg_id, .. }
| EventType::MsgFailed { msg_id, .. }
@@ -637,6 +643,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
data2.into_raw()
}
EventType::MsgsChanged { .. }
| EventType::ReactionsChanged { .. }
| EventType::IncomingMsg { .. }
| EventType::MsgsNoticed(_)
| EventType::MsgDelivered { .. }
@@ -948,6 +955,48 @@ pub unsafe extern "C" fn dc_send_videochat_invitation(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_reaction(
context: *mut dc_context_t,
msg_id: u32,
reaction: *const libc::c_char,
) -> u32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_send_reaction()");
return 0;
}
let ctx = &*context;
block_on(async move {
send_reaction(ctx, MsgId::new(msg_id), &to_string_lossy(reaction))
.await
.map(|msg_id| msg_id.to_u32())
.unwrap_or_log_default(ctx, "Failed to send reaction")
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_msg_reactions(
context: *mut dc_context_t,
msg_id: u32,
) -> *mut dc_reactions_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_msg_reactions()");
return ptr::null_mut();
}
let ctx = &*context;
let reactions = if let Ok(reactions) = block_on(get_msg_reactions(ctx, MsgId::new(msg_id)))
.log_err(ctx, "failed dc_get_msg_reactions() call")
{
reactions
} else {
return ptr::null_mut();
};
Box::into_raw(Box::new(reactions))
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_webxdc_status_update(
context: *mut dc_context_t,
@@ -3988,6 +4037,45 @@ pub unsafe extern "C" fn dc_lot_get_timestamp(lot: *mut dc_lot_t) -> i64 {
lot.get_timestamp()
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_get_contacts(
reactions: *mut dc_reactions_t,
) -> *mut dc_array::dc_array_t {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_get_contacts()");
return ptr::null_mut();
}
let reactions = &*reactions;
let array: dc_array_t = reactions.contacts().into();
Box::into_raw(Box::new(array))
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_get_by_contact_id(
reactions: *mut dc_reactions_t,
contact_id: u32,
) -> *mut libc::c_char {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_get_by_contact_id()");
return ptr::null_mut();
}
let reactions = &*reactions;
reactions.get(ContactId::new(contact_id)).as_str().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_unref(reactions: *mut dc_reactions_t) {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_unref()");
return;
}
drop(Box::from_raw(reactions));
}
#[no_mangle]
pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
libc::free(s as *mut _)
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.97.0"
version = "1.98.0"
description = "DeltaChat JSON-RPC API"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
+17
View File
@@ -102,6 +102,14 @@ pub enum JSONRPCEventType {
msg_id: u32,
},
/// Reactions for the message changed.
#[serde(rename_all = "camelCase")]
ReactionsChanged {
chat_id: u32,
msg_id: u32,
contact_id: u32,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
@@ -284,6 +292,15 @@ impl From<EventType> for JSONRPCEventType {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
} => ReactionsChanged {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
contact_id: contact_id.to_u32(),
},
EventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
+105 -1
View File
@@ -17,6 +17,7 @@ use deltachat::{
provider::get_provider_info,
qr,
qr_code_generator::get_securejoin_qr_svg,
reaction::send_reaction,
securejoin,
stock_str::StockMessage,
webxdc::StatusUpdateSerial,
@@ -34,7 +35,7 @@ pub mod events;
pub mod types;
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
use crate::api::types::QrObject;
use crate::api::types::qr::QrObject;
use types::account::Account;
use types::chat::FullChat;
@@ -1466,6 +1467,23 @@ impl CommandApi {
Ok(message_id.to_u32())
}
/// Send a reaction to message.
///
/// Reaction is a string of emojis separated by spaces. Reaction to a
/// single message can be sent multiple times. The last reaction
/// received overrides all previously received reactions. It is
/// possible to remove all reactions by sending an empty string.
async fn send_reaction(
&self,
account_id: u32,
message_id: u32,
reaction: Vec<String>,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let message_id = send_reaction(&ctx, MsgId::new(message_id), &reaction.join(" ")).await?;
Ok(message_id.to_u32())
}
// ---------------------------------------------
// functions for the composer
// the composer is the message input field
@@ -1692,3 +1710,89 @@ async fn get_config(
.await
}
}
#[cfg(test)]
#[test]
fn stuff() {
let root_namespace = Some("T");
let methods: Vec<yerpc::typescript::Method> = raw_ts_binding_methods();
let account_methods: Vec<String> = methods
.into_iter()
.filter_map(|m| {
// No idea how to check the type, to lazy to find out
// if let Some((name, _)) = m.args.get(0) {
// println!("{}", name);
// }
//println!("-> {}", m.to_string(root_namespace));
if m.to_string(root_namespace).contains("accountId: T.U32") {
//println!("old: {}", m.to_string(root_namespace));
let args2 = Vec::from_iter(m.args[1..].iter().map(|m| m.to_owned()));
let m2 = yerpc::typescript::Method::new(
&m.ts_name,
&m.rpc_name,
args2,
m.output,
m.is_notification,
m.is_positional,
m.docs.as_deref(),
);
let args_string = if m2.args.is_empty() {
"".to_string()
} else {
format!(
", {}",
m2.args
.iter()
.map(|(name, _)| name.as_ref())
.collect::<Vec<&str>>()
.join(", ")
)
};
let replacement_line = format!(
" return this.controller.rpc.{}(this.accountId{})",
m.ts_name, args_string
);
// println!("{}", m2.to_string(root_namespace));
let m_string = m2.to_string(root_namespace);
let new_string: String = m_string
.split('\n')
.map(|line| {
if line.contains("this._transport") {
&replacement_line
} else {
line
}
})
.collect::<Vec<&str>>()
.join("\n");
// println!("new: {}", new_string);
Some(new_string)
} else {
None
}
})
.collect();
let head = r#"import * as T from "./types.js"
import { RawClient } from "./client.js"
import { TinyEmitter } from "tiny-emitter";
export class Context<T> extends TinyEmitter<T> {
constructor(
private controller: {rpc: RawClient},
readonly accountId: T.U32
){
super()
}"#;
let context_methods = format!("{}\n{}\n}}\n", head, account_methods.join(""));
std::fs::write("typescript/generated/context_methods.ts", context_methods).unwrap();
}
@@ -8,6 +8,7 @@ use deltachat::download;
use deltachat::message::Message;
use deltachat::message::MsgId;
use deltachat::message::Viewtype;
use deltachat::reaction::get_msg_reactions;
use num_traits::cast::ToPrimitive;
use serde::Deserialize;
use serde::Serialize;
@@ -15,6 +16,7 @@ use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
use super::contact::ContactObject;
use super::reactions::JSONRPCReactions;
use super::webxdc::WebxdcMessageInfo;
#[derive(Serialize, TypeDef)]
@@ -64,6 +66,8 @@ pub struct MessageObject {
webxdc_info: Option<WebxdcMessageInfo>,
download_state: DownloadState,
reactions: Option<JSONRPCReactions>,
}
#[derive(Serialize, TypeDef)]
@@ -139,6 +143,13 @@ impl MessageObject {
None
};
let reactions = get_msg_reactions(context, msg_id).await?;
let reactions = if reactions.is_empty() {
None
} else {
Some(reactions.into())
};
Ok(MessageObject {
id: msg_id.to_u32(),
chat_id: message.get_chat_id().to_u32(),
@@ -193,6 +204,8 @@ impl MessageObject {
webxdc_info,
download_state,
reactions,
})
}
}
+2 -214
View File
@@ -1,7 +1,3 @@
use deltachat::qr::Qr;
use serde::Serialize;
use typescript_type_def::TypeDef;
pub mod account;
pub mod chat;
pub mod chat_list;
@@ -9,6 +5,8 @@ pub mod contact;
pub mod location;
pub mod message;
pub mod provider_info;
pub mod qr;
pub mod reactions;
pub mod webxdc;
pub fn color_int_to_hex_string(color: u32) -> String {
@@ -22,213 +20,3 @@ fn maybe_empty_string_to_option(string: String) -> Option<String> {
Some(string)
}
}
#[derive(Serialize, TypeDef)]
#[serde(rename = "Qr", rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum QrObject {
AskVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
AskVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
FprOk {
contact_id: u32,
},
FprMismatch {
contact_id: Option<u32>,
},
FprWithoutAddr {
fingerprint: String,
},
Account {
domain: String,
},
WebrtcInstance {
domain: String,
instance_pattern: String,
},
Addr {
contact_id: u32,
draft: Option<String>,
},
Url {
url: String,
},
Text {
text: String,
},
WithdrawVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
WithdrawVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
ReviveVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
ReviveVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
Login {
address: String,
},
}
impl From<Qr> for QrObject {
fn from(qr: Qr) -> Self {
match qr {
Qr::AskVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::AskVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::AskVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::AskVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::FprOk { contact_id } => {
let contact_id = contact_id.to_u32();
QrObject::FprOk { contact_id }
}
Qr::FprMismatch { contact_id } => {
let contact_id = contact_id.map(|contact_id| contact_id.to_u32());
QrObject::FprMismatch { contact_id }
}
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
Qr::Account { domain } => QrObject::Account { domain },
Qr::WebrtcInstance {
domain,
instance_pattern,
} => QrObject::WebrtcInstance {
domain,
instance_pattern,
},
Qr::Addr { contact_id, draft } => {
let contact_id = contact_id.to_u32();
QrObject::Addr { contact_id, draft }
}
Qr::Url { url } => QrObject::Url { url },
Qr::Text { text } => QrObject::Text { text },
Qr::WithdrawVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::WithdrawVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::WithdrawVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::WithdrawVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::ReviveVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::ReviveVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::ReviveVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::ReviveVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::Login { address, .. } => QrObject::Login { address },
}
}
}
+213
View File
@@ -0,0 +1,213 @@
use deltachat::qr::Qr;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef)]
#[serde(rename = "Qr", rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum QrObject {
AskVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
AskVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
FprOk {
contact_id: u32,
},
FprMismatch {
contact_id: Option<u32>,
},
FprWithoutAddr {
fingerprint: String,
},
Account {
domain: String,
},
WebrtcInstance {
domain: String,
instance_pattern: String,
},
Addr {
contact_id: u32,
draft: Option<String>,
},
Url {
url: String,
},
Text {
text: String,
},
WithdrawVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
WithdrawVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
ReviveVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
ReviveVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
Login {
address: String,
},
}
impl From<Qr> for QrObject {
fn from(qr: Qr) -> Self {
match qr {
Qr::AskVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::AskVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::AskVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::AskVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::FprOk { contact_id } => {
let contact_id = contact_id.to_u32();
QrObject::FprOk { contact_id }
}
Qr::FprMismatch { contact_id } => {
let contact_id = contact_id.map(|contact_id| contact_id.to_u32());
QrObject::FprMismatch { contact_id }
}
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
Qr::Account { domain } => QrObject::Account { domain },
Qr::WebrtcInstance {
domain,
instance_pattern,
} => QrObject::WebrtcInstance {
domain,
instance_pattern,
},
Qr::Addr { contact_id, draft } => {
let contact_id = contact_id.to_u32();
QrObject::Addr { contact_id, draft }
}
Qr::Url { url } => QrObject::Url { url },
Qr::Text { text } => QrObject::Text { text },
Qr::WithdrawVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::WithdrawVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::WithdrawVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::WithdrawVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::ReviveVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::ReviveVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::ReviveVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.to_string();
QrObject::ReviveVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
invitenumber,
authcode,
}
}
Qr::Login { address, .. } => QrObject::Login { address },
}
}
}
@@ -0,0 +1,47 @@
use std::collections::BTreeMap;
use deltachat::reaction::Reactions;
use serde::Serialize;
use typescript_type_def::TypeDef;
/// Structure representing all reactions to a particular message.
#[derive(Serialize, TypeDef)]
#[serde(rename = "Reactions", rename_all = "camelCase")]
pub struct JSONRPCReactions {
/// Map from a contact to it's reaction to message.
reactions_by_contact: BTreeMap<u32, Vec<String>>,
/// Unique reactions and their count
reactions: BTreeMap<String, u32>,
}
impl From<Reactions> for JSONRPCReactions {
fn from(reactions: Reactions) -> Self {
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
let mut unique_reactions: BTreeMap<String, u32> = BTreeMap::new();
for contact_id in reactions.contacts() {
let reaction = reactions.get(contact_id);
if reaction.is_empty() {
continue;
}
let emojis: Vec<String> = reaction
.emojis()
.into_iter()
.map(|emoji| emoji.to_owned())
.collect();
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
for emoji in emojis {
if let Some(x) = unique_reactions.get_mut(&emoji) {
*x += 1;
} else {
unique_reactions.insert(emoji, 1);
}
}
}
JSONRPCReactions {
reactions_by_contact,
reactions: unique_reactions,
}
}
}
@@ -18,6 +18,7 @@ async function run() {
const client = new DeltaChat("ws://localhost:20808/ws");
(window as any).dc = client;
(window as any).client = client.rpc;
client.on("ALL", (accountId, event) => {
@@ -878,6 +878,18 @@ export class RawClient {
return (this._transport.request('send_sticker', [accountId, chatId, stickerPath] as RPC.Params)) as Promise<T.U32>;
}
/**
* Send a reaction to message.
*
* Reaction is a string of emojis separated by spaces. Reaction to a
* single message can be sent multiple times. The last reaction
* received overrides all previously received reactions. It is
* possible to remove all reactions by sending an empty string.
*/
public sendReaction(accountId: T.U32, messageId: T.U32, reaction: (string)[]): Promise<T.U32> {
return (this._transport.request('send_reaction', [accountId, messageId, reaction] as RPC.Params)) as Promise<T.U32>;
}
public removeDraft(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('remove_draft', [accountId, chatId] as RPC.Params)) as Promise<null>;
@@ -0,0 +1,201 @@
// Generated!
export enum C {
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
DC_CERTCK_AUTO = 0,
DC_CERTCK_STRICT = 1,
DC_CHAT_ID_ALLDONE_HINT = 7,
DC_CHAT_ID_ARCHIVED_LINK = 6,
DC_CHAT_ID_LAST_SPECIAL = 9,
DC_CHAT_ID_TRASH = 3,
DC_CHAT_TYPE_BROADCAST = 160,
DC_CHAT_TYPE_GROUP = 120,
DC_CHAT_TYPE_MAILINGLIST = 140,
DC_CHAT_TYPE_SINGLE = 100,
DC_CHAT_TYPE_UNDEFINED = 0,
DC_CONNECTIVITY_CONNECTED = 4000,
DC_CONNECTIVITY_CONNECTING = 2000,
DC_CONNECTIVITY_NOT_CONNECTED = 1000,
DC_CONNECTIVITY_WORKING = 3000,
DC_CONTACT_ID_DEVICE = 5,
DC_CONTACT_ID_INFO = 2,
DC_CONTACT_ID_LAST_SPECIAL = 9,
DC_CONTACT_ID_SELF = 1,
DC_GCL_ADD_ALLDONE_HINT = 4,
DC_GCL_ADD_SELF = 2,
DC_GCL_ARCHIVED_ONLY = 1,
DC_GCL_FOR_FORWARDING = 8,
DC_GCL_NO_SPECIALS = 2,
DC_GCL_VERIFIED_ONLY = 1,
DC_GCM_ADDDAYMARKER = 1,
DC_GCM_INFO_ONLY = 2,
DC_INFO_PROTECTION_DISABLED = 12,
DC_INFO_PROTECTION_ENABLED = 11,
DC_KEY_GEN_DEFAULT = 0,
DC_KEY_GEN_ED25519 = 2,
DC_KEY_GEN_RSA2048 = 1,
DC_LP_AUTH_NORMAL = 4,
DC_LP_AUTH_OAUTH2 = 2,
DC_MEDIA_QUALITY_BALANCED = 0,
DC_MEDIA_QUALITY_WORSE = 1,
DC_MSG_ID_DAYMARKER = 9,
DC_MSG_ID_LAST_SPECIAL = 9,
DC_MSG_ID_MARKER1 = 1,
DC_PROVIDER_STATUS_BROKEN = 3,
DC_PROVIDER_STATUS_OK = 1,
DC_PROVIDER_STATUS_PREPARATION = 2,
DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1,
DC_SHOW_EMAILS_ALL = 2,
DC_SHOW_EMAILS_OFF = 0,
DC_SOCKET_AUTO = 0,
DC_SOCKET_PLAIN = 3,
DC_SOCKET_SSL = 1,
DC_SOCKET_STARTTLS = 2,
DC_STATE_IN_FRESH = 10,
DC_STATE_IN_NOTICED = 13,
DC_STATE_IN_SEEN = 16,
DC_STATE_OUT_DELIVERED = 26,
DC_STATE_OUT_DRAFT = 19,
DC_STATE_OUT_FAILED = 24,
DC_STATE_OUT_MDN_RCVD = 28,
DC_STATE_OUT_PENDING = 20,
DC_STATE_OUT_PREPARING = 18,
DC_STATE_UNDEFINED = 0,
DC_STR_AC_SETUP_MSG_BODY = 43,
DC_STR_AC_SETUP_MSG_SUBJECT = 42,
DC_STR_ADD_MEMBER_BY_OTHER = 129,
DC_STR_ADD_MEMBER_BY_YOU = 128,
DC_STR_AEAP_ADDR_CHANGED = 122,
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
DC_STR_ARCHIVEDCHATS = 40,
DC_STR_AUDIO = 11,
DC_STR_BAD_TIME_MSG_BODY = 85,
DC_STR_BROADCAST_LIST = 115,
DC_STR_CANNOT_LOGIN = 60,
DC_STR_CANTDECRYPT_MSG_BODY = 29,
DC_STR_CONFIGURATION_FAILED = 84,
DC_STR_CONNECTED = 107,
DC_STR_CONNTECTING = 108,
DC_STR_CONTACT_NOT_VERIFIED = 36,
DC_STR_CONTACT_SETUP_CHANGED = 37,
DC_STR_CONTACT_VERIFIED = 35,
DC_STR_DEVICE_MESSAGES = 68,
DC_STR_DEVICE_MESSAGES_HINT = 70,
DC_STR_DOWNLOAD_AVAILABILITY = 100,
DC_STR_DRAFT = 3,
DC_STR_E2E_AVAILABLE = 25,
DC_STR_E2E_PREFERRED = 34,
DC_STR_ENCRYPTEDMSG = 24,
DC_STR_ENCR_NONE = 28,
DC_STR_ENCR_TRANSP = 27,
DC_STR_EPHEMERAL_DAY = 79,
DC_STR_EPHEMERAL_DAYS = 95,
DC_STR_EPHEMERAL_DISABLED = 75,
DC_STR_EPHEMERAL_FOUR_WEEKS = 81,
DC_STR_EPHEMERAL_HOUR = 78,
DC_STR_EPHEMERAL_HOURS = 94,
DC_STR_EPHEMERAL_MINUTE = 77,
DC_STR_EPHEMERAL_MINUTES = 93,
DC_STR_EPHEMERAL_SECONDS = 76,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER = 147,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU = 146,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER = 145,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU = 144,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER = 143,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU = 142,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER = 149,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU = 148,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER = 155,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU = 154,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER = 139,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU = 138,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER = 153,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU = 152,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER = 151,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU = 150,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER = 141,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU = 140,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER = 157,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU = 156,
DC_STR_EPHEMERAL_WEEK = 80,
DC_STR_EPHEMERAL_WEEKS = 96,
DC_STR_ERROR = 112,
DC_STR_ERROR_NO_NETWORK = 87,
DC_STR_FAILED_SENDING_TO = 74,
DC_STR_FILE = 12,
DC_STR_FINGERPRINTS = 30,
DC_STR_FORWARDED = 97,
DC_STR_GIF = 23,
DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER = 127,
DC_STR_GROUP_IMAGE_CHANGED_BY_YOU = 126,
DC_STR_GROUP_IMAGE_DELETED_BY_OTHER = 135,
DC_STR_GROUP_IMAGE_DELETED_BY_YOU = 134,
DC_STR_GROUP_LEFT_BY_OTHER = 133,
DC_STR_GROUP_LEFT_BY_YOU = 132,
DC_STR_GROUP_NAME_CHANGED_BY_OTHER = 125,
DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124,
DC_STR_IMAGE = 9,
DC_STR_INCOMING_MESSAGES = 103,
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
DC_STR_LOCATION = 66,
DC_STR_LOCATION_ENABLED_BY_OTHER = 137,
DC_STR_LOCATION_ENABLED_BY_YOU = 136,
DC_STR_MESSAGES = 114,
DC_STR_MSGACTIONBYME = 63,
DC_STR_MSGACTIONBYUSER = 62,
DC_STR_MSGADDMEMBER = 17,
DC_STR_MSGDELMEMBER = 18,
DC_STR_MSGGROUPLEFT = 19,
DC_STR_MSGGRPIMGCHANGED = 16,
DC_STR_MSGGRPIMGDELETED = 33,
DC_STR_MSGGRPNAME = 15,
DC_STR_MSGLOCATIONDISABLED = 65,
DC_STR_MSGLOCATIONENABLED = 64,
DC_STR_NOMESSAGES = 1,
DC_STR_NOT_CONNECTED = 121,
DC_STR_NOT_SUPPORTED_BY_PROVIDER = 113,
DC_STR_ONE_MOMENT = 106,
DC_STR_OUTGOING_MESSAGES = 104,
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
DC_STR_PART_OF_TOTAL_USED = 116,
DC_STR_PROTECTION_DISABLED = 89,
DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
DC_STR_PROTECTION_ENABLED = 88,
DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
DC_STR_REMOVE_MEMBER_BY_YOU = 130,
DC_STR_REPLY_NOUN = 90,
DC_STR_SAVED_MESSAGES = 69,
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
DC_STR_SECURE_JOIN_REPLIES = 118,
DC_STR_SECURE_JOIN_STARTED = 117,
DC_STR_SELF = 2,
DC_STR_SELF_DELETED_MSG_BODY = 91,
DC_STR_SENDING = 110,
DC_STR_SERVER_TURNED_OFF = 92,
DC_STR_SETUP_CONTACT_QR_DESC = 119,
DC_STR_STICKER = 67,
DC_STR_STORAGE_ON_DOMAIN = 105,
DC_STR_SUBJECT_FOR_NEW_CONTACT = 73,
DC_STR_SYNC_MSG_BODY = 102,
DC_STR_SYNC_MSG_SUBJECT = 101,
DC_STR_UNKNOWN_SENDER_FOR_CHAT = 72,
DC_STR_UPDATE_REMINDER_MSG_BODY = 86,
DC_STR_UPDATING = 109,
DC_STR_VIDEO = 10,
DC_STR_VIDEOCHAT_INVITATION = 82,
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
DC_STR_VOICEMESSAGE = 7,
DC_STR_WELCOME_MESSAGE = 71,
DC_TEXT1_DRAFT = 1,
DC_TEXT1_SELF = 3,
DC_TEXT1_USERNAME = 2,
DC_VIDEOCHATTYPE_BASICWEBRTC = 1,
DC_VIDEOCHATTYPE_JITSI = 2,
DC_VIDEOCHATTYPE_UNKNOWN = 0,
}
@@ -0,0 +1,855 @@
import * as T from "./types.js"
import { RawClient } from "./client.js"
import { TinyEmitter } from "tiny-emitter";
export class Context<T> extends TinyEmitter<T> {
constructor(
private controller: {rpc: RawClient},
readonly accountId: T.U32
){
super()
}
public removeAccount(): Promise<null> {
return this.controller.rpc.removeAccount(this.accountId)
}
/**
* Get top-level info for an account.
*/
public getAccountInfo(): Promise<T.Account> {
return this.controller.rpc.getAccountInfo(this.accountId)
}
/**
* Get the combined filesize of an account in bytes
*/
public getAccountFileSize(): Promise<T.U64> {
return this.controller.rpc.getAccountFileSize(this.accountId)
}
/**
* Returns provider for the given domain.
*
* This function looks up domain in offline database.
*
* For compatibility, email address can be passed to this function
* instead of the domain.
*/
public getProviderInfo(email: string): Promise<(T.ProviderInfo|null)> {
return this.controller.rpc.getProviderInfo(this.accountId, email)
}
/**
* Checks if the context is already configured.
*/
public isConfigured(): Promise<boolean> {
return this.controller.rpc.isConfigured(this.accountId)
}
/**
* Get system info for an account.
*/
public getInfo(): Promise<Record<string,string>> {
return this.controller.rpc.getInfo(this.accountId)
}
public setConfig(key: string, value: (string|null)): Promise<null> {
return this.controller.rpc.setConfig(this.accountId, key, value)
}
public batchSetConfig(config: Record<string,(string|null)>): Promise<null> {
return this.controller.rpc.batchSetConfig(this.accountId, config)
}
/**
* Set configuration values from a QR code. (technically from the URI that is stored in the qrcode)
* Before this function is called, `checkQr()` should confirm the type of the
* QR code is `account` or `webrtcInstance`.
*
* Internally, the function will call dc_set_config() with the appropriate keys,
*/
public setConfigFromQr(qrContent: string): Promise<null> {
return this.controller.rpc.setConfigFromQr(this.accountId, qrContent)
}
public checkQr(qrContent: string): Promise<T.Qr> {
return this.controller.rpc.checkQr(this.accountId, qrContent)
}
public getConfig(key: string): Promise<(string|null)> {
return this.controller.rpc.getConfig(this.accountId, key)
}
public batchGetConfig(keys: (string)[]): Promise<Record<string,(string|null)>> {
return this.controller.rpc.batchGetConfig(this.accountId, keys)
}
/**
* Configures this account with the currently set parameters.
* Setup the credential config before calling this.
*/
public configure(): Promise<null> {
return this.controller.rpc.configure(this.accountId)
}
/**
* Signal an ongoing process to stop.
*/
public stopOngoingProcess(): Promise<null> {
return this.controller.rpc.stopOngoingProcess(this.accountId)
}
public exportSelfKeys(path: string, passphrase: (string|null)): Promise<null> {
return this.controller.rpc.exportSelfKeys(this.accountId, path, passphrase)
}
public importSelfKeys(path: string, passphrase: (string|null)): Promise<null> {
return this.controller.rpc.importSelfKeys(this.accountId, path, passphrase)
}
/**
* Returns the message IDs of all _fresh_ messages of any chat.
* Typically used for implementing notification summaries
* or badge counters e.g. on the app icon.
* The list is already sorted and starts with the most recent fresh message.
*
* Messages belonging to muted chats or to the contact requests are not returned;
* these messages should not be notified
* and also badge counters should not include these messages.
*
* To get the number of fresh messages for a single chat, muted or not,
* use `get_fresh_msg_cnt()`.
*/
public getFreshMsgs(): Promise<(T.U32)[]> {
return this.controller.rpc.getFreshMsgs(this.accountId)
}
/**
* Get the number of _fresh_ messages in a chat.
* Typically used to implement a badge with a number in the chatlist.
*
* If the specified chat is muted,
* the UI should show the badge counter "less obtrusive",
* e.g. using "gray" instead of "red" color.
*/
public getFreshMsgCnt(chatId: T.U32): Promise<T.Usize> {
return this.controller.rpc.getFreshMsgCnt(this.accountId, chatId)
}
/**
* Estimate the number of messages that will be deleted
* by the set_config()-options `delete_device_after` or `delete_server_after`.
* This is typically used to show the estimated impact to the user
* before actually enabling deletion of old messages.
*/
public estimateAutoDeletionCount(fromServer: boolean, seconds: T.I64): Promise<T.Usize> {
return this.controller.rpc.estimateAutoDeletionCount(this.accountId, fromServer, seconds)
}
public autocryptInitiateKeyTransfer(): Promise<string> {
return this.controller.rpc.autocryptInitiateKeyTransfer(this.accountId)
}
public autocryptContinueKeyTransfer(messageId: T.U32, setupCode: string): Promise<null> {
return this.controller.rpc.autocryptContinueKeyTransfer(this.accountId, messageId, setupCode)
}
public getChatlistEntries(listFlags: (T.U32|null), queryString: (string|null), queryContactId: (T.U32|null)): Promise<(T.ChatListEntry)[]> {
return this.controller.rpc.getChatlistEntries(this.accountId, listFlags, queryString, queryContactId)
}
public getChatlistItemsByEntries(entries: (T.ChatListEntry)[]): Promise<Record<T.U32,T.ChatListItemFetchResult>> {
return this.controller.rpc.getChatlistItemsByEntries(this.accountId, entries)
}
public chatlistGetFullChatById(chatId: T.U32): Promise<T.FullChat> {
return this.controller.rpc.chatlistGetFullChatById(this.accountId, chatId)
}
/**
* get basic info about a chat,
* use chatlist_get_full_chat_by_id() instead if you need more information
*/
public getBasicChatInfo(chatId: T.U32): Promise<T.BasicChat> {
return this.controller.rpc.getBasicChatInfo(this.accountId, chatId)
}
public acceptChat(chatId: T.U32): Promise<null> {
return this.controller.rpc.acceptChat(this.accountId, chatId)
}
public blockChat(chatId: T.U32): Promise<null> {
return this.controller.rpc.blockChat(this.accountId, chatId)
}
/**
* Delete a chat.
*
* Messages are deleted from the device and the chat database entry is deleted.
* After that, the event #DC_EVENT_MSGS_CHANGED is posted.
*
* Things that are _not done_ implicitly:
*
* - Messages are **not deleted from the server**.
* - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
* and the user may create the chat again.
* - **Groups are not left** - this would
* be unexpected as (1) deleting a normal chat also does not prevent new mails
* from arriving, (2) leaving a group requires sending a message to
* all group members - especially for groups not used for a longer time, this is
* really unexpected when deletion results in contacting all members again,
* (3) only leaving groups is also a valid usecase.
*
* To leave a chat explicitly, use leave_group()
*/
public deleteChat(chatId: T.U32): Promise<null> {
return this.controller.rpc.deleteChat(this.accountId, chatId)
}
/**
* Get encryption info for a chat.
* Get a multi-line encryption info, containing encryption preferences of all members.
* Can be used to find out why messages sent to group are not encrypted.
*
* returns Multi-line text
*/
public getChatEncryptionInfo(chatId: T.U32): Promise<string> {
return this.controller.rpc.getChatEncryptionInfo(this.accountId, chatId)
}
/**
* Get QR code (text and SVG) that will offer an Setup-Contact or Verified-Group invitation.
* The QR code is compatible to the OPENPGP4FPR format
* so that a basic fingerprint comparison also works e.g. with OpenKeychain.
*
* The scanning device will pass the scanned content to `checkQr()` then;
* if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
* an out-of-band-verification can be joined using `secure_join()`
*
* chat_id: If set to a group-chat-id,
* the Verified-Group-Invite protocol is offered in the QR code;
* works for protected groups as well as for normal groups.
* If not set, the Setup-Contact protocol is offered in the QR code.
* See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols.
*
* return format: `[code, svg]`
*/
public getChatSecurejoinQrCodeSvg(chatId: (T.U32|null)): Promise<[string,string]> {
return this.controller.rpc.getChatSecurejoinQrCodeSvg(this.accountId, chatId)
}
/**
* Continue a Setup-Contact or Verified-Group-Invite protocol
* started on another device with `get_chat_securejoin_qr_code_svg()`.
* This function is typically called when `check_qr()` returns
* type=AskVerifyContact or type=AskVerifyGroup.
*
* The function returns immediately and the handshake runs in background,
* sending and receiving several messages.
* During the handshake, info messages are added to the chat,
* showing progress, success or errors.
*
* Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
*
* See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols.
*
* **qr**: The text of the scanned QR code. Typically, the same string as given
* to `check_qr()`.
*
* **returns**: The chat ID of the joined chat, the UI may redirect to the this chat.
* A returned chat ID does not guarantee that the chat is protected or the belonging contact is verified.
*
*/
public secureJoin(qr: string): Promise<T.U32> {
return this.controller.rpc.secureJoin(this.accountId, qr)
}
public leaveGroup(chatId: T.U32): Promise<null> {
return this.controller.rpc.leaveGroup(this.accountId, chatId)
}
/**
* Remove a member from a group.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*/
public removeContactFromChat(chatId: T.U32, contactId: T.U32): Promise<null> {
return this.controller.rpc.removeContactFromChat(this.accountId, chatId, contactId)
}
/**
* Add a member to a group.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* If the group has group protection enabled, only verified contacts can be added to the group.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*/
public addContactToChat(chatId: T.U32, contactId: T.U32): Promise<null> {
return this.controller.rpc.addContactToChat(this.accountId, chatId, contactId)
}
/**
* Get the contact IDs belonging to a chat.
*
* - for normal chats, the function always returns exactly one contact,
* DC_CONTACT_ID_SELF is returned only for SELF-chats.
*
* - for group chats all members are returned, DC_CONTACT_ID_SELF is returned
* explicitly as it may happen that oneself gets removed from a still existing
* group
*
* - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included
*
* - for mailing lists, the behavior is not documented currently, we will decide on that later.
* for now, the UI should not show the list for mailing lists.
* (we do not know all members and there is not always a global mailing list address,
* so we could return only SELF or the known members; this is not decided yet)
*/
public getChatContacts(chatId: T.U32): Promise<(T.U32)[]> {
return this.controller.rpc.getChatContacts(this.accountId, chatId)
}
/**
* Create a new group chat.
*
* After creation,
* the group has one member with the ID DC_CONTACT_ID_SELF
* and is in _unpromoted_ state.
* This means, you can add or remove members, change the name,
* the group image and so on without messages being sent to all group members.
*
* This changes as soon as the first message is sent to the group members
* and the group becomes _promoted_.
* After that, all changes are synced with all group members
* by sending status message.
*
* To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of `BasicChat` or `FullChat`.
* This may be useful if you want to show some help for just created groups.
*
* @param protect If set to 1 the function creates group with protection initially enabled.
* Only verified members are allowed in these groups
* and end-to-end-encryption is always enabled.
*/
public createGroupChat(name: string, protect: boolean): Promise<T.U32> {
return this.controller.rpc.createGroupChat(this.accountId, name, protect)
}
/**
* Create a new broadcast list.
*
* Broadcast lists are similar to groups on the sending device,
* however, recipients get the messages in normal one-to-one chats
* and will not be aware of other members.
*
* Replies to broadcasts go only to the sender
* and not to all broadcast recipients.
* Moreover, replies will not appear in the broadcast list
* but in the one-to-one chat with the person answering.
*
* The name and the image of the broadcast list is set automatically
* and is visible to the sender only.
* Not asking for these data allows more focused creation
* and we bypass the question who will get which data.
* Also, many users will have at most one broadcast list
* so, a generic name and image is sufficient at the first place.
*
* Later on, however, the name can be changed using dc_set_chat_name().
* The image cannot be changed to have a unique, recognizable icon in the chat lists.
* All in all, this is also what other messengers are doing here.
*/
public createBroadcastList(): Promise<T.U32> {
return this.controller.rpc.createBroadcastList(this.accountId)
}
/**
* Set group name.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*/
public setChatName(chatId: T.U32, newName: string): Promise<null> {
return this.controller.rpc.setChatName(this.accountId, chatId, newName)
}
/**
* Set group profile image.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*
* To find out the profile image of a chat, use dc_chat_get_profile_image()
*
* @param image_path Full path of the image to use as the group image. The image will immediately be copied to the
* `blobdir`; the original image will not be needed anymore.
* If you pass null here, the group image is deleted (for promoted groups, all members are informed about
* this change anyway).
*/
public setChatProfileImage(chatId: T.U32, imagePath: (string|null)): Promise<null> {
return this.controller.rpc.setChatProfileImage(this.accountId, chatId, imagePath)
}
public setChatVisibility(chatId: T.U32, visibility: T.ChatVisibility): Promise<null> {
return this.controller.rpc.setChatVisibility(this.accountId, chatId, visibility)
}
public setChatEphemeralTimer(chatId: T.U32, timer: T.U32): Promise<null> {
return this.controller.rpc.setChatEphemeralTimer(this.accountId, chatId, timer)
}
public getChatEphemeralTimer(chatId: T.U32): Promise<T.U32> {
return this.controller.rpc.getChatEphemeralTimer(this.accountId, chatId)
}
public addDeviceMessage(label: string, text: string): Promise<T.U32> {
return this.controller.rpc.addDeviceMessage(this.accountId, label, text)
}
/**
* Mark all messages in a chat as _noticed_.
* _Noticed_ messages are no longer _fresh_ and do not count as being unseen
* but are still waiting for being marked as "seen" using markseen_msgs()
* (IMAP/MDNs is not done for noticed messages).
*
* Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
* See also markseen_msgs().
*/
public marknoticedChat(chatId: T.U32): Promise<null> {
return this.controller.rpc.marknoticedChat(this.accountId, chatId)
}
public getFirstUnreadMessageOfChat(chatId: T.U32): Promise<(T.U32|null)> {
return this.controller.rpc.getFirstUnreadMessageOfChat(this.accountId, chatId)
}
/**
* Set mute duration of a chat.
*
* The UI can then call is_chat_muted() when receiving a new message
* to decide whether it should trigger an notification.
*
* Muted chats should not sound or vibrate
* and should not show a visual notification in the system area.
* Moreover, muted chats should be excluded from global badge counter
* (get_fresh_msgs() skips muted chats therefore)
* and the in-app, per-chat badge counter should use a less obtrusive color.
*
* Sends out #DC_EVENT_CHAT_MODIFIED.
*/
public setChatMuteDuration(chatId: T.U32, duration: T.MuteDuration): Promise<null> {
return this.controller.rpc.setChatMuteDuration(this.accountId, chatId, duration)
}
/**
* Check whether the chat is currently muted (can be changed by set_chat_mute_duration()).
*
* This is available as a standalone function outside of fullchat, because it might be only needed for notification
*/
public isChatMuted(chatId: T.U32): Promise<boolean> {
return this.controller.rpc.isChatMuted(this.accountId, chatId)
}
/**
* Mark messages as presented to the user.
* Typically, UIs call this function on scrolling through the message list,
* when the messages are presented at least for a little moment.
* The concrete action depends on the type of the chat and on the users settings
* (dc_msgs_presented() may be a better name therefore, but well. :)
*
* - For normal chats, the IMAP state is updated, MDN is sent
* (if set_config()-options `mdns_enabled` is set)
* and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
*
* - For contact requests, no IMAP or MDNs is done
* and the internal state is not changed therefore.
* See also marknoticed_chat().
*
* Moreover, timer is started for incoming ephemeral messages.
* This also happens for contact requests chats.
*
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
*/
public markseenMsgs(msgIds: (T.U32)[]): Promise<null> {
return this.controller.rpc.markseenMsgs(this.accountId, msgIds)
}
public getMessageIds(chatId: T.U32, flags: T.U32): Promise<(T.U32)[]> {
return this.controller.rpc.getMessageIds(this.accountId, chatId, flags)
}
public getMessageListItems(chatId: T.U32, flags: T.U32): Promise<(T.MessageListItem)[]> {
return this.controller.rpc.getMessageListItems(this.accountId, chatId, flags)
}
public messageGetMessage(messageId: T.U32): Promise<T.Message> {
return this.controller.rpc.messageGetMessage(this.accountId, messageId)
}
public getMessageHtml(messageId: T.U32): Promise<(string|null)> {
return this.controller.rpc.getMessageHtml(this.accountId, messageId)
}
public messageGetMessages(messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
return this.controller.rpc.messageGetMessages(this.accountId, messageIds)
}
/**
* Fetch info desktop needs for creating a notification for a message
*/
public messageGetNotificationInfo(messageId: T.U32): Promise<T.MessageNotificationInfo> {
return this.controller.rpc.messageGetNotificationInfo(this.accountId, messageId)
}
/**
* Delete messages. The messages are deleted on the current device and
* on the IMAP server.
*/
public deleteMessages(messageIds: (T.U32)[]): Promise<null> {
return this.controller.rpc.deleteMessages(this.accountId, messageIds)
}
/**
* Get an informational text for a single message. The text is multiline and may
* contain e.g. the raw text of the message.
*
* The max. text returned is typically longer (about 100000 characters) than the
* max. text returned by dc_msg_get_text() (about 30000 characters).
*/
public getMessageInfo(messageId: T.U32): Promise<string> {
return this.controller.rpc.getMessageInfo(this.accountId, messageId)
}
/**
* Asks the core to start downloading a message fully.
* This function is typically called when the user hits the "Download" button
* that is shown by the UI in case `download_state` is `'Available'` or `'Failure'`
*
* On success, the @ref DC_MSG "view type of the message" may change
* or the message may be replaced completely by one or more messages with other message IDs.
* That may happen e.g. in cases where the message was encrypted
* and the type could not be determined without fully downloading.
* Downloaded content can be accessed as usual after download.
*
* To reflect these changes a @ref DC_EVENT_MSGS_CHANGED event will be emitted.
*/
public downloadFullMessage(messageId: T.U32): Promise<null> {
return this.controller.rpc.downloadFullMessage(this.accountId, messageId)
}
/**
* Search messages containing the given query string.
* Searching can be done globally (chat_id=0) or in a specified chat only (chat_id set).
*
* Global chat results are typically displayed using dc_msg_get_summary(), chat
* search results may just hilite the corresponding messages and present a
* prev/next button.
*
* For global search, result is limited to 1000 messages,
* this allows incremental search done fast.
* So, when getting exactly 1000 results, the result may be truncated;
* the UIs may display sth. as "1000+ messages found" in this case.
* Chat search (if a chat_id is set) is not limited.
*/
public searchMessages(query: string, chatId: (T.U32|null)): Promise<(T.U32)[]> {
return this.controller.rpc.searchMessages(this.accountId, query, chatId)
}
public messageIdsToSearchResults(messageIds: (T.U32)[]): Promise<Record<T.U32,T.MessageSearchResult>> {
return this.controller.rpc.messageIdsToSearchResults(this.accountId, messageIds)
}
/**
* Get a single contact options by ID.
*/
public contactsGetContact(contactId: T.U32): Promise<T.Contact> {
return this.controller.rpc.contactsGetContact(this.accountId, contactId)
}
/**
* Add a single contact as a result of an explicit user action.
*
* Returns contact id of the created or existing contact
*/
public contactsCreateContact(email: string, name: (string|null)): Promise<T.U32> {
return this.controller.rpc.contactsCreateContact(this.accountId, email, name)
}
/**
* Returns contact id of the created or existing DM chat with that contact
*/
public contactsCreateChatByContactId(contactId: T.U32): Promise<T.U32> {
return this.controller.rpc.contactsCreateChatByContactId(this.accountId, contactId)
}
public contactsBlock(contactId: T.U32): Promise<null> {
return this.controller.rpc.contactsBlock(this.accountId, contactId)
}
public contactsUnblock(contactId: T.U32): Promise<null> {
return this.controller.rpc.contactsUnblock(this.accountId, contactId)
}
public contactsGetBlocked(): Promise<(T.Contact)[]> {
return this.controller.rpc.contactsGetBlocked(this.accountId)
}
public contactsGetContactIds(listFlags: T.U32, query: (string|null)): Promise<(T.U32)[]> {
return this.controller.rpc.contactsGetContactIds(this.accountId, listFlags, query)
}
/**
* Get a list of contacts.
* (formerly called getContacts2 in desktop)
*/
public contactsGetContacts(listFlags: T.U32, query: (string|null)): Promise<(T.Contact)[]> {
return this.controller.rpc.contactsGetContacts(this.accountId, listFlags, query)
}
public contactsGetContactsByIds(ids: (T.U32)[]): Promise<Record<T.U32,T.Contact>> {
return this.controller.rpc.contactsGetContactsByIds(this.accountId, ids)
}
public deleteContact(contactId: T.U32): Promise<boolean> {
return this.controller.rpc.deleteContact(this.accountId, contactId)
}
public changeContactName(contactId: T.U32, name: string): Promise<null> {
return this.controller.rpc.changeContactName(this.accountId, contactId, name)
}
/**
* Get encryption info for a contact.
* Get a multi-line encryption info, containing your fingerprint and the
* fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
*/
public getContactEncryptionInfo(contactId: T.U32): Promise<string> {
return this.controller.rpc.getContactEncryptionInfo(this.accountId, contactId)
}
/**
* Check if an e-mail address belongs to a known and unblocked contact.
* To get a list of all known and unblocked contacts, use contacts_get_contacts().
*
* To validate an e-mail address independently of the contact database
* use check_email_validity().
*/
public lookupContactIdByAddr(addr: string): Promise<(T.U32|null)> {
return this.controller.rpc.lookupContactIdByAddr(this.accountId, addr)
}
/**
* Returns all message IDs of the given types in a chat.
* Typically used to show a gallery.
*
* The list is already sorted and starts with the oldest message.
* Clients should not try to re-sort the list as this would be an expensive action
* and would result in inconsistencies between clients.
*
* Setting `chat_id` to `None` (`null` in typescript) means get messages with media
* from any chat of the currently used account.
*/
public chatGetMedia(chatId: (T.U32|null), messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<(T.U32)[]> {
return this.controller.rpc.chatGetMedia(this.accountId, chatId, messageType, orMessageType2, orMessageType3)
}
/**
* Search next/previous message based on a given message and a list of types.
* Typically used to implement the "next" and "previous" buttons
* in a gallery or in a media player.
*
* one combined call for getting chat::get_next_media for both directions
* the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
*/
public chatGetNeighboringMedia(msgId: T.U32, messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<[(T.U32|null),(T.U32|null)]> {
return this.controller.rpc.chatGetNeighboringMedia(this.accountId, msgId, messageType, orMessageType2, orMessageType3)
}
public exportBackup(destination: string, passphrase: (string|null)): Promise<null> {
return this.controller.rpc.exportBackup(this.accountId, destination, passphrase)
}
public importBackup(path: string, passphrase: (string|null)): Promise<null> {
return this.controller.rpc.importBackup(this.accountId, path, passphrase)
}
/**
* Get the current connectivity, i.e. whether the device is connected to the IMAP server.
* One of:
* - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
* - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
* - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
* - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
*
* We don't use exact values but ranges here so that we can split up
* states into multiple states in the future.
*
* Meant as a rough overview that can be shown
* e.g. in the title of the main screen.
*
* If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
*/
public getConnectivity(): Promise<T.U32> {
return this.controller.rpc.getConnectivity(this.accountId)
}
/**
* Get an overview of the current connectivity, and possibly more statistics.
* Meant to give the user more insight about the current status than
* the basic connectivity info returned by get_connectivity(); show this
* e.g., if the user taps on said basic connectivity info.
*
* If this page changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
*
* This comes as an HTML from the core so that we can easily improve it
* and the improvement instantly reaches all UIs.
*/
public getConnectivityHtml(): Promise<string> {
return this.controller.rpc.getConnectivityHtml(this.accountId)
}
public getLocations(chatId: (T.U32|null), contactId: (T.U32|null), timestampBegin: T.I64, timestampEnd: T.I64): Promise<(T.Location)[]> {
return this.controller.rpc.getLocations(this.accountId, chatId, contactId, timestampBegin, timestampEnd)
}
public webxdcSendStatusUpdate(instanceMsgId: T.U32, updateStr: string, description: string): Promise<null> {
return this.controller.rpc.webxdcSendStatusUpdate(this.accountId, instanceMsgId, updateStr, description)
}
public webxdcGetStatusUpdates(instanceMsgId: T.U32, lastKnownSerial: T.U32): Promise<string> {
return this.controller.rpc.webxdcGetStatusUpdates(this.accountId, instanceMsgId, lastKnownSerial)
}
/**
* Get info from a webxdc message
*/
public messageGetWebxdcInfo(instanceMsgId: T.U32): Promise<T.WebxdcMessageInfo> {
return this.controller.rpc.messageGetWebxdcInfo(this.accountId, instanceMsgId)
}
/**
* Forward messages to another chat.
*
* All types of messages can be forwarded,
* however, they will be flagged as such (dc_msg_is_forwarded() is set).
*
* Original sender, info-state and webxdc updates are not forwarded on purpose.
*/
public forwardMessages(messageIds: (T.U32)[], chatId: T.U32): Promise<null> {
return this.controller.rpc.forwardMessages(this.accountId, messageIds, chatId)
}
public sendSticker(chatId: T.U32, stickerPath: string): Promise<T.U32> {
return this.controller.rpc.sendSticker(this.accountId, chatId, stickerPath)
}
/**
* Send a reaction to message.
*
* Reaction is a string of emojis separated by spaces. Reaction to a
* single message can be sent multiple times. The last reaction
* received overrides all previously received reactions. It is
* possible to remove all reactions by sending an empty string.
*/
public sendReaction(messageId: T.U32, reaction: (string)[]): Promise<T.U32> {
return this.controller.rpc.sendReaction(this.accountId, messageId, reaction)
}
public removeDraft(chatId: T.U32): Promise<null> {
return this.controller.rpc.removeDraft(this.accountId, chatId)
}
/**
* Get draft for a chat, if any.
*/
public getDraft(chatId: T.U32): Promise<(T.Message|null)> {
return this.controller.rpc.getDraft(this.accountId, chatId)
}
public sendVideochatInvitation(chatId: T.U32): Promise<T.U32> {
return this.controller.rpc.sendVideochatInvitation(this.accountId, chatId)
}
public miscGetStickerFolder(): Promise<string> {
return this.controller.rpc.miscGetStickerFolder(this.accountId)
}
/**
* for desktop, get stickers from stickers folder,
* grouped by the folder they are in.
*/
public miscGetStickers(): Promise<Record<string,(string)[]>> {
return this.controller.rpc.miscGetStickers(this.accountId)
}
/**
* Returns the messageid of the sent message
*/
public miscSendTextMessage(text: string, chatId: T.U32): Promise<T.U32> {
return this.controller.rpc.miscSendTextMessage(this.accountId, text, chatId)
}
public miscSendMsg(chatId: T.U32, text: (string|null), file: (string|null), location: ([T.F64,T.F64]|null), quotedMessageId: (T.U32|null)): Promise<[T.U32,T.Message]> {
return this.controller.rpc.miscSendMsg(this.accountId, chatId, text, file, location, quotedMessageId)
}
public miscSetDraft(chatId: T.U32, text: (string|null), file: (string|null), quotedMessageId: (T.U32|null)): Promise<null> {
return this.controller.rpc.miscSetDraft(this.accountId, chatId, text, file, quotedMessageId)
}
}
@@ -77,6 +77,10 @@ export type Event=(({
* `msgId` is set if only a single message is affected by the changes, otherwise 0.
*/
"type":"MsgsChanged";}&{"chatId":U32;"msgId":U32;})|({
/**
* Reactions for the message changed.
*/
"type":"ReactionsChanged";}&{"chatId":U32;"msgId":U32;"contactId":U32;})|({
/**
* There is a fresh message. Typically, the user will show an notification
* when receiving this message.
+81 -64
View File
@@ -1,67 +1,6 @@
// AUTO-GENERATED by typescript-type-def
export type U32=number;
export type Account=(({"type":"Configured";}&{"id":U32;"displayName":(string|null);"addr":(string|null);"profileImage":(string|null);"color":string;})|({"type":"Unconfigured";}&{"id":U32;}));
export type U64=number;
export type ProviderInfo={"beforeLoginHint":string;"overviewPage":string;"status":U32;};
export type Qr=(({"type":"askVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"askVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"fprOk";}&{"contact_id":U32;})|({"type":"fprMismatch";}&{"contact_id":(U32|null);})|({"type":"fprWithoutAddr";}&{"fingerprint":string;})|({"type":"account";}&{"domain":string;})|({"type":"webrtcInstance";}&{"domain":string;"instance_pattern":string;})|({"type":"addr";}&{"contact_id":U32;"draft":(string|null);})|({"type":"url";}&{"url":string;})|({"type":"text";}&{"text":string;})|({"type":"withdrawVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"withdrawVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"login";}&{"address":string;}));
export type Usize=number;
export type I64=number;
export type ChatListEntry=[U32,U32];
export type ChatListItemFetchResult=(({"type":"ChatListItem";}&{"id":U32;"name":string;"avatarPath":(string|null);"color":string;"lastUpdated":(I64|null);"summaryText1":string;"summaryText2":string;"summaryStatus":U32;"isProtected":boolean;"isGroup":boolean;"freshMessageCounter":Usize;"isSelfTalk":boolean;"isDeviceTalk":boolean;"isSendingLocation":boolean;"isSelfInGroup":boolean;"isArchived":boolean;"isPinned":boolean;"isMuted":boolean;"isContactRequest":boolean;
/**
* true when chat is a broadcastlist
*/
"isBroadcast":boolean;
/**
* contact id if this is a dm chat (for view profile entry in context menu)
*/
"dmChatContact":(U32|null);"wasSeenRecently":boolean;})|{"type":"ArchiveLink";}|({"type":"Error";}&{"id":U32;"error":string;}));
export type Contact={"address":string;"color":string;"authName":string;"status":string;"displayName":string;"id":U32;"name":string;"profileImage":(string|null);"nameAndAddr":string;"isBlocked":boolean;"isVerified":boolean;
/**
* the contact's last seen timestamp
*/
"lastSeen":I64;"wasSeenRecently":boolean;};
export type FullChat={"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"contacts":(Contact)[];"contactIds":(U32)[];"color":string;"freshMessageCounter":Usize;"isContactRequest":boolean;"isDeviceChat":boolean;"selfInGroup":boolean;"isMuted":boolean;"ephemeralTimer":U32;"canSend":boolean;"wasSeenRecently":boolean;"mailingListAddress":string;};
/**
* cheaper version of fullchat, omits:
* - contacts
* - contact_ids
* - fresh_message_counter
* - ephemeral_timer
* - self_in_group
* - was_seen_recently
* - can_send
*
* used when you only need the basic metadata of a chat like type, name, profile picture
*/
export type BasicChat=
/**
* cheaper version of fullchat, omits:
* - contacts
* - contact_ids
* - fresh_message_counter
* - ephemeral_timer
* - self_in_group
* - was_seen_recently
* - can_send
*
* used when you only need the basic metadata of a chat like type, name, profile picture
*/
{"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"color":string;"isContactRequest":boolean;"isDeviceChat":boolean;"isMuted":boolean;};
export type ChatVisibility=("Normal"|"Archived"|"Pinned");
export type MuteDuration=("NotMuted"|"Forever"|{"Until":I64;});
export type MessageListItem=(({"kind":"message";}&{"msg_id":U32;})|({
/**
* Day marker, separating messages that correspond to different
* days according to local time.
*/
"kind":"dayMarker";}&{
/**
* Marker timestamp, for day markers, in unix milliseconds
*/
"timestamp":I64;}));
export type Viewtype=("Unknown"|
/**
* Text message.
@@ -108,7 +47,14 @@ export type Viewtype=("Unknown"|
*/
"Webxdc");
export type MessageQuote=(({"kind":"JustText";}&{"text":string;})|({"kind":"WithMessage";}&{"text":string;"messageId":U32;"authorDisplayName":string;"authorDisplayColor":string;"overrideSenderName":(string|null);"image":(string|null);"isForwarded":boolean;"viewType":Viewtype;}));
export type I64=number;
export type I32=number;
export type Contact={"address":string;"color":string;"authName":string;"status":string;"displayName":string;"id":U32;"name":string;"profileImage":(string|null);"nameAndAddr":string;"isBlocked":boolean;"isVerified":boolean;
/**
* the contact's last seen timestamp
*/
"lastSeen":I64;"wasSeenRecently":boolean;};
export type U64=number;
export type WebxdcMessageInfo={
/**
* The name of the app.
@@ -147,7 +93,65 @@ export type WebxdcMessageInfo={
*/
"internetAccess":boolean;};
export type DownloadState=("Done"|"Available"|"Failure"|"InProgress");
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;};
/**
* Structure representing all reactions to a particular message.
*/
export type Reactions=
/**
* Structure representing all reactions to a particular message.
*/
{
/**
* Map from a contact to it's reaction to message.
*/
"reactionsByContact":Record<U32,(string)[]>;
/**
* Unique reactions and their count
*/
"reactions":Record<string,U32>;};
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;"reactions":(Reactions|null);};
export type Account=(({"type":"Configured";}&{"id":U32;"displayName":(string|null);"addr":(string|null);"profileImage":(string|null);"color":string;})|({"type":"Unconfigured";}&{"id":U32;}));
/**
* cheaper version of fullchat, omits:
* - contacts
* - contact_ids
* - fresh_message_counter
* - ephemeral_timer
* - self_in_group
* - was_seen_recently
* - can_send
*
* used when you only need the basic metadata of a chat like type, name, profile picture
*/
export type BasicChat=
/**
* cheaper version of fullchat, omits:
* - contacts
* - contact_ids
* - fresh_message_counter
* - ephemeral_timer
* - self_in_group
* - was_seen_recently
* - can_send
*
* used when you only need the basic metadata of a chat like type, name, profile picture
*/
{"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"color":string;"isContactRequest":boolean;"isDeviceChat":boolean;"isMuted":boolean;};
export type Usize=number;
export type FullChat={"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"contacts":(Contact)[];"contactIds":(U32)[];"color":string;"freshMessageCounter":Usize;"isContactRequest":boolean;"isDeviceChat":boolean;"selfInGroup":boolean;"isMuted":boolean;"ephemeralTimer":U32;"canSend":boolean;"wasSeenRecently":boolean;"mailingListAddress":string;};
export type ChatListItemFetchResult=(({"type":"ChatListItem";}&{"id":U32;"name":string;"avatarPath":(string|null);"color":string;"lastUpdated":(I64|null);"summaryText1":string;"summaryText2":string;"summaryStatus":U32;"isProtected":boolean;"isGroup":boolean;"freshMessageCounter":Usize;"isSelfTalk":boolean;"isDeviceTalk":boolean;"isSendingLocation":boolean;"isSelfInGroup":boolean;"isArchived":boolean;"isPinned":boolean;"isMuted":boolean;"isContactRequest":boolean;
/**
* true when chat is a broadcastlist
*/
"isBroadcast":boolean;
/**
* contact id if this is a dm chat (for view profile entry in context menu)
*/
"dmChatContact":(U32|null);"wasSeenRecently":boolean;})|{"type":"ArchiveLink";}|({"type":"Error";}&{"id":U32;"error":string;}));
export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
export type ChatVisibility=("Normal"|"Archived"|"Pinned");
export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"image":(string|null);"imageMimeType":(string|null);"chatName":string;"chatProfileImage":(string|null);
/**
* also known as summary_text1
@@ -157,7 +161,20 @@ export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"imag
* also known as summary_text2
*/
"summaryText":string;};
export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
export type MuteDuration=("NotMuted"|"Forever"|{"Until":I64;});
export type F64=number;
export type ProviderInfo={"beforeLoginHint":string;"overviewPage":string;"status":U32;};
export type Qr=(({"type":"askVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"askVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"fprOk";}&{"contact_id":U32;})|({"type":"fprMismatch";}&{"contact_id":(U32|null);})|({"type":"fprWithoutAddr";}&{"fingerprint":string;})|({"type":"account";}&{"domain":string;})|({"type":"webrtcInstance";}&{"domain":string;"instance_pattern":string;})|({"type":"addr";}&{"contact_id":U32;"draft":(string|null);})|({"type":"url";}&{"url":string;})|({"type":"text";}&{"text":string;})|({"type":"withdrawVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"withdrawVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"login";}&{"address":string;}));
export type ChatListEntry=[U32,U32];
export type MessageListItem=(({"kind":"message";}&{"msg_id":U32;})|({
/**
* Day marker, separating messages that correspond to different
* days according to local time.
*/
"kind":"dayMarker";}&{
/**
* Marker timestamp, for day markers, in unix milliseconds
*/
"timestamp":I64;}));
export type Location={"locationId":U32;"isIndependent":boolean;"latitude":F64;"longitude":F64;"accuracy":F64;"timestamp":I64;"contactId":U32;"msgId":U32;"chatId":U32;"marker":(string|null);};
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,U32,(string|null),U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,Record<string,(string)[]>,U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];
export type __AllTyps=[null,[(U32|null),(U32|null)],[string,string],[U32,Message],Account,Record<string,string>,BasicChat,Contact,FullChat,Record<string,(string|null)>,Record<string,(string)[]>,Record<U32,ChatListItemFetchResult>,Record<U32,Contact>,Record<U32,Message>,Record<U32,MessageSearchResult>,Record<U32,string>,ChatVisibility,MessageNotificationInfo,Message,Viewtype,MuteDuration,([F64,F64]|null),(Message|null),(Viewtype|null),(ProviderInfo|null),(string|null),(U32|null),Qr,string,(Account)[],(ChatListEntry)[],(Contact)[],(MessageListItem)[],(Location)[],(string)[],(U32)[],WebxdcMessageInfo,boolean,I64,U32,U64,Usize];
+3 -2
View File
@@ -28,7 +28,7 @@
"main": "dist/deltachat.js",
"name": "@deltachat/jsonrpc-client",
"scripts": {
"build": "run-s generate-bindings build:tsc build:bundle",
"build": "run-s generate-bindings extract-constants build:tsc build:bundle",
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
"build:tsc": "tsc",
"docs": "typedoc --out docs deltachat.ts",
@@ -36,6 +36,7 @@
"example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
"example:start": "http-server .",
"extract-constants": "node ./scripts/generate-constants.js",
"generate-bindings": "cargo test",
"prettier:check": "prettier --check **.ts",
"prettier:fix": "prettier --write **.ts",
@@ -47,5 +48,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.97.0"
"version": "1.98.0"
}
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from "fs";
import { resolve } from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const data = [];
const header = resolve(__dirname, "../../../deltachat-ffi/deltachat.h");
console.log("Generating constants...");
const header_data = readFileSync(header, "UTF-8");
const regex = /^#define\s+(\w+)\s+(\w+)/gm;
let match;
while (null != (match = regex.exec(header_data))) {
const key = match[1];
const value = parseInt(match[2]);
if (!isNaN(value)) {
data.push({ key, value });
}
}
const constants = data
.filter(
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
)
.sort((lhs, rhs) => {
if (lhs.key < rhs.key) return -1;
else if (lhs.key > rhs.key) return 1;
return 0;
})
.filter(({ key }) => {
// filter out what we don't need it
return !(
key.startsWith("DC_EVENT_") ||
key.startsWith("DC_IMEX_") ||
key.startsWith("DC_CHAT_VISIBILITY") ||
key.startsWith("DC_DOWNLOAD") ||
(key.startsWith("DC_MSG") && !key.startsWith("DC_MSG_ID")) ||
key.startsWith("DC_QR_")
);
})
.map((row) => {
return ` ${row.key}: ${row.value}`;
})
.join(",\n");
writeFileSync(
resolve(__dirname, "../generated/constants.ts"),
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`
);
+8 -3
View File
@@ -2,6 +2,7 @@ import * as T from "../generated/types.js";
import * as RPC from "../generated/jsonrpc.js";
import { RawClient } from "../generated/client.js";
import { Event } from "../generated/events.js";
import { Context } from "../generated/context_methods.js";
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "tiny-emitter";
@@ -28,14 +29,14 @@ type ContextEvents = { ALL: (event: Event) => void } & {
};
export type DcEvent = Event;
export type DcEventType<T extends Event["type"]> = Extract<Event, { type: T }>
export type DcEventType<T extends Event["type"]> = Extract<Event, { type: T }>;
export class BaseDeltaChat<
Transport extends BaseTransport<any>
> extends TinyEmitter<Events> {
rpc: RawClient;
account?: T.Account;
private contextEmitters: TinyEmitter<ContextEvents>[] = [];
private contextEmitters: Context<ContextEvents>[] = [];
constructor(public transport: Transport) {
super();
this.rpc = new RawClient(this.transport);
@@ -62,10 +63,14 @@ export class BaseDeltaChat<
}
getContextEvents(account_id: number) {
return this.getContext(account_id);
}
getContext(account_id: number) {
if (this.contextEmitters[account_id]) {
return this.contextEmitters[account_id];
} else {
this.contextEmitters[account_id] = new TinyEmitter();
this.contextEmitters[account_id] = new Context(this, account_id);
return this.contextEmitters[account_id];
}
}
+1
View File
@@ -4,3 +4,4 @@ export * from "../generated/events.js";
export { RawClient } from "../generated/client.js";
export * from "./client.js";
export * as yerpc from "yerpc";
export { C } from "../generated/constants.js";
+8
View File
@@ -20,6 +20,7 @@ use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::reaction::send_reaction;
use deltachat::receive_imf::*;
use deltachat::sql;
use deltachat::tools::*;
@@ -407,6 +408,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
resend <msg-id>\n\
markseen <msg-id>\n\
delmsg <msg-id>\n\
react <msg-id> [<reaction>]\n\
===========================Contact commands==\n\
listcontacts [<query>]\n\
listverified [<query>]\n\
@@ -1121,6 +1123,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ids[0] = MsgId::new(arg1.parse()?);
message::delete_msgs(&context, &ids).await?;
}
"react" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let msg_id = MsgId::new(arg1.parse()?);
let reaction = arg2;
send_reaction(&context, msg_id, reaction).await?;
}
"listcontacts" | "contacts" | "listverified" => {
let contacts = Contact::get_all(
&context,
+15 -1
View File
@@ -72,6 +72,19 @@ fn receive_event(event: EventType) {
))
);
}
EventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
} => {
info!(
"{}",
yellow.paint(format!(
"Received REACTIONS_CHANGED(chat_id={}, msg_id={}, contact_id={})",
chat_id, msg_id, contact_id
))
);
}
EventType::ContactsChanged(_) => {
info!("{}", yellow.paint("Received CONTACTS_CHANGED()"));
}
@@ -208,7 +221,7 @@ const CHAT_COMMANDS: [&str; 36] = [
"accept",
"blockchat",
];
const MESSAGE_COMMANDS: [&str; 8] = [
const MESSAGE_COMMANDS: [&str; 9] = [
"listmsgs",
"msginfo",
"listfresh",
@@ -217,6 +230,7 @@ const MESSAGE_COMMANDS: [&str; 8] = [
"markseen",
"delmsg",
"download",
"react",
];
const CONTACT_COMMANDS: [&str; 9] = [
"listcontacts",
+1
View File
@@ -50,6 +50,7 @@ module.exports = {
DC_EVENT_MSG_FAILED: 2012,
DC_EVENT_MSG_READ: 2015,
DC_EVENT_NEW_BLOB_FILE: 150,
DC_EVENT_REACTIONS_CHANGED: 2001,
DC_EVENT_SECUREJOIN_INVITER_PROGRESS: 2060,
DC_EVENT_SECUREJOIN_JOINER_PROGRESS: 2061,
DC_EVENT_SELFAVATAR_CHANGED: 2110,
+1
View File
@@ -14,6 +14,7 @@ module.exports = {
400: 'DC_EVENT_ERROR',
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2005: 'DC_EVENT_INCOMING_MSG',
2008: 'DC_EVENT_MSGS_NOTICED',
2010: 'DC_EVENT_MSG_DELIVERED',
+2
View File
@@ -50,6 +50,7 @@ export enum C {
DC_EVENT_MSG_FAILED = 2012,
DC_EVENT_MSG_READ = 2015,
DC_EVENT_NEW_BLOB_FILE = 150,
DC_EVENT_REACTIONS_CHANGED = 2001,
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060,
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061,
DC_EVENT_SELFAVATAR_CHANGED = 2110,
@@ -282,6 +283,7 @@ export const EventId2EventName: { [key: number]: string } = {
400: 'DC_EVENT_ERROR',
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2005: 'DC_EVENT_INCOMING_MSG',
2008: 'DC_EVENT_MSGS_NOTICED',
2010: 'DC_EVENT_MSG_DELIVERED',
+1 -1
View File
@@ -60,5 +60,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.97.0"
"version": "1.98.0"
}
+10
View File
@@ -450,6 +450,16 @@ This allows the receiver to show the time without knowing the file format.
Chat-Duration: 10000
# Reactions
Messengers MAY implement [RFC 9078](https://tools.ietf.org/html/rfc9078) reactions.
Received reaction should be interpreted as overwriting all previous reactions
received from the same contact.
This semantics is compatible to [XEP-0444](https://xmpp.org/extensions/xep-0444.html).
As an extension to RFC 9078, it is allowed to send empty reaction message,
in which case all previously sent reactions are retracted.
# Miscellaneous
Messengers SHOULD use the header `In-Reply-To` as usual.
+3 -1
View File
@@ -34,7 +34,9 @@ const SEEN_RECENTLY_SECONDS: i64 = 600;
///
/// Some contact IDs are reserved to identify special contacts. This
/// type can represent both the special as well as normal contacts.
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(
Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
pub struct ContactId(u32);
impl ContactId {
+56 -1
View File
@@ -650,7 +650,7 @@ impl Context {
ON m.chat_id=c.id
WHERE m.chat_id>9
AND m.hidden=0
AND c.blocked=0
AND c.blocked!=1
AND ct.blocked=0
AND m.txt LIKE ?
ORDER BY m.id DESC LIMIT 1000",
@@ -703,6 +703,8 @@ mod tests {
use crate::chat::{
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
};
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::contact::ContactId;
use crate::message::{Message, Viewtype};
use crate::receive_imf::receive_imf;
@@ -1038,6 +1040,59 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_search_unaccepted_requests() -> Result<()> {
let t = TestContext::new_alice().await;
receive_imf(
&t,
b"From: BobBar <bob@example.org>\n\
To: alice@example.org\n\
Subject: foo\n\
Message-ID: <msg1234@example.org>\n\
Chat-Version: 1.0\n\
Date: Tue, 25 Oct 2022 13:37:00 +0000\n\
\n\
hello bob, foobar test!\n",
false,
)
.await?;
let chat_id = t.get_last_msg().await.get_chat_id();
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_type(), Chattype::Single);
assert!(chat.is_contact_request());
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
assert_eq!(
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
1
);
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
chat_id.block(&t).await?;
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0);
assert_eq!(
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
0
);
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 0);
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 0);
let contact_ids = get_chat_contacts(&t, chat_id).await?;
Contact::unblock(&t, *contact_ids.first().unwrap()).await?;
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
assert_eq!(
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
1
);
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_limit_search_msgs() -> Result<()> {
let alice = TestContext::new_alice().await;
+1 -37
View File
@@ -11,7 +11,7 @@ use crate::imap::{Imap, ImapActionResult};
use crate::job::{self, Action, Job, Status};
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, Part};
use crate::param::{Param, Params};
use crate::param::Params;
use crate::tools::time;
use crate::{job_try, stock_str, EventType};
use std::cmp::max;
@@ -69,42 +69,6 @@ impl Context {
Ok(Some(max(MIN_DOWNLOAD_LIMIT, download_limit as u32)))
}
}
// Merges the two messages to `placeholder_msg_id`;
// `full_msg_id` is no longer used afterwards.
pub(crate) async fn merge_messages(
&self,
full_msg_id: MsgId,
placeholder_msg_id: MsgId,
) -> Result<()> {
let placeholder = Message::load_from_db(self, placeholder_msg_id).await?;
self.sql
.transaction(move |transaction| {
transaction
.execute("DELETE FROM msgs WHERE id=?;", paramsv![placeholder_msg_id])?;
transaction.execute(
"UPDATE msgs SET id=? WHERE id=?",
paramsv![placeholder_msg_id, full_msg_id],
)?;
Ok(())
})
.await?;
let mut full = Message::load_from_db(self, placeholder_msg_id).await?;
for key in [
Param::WebxdcSummary,
Param::WebxdcSummaryTimestamp,
Param::WebxdcDocument,
Param::WebxdcDocumentTimestamp,
] {
if let Some(value) = placeholder.param.get(key) {
full.param.set(key, value);
}
}
full.update_param(self).await?;
Ok(())
}
}
impl MsgId {
+7
View File
@@ -173,6 +173,13 @@ pub enum EventType {
msg_id: MsgId,
},
/// Reactions for the message changed.
ReactionsChanged {
chat_id: ChatId,
msg_id: MsgId,
contact_id: ContactId,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
+1
View File
@@ -103,6 +103,7 @@ pub mod receive_imf;
pub mod tools;
pub mod accounts;
pub mod reaction;
/// if set imap/incoming and smtp/outgoing MIME messages will be printed
pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";
+11
View File
@@ -22,6 +22,7 @@ use crate::imap::markseen_on_imap_table;
use crate::mimeparser::{parse_message_id, DeliveryReport, SystemMessage};
use crate::param::{Param, Params};
use crate::pgp::split_armored_data;
use crate::reaction::get_msg_reactions;
use crate::scheduler::InterruptInfo;
use crate::sql;
use crate::stock_str;
@@ -751,6 +752,11 @@ impl Message {
self.param.set_int(Param::Duration, duration);
}
/// Marks the message as reaction.
pub(crate) fn set_reaction(&mut self) {
self.param.set_int(Param::Reaction, 1);
}
pub async fn latefiling_mediasize(
&mut self,
context: &Context,
@@ -1082,6 +1088,11 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
ret += "\n";
let reactions = get_msg_reactions(context, msg_id).await?;
if !reactions.is_empty() {
ret += &format!("Reactions: {}\n", reactions);
}
if let Some(error) = msg.error.as_ref() {
ret += &format!("Error: {}", error);
}
+9 -1
View File
@@ -183,7 +183,10 @@ impl<'a> MimeFactory<'a> {
)
.await?;
if !msg.is_system_message() && context.get_config_bool(Config::MdnsEnabled).await? {
if !msg.is_system_message()
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
&& context.get_config_bool(Config::MdnsEnabled).await?
{
req_mdn = true;
}
}
@@ -1122,6 +1125,11 @@ impl<'a> MimeFactory<'a> {
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
))
.body(message_text);
if self.msg.param.get_int(Param::Reaction).unwrap_or_default() != 0 {
main_part = main_part.header(("Content-Disposition", "reaction"));
}
let mut parts = Vec::new();
// add HTML-part, this is needed only if a HTML-message from a non-delta-client is forwarded;
+67 -1
View File
@@ -551,7 +551,10 @@ impl MimeMessage {
}
if prepend_subject && !subject.is_empty() {
let part_with_text = self.parts.iter_mut().find(|part| !part.msg.is_empty());
let part_with_text = self
.parts
.iter_mut()
.find(|part| !part.msg.is_empty() && !part.is_reaction);
if let Some(mut part) = part_with_text {
part.msg = format!("{} – {}", subject, part.msg);
}
@@ -913,6 +916,7 @@ impl MimeMessage {
Ok(any_part_added)
}
/// Returns true if any part was added, false otherwise.
async fn add_single_part_if_known(
&mut self,
context: &Context,
@@ -946,6 +950,30 @@ impl MimeMessage {
warn!(context, "Missing attachment");
return Ok(false);
}
mime::TEXT
if mail.get_content_disposition().disposition
== DispositionType::Extension("reaction".to_string()) =>
{
// Reaction.
let decoded_data = match mail.get_body() {
Ok(decoded_data) => decoded_data,
Err(err) => {
warn!(context, "Invalid body parsed {:?}", err);
// Note that it's not always an error - might be no data
return Ok(false);
}
};
let part = Part {
typ: Viewtype::Text,
mimetype: Some(mime_type),
msg: decoded_data,
is_reaction: true,
..Default::default()
};
self.do_add_single_part(part);
return Ok(true);
}
mime::TEXT | mime::HTML => {
let decoded_data = match mail.get_body() {
Ok(decoded_data) => decoded_data,
@@ -1644,6 +1672,9 @@ pub struct Part {
/// note that multipart/related may contain further multipart nestings
/// and all of them needs to be marked with `is_related`.
pub(crate) is_related: bool,
/// Part is an RFC 9078 reaction.
pub(crate) is_reaction: bool,
}
/// return mimetype and viewtype for a parsed mail
@@ -3329,4 +3360,39 @@ Message.
Ok(())
}
/// Tests parsing of MIME message containing RFC 9078 reaction.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
let mime_message = MimeMessage::from_bytes(
&alice,
"To: alice@example.org\n\
From: bob@example.net\n\
Date: Today, 29 February 2021 00:00:10 -800\n\
Message-ID: 56789@example.net\n\
In-Reply-To: 12345@example.org\n\
Subject: Meeting\n\
Mime-Version: 1.0 (1.0)\n\
Content-Type: text/plain; charset=utf-8\n\
Content-Disposition: reaction\n\
\n\
\u{1F44D}"
.as_bytes(),
)
.await?;
assert_eq!(mime_message.parts.len(), 1);
assert_eq!(mime_message.parts[0].is_reaction, true);
assert_eq!(
mime_message
.get_header(HeaderDef::InReplyTo)
.and_then(|msgid| parse_message_id(msgid).ok())
.unwrap(),
"12345@example.org"
);
Ok(())
}
}
+3
View File
@@ -59,6 +59,9 @@ pub enum Param {
/// For Messages
WantsMdn = b'r',
/// For Messages: the message is a reaction.
Reaction = b'x',
/// For Messages: a message with Auto-Submitted header ("bot").
Bot = b'b',
+551
View File
@@ -0,0 +1,551 @@
//! # Reactions.
//!
//! Reactions are short messages consisting of emojis sent in reply to
//! messages. Unlike normal messages which are added to the end of the chat,
//! reactions are supposed to be displayed near the original messages.
//!
//! RFC 9078 specifies how reactions are transmitted in MIME messages.
//!
//! Reaction update semantics is not well-defined in RFC 9078, so
//! Delta Chat uses the same semantics as in
//! [XEP-0444](https://xmpp.org/extensions/xep-0444.html) section
//! "3.2 Updating reactions to a message". Received reactions override
//! all previously received reactions from the same user and it is
//! possible to remove all reactions by sending an empty string as a reaction,
//! even though RFC 9078 requires at least one emoji to be sent.
use std::collections::BTreeMap;
use std::fmt;
use anyhow::Result;
use crate::chat::{send_msg, ChatId};
use crate::contact::ContactId;
use crate::context::Context;
use crate::events::EventType;
use crate::message::{rfc724_mid_exists, Message, MsgId, Viewtype};
/// A single reaction consisting of multiple emoji sequences.
///
/// It is guaranteed to have all emojis sorted and deduplicated inside.
#[derive(Debug, Default, Clone)]
pub struct Reaction {
/// Canonical represntation of reaction as a string of space-separated emojis.
reaction: String,
}
// We implement From<&str> instead of std::str::FromStr, because
// FromStr requires error type and reaction parsing never returns an
// error.
impl From<&str> for Reaction {
/// Parses a string containing a reaction.
///
/// Reaction string is separated by spaces or tabs (`WSP` in ABNF),
/// but this function accepts any ASCII whitespace, so even a CRLF at
/// the end of string is acceptable.
///
/// Any short enough string is accepted as a reaction to avoid the
/// complexity of validating emoji sequences as required by RFC
/// 9078. On the sender side UI is responsible to provide only
/// valid emoji sequences via reaction picker. On the receiver
/// side, abuse of the possibility to use arbitrary strings as
/// reactions is not different from other kinds of spam attacks
/// such as sending large numbers of large messages, and should be
/// dealt with the same way, e.g. by blocking the user.
fn from(reaction: &str) -> Self {
let mut emojis: Vec<&str> = reaction
.split_ascii_whitespace()
.filter(|&emoji| emoji.len() < 30)
.collect();
emojis.sort();
emojis.dedup();
let reaction = emojis.join(" ");
Self { reaction }
}
}
impl Reaction {
/// Returns true if reaction contains no emojis.
pub fn is_empty(&self) -> bool {
self.reaction.is_empty()
}
/// Returns a vector of emojis composing a reaction.
pub fn emojis(&self) -> Vec<&str> {
self.reaction.split(' ').collect()
}
/// Returns space-separated string of emojis
pub fn as_str(&self) -> &str {
&self.reaction
}
/// Appends emojis from another reaction to this reaction.
pub fn add(&self, other: Self) -> Self {
let mut emojis: Vec<&str> = self.emojis();
emojis.append(&mut other.emojis());
emojis.sort();
emojis.dedup();
let reaction = emojis.join(" ");
Self { reaction }
}
}
/// Structure representing all reactions to a particular message.
#[derive(Debug)]
pub struct Reactions {
/// Map from a contact to its reaction to message.
reactions: BTreeMap<ContactId, Reaction>,
}
impl Reactions {
/// Returns vector of contacts that reacted to the message.
pub fn contacts(&self) -> Vec<ContactId> {
self.reactions.keys().copied().collect()
}
/// Returns reaction of a given contact to message.
///
/// If contact did not react to message or removed the reaction,
/// this method returns an empty reaction.
pub fn get(&self, contact_id: ContactId) -> Reaction {
self.reactions.get(&contact_id).cloned().unwrap_or_default()
}
/// Returns true if the message has no reactions.
pub fn is_empty(&self) -> bool {
self.reactions.is_empty()
}
}
impl fmt::Display for Reactions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut emoji_frequencies: BTreeMap<String, usize> = BTreeMap::new();
for reaction in self.reactions.values() {
for emoji in reaction.emojis() {
emoji_frequencies
.entry(emoji.to_string())
.and_modify(|x| *x += 1)
.or_insert(1);
}
}
let mut first = true;
for (emoji, frequency) in emoji_frequencies {
if !first {
write!(f, " ")?;
}
first = false;
write!(f, "{}{}", emoji, frequency)?;
}
Ok(())
}
}
async fn set_msg_id_reaction(
context: &Context,
msg_id: MsgId,
chat_id: ChatId,
contact_id: ContactId,
reaction: Reaction,
) -> Result<()> {
if reaction.is_empty() {
// Simply remove the record instead of setting it to empty string.
context
.sql
.execute(
"DELETE FROM reactions
WHERE msg_id = ?1
AND contact_id = ?2",
paramsv![msg_id, contact_id],
)
.await?;
} else {
context
.sql
.execute(
"INSERT INTO reactions (msg_id, contact_id, reaction)
VALUES (?1, ?2, ?3)
ON CONFLICT(msg_id, contact_id)
DO UPDATE SET reaction=excluded.reaction",
paramsv![msg_id, contact_id, reaction.as_str()],
)
.await?;
}
context.emit_event(EventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
});
Ok(())
}
/// Sends a reaction to message `msg_id`, overriding previously sent reactions.
///
/// `reaction` is a string consisting of space-separated emoji. Use
/// empty string to retract a reaction.
pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) -> Result<MsgId> {
let msg = Message::load_from_db(context, msg_id).await?;
let chat_id = msg.chat_id;
let reaction: Reaction = reaction.into();
let mut reaction_msg = Message::new(Viewtype::Text);
reaction_msg.text = Some(reaction.as_str().to_string());
reaction_msg.set_reaction();
reaction_msg.in_reply_to = Some(msg.rfc724_mid);
reaction_msg.hidden = true;
// Send messsage first.
let reaction_msg_id = send_msg(context, chat_id, &mut reaction_msg).await?;
// Only set reaction if we successfully sent the message.
set_msg_id_reaction(context, msg_id, msg.chat_id, ContactId::SELF, reaction).await?;
Ok(reaction_msg_id)
}
/// Adds given reaction to message `msg_id` and sends an update.
///
/// This can be used to implement advanced clients that allow reacting
/// with multiple emojis. For a simple messenger UI, you probably want
/// to use [`send_reaction()`] instead so reacting with a new emoji
/// removes previous emoji at the same time.
pub async fn add_reaction(context: &Context, msg_id: MsgId, reaction: &str) -> Result<MsgId> {
let self_reaction = get_self_reaction(context, msg_id).await?;
let reaction = self_reaction.add(Reaction::from(reaction));
send_reaction(context, msg_id, reaction.as_str()).await
}
/// Updates reaction of `contact_id` on the message with `in_reply_to`
/// Message-ID. If no such message is found in the database, reaction
/// is ignored.
///
/// `reaction` is a space-separated string of emojis. It can be empty
/// if contact wants to remove all reactions.
pub(crate) async fn set_msg_reaction(
context: &Context,
in_reply_to: &str,
chat_id: ChatId,
contact_id: ContactId,
reaction: Reaction,
) -> Result<()> {
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
set_msg_id_reaction(context, msg_id, chat_id, contact_id, reaction).await
} else {
info!(
context,
"Can't assign reaction to unknown message with Message-ID {}", in_reply_to
);
Ok(())
}
}
/// Get our own reaction for a given message.
async fn get_self_reaction(context: &Context, msg_id: MsgId) -> Result<Reaction> {
let reaction_str: Option<String> = context
.sql
.query_get_value(
"SELECT reaction
FROM reactions
WHERE msg_id=? AND contact_id=?",
paramsv![msg_id, ContactId::SELF],
)
.await?;
Ok(reaction_str
.as_deref()
.map(Reaction::from)
.unwrap_or_default())
}
/// Returns a structure containing all reactions to the message.
pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result<Reactions> {
let reactions = context
.sql
.query_map(
"SELECT contact_id, reaction FROM reactions WHERE msg_id=?",
paramsv![msg_id],
|row| {
let contact_id: ContactId = row.get(0)?;
let reaction: String = row.get(1)?;
Ok((contact_id, reaction))
},
|rows| {
let mut reactions = Vec::new();
for row in rows {
let (contact_id, reaction) = row?;
reactions.push((contact_id, Reaction::from(reaction.as_str())));
}
Ok(reactions)
},
)
.await?
.into_iter()
.collect();
Ok(Reactions { reactions })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::get_chat_msgs;
use crate::config::Config;
use crate::constants::DC_CHAT_ID_TRASH;
use crate::contact::{Contact, Origin};
use crate::download::DownloadState;
use crate::message::MessageState;
use crate::receive_imf::{receive_imf, receive_imf_inner};
use crate::test_utils::TestContext;
#[test]
fn test_parse_reaction() {
// Check that basic set of emojis from RFC 9078 is supported.
assert_eq!(Reaction::from("👍").emojis(), vec!["👍"]);
assert_eq!(Reaction::from("👎").emojis(), vec!["👎"]);
assert_eq!(Reaction::from("😀").emojis(), vec!["😀"]);
assert_eq!(Reaction::from("☹").emojis(), vec!["☹"]);
assert_eq!(Reaction::from("😢").emojis(), vec!["😢"]);
// Empty string can be used to remove all reactions.
assert!(Reaction::from("").is_empty());
// Short strings can be used as emojis, could be used to add
// support for custom emojis via emoji shortcodes.
assert_eq!(Reaction::from(":deltacat:").emojis(), vec![":deltacat:"]);
// Check that long strings are not valid emojis.
assert!(
Reaction::from(":foobarbazquuxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:").is_empty()
);
// Multiple reactions separated by spaces or tabs are supported.
assert_eq!(Reaction::from("👍 ❤").emojis(), vec!["❤", "👍"]);
assert_eq!(Reaction::from("👍\t❤").emojis(), vec!["❤", "👍"]);
// Invalid emojis are removed, but valid emojis are retained.
assert_eq!(
Reaction::from("👍\t:foo: ❤").emojis(),
vec![":foo:", "❤", "👍"]
);
assert_eq!(Reaction::from("👍\t:foo: ❤").as_str(), ":foo: ❤ 👍");
// Duplicates are removed.
assert_eq!(Reaction::from("👍 👍").emojis(), vec!["👍"]);
}
#[test]
fn test_add_reaction() {
let reaction1 = Reaction::from("👍 😀");
let reaction2 = Reaction::from("❤");
let reaction_sum = reaction1.add(reaction2);
assert_eq!(reaction_sum.emojis(), vec!["❤", "👍", "😀"]);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_receive_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
alice.set_config(Config::ShowEmails, Some("2")).await?;
// Alice receives BCC-self copy of a message sent to Bob.
receive_imf(
&alice,
"To: bob@example.net\n\
From: alice@example.org\n\
Date: Today, 29 February 2021 00:00:00 -800\n\
Message-ID: 12345@example.org\n\
Subject: Meeting\n\
\n\
Can we chat at 1pm pacific, today?"
.as_bytes(),
false,
)
.await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.state, MessageState::OutDelivered);
let reactions = get_msg_reactions(&alice, msg.id).await?;
let contacts = reactions.contacts();
assert_eq!(contacts.len(), 0);
let bob_id = Contact::add_or_lookup(&alice, "", "bob@example.net", Origin::ManuallyCreated)
.await?
.0;
let bob_reaction = reactions.get(bob_id);
assert!(bob_reaction.is_empty()); // Bob has not reacted to message yet.
// Alice receives reaction to her message from Bob.
receive_imf(
&alice,
"To: alice@example.org\n\
From: bob@example.net\n\
Date: Today, 29 February 2021 00:00:10 -800\n\
Message-ID: 56789@example.net\n\
In-Reply-To: 12345@example.org\n\
Subject: Meeting\n\
Mime-Version: 1.0 (1.0)\n\
Content-Type: text/plain; charset=utf-8\n\
Content-Disposition: reaction\n\
\n\
\u{1F44D}"
.as_bytes(),
false,
)
.await?;
let reactions = get_msg_reactions(&alice, msg.id).await?;
assert_eq!(reactions.to_string(), "👍1");
let contacts = reactions.contacts();
assert_eq!(contacts.len(), 1);
assert_eq!(contacts.get(0), Some(&bob_id));
let bob_reaction = reactions.get(bob_id);
assert_eq!(bob_reaction.is_empty(), false);
assert_eq!(bob_reaction.emojis(), vec!["👍"]);
assert_eq!(bob_reaction.as_str(), "👍");
Ok(())
}
async fn expect_reactions_changed_event(
t: &TestContext,
expected_chat_id: ChatId,
expected_msg_id: MsgId,
expected_contact_id: ContactId,
) -> Result<()> {
let event = t
.evtracker
.get_matching(|evt| matches!(evt, EventType::ReactionsChanged { .. }))
.await;
match event {
EventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
} => {
assert_eq!(chat_id, expected_chat_id);
assert_eq!(msg_id, expected_msg_id);
assert_eq!(contact_id, expected_contact_id);
}
_ => unreachable!(),
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_alice = alice.create_chat(&bob).await;
let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
let bob_msg = bob.recv_msg(&alice_msg).await;
assert_eq!(get_chat_msgs(&alice, chat_alice.id, 0).await?.len(), 1);
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id, 0).await?.len(), 1);
let alice_msg2 = alice.send_text(chat_alice.id, "Hi again!").await;
bob.recv_msg(&alice_msg2).await;
assert_eq!(get_chat_msgs(&alice, chat_alice.id, 0).await?.len(), 2);
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id, 0).await?.len(), 2);
bob_msg.chat_id.accept(&bob).await?;
send_reaction(&bob, bob_msg.id, "👍").await.unwrap();
expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id, 0).await?.len(), 2);
let bob_reaction_msg = bob.pop_sent_msg().await;
let alice_reaction_msg = alice.recv_msg_opt(&bob_reaction_msg).await.unwrap();
assert_eq!(alice_reaction_msg.chat_id, DC_CHAT_ID_TRASH);
assert_eq!(get_chat_msgs(&alice, chat_alice.id, 0).await?.len(), 2);
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
assert_eq!(reactions.to_string(), "👍1");
let contacts = reactions.contacts();
assert_eq!(contacts.len(), 1);
let bob_id = contacts.get(0).unwrap();
let bob_reaction = reactions.get(*bob_id);
assert_eq!(bob_reaction.is_empty(), false);
assert_eq!(bob_reaction.emojis(), vec!["👍"]);
assert_eq!(bob_reaction.as_str(), "👍");
expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id)
.await?;
// Alice reacts to own message.
send_reaction(&alice, alice_msg.sender_msg_id, "👍 😀")
.await
.unwrap();
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
assert_eq!(reactions.to_string(), "👍2 😀1");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_partial_download_and_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
alice
.create_chat_with_contact("Bob", "bob@example.net")
.await;
let msg_header = "From: Bob <bob@example.net>\n\
To: Alice <alice@example.org>\n\
Chat-Version: 1.0\n\
Subject: subject\n\
Message-ID: <first@example.org>\n\
Date: Sun, 14 Nov 2021 00:10:00 +0000\
Content-Type: text/plain";
let msg_full = format!("{}\n\n100k text...", msg_header);
// Alice downloads message from Bob partially.
let alice_received_message = receive_imf_inner(
&alice,
"first@example.org",
msg_header.as_bytes(),
false,
Some(100000),
false,
)
.await?
.unwrap();
let alice_msg_id = *alice_received_message.msg_ids.get(0).unwrap();
// Bob downloads own message on the other device.
let bob_received_message = receive_imf(&bob, msg_full.as_bytes(), false)
.await?
.unwrap();
let bob_msg_id = *bob_received_message.msg_ids.get(0).unwrap();
// Bob reacts to own message.
send_reaction(&bob, bob_msg_id, "👍").await.unwrap();
let bob_reaction_msg = bob.pop_sent_msg().await;
// Alice receives a reaction.
alice.recv_msg_opt(&bob_reaction_msg).await.unwrap();
let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
assert_eq!(reactions.to_string(), "👍1");
let msg = Message::load_from_db(&alice, alice_msg_id).await?;
assert_eq!(msg.download_state(), DownloadState::Available);
// Alice downloads full message.
receive_imf_inner(
&alice,
"first@example.org",
msg_full.as_bytes(),
false,
None,
false,
)
.await?;
// Check that reaction is still on the message after full download.
let msg = Message::load_from_db(&alice, alice_msg_id).await?;
assert_eq!(msg.download_state(), DownloadState::Done);
let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
assert_eq!(reactions.to_string(), "👍1");
Ok(())
}
}
+63 -26
View File
@@ -33,6 +33,7 @@ use crate::mimeparser::{
};
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
use crate::reaction::{set_msg_reaction, Reaction};
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
use crate::sql;
use crate::stock_str;
@@ -404,7 +405,7 @@ async fn add_parts(
from_id: ContactId,
seen: bool,
is_partial_download: Option<u32>,
replace_msg_id: Option<MsgId>,
mut replace_msg_id: Option<MsgId>,
fetching_existing_messages: bool,
prevent_rename: bool,
) -> Result<ReceivedMsg> {
@@ -430,8 +431,9 @@ async fn add_parts(
};
// incoming non-chat messages may be discarded
let location_kml_is = mime_parser.location_kml.is_some();
let is_location_kml = mime_parser.location_kml.is_some();
let is_mdn = !mime_parser.mdn_reports.is_empty();
let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction);
let show_emails =
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
@@ -450,7 +452,7 @@ async fn add_parts(
ShowEmails::All => allow_creation = !is_mdn,
}
} else {
allow_creation = !is_mdn;
allow_creation = !is_mdn && !is_reaction;
}
// check if the message introduces a new chat:
@@ -689,7 +691,8 @@ async fn add_parts(
state = if seen
|| fetching_existing_messages
|| is_mdn
|| location_kml_is
|| is_reaction
|| is_location_kml
|| securejoin_seen
|| chat_id_blocked == Blocked::Yes
{
@@ -841,14 +844,15 @@ async fn add_parts(
}
}
if is_mdn {
chat_id = Some(DC_CHAT_ID_TRASH);
}
let chat_id = chat_id.unwrap_or_else(|| {
info!(context, "No chat id for message (TRASH)");
let orig_chat_id = chat_id;
let chat_id = if is_mdn || is_reaction {
DC_CHAT_ID_TRASH
});
} else {
chat_id.unwrap_or_else(|| {
info!(context, "No chat id for message (TRASH)");
DC_CHAT_ID_TRASH
})
};
// Extract ephemeral timer from the message or use the existing timer if the message is not fully downloaded.
let mut ephemeral_timer = if is_partial_download.is_some() {
@@ -1053,11 +1057,41 @@ async fn add_parts(
let conn = context.sql.get_conn().await?;
for part in &mime_parser.parts {
if part.is_reaction {
set_msg_reaction(
context,
&mime_in_reply_to,
orig_chat_id.unwrap_or_default(),
from_id,
Reaction::from(part.msg.as_str()),
)
.await?;
}
let mut param = part.param.clone();
if is_system_message != SystemMessage::Unknown {
param.set_int(Param::Cmd, is_system_message as i32);
}
if let Some(replace_msg_id) = replace_msg_id {
let placeholder = Message::load_from_db(context, replace_msg_id).await?;
for key in [
Param::WebxdcSummary,
Param::WebxdcSummaryTimestamp,
Param::WebxdcDocument,
Param::WebxdcDocumentTimestamp,
] {
if let Some(value) = placeholder.param.get(key) {
param.set(key, value);
}
}
}
let mut txt_raw = "".to_string();
let mut stmt = conn.prepare_cached(
r#"
INSERT INTO msgs
(
id,
rfc724_mid, chat_id,
from_id, to_id, timestamp, timestamp_sent,
timestamp_rcvd, type, state, msgrmsg,
@@ -1067,13 +1101,22 @@ INSERT INTO msgs
ephemeral_timestamp, download_state, hop_info
)
VALUES (
?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?
);
)
ON CONFLICT (id) DO UPDATE
SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
from_id=excluded.from_id, to_id=excluded.to_id, timestamp=excluded.timestamp, timestamp_sent=excluded.timestamp_sent,
timestamp_rcvd=excluded.timestamp_rcvd, type=excluded.type, state=excluded.state, msgrmsg=excluded.msgrmsg,
txt=excluded.txt, subject=excluded.subject, txt_raw=excluded.txt_raw, param=excluded.param,
bytes=excluded.bytes, mime_headers=excluded.mime_headers, mime_in_reply_to=excluded.mime_in_reply_to,
mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
ephemeral_timestamp=excluded.ephemeral_timestamp, download_state=excluded.download_state, hop_info=excluded.hop_info
"#,
)?;
@@ -1095,11 +1138,6 @@ INSERT INTO msgs
txt_raw = format!("{}\n\n{}", subject, msg_raw);
}
let mut param = part.param.clone();
if is_system_message != SystemMessage::Unknown {
param.set_int(Param::Cmd, is_system_message as i32);
}
let ephemeral_timestamp = if in_fresh {
0
} else {
@@ -1113,9 +1151,10 @@ INSERT INTO msgs
// If you change which information is skipped if the message is trashed,
// also change `MsgId::trash()` and `delete_expired_messages()`
let trash = chat_id.is_trash() || (location_kml_is && msg.is_empty());
let trash = chat_id.is_trash() || (is_location_kml && msg.is_empty());
stmt.execute(paramsv![
replace_msg_id,
rfc724_mid,
if trash { DC_CHAT_ID_TRASH } else { chat_id },
if trash { ContactId::UNDEFINED } else { from_id },
@@ -1154,6 +1193,10 @@ INSERT INTO msgs
},
mime_parser.hop_info
])?;
// We only replace placeholder with a first part,
// afterwards insert additional parts.
replace_msg_id = None;
let row_id = conn.last_insert_rowid();
drop(stmt);
@@ -1162,14 +1205,8 @@ INSERT INTO msgs
drop(conn);
if let Some(replace_msg_id) = replace_msg_id {
if let Some(created_msg_id) = created_db_entries.pop() {
context
.merge_messages(created_msg_id, replace_msg_id)
.await?;
created_db_entries.push(replace_msg_id);
} else {
replace_msg_id.delete_from_db(context).await?;
}
// "Replace" placeholder with a message that has no parts.
replace_msg_id.delete_from_db(context).await?;
}
chat_id.unarchive_if_not_muted(context).await?;
+7 -7
View File
@@ -396,7 +396,7 @@ impl Sql {
}
/// Used for executing `SELECT COUNT` statements only. Returns the resulting count.
pub async fn count(&self, query: &str, params: impl rusqlite::Params) -> anyhow::Result<usize> {
pub async fn count(&self, query: &str, params: impl rusqlite::Params) -> Result<usize> {
let count: isize = self.query_row(query, params, |row| row.get(0)).await?;
Ok(usize::try_from(count)?)
}
@@ -429,10 +429,10 @@ impl Sql {
///
/// If the function returns an error, the transaction will be rolled back. If it does not return an
/// error, the transaction will be committed.
pub async fn transaction<G, H>(&self, callback: G) -> anyhow::Result<H>
pub async fn transaction<G, H>(&self, callback: G) -> Result<H>
where
H: Send + 'static,
G: Send + 'static + FnOnce(&mut rusqlite::Transaction<'_>) -> anyhow::Result<H>,
G: Send + 'static + FnOnce(&mut rusqlite::Transaction<'_>) -> Result<H>,
{
let mut conn = self.get_conn().await?;
tokio::task::block_in_place(move || {
@@ -453,7 +453,7 @@ impl Sql {
}
/// Query the database if the requested table already exists.
pub async fn table_exists(&self, name: &str) -> anyhow::Result<bool> {
pub async fn table_exists(&self, name: &str) -> Result<bool> {
let conn = self.get_conn().await?;
tokio::task::block_in_place(move || {
let mut exists = false;
@@ -468,7 +468,7 @@ impl Sql {
}
/// Check if a column exists in a given table.
pub async fn col_exists(&self, table_name: &str, col_name: &str) -> anyhow::Result<bool> {
pub async fn col_exists(&self, table_name: &str, col_name: &str) -> Result<bool> {
let conn = self.get_conn().await?;
tokio::task::block_in_place(move || {
let mut exists = false;
@@ -492,7 +492,7 @@ impl Sql {
sql: &str,
params: impl rusqlite::Params,
f: F,
) -> anyhow::Result<Option<T>>
) -> Result<Option<T>>
where
F: FnOnce(&rusqlite::Row) -> rusqlite::Result<T>,
{
@@ -516,7 +516,7 @@ impl Sql {
&self,
query: &str,
params: impl rusqlite::Params,
) -> anyhow::Result<Option<T>>
) -> Result<Option<T>>
where
T: rusqlite::types::FromSql,
{
+13
View File
@@ -596,6 +596,19 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
)
.await?;
}
if dbversion < 92 {
sql.execute_migration(
r#"CREATE TABLE reactions (
msg_id INTEGER NOT NULL, -- id of the message reacted to
contact_id INTEGER NOT NULL, -- id of the contact reacting to the message
reaction TEXT DEFAULT '' NOT NULL, -- a sequence of emojis separated by spaces
PRIMARY KEY(msg_id, contact_id),
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE -- delete reactions when message is deleted
FOREIGN KEY(contact_id) REFERENCES contacts(id) ON DELETE CASCADE -- delete reactions when contact is deleted
)"#,
92
).await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
+1
View File
@@ -8,6 +8,7 @@ Transport | IMAP v4 ([RFC 3501](https://tools.ietf.org/ht
Proxy | SOCKS5 ([RFC 1928](https://tools.ietf.org/html/rfc1928))
Embedded media | MIME Document Series ([RFC 2045](https://tools.ietf.org/html/rfc2045), [RFC 2046](https://tools.ietf.org/html/rfc2046)), Content-Disposition Header ([RFC 2183](https://tools.ietf.org/html/rfc2183)), Multipart/Related ([RFC 2387](https://tools.ietf.org/html/rfc2387))
Text and Quote encoding | Fixed, Flowed ([RFC 3676](https://tools.ietf.org/html/rfc3676))
Reactions | Reaction: Indicating Summary Reaction to a Message [RFC 9078](https://datatracker.ietf.org/doc/rfc9078/)
Filename encoding | Encoded Words ([RFC 2047](https://tools.ietf.org/html/rfc2047)), Encoded Word Extensions ([RFC 2231](https://tools.ietf.org/html/rfc2231))
Identify server folders | IMAP LIST Extension ([RFC 6154](https://tools.ietf.org/html/rfc6154))
Push | IMAP IDLE ([RFC 2177](https://tools.ietf.org/html/rfc2177))