From 3e6d1d57898c1e534818f8ba6fb4620266688c10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Oct 2022 21:02:36 +0000 Subject: [PATCH 01/18] 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] --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8f4267ad1..d003bd961 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1260,9 +1260,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", @@ -3658,9 +3658,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", @@ -3672,32 +3672,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", ] diff --git a/Cargo.toml b/Cargo.toml index 50e787f55..ab579209e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" From f85f088d6516adadf1cbd017dbfa135bb2acceb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Oct 2022 12:33:13 +0000 Subject: [PATCH 02/18] 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] --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 097964508..c4cf4f254 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2431,9 +2431,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pgp" diff --git a/Cargo.toml b/Cargo.toml index bb11973b8..6d2d4424f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" From f80c78536fbb4a0e8c107bd6efce4e2cc88a94fd Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 14 Oct 2022 00:39:10 +0000 Subject: [PATCH 03/18] fix unused result error --- deltachat-ffi/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index a8c7f20f3..1f521dc74 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -4515,7 +4515,7 @@ mod jsonrpc { return; } (*jsonrpc_instance).event_thread.abort(); - Box::from_raw(jsonrpc_instance); + drop(Box::from_raw(jsonrpc_instance)); } #[no_mangle] From e8ea9b71276d053957fa209bcfad92d9dc96da0b Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 15 Oct 2022 20:35:30 +0200 Subject: [PATCH 04/18] jsonrpc/events: commit type I forgot to commit (#3670) commit line I forgot to commit --- .../typescript/generated/events.ts | 32 +++++++++---------- deltachat-jsonrpc/typescript/src/client.ts | 1 + 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/deltachat-jsonrpc/typescript/generated/events.ts b/deltachat-jsonrpc/typescript/generated/events.ts index 2f0fa5084..a483c68cb 100644 --- a/deltachat-jsonrpc/typescript/generated/events.ts +++ b/deltachat-jsonrpc/typescript/generated/events.ts @@ -52,7 +52,7 @@ export type Event=(({ * should not be disturbed by a dialog or so. Instead, use a bubble or so. * * However, for ongoing processes (eg. configure()) - * or for functions that are expected to fail (eg. dc_continue_key_transfer()) + * or for functions that are expected to fail (eg. autocryptContinueKeyTransfer()) * it might be better to delay showing these events until the function has really * failed (returned false). It should be sufficient to report only the *last* error * in a messasge box then. @@ -61,9 +61,9 @@ export type Event=(({ /** * An action cannot be performed because the user is not in the group. * Reported eg. after a call to - * dc_set_chat_name(), dc_set_chat_profile_image(), - * dc_add_contact_to_chat(), dc_remove_contact_from_chat(), - * dc_send_text_msg() or another sending function. + * setChatName(), setChatProfileImage(), + * addContactToChat(), removeContactFromChat(), + * and messages sending functions. */ "type":"ErrorSelfNotInGroup";}&{"msg":string;})|({ /** @@ -73,8 +73,8 @@ export type Event=(({ * - Chats created, deleted or archived * - A draft has been set * - * `chat_id` is set if only a single chat is affected by the changes, otherwise 0. - * `msg_id` is set if only a single message is affected by the changes, otherwise 0. + * `chatId` is set if only a single chat is affected by the changes, otherwise 0. + * `msgId` is set if only a single message is affected by the changes, otherwise 0. */ "type":"MsgsChanged";}&{"chatId":U32;"msgId":U32;})|({ /** @@ -91,24 +91,24 @@ export type Event=(({ "type":"MsgsNoticed";}&{"chatId":U32;})|({ /** * A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to - * DC_STATE_OUT_DELIVERED, see dc_msg_get_state(). + * DC_STATE_OUT_DELIVERED, see `Message.state`. */ "type":"MsgDelivered";}&{"chatId":U32;"msgId":U32;})|({ /** * A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to - * DC_STATE_OUT_FAILED, see dc_msg_get_state(). + * DC_STATE_OUT_FAILED, see `Message.state`. */ "type":"MsgFailed";}&{"chatId":U32;"msgId":U32;})|({ /** * A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to - * DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state(). + * DC_STATE_OUT_MDN_RCVD, see `Message.state`. */ "type":"MsgRead";}&{"chatId":U32;"msgId":U32;})|({ /** * Chat changed. The name or the image of a chat group was changed or members were added or removed. * Or the verify state of a chat has changed. - * See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat() - * and dc_remove_contact_from_chat(). + * See setChatName(), setChatProfileImage(), addContactToChat() + * and removeContactFromChat(). * * This event does not include ephemeral timer modification, which * is a separate event. @@ -129,7 +129,7 @@ export type Event=(({ * * @param data1 (u32) contact_id of the contact for which the location has changed. * If the locations of several contacts have been changed, - * eg. after calling dc_delete_all_locations(), this parameter is set to `None`. + * this parameter is set to `None`. */ "type":"LocationChanged";}&{"contactId":(U32|null);})|({ /** @@ -168,7 +168,7 @@ export type Event=(({ * (Alice, the person who shows the QR code). * * These events are typically sent after a joiner has scanned the QR code - * generated by dc_get_securejoin_qr(). + * generated by getChatSecurejoinQrCodeSvg(). * * @param data1 (int) ID of the contact that wants to join. * @param data2 (int) Progress as: @@ -181,7 +181,7 @@ export type Event=(({ /** * Progress information of a secure-join handshake from the view of the joiner * (Bob, the person who scans the QR code). - * The events are typically sent while dc_join_securejoin(), which + * The events are typically sent while secureJoin(), which * may take some time, is executed. * @param data1 (int) ID of the inviting contact. * @param data2 (int) Progress as: @@ -192,8 +192,8 @@ export type Event=(({ /** * The connectivity to the server changed. * This means that you should refresh the connectivity view - * and possibly the connectivtiy HTML; see dc_get_connectivity() and - * dc_get_connectivity_html() for details. + * and possibly the connectivtiy HTML; see getConnectivity() and + * getConnectivityHtml() for details. */ "type":"ConnectivityChanged";}|{"type":"SelfavatarChanged";}|({"type":"WebxdcStatusUpdate";}&{"msgId":U32;"statusUpdateSerial":U32;})|({ /** diff --git a/deltachat-jsonrpc/typescript/src/client.ts b/deltachat-jsonrpc/typescript/src/client.ts index 993ced6b4..8da65c055 100644 --- a/deltachat-jsonrpc/typescript/src/client.ts +++ b/deltachat-jsonrpc/typescript/src/client.ts @@ -28,6 +28,7 @@ type ContextEvents = { ALL: (event: Event) => void } & { }; export type DcEvent = Event; +export type DcEventType = Extract export class BaseDeltaChat< Transport extends BaseTransport From 836c016f97615de8eb944060b2d38f5f8518b10e Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 15 Oct 2022 20:47:31 +0200 Subject: [PATCH 05/18] jsonrpc: add getMessageHtml (#3671) * add getMessageHtml function * add changelog entry --- CHANGELOG.md | 1 + deltachat-jsonrpc/src/api/mod.rs | 5 +++++ deltachat-jsonrpc/typescript/generated/client.ts | 5 +++++ deltachat-jsonrpc/typescript/generated/types.ts | 2 +- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3be488bc..974b809eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ - `stopIo()` - `exportBackup()` - `importBackup()` + - `getMessageHtml()` #3671 - breaking: jsonrpc: remove function `messageListGetMessageIds()`, it is replaced by `getMessageIds()` and `getMessageListItems()` the latter returns a new `MessageListItem` type, which is the now prefered way of using the message list. - jsonrpc: add type: #3641, #3645 - `MessageSearchResult` diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index 1978b7aac..aa1667fb4 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -915,6 +915,11 @@ impl CommandApi { MessageObject::from_message_id(&ctx, message_id).await } + async fn get_message_html(&self, account_id: u32, message_id: u32)->Result>{ + let ctx = self.get_context(account_id).await?; + MsgId::new(message_id).get_html(&ctx).await + } + async fn message_get_messages( &self, account_id: u32, diff --git a/deltachat-jsonrpc/typescript/generated/client.ts b/deltachat-jsonrpc/typescript/generated/client.ts index 4c7e79be0..5676406ba 100644 --- a/deltachat-jsonrpc/typescript/generated/client.ts +++ b/deltachat-jsonrpc/typescript/generated/client.ts @@ -598,6 +598,11 @@ export class RawClient { } + public getMessageHtml(accountId: T.U32, messageId: T.U32): Promise<(string|null)> { + return (this._transport.request('get_message_html', [accountId, messageId] as RPC.Params)) as Promise<(string|null)>; + } + + public messageGetMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise> { return (this._transport.request('message_get_messages', [accountId, messageIds] as RPC.Params)) as Promise>; } diff --git a/deltachat-jsonrpc/typescript/generated/types.ts b/deltachat-jsonrpc/typescript/generated/types.ts index a28cd136d..283962df1 100644 --- a/deltachat-jsonrpc/typescript/generated/types.ts +++ b/deltachat-jsonrpc/typescript/generated/types.ts @@ -160,4 +160,4 @@ export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"imag export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;}; export type F64=number; export type Location={"locationId":U32;"isIndependent":boolean;"latitude":F64;"longitude":F64;"accuracy":F64;"timestamp":I64;"contactId":U32;"msgId":U32;"chatId":U32;"marker":(string|null);}; -export type __AllTyps=[string,boolean,Record,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,U32,string,(string|null),null,U32,Record,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record,Record,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,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,(U32)[],Record,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record,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,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null]; +export type __AllTyps=[string,boolean,Record,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,U32,string,(string|null),null,U32,Record,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record,Record,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,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,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record,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,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null]; From 72941e51fc173b5a8fb1b1ed106a93d508a1c2c5 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 15 Oct 2022 22:23:58 +0200 Subject: [PATCH 06/18] exit node test when it failed (#3673) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4b56c86e6..e17245d28 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "prebuildify": "cd node && prebuildify -t 16.13.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"", "test": "npm run test:lint && npm run test:mocha", "test:lint": "npm run lint", - "test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail" + "test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit" }, "types": "node/dist/index.d.ts", "version": "1.96.0" From 137567554dff7a6f44346dc939326cd54bb8c255 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 15 Oct 2022 22:58:48 +0200 Subject: [PATCH 07/18] set timeout for node ci tests to 10min (#3675) * set timeout for node ci tests to 10min set timeout for node ci tests to 10min for the test step, macOS takes 12min for the whole workflow with cached core build, so 10min just for the test step should be plenty. * don't forget to set the limit on windows, too --- .github/workflows/node-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/node-tests.yml b/.github/workflows/node-tests.yml index 8beb22b17..6f90530da 100644 --- a/.github/workflows/node-tests.yml +++ b/.github/workflows/node-tests.yml @@ -52,6 +52,7 @@ jobs: npm install --verbose - name: Test + timeout-minutes: 10 if: runner.os != 'Windows' run: | cd node @@ -59,6 +60,7 @@ jobs: env: DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }} - name: Run tests on Windows, except lint + timeout-minutes: 10 if: runner.os == 'Windows' run: | cd node From 36f85a6a5a627cfed6930a83df384f762882e2a5 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 15 Oct 2022 23:03:54 +0200 Subject: [PATCH 08/18] fix nodejs jsonrpc smoke tests (#3674) the solution was to ignore events --- node/test/test.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/node/test/test.js b/node/test/test.js index ee9aa3bad..c15f5e7d3 100644 --- a/node/test/test.js +++ b/node/test/test.js @@ -89,7 +89,11 @@ describe('JSON RPC', function () { const { dc } = DeltaChat.newTemporary() let promise_resolve const promise = new Promise((res, _rej) => { - promise_resolve = res + promise_resolve = (response) => { + // ignore events + const answer = JSON.parse(response) + if (answer['method'] !== 'event') res(answer) + } }) dc.startJsonRpcHandler(promise_resolve) dc.jsonRpcRequest( @@ -106,7 +110,7 @@ describe('JSON RPC', function () { id: 2, result: [1], }, - JSON.parse(await promise) + await promise ) dc.close() }) From f0dede26a3d773e73da20c32cbf728848ae18d50 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 16 Oct 2022 11:30:01 +0000 Subject: [PATCH 09/18] cargo fmt --- deltachat-jsonrpc/src/api/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index aa1667fb4..04bec904c 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -915,7 +915,7 @@ impl CommandApi { MessageObject::from_message_id(&ctx, message_id).await } - async fn get_message_html(&self, account_id: u32, message_id: u32)->Result>{ + async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result> { let ctx = self.get_context(account_id).await?; MsgId::new(message_id).get_html(&ctx).await } From 427adefb42158513d4f4ca7c655c6b1aa392cb96 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sun, 16 Oct 2022 14:53:06 +0200 Subject: [PATCH 10/18] jsonrpc: add `miscGetStickerFolder` and `miscGetStickers` (#3672) * jsonrpc: add `miscGetStickerFolder` and `miscGetStickers` * add pr number to changelog * refactor * fix clippy --- CHANGELOG.md | 1 + deltachat-jsonrpc/src/api/mod.rs | 59 ++++++++++++++++++- .../typescript/generated/client.ts | 13 ++++ .../typescript/generated/types.ts | 2 +- 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 974b809eb..6788711f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - `exportBackup()` - `importBackup()` - `getMessageHtml()` #3671 + - `miscGetStickerFolder` and `miscGetStickers` #3672 - breaking: jsonrpc: remove function `messageListGetMessageIds()`, it is replaced by `getMessageIds()` and `getMessageListItems()` the latter returns a new `MessageListItem` type, which is the now prefered way of using the message list. - jsonrpc: add type: #3641, #3645 - `MessageSearchResult` diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index 04bec904c..20ad089f5 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -24,7 +24,7 @@ use deltachat::{ use std::collections::BTreeMap; use std::sync::Arc; use std::{collections::HashMap, str::FromStr}; -use tokio::sync::RwLock; +use tokio::{fs, sync::RwLock}; use walkdir::WalkDir; use yerpc::rpc; @@ -1500,6 +1500,63 @@ impl CommandApi { // that might get removed later again // --------------------------------------------- + async fn misc_get_sticker_folder(&self, account_id: u32) -> Result { + let ctx = self.get_context(account_id).await?; + let account_folder = ctx + .get_dbfile() + .parent() + .context("account folder not found")?; + let sticker_folder_path = account_folder.join("./stickers"); + fs::create_dir_all(&sticker_folder_path).await?; + sticker_folder_path + .to_str() + .map(|s| s.to_owned()) + .context("path conversion to string failed") + } + + /// for desktop, get stickers from stickers folder, + /// grouped by the folder they are in. + async fn misc_get_stickers(&self, account_id: u32) -> Result>> { + let ctx = self.get_context(account_id).await?; + let account_folder = ctx + .get_dbfile() + .parent() + .context("account folder not found")?; + let sticker_folder_path = account_folder.join("./stickers"); + fs::create_dir_all(&sticker_folder_path).await?; + let mut result = HashMap::new(); + + let mut packs = tokio::fs::read_dir(sticker_folder_path).await?; + while let Some(entry) = packs.next_entry().await? { + if !entry.file_type().await?.is_dir() { + continue; + } + let pack_name = entry.file_name().into_string().unwrap_or_default(); + let mut stickers = tokio::fs::read_dir(entry.path()).await?; + let mut sticker_paths = Vec::new(); + while let Some(sticker_entry) = stickers.next_entry().await? { + if !sticker_entry.file_type().await?.is_file() { + continue; + } + let sticker_name = sticker_entry.file_name().into_string().unwrap_or_default(); + if sticker_name.ends_with(".png") || sticker_name.ends_with(".webp") { + sticker_paths.push( + sticker_entry + .path() + .to_str() + .map(|s| s.to_owned()) + .context("path conversion to string failed")?, + ); + } + } + if !sticker_paths.is_empty() { + result.insert(pack_name, sticker_paths); + } + } + + Ok(result) + } + /// Returns the messageid of the sent message async fn misc_send_text_message( &self, diff --git a/deltachat-jsonrpc/typescript/generated/client.ts b/deltachat-jsonrpc/typescript/generated/client.ts index 5676406ba..875aa6d19 100644 --- a/deltachat-jsonrpc/typescript/generated/client.ts +++ b/deltachat-jsonrpc/typescript/generated/client.ts @@ -895,6 +895,19 @@ export class RawClient { return (this._transport.request('send_videochat_invitation', [accountId, chatId] as RPC.Params)) as Promise; } + + public miscGetStickerFolder(accountId: T.U32): Promise { + return (this._transport.request('misc_get_sticker_folder', [accountId] as RPC.Params)) as Promise; + } + + /** + * for desktop, get stickers from stickers folder, + * grouped by the folder they are in. + */ + public miscGetStickers(accountId: T.U32): Promise> { + return (this._transport.request('misc_get_stickers', [accountId] as RPC.Params)) as Promise>; + } + /** * Returns the messageid of the sent message */ diff --git a/deltachat-jsonrpc/typescript/generated/types.ts b/deltachat-jsonrpc/typescript/generated/types.ts index 283962df1..b36ad0d8e 100644 --- a/deltachat-jsonrpc/typescript/generated/types.ts +++ b/deltachat-jsonrpc/typescript/generated/types.ts @@ -160,4 +160,4 @@ export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"imag export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;}; export type F64=number; export type Location={"locationId":U32;"isIndependent":boolean;"latitude":F64;"longitude":F64;"accuracy":F64;"timestamp":I64;"contactId":U32;"msgId":U32;"chatId":U32;"marker":(string|null);}; -export type __AllTyps=[string,boolean,Record,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,U32,string,(string|null),null,U32,Record,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record,Record,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,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,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record,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,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null]; +export type __AllTyps=[string,boolean,Record,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,U32,string,(string|null),null,U32,Record,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record,Record,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,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,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record,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,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,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]; From 54a157a62993e0a1cddbf437eb3a16d5c0411fa5 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 16 Oct 2022 16:08:55 +0300 Subject: [PATCH 11/18] Prepare 1.97.0 release (#3668) --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 6 +++--- Cargo.toml | 2 +- deltachat-ffi/Cargo.toml | 2 +- deltachat-jsonrpc/Cargo.toml | 2 +- deltachat-jsonrpc/typescript/package.json | 2 +- package.json | 2 +- 7 files changed, 17 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6788711f6..0803b71a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +### API-Changes + +### Changes + +### Fixes + + +## 1.97.0 + ### API-Changes - jsonrpc: add function: #3641, #3645, #3653 - `getChatContacts()` diff --git a/Cargo.lock b/Cargo.lock index 097964508..c3e202187 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -895,7 +895,7 @@ dependencies = [ [[package]] name = "deltachat" -version = "1.96.0" +version = "1.97.0" dependencies = [ "ansi_term", "anyhow", @@ -967,7 +967,7 @@ dependencies = [ [[package]] name = "deltachat-jsonrpc" -version = "1.96.0" +version = "1.97.0" dependencies = [ "anyhow", "async-channel", @@ -996,7 +996,7 @@ dependencies = [ [[package]] name = "deltachat_ffi" -version = "1.96.0" +version = "1.97.0" dependencies = [ "anyhow", "deltachat", diff --git a/Cargo.toml b/Cargo.toml index bb11973b8..6127b7572 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat" -version = "1.96.0" +version = "1.97.0" authors = ["Delta Chat Developers (ML) "] edition = "2021" license = "MPL-2.0" diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index 8d52b53cd..d744f1736 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat_ffi" -version = "1.96.0" +version = "1.97.0" description = "Deltachat FFI" authors = ["Delta Chat Developers (ML) "] edition = "2018" diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index 1eece97cf..65578f890 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-jsonrpc" -version = "1.96.0" +version = "1.97.0" description = "DeltaChat JSON-RPC API" authors = ["Delta Chat Developers (ML) "] edition = "2021" diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json index 17124b5b4..dab8cace9 100644 --- a/deltachat-jsonrpc/typescript/package.json +++ b/deltachat-jsonrpc/typescript/package.json @@ -47,5 +47,5 @@ }, "type": "module", "types": "dist/deltachat.d.ts", - "version": "1.96.0" + "version": "1.97.0" } \ No newline at end of file diff --git a/package.json b/package.json index e17245d28..9e2c316cb 100644 --- a/package.json +++ b/package.json @@ -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.96.0" + "version": "1.97.0" } \ No newline at end of file From b2939d3df32f497f1b5936f598107c29b7649004 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 16 Oct 2022 16:31:53 +0000 Subject: [PATCH 12/18] imap: simplify UPSERT queries on `imap_sync` Use `excluded` and remove noop `WHERE` query. See for official SQLite documentation. --- CHANGELOG.md | 1 + src/imap.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0803b71a7..e7677e674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### API-Changes ### Changes +- simplify `UPSERT` queries #3676 ### Fixes diff --git a/src/imap.rs b/src/imap.rs index 08042f5b2..b732bfbd9 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -2157,8 +2157,8 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32) .sql .execute( "INSERT INTO imap_sync (folder, uid_next) VALUES (?,?) - ON CONFLICT(folder) DO UPDATE SET uid_next=? WHERE folder=?;", - paramsv![folder, uid_next, uid_next, folder], + ON CONFLICT(folder) DO UPDATE SET uid_next=excluded.uid_next", + paramsv![folder, uid_next], ) .await?; Ok(()) @@ -2189,8 +2189,8 @@ pub(crate) async fn set_uidvalidity( .sql .execute( "INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?) - ON CONFLICT(folder) DO UPDATE SET uidvalidity=? WHERE folder=?;", - paramsv![folder, uidvalidity, uidvalidity, folder], + ON CONFLICT(folder) DO UPDATE SET uidvalidity=excluded.uidvalidity", + paramsv![folder, uidvalidity], ) .await?; Ok(()) @@ -2212,8 +2212,8 @@ pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) -> .sql .execute( "INSERT INTO imap_sync (folder, modseq) VALUES (?,?) - ON CONFLICT(folder) DO UPDATE SET modseq=? WHERE folder=?;", - paramsv![folder, modseq, modseq, folder], + ON CONFLICT(folder) DO UPDATE SET modseq=excluded.modseq", + paramsv![folder, modseq], ) .await?; Ok(()) From cd15a0e966f5e90f01f9dfd926e2f7709babe943 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Fri, 21 Oct 2022 19:51:38 +0200 Subject: [PATCH 13/18] 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 --- CHANGELOG.md | 1 + .../typescript/generated/constants.ts | 201 ++++++++++++++++++ deltachat-jsonrpc/typescript/package.json | 3 +- .../typescript/scripts/generate-constants.js | 53 +++++ deltachat-jsonrpc/typescript/src/lib.ts | 1 + 5 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 deltachat-jsonrpc/typescript/generated/constants.ts create mode 100755 deltachat-jsonrpc/typescript/scripts/generate-constants.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e7677e674..b7365a560 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### API-Changes +- jsonrpc: typescript client: export constants under `C` enum, similar to how its exported from `deltachat-node` #3681 ### Changes - simplify `UPSERT` queries #3676 diff --git a/deltachat-jsonrpc/typescript/generated/constants.ts b/deltachat-jsonrpc/typescript/generated/constants.ts new file mode 100644 index 000000000..3f50b6653 --- /dev/null +++ b/deltachat-jsonrpc/typescript/generated/constants.ts @@ -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, +} diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json index dab8cace9..8aa538f09 100644 --- a/deltachat-jsonrpc/typescript/package.json +++ b/deltachat-jsonrpc/typescript/package.json @@ -28,7 +28,7 @@ "main": "dist/deltachat.js", "name": "@deltachat/jsonrpc-client", "scripts": { - "build": "run-s generate-bindings build:tsc build:bundle", + "build": "run-s generate-bindings extract-constants build:tsc build:bundle", "build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js", "build:tsc": "tsc", "docs": "typedoc --out docs deltachat.ts", @@ -37,6 +37,7 @@ "example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.", "example:start": "http-server .", "generate-bindings": "cargo test", + "extract-constants": "node ./scripts/generate-constants.js", "prettier:check": "prettier --check **.ts", "prettier:fix": "prettier --write **.ts", "test": "run-s test:prepare test:run-coverage test:report-coverage", diff --git a/deltachat-jsonrpc/typescript/scripts/generate-constants.js b/deltachat-jsonrpc/typescript/scripts/generate-constants.js new file mode 100755 index 000000000..525a9d42b --- /dev/null +++ b/deltachat-jsonrpc/typescript/scripts/generate-constants.js @@ -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` +); diff --git a/deltachat-jsonrpc/typescript/src/lib.ts b/deltachat-jsonrpc/typescript/src/lib.ts index 153c1d6a2..473d2bd33 100644 --- a/deltachat-jsonrpc/typescript/src/lib.ts +++ b/deltachat-jsonrpc/typescript/src/lib.ts @@ -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"; From e5c9fea52d30260a0c95af8a403da8e1e2f1c98f Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 21 Oct 2022 19:29:02 +0000 Subject: [PATCH 14/18] Implement reactions Co-Authored-By: bjoern Co-Authored-By: Simon Laux --- CHANGELOG.md | 2 + deltachat-ffi/deltachat.h | 102 +++- deltachat-ffi/src/dc_array.rs | 12 + deltachat-ffi/src/lib.rs | 88 ++++ deltachat-jsonrpc/src/api/events.rs | 17 + deltachat-jsonrpc/src/api/mod.rs | 18 + deltachat-jsonrpc/src/api/types/message.rs | 13 + deltachat-jsonrpc/src/api/types/mod.rs | 1 + deltachat-jsonrpc/src/api/types/reactions.rs | 47 ++ .../typescript/generated/client.ts | 12 + .../typescript/generated/events.ts | 4 + .../typescript/generated/types.ts | 21 +- examples/repl/cmdline.rs | 8 + examples/repl/main.rs | 16 +- node/constants.js | 1 + node/events.js | 1 + node/lib/constants.ts | 2 + spec.md | 10 + src/contact.rs | 4 +- src/events.rs | 7 + src/lib.rs | 1 + src/message.rs | 11 + src/mimefactory.rs | 10 +- src/mimeparser.rs | 68 ++- src/param.rs | 3 + src/reaction.rs | 481 ++++++++++++++++++ src/receive_imf.rs | 37 +- src/sql/migrations.rs | 13 + standards.md | 1 + 29 files changed, 983 insertions(+), 28 deletions(-) create mode 100644 deltachat-jsonrpc/src/api/types/reactions.rs create mode 100644 src/reaction.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b7365a560..71076f401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### 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 diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index c9eaf19f4..8417a81ef 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -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. diff --git a/deltachat-ffi/src/dc_array.rs b/deltachat-ffi/src/dc_array.rs index e8997b05a..98def5d0e 100644 --- a/deltachat-ffi/src/dc_array.rs +++ b/deltachat-ffi/src/dc_array.rs @@ -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), + ContactIds(Vec), Chat(Vec), Locations(Vec), Uint(Vec), @@ -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 { 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> for dc_array_t { } } +impl From> for dc_array_t { + fn from(array: Vec) -> Self { + dc_array_t::ContactIds(array) + } +} + impl From> for dc_array_t { fn from(array: Vec) -> Self { dc_array_t::Chat(array) diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 1f521dc74..2b1594a7a 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -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 = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime")); fn block_on(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 _) diff --git a/deltachat-jsonrpc/src/api/events.rs b/deltachat-jsonrpc/src/api/events.rs index 89786697b..9e2a63b67 100644 --- a/deltachat-jsonrpc/src/api/events.rs +++ b/deltachat-jsonrpc/src/api/events.rs @@ -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 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(), diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index 20ad089f5..ba5b9cfd8 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -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, @@ -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, + ) -> Result { + 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 diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index be66ff1fa..181cbb621 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -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, download_state: DownloadState, + + reactions: Option, } #[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, }) } } diff --git a/deltachat-jsonrpc/src/api/types/mod.rs b/deltachat-jsonrpc/src/api/types/mod.rs index 6f344d498..bb44323fb 100644 --- a/deltachat-jsonrpc/src/api/types/mod.rs +++ b/deltachat-jsonrpc/src/api/types/mod.rs @@ -9,6 +9,7 @@ pub mod contact; pub mod location; pub mod message; pub mod provider_info; +pub mod reactions; pub mod webxdc; pub fn color_int_to_hex_string(color: u32) -> String { diff --git a/deltachat-jsonrpc/src/api/types/reactions.rs b/deltachat-jsonrpc/src/api/types/reactions.rs new file mode 100644 index 000000000..8717ebdc2 --- /dev/null +++ b/deltachat-jsonrpc/src/api/types/reactions.rs @@ -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>, + /// Unique reactions and their count + reactions: BTreeMap, +} + +impl From for JSONRPCReactions { + fn from(reactions: Reactions) -> Self { + let mut reactions_by_contact: BTreeMap> = BTreeMap::new(); + let mut unique_reactions: BTreeMap = BTreeMap::new(); + + for contact_id in reactions.contacts() { + let reaction = reactions.get(contact_id); + if reaction.is_empty() { + continue; + } + let emojis: Vec = 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, + } + } +} diff --git a/deltachat-jsonrpc/typescript/generated/client.ts b/deltachat-jsonrpc/typescript/generated/client.ts index 875aa6d19..c243b05d3 100644 --- a/deltachat-jsonrpc/typescript/generated/client.ts +++ b/deltachat-jsonrpc/typescript/generated/client.ts @@ -878,6 +878,18 @@ export class RawClient { return (this._transport.request('send_sticker', [accountId, chatId, stickerPath] as RPC.Params)) as Promise; } + /** + * 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 { + return (this._transport.request('send_reaction', [accountId, messageId, reaction] as RPC.Params)) as Promise; + } + public removeDraft(accountId: T.U32, chatId: T.U32): Promise { return (this._transport.request('remove_draft', [accountId, chatId] as RPC.Params)) as Promise; diff --git a/deltachat-jsonrpc/typescript/generated/events.ts b/deltachat-jsonrpc/typescript/generated/events.ts index a483c68cb..287a9150d 100644 --- a/deltachat-jsonrpc/typescript/generated/events.ts +++ b/deltachat-jsonrpc/typescript/generated/events.ts @@ -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. diff --git a/deltachat-jsonrpc/typescript/generated/types.ts b/deltachat-jsonrpc/typescript/generated/types.ts index b36ad0d8e..d776ccd7e 100644 --- a/deltachat-jsonrpc/typescript/generated/types.ts +++ b/deltachat-jsonrpc/typescript/generated/types.ts @@ -147,7 +147,24 @@ export type WebxdcMessageInfo={ */ "internetAccess":boolean;}; export type DownloadState=("Done"|"Available"|"Failure"|"InProgress"); -export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;}; + +/** + * Structure representing all reactions to a particular message. + */ +export type Reactions= +/** + * Structure representing all reactions to a particular message. + */ +{ +/** + * Map from a contact to it's reaction to message. + */ +"reactionsByContact":Record; +/** + * Unique reactions and their count + */ +"reactions":Record;}; +export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;"reactions":(Reactions|null);}; export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"image":(string|null);"imageMimeType":(string|null);"chatName":string;"chatProfileImage":(string|null); /** * also known as summary_text1 @@ -160,4 +177,4 @@ export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"imag export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;}; export type F64=number; export type Location={"locationId":U32;"isIndependent":boolean;"latitude":F64;"longitude":F64;"accuracy":F64;"timestamp":I64;"contactId":U32;"msgId":U32;"chatId":U32;"marker":(string|null);}; -export type __AllTyps=[string,boolean,Record,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,U32,string,(string|null),null,U32,Record,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record,Record,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,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,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record,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,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,U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null]; +export type __AllTyps=[string,boolean,Record,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,U32,string,(string|null),null,U32,Record,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record,Record,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,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,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record,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,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,(string)[],U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,Record,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]; diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index ad5a53681..8854cc92e 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -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 \n\ markseen \n\ delmsg \n\ + react []\n\ ===========================Contact commands==\n\ listcontacts []\n\ listverified []\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 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, diff --git a/examples/repl/main.rs b/examples/repl/main.rs index 46c2fb746..f9403d064 100644 --- a/examples/repl/main.rs +++ b/examples/repl/main.rs @@ -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", diff --git a/node/constants.js b/node/constants.js index 510b22a60..ba5bd1a71 100644 --- a/node/constants.js +++ b/node/constants.js @@ -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, diff --git a/node/events.js b/node/events.js index 58d13f531..dba37e240 100644 --- a/node/events.js +++ b/node/events.js @@ -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', diff --git a/node/lib/constants.ts b/node/lib/constants.ts index 0a25d69c4..5b32cf7e5 100644 --- a/node/lib/constants.ts +++ b/node/lib/constants.ts @@ -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', diff --git a/spec.md b/spec.md index e2042d6d7..2a9e675be 100644 --- a/spec.md +++ b/spec.md @@ -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. diff --git a/src/contact.rs b/src/contact.rs index 0d9af4a4c..5b9adceaa 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -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 { diff --git a/src/events.rs b/src/events.rs index 29fb039b2..7ea3c67a3 100644 --- a/src/events.rs +++ b/src/events.rs @@ -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. /// diff --git a/src/lib.rs b/src/lib.rs index 1b510068a..04f2a2013 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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"; diff --git a/src/message.rs b/src/message.rs index 7ff5b3667..3da05a34c 100644 --- a/src/message.rs +++ b/src/message.rs @@ -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 { 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); } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 0221ff7b6..90c22044c 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -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; diff --git a/src/mimeparser.rs b/src/mimeparser.rs index a8960dfd0..0683a0064 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -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(()) + } } diff --git a/src/param.rs b/src/param.rs index 44c81e598..cd791d502 100644 --- a/src/param.rs +++ b/src/param.rs @@ -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', diff --git a/src/reaction.rs b/src/reaction.rs new file mode 100644 index 000000000..b00824ba9 --- /dev/null +++ b/src/reaction.rs @@ -0,0 +1,481 @@ +//! # 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, +} + +impl Reactions { + /// Returns vector of contacts that reacted to the message. + pub fn contacts(&self) -> Vec { + 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 = 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 { + 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 { + 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 { + let reaction_str: Option = 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 { + 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::message::MessageState; + use crate::receive_imf::receive_imf; + 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(()) + } +} diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 3880452ec..7e71d0081 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -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; @@ -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,6 +1057,17 @@ 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 txt_raw = "".to_string(); let mut stmt = conn.prepare_cached( r#" @@ -1113,7 +1128,7 @@ 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![ rfc724_mid, diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 03703c357..73abbe2cf 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -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) diff --git a/standards.md b/standards.md index 87f1f8d20..916038bca 100644 --- a/standards.md +++ b/standards.md @@ -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)) From b5d238f7f4f4d6d633414abe94680833467ae2c6 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 22 Oct 2022 09:55:43 +0000 Subject: [PATCH 15/18] Keep reactions when downloading partially downloaded message --- src/download.rs | 40 ++++++++++++++++++++++++--- src/reaction.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/src/download.rs b/src/download.rs index e64c26d4e..3a7113ef0 100644 --- a/src/download.rs +++ b/src/download.rs @@ -80,12 +80,46 @@ impl Context { 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])?; + // Move all the data from full message to placeholder. + // `id` stays the same, so foreign key constraints are not violated. + // For example, `reactions.msg_id` foreign key keeps pointing + // to the same message. transaction.execute( - "UPDATE msgs SET id=? WHERE id=?", + "UPDATE msgs + SET + rfc724_mid=full.rfc724_mid, + chat_id=full.chat_id, + from_id=full.from_id, + to_id=full.to_id, + timestamp=full.timestamp, + type=full.type, + state=full.state, + msgrmsg=full.msgrmsg, + bytes=full.bytes, + txt=full.txt, + txt_raw=full.txt_raw, + param=full.param, + starred=full.starred, + timestamp_sent=full.timestamp_sent, + timestamp_rcvd=full.timestamp_rcvd, + hidden=full.hidden, + mime_headers=full.mime_headers, + mime_in_reply_to=full.mime_in_reply_to, + mime_references=full.mime_references, + move_state=full.move_state, + location_id=full.location_id, + error=full.error, + ephemeral_timer=full.ephemeral_timer, + ephemeral_timestamp=full.ephemeral_timestamp, + mime_modified=full.mime_modified, + subject=full.subject, + download_state=full.download_state, + hop_info=full.hop_info + FROM msgs AS full + WHERE msgs.id=?1 AND full.id=?2", paramsv![placeholder_msg_id, full_msg_id], )?; + transaction.execute("DELETE FROM msgs WHERE id=?;", paramsv![full_msg_id])?; Ok(()) }) .await?; diff --git a/src/reaction.rs b/src/reaction.rs index b00824ba9..f463e3f00 100644 --- a/src/reaction.rs +++ b/src/reaction.rs @@ -291,8 +291,9 @@ mod tests { 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; + use crate::receive_imf::{receive_imf, receive_imf_inner}; use crate::test_utils::TestContext; #[test] @@ -478,4 +479,73 @@ Content-Disposition: reaction\n\ 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 \n\ + To: Alice \n\ + Chat-Version: 1.0\n\ + Subject: subject\n\ + Message-ID: \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(()) + } } From 434e53e922a49f348856b665d70eebda2645d5ce Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 22 Oct 2022 19:34:14 +0000 Subject: [PATCH 16/18] 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. --- src/download.rs | 72 +--------------------------------------------- src/receive_imf.rs | 52 +++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 86 deletions(-) diff --git a/src/download.rs b/src/download.rs index 3a7113ef0..a05a35e53 100644 --- a/src/download.rs +++ b/src/download.rs @@ -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,76 +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| { - // Move all the data from full message to placeholder. - // `id` stays the same, so foreign key constraints are not violated. - // For example, `reactions.msg_id` foreign key keeps pointing - // to the same message. - transaction.execute( - "UPDATE msgs - SET - rfc724_mid=full.rfc724_mid, - chat_id=full.chat_id, - from_id=full.from_id, - to_id=full.to_id, - timestamp=full.timestamp, - type=full.type, - state=full.state, - msgrmsg=full.msgrmsg, - bytes=full.bytes, - txt=full.txt, - txt_raw=full.txt_raw, - param=full.param, - starred=full.starred, - timestamp_sent=full.timestamp_sent, - timestamp_rcvd=full.timestamp_rcvd, - hidden=full.hidden, - mime_headers=full.mime_headers, - mime_in_reply_to=full.mime_in_reply_to, - mime_references=full.mime_references, - move_state=full.move_state, - location_id=full.location_id, - error=full.error, - ephemeral_timer=full.ephemeral_timer, - ephemeral_timestamp=full.ephemeral_timestamp, - mime_modified=full.mime_modified, - subject=full.subject, - download_state=full.download_state, - hop_info=full.hop_info - FROM msgs AS full - WHERE msgs.id=?1 AND full.id=?2", - paramsv![placeholder_msg_id, full_msg_id], - )?; - transaction.execute("DELETE FROM msgs WHERE id=?;", paramsv![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 { diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 7e71d0081..ddf33ab35 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -405,7 +405,7 @@ async fn add_parts( from_id: ContactId, seen: bool, is_partial_download: Option, - replace_msg_id: Option, + mut replace_msg_id: Option, fetching_existing_messages: bool, prevent_rename: bool, ) -> Result { @@ -1068,11 +1068,30 @@ async fn add_parts( .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, @@ -1082,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 "#, )?; @@ -1110,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 { @@ -1131,6 +1154,7 @@ INSERT INTO msgs 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 }, @@ -1169,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); @@ -1177,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?; From 7551c84c4f484e9dd944bd18053e3332a9f7e82c Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sun, 23 Oct 2022 10:41:32 +0200 Subject: [PATCH 17/18] jsonrpc: move qr/uri type to dedicated file (#3687) #skip-changelog --- deltachat-jsonrpc/src/api/mod.rs | 2 +- deltachat-jsonrpc/src/api/types/mod.rs | 215 +------------------------ deltachat-jsonrpc/src/api/types/qr.rs | 213 ++++++++++++++++++++++++ 3 files changed, 215 insertions(+), 215 deletions(-) create mode 100644 deltachat-jsonrpc/src/api/types/qr.rs diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index ba5b9cfd8..31f09e43f 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -35,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; diff --git a/deltachat-jsonrpc/src/api/types/mod.rs b/deltachat-jsonrpc/src/api/types/mod.rs index bb44323fb..5f8b541bd 100644 --- a/deltachat-jsonrpc/src/api/types/mod.rs +++ b/deltachat-jsonrpc/src/api/types/mod.rs @@ -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,7 @@ pub mod contact; pub mod location; pub mod message; pub mod provider_info; +pub mod qr; pub mod reactions; pub mod webxdc; @@ -23,213 +20,3 @@ fn maybe_empty_string_to_option(string: String) -> Option { 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, - }, - FprWithoutAddr { - fingerprint: String, - }, - Account { - domain: String, - }, - WebrtcInstance { - domain: String, - instance_pattern: String, - }, - Addr { - contact_id: u32, - draft: Option, - }, - 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 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 }, - } - } -} diff --git a/deltachat-jsonrpc/src/api/types/qr.rs b/deltachat-jsonrpc/src/api/types/qr.rs new file mode 100644 index 000000000..0a38d43ce --- /dev/null +++ b/deltachat-jsonrpc/src/api/types/qr.rs @@ -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, + }, + FprWithoutAddr { + fingerprint: String, + }, + Account { + domain: String, + }, + WebrtcInstance { + domain: String, + instance_pattern: String, + }, + Addr { + contact_id: u32, + draft: Option, + }, + 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 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 }, + } + } +} From aa140159196c3f7db0b1e230614bb44e4d387039 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 23 Oct 2022 11:25:27 +0000 Subject: [PATCH 18/18] sql: every Result is anyhow::Result --- src/sql.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/sql.rs b/src/sql.rs index 0a56ada1f..ce31b5995 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -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 { + pub async fn count(&self, query: &str, params: impl rusqlite::Params) -> Result { 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(&self, callback: G) -> anyhow::Result + pub async fn transaction(&self, callback: G) -> Result where H: Send + 'static, - G: Send + 'static + FnOnce(&mut rusqlite::Transaction<'_>) -> anyhow::Result, + G: Send + 'static + FnOnce(&mut rusqlite::Transaction<'_>) -> Result, { 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 { + pub async fn table_exists(&self, name: &str) -> Result { 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 { + pub async fn col_exists(&self, table_name: &str, col_name: &str) -> Result { 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> + ) -> Result> where F: FnOnce(&rusqlite::Row) -> rusqlite::Result, { @@ -516,7 +516,7 @@ impl Sql { &self, query: &str, params: impl rusqlite::Params, - ) -> anyhow::Result> + ) -> Result> where T: rusqlite::types::FromSql, {