mirror of
https://github.com/chatmail/core.git
synced 2026-04-24 08:56:29 +03:00
Merge branch 'master' into flub/ongoing-guard
This commit is contained in:
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -120,7 +120,7 @@ jobs:
|
||||
- name: Upload C library
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}-${{matrix.rust}}-libdeltachat.a
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug/libdeltachat.a
|
||||
retention-days: 1
|
||||
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
- name: Upload deltachat-rpc-server
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}-${{matrix.rust}}-deltachat-rpc-server
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: target/debug/deltachat-rpc-server
|
||||
retention-days: 1
|
||||
|
||||
@@ -196,7 +196,7 @@ jobs:
|
||||
- name: Download libdeltachat.a
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}-${{matrix.rust}}-libdeltachat.a
|
||||
name: ${{ matrix.os }}-libdeltachat.a
|
||||
path: target/debug
|
||||
|
||||
- name: Install python
|
||||
@@ -255,7 +255,7 @@ jobs:
|
||||
- name: Download deltachat-rpc-server
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}-${{matrix.rust}}-deltachat-rpc-server
|
||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||
path: target/debug
|
||||
|
||||
- name: Make deltachat-rpc-server executable
|
||||
|
||||
33
CHANGELOG.md
33
CHANGELOG.md
@@ -7,10 +7,37 @@
|
||||
- Remove upper limit on the attachment size. #4253
|
||||
- Update rPGP to 0.10.1. #4236
|
||||
- Compress `mime_headers` column with HTML emails stored in database
|
||||
- Strip BIDI characters in system messages, files, group names and contact names #3479
|
||||
- maybe_add_time_based_warnings(): Use release date instead of the provider DB update one
|
||||
- Remove confusing log line "ignoring unsolicited response Recent(…)" #3934
|
||||
- Cleanly terminate deltachat-rpc-server.
|
||||
Also terminate on ctrl-c.
|
||||
- Refactorings #4317
|
||||
- Add JSON-RPC API `can_send()`.
|
||||
- New `dc_get_next_msgs()` and `dc_wait_next_msgs()` C APIs.
|
||||
New `get_next_msgs()` and `wait_next_msgs()` JSON-RPC API.
|
||||
These APIs can be used by bots to get all unprocessed messages
|
||||
in the order of their arrival and wait for them without relying on events.
|
||||
- Async Python API `get_fresh_messages_in_arrival_order()` is deprecated
|
||||
in favor of `get_next_msgs()` and `wait_next_msgs()`.
|
||||
- New Python bindings API `Account.wait_next_incoming_message()`.
|
||||
- New Python bindings APIs `Message.is_from_self()` and `Message.is_from_device()`.
|
||||
|
||||
### Fixes
|
||||
- Fix python bindings README documentation on installing the bindings from source.
|
||||
- Show a warning if quota list is empty #4261
|
||||
- Update "accounts.toml" atomically
|
||||
- Don't let blocking be bypassed using groups #4316
|
||||
|
||||
## [1.112.6] - 2023-04-04
|
||||
|
||||
### Changes
|
||||
|
||||
- Add a device message after backup transfer #4301
|
||||
|
||||
### Fixed
|
||||
|
||||
- Updated `iroh` from 0.4.0 to 0.4.1 to fix transfer of large accounts with many blob files.
|
||||
|
||||
## [1.112.5] - 2023-04-02
|
||||
|
||||
@@ -32,8 +59,8 @@
|
||||
|
||||
### Changes
|
||||
- Update iroh, remove `default-net` from `[patch.crates-io]` section.
|
||||
- transfer backup: Connect to mutliple provider addresses concurrently. This should speed up connection time significantly on the getter side. #4240
|
||||
- Make sure BackupProvider is cancelled on drop (or dc_backup_provider_unref). The BackupProvider will now alaway finish with an IMEX event of 1000 or 0, previoulsy it would sometimes finishe with 1000 (success) when it really was 0 (failure). #4242
|
||||
- transfer backup: Connect to multiple provider addresses concurrently. This should speed up connection time significantly on the getter side. #4240
|
||||
- Make sure BackupProvider is cancelled on drop (or `dc_backup_provider_unref`). The BackupProvider will now always finish with an IMEX event of 1000 or 0, previously it would sometimes finished with 1000 (success) when it really was 0 (failure). #4242
|
||||
|
||||
### Fixes
|
||||
- Do not return media from trashed messages in the "All media" view. #4247
|
||||
@@ -207,7 +234,6 @@
|
||||
- Do not treat invalid email addresses as an exception #3942
|
||||
- Add timeouts to HTTP requests #3948
|
||||
|
||||
|
||||
## 1.105.0
|
||||
|
||||
### Changes
|
||||
@@ -293,7 +319,6 @@
|
||||
- Disable read timeout during IMAP IDLE #3826
|
||||
- Bots automatically accept mailing lists #3831
|
||||
|
||||
|
||||
## 1.102.0
|
||||
|
||||
### Changes
|
||||
|
||||
19
Cargo.lock
generated
19
Cargo.lock
generated
@@ -905,9 +905,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.7"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c"
|
||||
checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
@@ -1157,7 +1157,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.112.5"
|
||||
version = "1.112.6"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1231,7 +1231,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.112.5"
|
||||
version = "1.112.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
@@ -1254,7 +1254,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "1.112.5"
|
||||
version = "1.112.6"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1269,7 +1269,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.112.5"
|
||||
version = "1.112.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1280,6 +1280,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"yerpc",
|
||||
]
|
||||
|
||||
@@ -1293,7 +1294,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.112.5"
|
||||
version = "1.112.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -2176,9 +2177,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.16"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d"
|
||||
checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.112.5"
|
||||
version = "1.112.6"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.65"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.112.5"
|
||||
version = "1.112.6"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -181,12 +181,17 @@ typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
|
||||
* and check it in the event loop thread
|
||||
* every time before calling dc_get_next_event().
|
||||
* To terminate the event loop, main thread should:
|
||||
* 1. Notify event loop that it should terminate by atomically setting the
|
||||
* boolean flag in the memory shared between the main thread and event loop.
|
||||
* 1. Notify background threads,
|
||||
* such as event loop (blocking in dc_get_next_event())
|
||||
* and message processing loop (blocking in dc_wait_next_msgs()),
|
||||
* that they should terminate by atomically setting the
|
||||
* boolean flag in the memory
|
||||
* shared between the main thread and background loop threads.
|
||||
* 2. Call dc_stop_io() or dc_accounts_stop_io(), depending
|
||||
* on whether a single account or account manager is used.
|
||||
* Stopping I/O is guaranteed to emit at least one event
|
||||
* and interrupt the event loop even if it was blocked on dc_get_next_event().
|
||||
* Stopping I/O is guaranteed to interrupt a single dc_wait_next_msgs().
|
||||
* 3. Wait until the event loop thread notices the flag,
|
||||
* exits the event loop and terminates.
|
||||
* 4. Call dc_context_unref() or dc_accounts_unref().
|
||||
@@ -457,6 +462,16 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* Prevents adding the "Device messages" and "Saved messages" chats,
|
||||
* adds Auto-Submitted header to outgoing messages
|
||||
* and accepts contact requests automatically (calling dc_accept_chat() is not needed for bots).
|
||||
* - `last_msg_id` = database ID of the last message processed by the bot.
|
||||
* This ID and IDs below it are guaranteed not to be returned
|
||||
* by dc_get_next_msgs() and dc_wait_next_msgs().
|
||||
* The value is updated automatically
|
||||
* when dc_markseen_msgs() is called,
|
||||
* but the bot can also set it manually if it processed
|
||||
* the message but does not want to mark it as seen.
|
||||
* For most bots calling `dc_markseen_msgs()` is the
|
||||
* recommended way to update this value
|
||||
* even for self-sent messages.
|
||||
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
|
||||
* 0=do not fetch existing messages on configure.
|
||||
* In both cases, existing recipients are added to the contact database.
|
||||
@@ -1343,6 +1358,56 @@ int dc_estimate_deletion_cnt (dc_context_t* context, int from_ser
|
||||
dc_array_t* dc_get_fresh_msgs (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Returns the message IDs of all messages of any chat
|
||||
* with a database ID higher than `last_msg_id` config value.
|
||||
*
|
||||
* This function is intended for use by bots.
|
||||
* Self-sent messages, device messages,
|
||||
* messages from contact requests
|
||||
* and muted chats are included,
|
||||
* but messages from explicitly blocked contacts
|
||||
* and chats are ignored.
|
||||
*
|
||||
* This function may be called as a part of event loop
|
||||
* triggered by DC_EVENT_INCOMING_MSG if you are only interested
|
||||
* in the incoming messages.
|
||||
* Otherwise use a separate message processing loop
|
||||
* calling dc_wait_next_msgs() in a separate thread.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @return An array of message IDs, must be dc_array_unref()'d when no longer used.
|
||||
* On errors, the list is empty. NULL is never returned.
|
||||
*/
|
||||
dc_array_t* dc_get_next_msgs (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Waits for notification of new messages
|
||||
* and returns an array of new message IDs.
|
||||
* See the documentation for dc_get_next_msgs()
|
||||
* for the details of return value.
|
||||
*
|
||||
* This function waits for internal notification of
|
||||
* a new message in the database and returns afterwards.
|
||||
* Notification is also sent when I/O is started
|
||||
* to allow processing new messages
|
||||
* and when I/O is stopped using dc_stop_io() or dc_accounts_stop_io()
|
||||
* to allow for manual interruption of the message processing loop.
|
||||
* The function may return an empty array if there are
|
||||
* no messages after notification,
|
||||
* which may happen on start or if the message is quickly deleted
|
||||
* after adding it to the database.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @return An array of message IDs, must be dc_array_unref()'d when no longer used.
|
||||
* On errors, the list is empty. NULL is never returned.
|
||||
*/
|
||||
dc_array_t* dc_wait_next_msgs (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Mark all messages in a chat as _noticed_.
|
||||
* _Noticed_ messages are no longer _fresh_ and do not count as being unseen
|
||||
@@ -1942,6 +2007,11 @@ int dc_resend_msgs (dc_context_t* context, const uint3
|
||||
* Moreover, timer is started for incoming ephemeral messages.
|
||||
* This also happens for contact requests chats.
|
||||
*
|
||||
* This function updates last_msg_id configuration value
|
||||
* to the maximum of the current value and IDs passed to this function.
|
||||
* Bots which mark messages as seen can rely on this side effect
|
||||
* to avoid updating last_msg_id value manually.
|
||||
*
|
||||
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -7069,6 +7139,11 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// `%1$s` will be replaced by name and address of the account.
|
||||
#define DC_STR_BACKUP_TRANSFER_QR 162
|
||||
|
||||
/// "Account transferred to your second device."
|
||||
///
|
||||
/// Used as a device message after a successful backup transfer.
|
||||
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -1280,6 +1280,50 @@ pub unsafe extern "C" fn dc_get_fresh_msgs(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_next_msgs(context: *mut dc_context_t) -> *mut dc_array::dc_array_t {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_next_msgs()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
let msg_ids = block_on(ctx.get_next_msgs())
|
||||
.context("failed to get next messages")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default();
|
||||
let arr = dc_array_t::from(
|
||||
msg_ids
|
||||
.iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
);
|
||||
Box::into_raw(Box::new(arr))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_wait_next_msgs(
|
||||
context: *mut dc_context_t,
|
||||
) -> *mut dc_array::dc_array_t {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_wait_next_msgs()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
let msg_ids = block_on(ctx.wait_next_msgs())
|
||||
.context("failed to wait for next messages")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default();
|
||||
let arr = dc_array_t::from(
|
||||
msg_ids
|
||||
.iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
);
|
||||
Box::into_raw(Box::new(arr))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id: u32) {
|
||||
if context.is_null() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.112.5"
|
||||
version = "1.112.6"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -453,6 +453,49 @@ impl CommandApi {
|
||||
ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await
|
||||
}
|
||||
|
||||
/// Gets messages to be processed by the bot and returns their IDs.
|
||||
///
|
||||
/// Only messages with database ID higher than `last_msg_id` config value
|
||||
/// are returned. After processing the messages, the bot should
|
||||
/// update `last_msg_id` by calling [`markseen_msgs`]
|
||||
/// or manually updating the value to avoid getting already
|
||||
/// processed messages.
|
||||
///
|
||||
/// [`markseen_msgs`]: Self::markseen_msgs
|
||||
async fn get_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg_ids = ctx
|
||||
.get_next_msgs()
|
||||
.await?
|
||||
.iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect();
|
||||
Ok(msg_ids)
|
||||
}
|
||||
|
||||
/// Waits for messages to be processed by the bot and returns their IDs.
|
||||
///
|
||||
/// This function is similar to [`get_next_msgs`],
|
||||
/// but waits for internal new message notification before returning.
|
||||
/// New message notification is sent when new message is added to the database,
|
||||
/// on initialization, when I/O is started and when I/O is stopped.
|
||||
/// This allows bots to use `wait_next_msgs` in a loop to process
|
||||
/// old messages after initialization and during the bot runtime.
|
||||
/// To shutdown the bot, stopping I/O can be used to interrupt
|
||||
/// pending or next `wait_next_msgs` call.
|
||||
///
|
||||
/// [`get_next_msgs`]: Self::get_next_msgs
|
||||
async fn wait_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg_ids = ctx
|
||||
.wait_next_msgs()
|
||||
.await?
|
||||
.iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect();
|
||||
Ok(msg_ids)
|
||||
}
|
||||
|
||||
/// Estimate the number of messages that will be deleted
|
||||
/// by the set_config()-options `delete_device_after` or `delete_server_after`.
|
||||
/// This is typically used to show the estimated impact to the user
|
||||
@@ -944,6 +987,11 @@ impl CommandApi {
|
||||
/// Moreover, timer is started for incoming ephemeral messages.
|
||||
/// This also happens for contact requests chats.
|
||||
///
|
||||
/// This function updates `last_msg_id` configuration value
|
||||
/// to the maximum of the current value and IDs passed to this function.
|
||||
/// Bots which mark messages as seen can rely on this side effect
|
||||
/// to avoid updating `last_msg_id` value manually.
|
||||
///
|
||||
/// One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
|
||||
async fn markseen_msgs(&self, account_id: u32, msg_ids: Vec<u32>) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -1701,6 +1749,15 @@ impl CommandApi {
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
/// Checks if messages can be sent to a given chat.
|
||||
async fn can_send(&self, account_id: u32, chat_id: u32) -> Result<bool> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
let chat = Chat::load_from_db(&ctx, chat_id).await?;
|
||||
let can_send = chat.can_send(&ctx).await?;
|
||||
Ok(can_send)
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// functions for the composer
|
||||
// the composer is the message input field
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.112.5"
|
||||
"version": "1.112.6"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.112.5"
|
||||
version = "1.112.6"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import asyncio
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from deltachat_rpc_client import DeltaChat, EventType, Rpc
|
||||
from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -30,9 +30,9 @@ async def main():
|
||||
await deltachat.start_io()
|
||||
|
||||
async def process_messages():
|
||||
for message in await account.get_fresh_messages_in_arrival_order():
|
||||
for message in await account.get_next_messages():
|
||||
snapshot = await message.get_snapshot()
|
||||
if not snapshot.is_bot and not snapshot.is_info:
|
||||
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
|
||||
await snapshot.chat.send_text(snapshot.text)
|
||||
await snapshot.message.mark_seen()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from ._utils import AttrDict, run_bot_cli, run_client_cli
|
||||
from .account import Account
|
||||
from .chat import Chat
|
||||
from .client import Bot, Client
|
||||
from .const import EventType
|
||||
from .const import EventType, SpecialContactId
|
||||
from .contact import Contact
|
||||
from .deltachat import DeltaChat
|
||||
from .message import Message
|
||||
@@ -19,6 +19,7 @@ __all__ = [
|
||||
"DeltaChat",
|
||||
"EventType",
|
||||
"Message",
|
||||
"SpecialContactId",
|
||||
"Rpc",
|
||||
"run_bot_cli",
|
||||
"run_client_cli",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
||||
from warnings import warn
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .chat import Chat
|
||||
@@ -239,7 +240,22 @@ class Account:
|
||||
fresh_msg_ids = await self._rpc.get_fresh_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
|
||||
async def get_next_messages(self) -> List[Message]:
|
||||
"""Return a list of next messages."""
|
||||
next_msg_ids = await self._rpc.get_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
async def wait_next_messages(self) -> List[Message]:
|
||||
"""Wait for new messages and return a list of them."""
|
||||
next_msg_ids = await self._rpc.wait_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
async def get_fresh_messages_in_arrival_order(self) -> List[Message]:
|
||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||
warn(
|
||||
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id))
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
|
||||
@@ -105,6 +105,10 @@ class Chat:
|
||||
info = await self._rpc.get_full_chat_by_id(self.account.id, self.id)
|
||||
return AttrDict(chat=self, **info)
|
||||
|
||||
async def can_send(self) -> bool:
|
||||
"""Return true if messages can be sent to the chat."""
|
||||
return await self._rpc.can_send(self.account.id, self.id)
|
||||
|
||||
async def send_message(
|
||||
self,
|
||||
text: Optional[str] = None,
|
||||
|
||||
@@ -20,7 +20,7 @@ from ._utils import (
|
||||
parse_system_image_changed,
|
||||
parse_system_title_changed,
|
||||
)
|
||||
from .const import COMMAND_PREFIX, EventType, SystemMessageType
|
||||
from .const import COMMAND_PREFIX, EventType, SpecialContactId, SystemMessageType
|
||||
from .events import (
|
||||
EventFilter,
|
||||
GroupImageChanged,
|
||||
@@ -189,9 +189,10 @@ class Client:
|
||||
|
||||
async def _process_messages(self) -> None:
|
||||
if self._should_process_messages:
|
||||
for message in await self.account.get_fresh_messages_in_arrival_order():
|
||||
for message in await self.account.get_next_messages():
|
||||
snapshot = await message.get_snapshot()
|
||||
await self._on_new_msg(snapshot)
|
||||
if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]:
|
||||
await self._on_new_msg(snapshot)
|
||||
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
|
||||
await self._handle_info_msg(snapshot)
|
||||
await snapshot.message.mark_seen()
|
||||
|
||||
@@ -98,8 +98,8 @@ async def test_account(acfactory) -> None:
|
||||
assert await alice.get_chatlist()
|
||||
assert await alice.get_chatlist(snapshot=True)
|
||||
assert await alice.get_qr_code()
|
||||
await alice.get_fresh_messages()
|
||||
await alice.get_fresh_messages_in_arrival_order()
|
||||
assert await alice.get_fresh_messages()
|
||||
assert await alice.get_next_messages()
|
||||
|
||||
group = await alice.create_group("test group")
|
||||
await group.add_contact(alice_contact_bob)
|
||||
@@ -147,7 +147,9 @@ async def test_chat(acfactory) -> None:
|
||||
assert alice_chat_bob != bob_chat_alice
|
||||
assert repr(alice_chat_bob)
|
||||
await alice_chat_bob.delete()
|
||||
assert not await bob_chat_alice.can_send()
|
||||
await bob_chat_alice.accept()
|
||||
assert await bob_chat_alice.can_send()
|
||||
await bob_chat_alice.block()
|
||||
bob_chat_alice = await snapshot.sender.create_chat()
|
||||
await bob_chat_alice.mute()
|
||||
@@ -303,3 +305,29 @@ async def test_bot(acfactory) -> None:
|
||||
await acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="/help")
|
||||
mock.hook.assert_called_once_with(event.msg_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_wait_next_messages(acfactory) -> None:
|
||||
alice = await acfactory.new_configured_account()
|
||||
|
||||
# Create a bot account so it does not receive device messages in the beginning.
|
||||
bot = await acfactory.new_preconfigured_account()
|
||||
await bot.set_config("bot", "1")
|
||||
await bot.configure()
|
||||
|
||||
# There are no old messages and the call returns immediately.
|
||||
assert not await bot.wait_next_messages()
|
||||
|
||||
# Bot starts waiting for messages.
|
||||
next_messages_task = asyncio.create_task(bot.wait_next_messages())
|
||||
|
||||
bot_addr = await bot.get_config("addr")
|
||||
alice_contact_bot = await alice.create_contact(bot_addr, "Bob")
|
||||
alice_chat_bot = await alice_contact_bot.create_chat()
|
||||
await alice_chat_bot.send_text("Hello!")
|
||||
|
||||
next_messages = await next_messages_task
|
||||
assert len(next_messages) == 1
|
||||
snapshot = await next_messages[0].get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.112.5"
|
||||
version = "1.112.6"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
@@ -20,6 +20,7 @@ log = "0.4"
|
||||
serde_json = "1.0.95"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.27.0", features = ["io-std"] }
|
||||
tokio-util = "0.7.7"
|
||||
yerpc = { version = "0.4.0", features = ["anyhow_expose"] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::env;
|
||||
///!
|
||||
///! It speaks JSON Lines over stdio.
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use deltachat::constants::DC_VERSION_STR;
|
||||
@@ -10,7 +11,9 @@ use deltachat_jsonrpc::api::events::event_to_json_rpc_notification;
|
||||
use deltachat_jsonrpc::api::{Accounts, CommandApi};
|
||||
use futures_lite::stream::StreamExt;
|
||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use yerpc::{RpcClient, RpcSession};
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
@@ -40,24 +43,38 @@ async fn main() -> Result<()> {
|
||||
let events = accounts.get_event_emitter();
|
||||
|
||||
log::info!("Creating JSON-RPC API.");
|
||||
let state = CommandApi::new(accounts);
|
||||
let accounts = Arc::new(RwLock::new(accounts));
|
||||
let state = CommandApi::from_arc(accounts.clone());
|
||||
|
||||
let (client, mut out_receiver) = RpcClient::new();
|
||||
let session = RpcSession::new(client.clone(), state);
|
||||
let session = RpcSession::new(client.clone(), state.clone());
|
||||
let canceler = CancellationToken::new();
|
||||
|
||||
// Events task converts core events to JSON-RPC notifications.
|
||||
let events_task: JoinHandle<Result<()>> = tokio::spawn(async move {
|
||||
let mut r = Ok(());
|
||||
while let Some(event) = events.recv().await {
|
||||
if r.is_err() {
|
||||
continue;
|
||||
}
|
||||
let event = event_to_json_rpc_notification(event);
|
||||
client.send_notification("event", Some(event)).await?;
|
||||
r = client.send_notification("event", Some(event)).await;
|
||||
}
|
||||
r?;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// Send task prints JSON responses to stdout.
|
||||
let cancelable = canceler.clone();
|
||||
let send_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
|
||||
while let Some(message) = out_receiver.next().await {
|
||||
let message = serde_json::to_string(&message)?;
|
||||
loop {
|
||||
let message = tokio::select! {
|
||||
_ = cancelable.cancelled() => break,
|
||||
message = out_receiver.next() => match message {
|
||||
None => break,
|
||||
Some(message) => serde_json::to_string(&message)?,
|
||||
}
|
||||
};
|
||||
log::trace!("RPC send {}", message);
|
||||
println!("{message}");
|
||||
}
|
||||
@@ -68,23 +85,41 @@ async fn main() -> Result<()> {
|
||||
let recv_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
|
||||
let stdin = io::stdin();
|
||||
let mut lines = BufReader::new(stdin).lines();
|
||||
while let Some(message) = lines.next_line().await? {
|
||||
loop {
|
||||
let message = tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
log::info!("got ctrl-c event");
|
||||
break;
|
||||
}
|
||||
message = lines.next_line() => match message? {
|
||||
None => {
|
||||
log::info!("EOF reached on stdin");
|
||||
break;
|
||||
}
|
||||
Some(message) => message,
|
||||
}
|
||||
};
|
||||
log::trace!("RPC recv {}", message);
|
||||
let session = session.clone();
|
||||
tokio::spawn(async move {
|
||||
session.handle_incoming(&message).await;
|
||||
});
|
||||
}
|
||||
log::info!("EOF reached on stdin");
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// Wait for the end of stdin.
|
||||
recv_task.await??;
|
||||
// Wait for the end of stdin / ctrl-c.
|
||||
recv_task.await?.ok();
|
||||
|
||||
// Shutdown the server.
|
||||
send_task.abort();
|
||||
events_task.abort();
|
||||
// See "Thread safety" section in deltachat-ffi/deltachat.h for explanation.
|
||||
// NB: Events are drained by events_task.
|
||||
canceler.cancel();
|
||||
accounts.read().await.stop_io().await;
|
||||
drop(state);
|
||||
let (r0, r1) = tokio::join!(events_task, send_task);
|
||||
for r in [r0, r1] {
|
||||
r??;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
900
fuzz/Cargo.lock
generated
900
fuzz/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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.112.5"
|
||||
"version": "1.112.6"
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ Build and install the bindings:
|
||||
|
||||
export DCC_RS_DEV="$PWD"
|
||||
export DCC_RS_TARGET=release
|
||||
python -m pip install python
|
||||
python -m pip install ./python
|
||||
|
||||
`DCC_RS_DEV` environment variable specifies the location of
|
||||
the core development tree. If this variable is not set,
|
||||
|
||||
@@ -376,6 +376,22 @@ class Account:
|
||||
dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref)
|
||||
return (x for x in iter_array(dc_array, lambda x: Message.from_db(self, x)) if x is not None)
|
||||
|
||||
def _wait_next_message_ids(self) -> List[int]:
|
||||
"""Return IDs of all next messages from all chats."""
|
||||
dc_array = ffi.gc(lib.dc_wait_next_msgs(self._dc_context), lib.dc_array_unref)
|
||||
return [lib.dc_array_get_id(dc_array, i) for i in range(lib.dc_array_get_cnt(dc_array))]
|
||||
|
||||
def wait_next_incoming_message(self) -> Message:
|
||||
"""Waits until the next incoming message
|
||||
with ID higher than given is received and returns it."""
|
||||
while True:
|
||||
message_ids = self._wait_next_message_ids()
|
||||
for msg_id in message_ids:
|
||||
message = Message.from_db(self, msg_id)
|
||||
if message and not message.is_from_self() and not message.is_from_device():
|
||||
self.set_config("last_msg_id", str(msg_id))
|
||||
return message
|
||||
|
||||
def create_chat(self, obj) -> Chat:
|
||||
"""Create a 1:1 chat with Account, Contact or e-mail address."""
|
||||
return self.create_contact(obj).create_chat()
|
||||
|
||||
@@ -344,6 +344,16 @@ class Message:
|
||||
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
||||
return Contact(self.account, contact_id)
|
||||
|
||||
def is_from_self(self):
|
||||
"""Return true if the message is sent by self."""
|
||||
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
||||
return contact_id == const.DC_CONTACT_ID_SELF
|
||||
|
||||
def is_from_device(self):
|
||||
"""Return true if the message is sent by the device."""
|
||||
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
||||
return contact_id == const.DC_CONTACT_ID_DEVICE
|
||||
|
||||
#
|
||||
# Message State query methods
|
||||
#
|
||||
|
||||
@@ -44,21 +44,21 @@ def test_configure_generate_key(acfactory, lp):
|
||||
lp.sec("ac1: send unencrypted message to ac2")
|
||||
chat.send_text("message1")
|
||||
lp.sec("ac2: waiting for message from ac1")
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
msg_in = ac2.wait_next_incoming_message()
|
||||
assert msg_in.text == "message1"
|
||||
assert not msg_in.is_encrypted()
|
||||
|
||||
lp.sec("ac2: send encrypted message to ac1")
|
||||
msg_in.chat.send_text("message2")
|
||||
lp.sec("ac1: waiting for message from ac2")
|
||||
msg2_in = ac1._evtracker.wait_next_incoming_message()
|
||||
msg2_in = ac1.wait_next_incoming_message()
|
||||
assert msg2_in.text == "message2"
|
||||
assert msg2_in.is_encrypted()
|
||||
|
||||
lp.sec("ac1: send encrypted message to ac2")
|
||||
msg2_in.chat.send_text("message3")
|
||||
lp.sec("ac2: waiting for message from ac1")
|
||||
msg3_in = ac2._evtracker.wait_next_incoming_message()
|
||||
msg3_in = ac2.wait_next_incoming_message()
|
||||
assert msg3_in.text == "message3"
|
||||
assert msg3_in.is_encrypted()
|
||||
|
||||
|
||||
1
release-date.in
Normal file
1
release-date.in
Normal file
@@ -0,0 +1 @@
|
||||
2023-04-04
|
||||
@@ -233,7 +233,7 @@ if __name__ == "__main__":
|
||||
else:
|
||||
now = datetime.datetime.fromisoformat(sys.argv[2])
|
||||
out_all += (
|
||||
"pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> = "
|
||||
"pub static _PROVIDER_UPDATED: Lazy<chrono::NaiveDate> = "
|
||||
"Lazy::new(|| chrono::NaiveDate::from_ymd_opt("
|
||||
+ str(now.year)
|
||||
+ ", "
|
||||
|
||||
@@ -22,4 +22,5 @@ export PYTHONDONTWRITEBYTECODE=1
|
||||
# run python tests (tox invokes pytest to run tests in python/tests)
|
||||
#TOX_PARALLEL_NO_SPINNER=1 tox -e lint,doc
|
||||
tox -e lint
|
||||
tox -e doc,py
|
||||
tox -e doc
|
||||
tox -e py -- "$@"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
@@ -91,15 +92,25 @@ def main():
|
||||
ffi_toml = read_toml_version("deltachat-ffi/Cargo.toml")
|
||||
assert core_toml == ffi_toml, (core_toml, ffi_toml)
|
||||
|
||||
today = datetime.date.today().isoformat()
|
||||
|
||||
if "alpha" not in newversion:
|
||||
for line in open("CHANGELOG.md"):
|
||||
changelog_name = "CHANGELOG.md"
|
||||
changelog_tmpname = changelog_name + ".tmp"
|
||||
changelog_tmp = open(changelog_tmpname, "w")
|
||||
found = False
|
||||
for line in open(changelog_name):
|
||||
## 1.25.0
|
||||
if line.startswith("## [") and line[4:].strip().startswith(newversion):
|
||||
break
|
||||
else:
|
||||
if line == f"## [{newversion}]\n":
|
||||
line = f"## [{newversion}] - {today}\n"
|
||||
found = True
|
||||
changelog_tmp.write(line)
|
||||
if not found:
|
||||
raise SystemExit(
|
||||
f"CHANGELOG.md contains no entry for version: {newversion}"
|
||||
f"{changelog_name} contains no entry for version: {newversion}"
|
||||
)
|
||||
changelog_tmp.close()
|
||||
os.rename(changelog_tmpname, changelog_name)
|
||||
|
||||
for toml_filename in toml_list:
|
||||
replace_toml_version(toml_filename, newversion)
|
||||
@@ -107,6 +118,9 @@ def main():
|
||||
for json_filename in json_list:
|
||||
update_package_json(json_filename, newversion)
|
||||
|
||||
with open("release-date.in", "w") as f:
|
||||
f.write(today)
|
||||
|
||||
print("running cargo check")
|
||||
subprocess.call(["cargo", "check"])
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::path::{Path, PathBuf};
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::context::Context;
|
||||
@@ -301,7 +302,7 @@ pub const DB_NAME: &str = "dc.db";
|
||||
|
||||
/// Account manager configuration file.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Config {
|
||||
struct Config {
|
||||
file: PathBuf,
|
||||
inner: InnerConfig,
|
||||
}
|
||||
@@ -325,10 +326,8 @@ impl Config {
|
||||
selected_account: 0,
|
||||
next_id: 1,
|
||||
};
|
||||
let cfg = Config {
|
||||
file: dir.join(CONFIG_NAME),
|
||||
inner,
|
||||
};
|
||||
let file = dir.join(CONFIG_NAME);
|
||||
let mut cfg = Self { file, inner };
|
||||
|
||||
cfg.sync().await?;
|
||||
|
||||
@@ -336,10 +335,24 @@ impl Config {
|
||||
}
|
||||
|
||||
/// Sync the inmemory representation to disk.
|
||||
async fn sync(&self) -> Result<()> {
|
||||
fs::write(&self.file, toml::to_string_pretty(&self.inner)?)
|
||||
/// Takes a mutable reference because the saved file is a part of the `Config` state. This
|
||||
/// protects from parallel calls resulting to a wrong file contents.
|
||||
async fn sync(&mut self) -> Result<()> {
|
||||
let tmp_path = self.file.with_extension("toml.tmp");
|
||||
let mut file = fs::File::create(&tmp_path)
|
||||
.await
|
||||
.context("failed to write config")
|
||||
.context("failed to create a tmp config")?;
|
||||
file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes())
|
||||
.await
|
||||
.context("failed to write a tmp config")?;
|
||||
file.sync_data()
|
||||
.await
|
||||
.context("failed to sync a tmp config")?;
|
||||
drop(file);
|
||||
fs::rename(&tmp_path, &self.file)
|
||||
.await
|
||||
.context("failed to rename config")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read a configuration from the given file into memory.
|
||||
@@ -359,7 +372,7 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
let config = Self { file, inner };
|
||||
let mut config = Self { file, inner };
|
||||
if modified {
|
||||
config.sync().await?;
|
||||
}
|
||||
@@ -503,17 +516,19 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts1");
|
||||
|
||||
let mut accounts1 = Accounts::new(p.clone()).await.unwrap();
|
||||
accounts1.add_account().await.unwrap();
|
||||
{
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
accounts.add_account().await.unwrap();
|
||||
|
||||
let accounts2 = Accounts::open(p).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account(), 1);
|
||||
}
|
||||
{
|
||||
let accounts = Accounts::open(p).await.unwrap();
|
||||
|
||||
assert_eq!(accounts1.accounts.len(), 1);
|
||||
assert_eq!(accounts1.config.get_selected_account(), 1);
|
||||
|
||||
assert_eq!(accounts1.dir, accounts2.dir);
|
||||
assert_eq!(accounts1.config, accounts2.config,);
|
||||
assert_eq!(accounts1.accounts.len(), accounts2.accounts.len());
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -308,7 +308,7 @@ async fn dkim_works_timestamp(context: &Context, from_domain: &str) -> Result<i6
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT dkim_works FROM sending_domains WHERE domain=?",
|
||||
paramsv![from_domain],
|
||||
(from_domain,),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
@@ -325,7 +325,7 @@ async fn set_dkim_works_timestamp(
|
||||
.execute(
|
||||
"INSERT INTO sending_domains (domain, dkim_works) VALUES (?,?)
|
||||
ON CONFLICT(domain) DO UPDATE SET dkim_works=excluded.dkim_works",
|
||||
paramsv![from_domain, timestamp],
|
||||
(from_domain, timestamp),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
|
||||
223
src/chat.rs
223
src/chat.rs
@@ -36,8 +36,8 @@ use crate::smtp::send_msg_to_smtp;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{
|
||||
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
|
||||
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input, time,
|
||||
IsNoneOrEmpty,
|
||||
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
|
||||
strip_rtlo_characters, time, IsNoneOrEmpty,
|
||||
};
|
||||
use crate::webxdc::WEBXDC_SUFFIX;
|
||||
use crate::{location, sql};
|
||||
@@ -269,25 +269,26 @@ impl ChatId {
|
||||
create_protected: ProtectionStatus,
|
||||
param: Option<String>,
|
||||
) -> Result<Self> {
|
||||
let grpname = strip_rtlo_characters(grpname);
|
||||
let row_id =
|
||||
context.sql.insert(
|
||||
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);",
|
||||
paramsv![
|
||||
(
|
||||
chattype,
|
||||
grpname,
|
||||
&grpname,
|
||||
grpid,
|
||||
create_blocked,
|
||||
create_smeared_timestamp(context),
|
||||
create_protected,
|
||||
param.unwrap_or_default(),
|
||||
],
|
||||
),
|
||||
).await?;
|
||||
|
||||
let chat_id = ChatId::new(u32::try_from(row_id)?);
|
||||
info!(
|
||||
context,
|
||||
"Created group/mailinglist '{}' grpid={} as {}, blocked={}.",
|
||||
grpname,
|
||||
&grpname,
|
||||
grpid,
|
||||
chat_id,
|
||||
create_blocked,
|
||||
@@ -303,7 +304,7 @@ impl ChatId {
|
||||
"UPDATE contacts
|
||||
SET selfavatar_sent=?
|
||||
WHERE id IN(SELECT contact_id FROM chats_contacts WHERE chat_id=?);",
|
||||
paramsv![timestamp, self],
|
||||
(timestamp, self),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -320,7 +321,7 @@ impl ChatId {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET blocked=?1 WHERE id=?2 AND blocked != ?1",
|
||||
paramsv![new_blocked, self],
|
||||
(new_blocked, self),
|
||||
)
|
||||
.await?;
|
||||
Ok(count > 0)
|
||||
@@ -434,10 +435,7 @@ impl ChatId {
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET protected=? WHERE id=?;",
|
||||
paramsv![protect, self],
|
||||
)
|
||||
.execute("UPDATE chats SET protected=? WHERE id=?;", (protect, self))
|
||||
.await?;
|
||||
|
||||
context.emit_event(EventType::ChatModified(self));
|
||||
@@ -523,12 +521,12 @@ impl ChatId {
|
||||
if visibility == ChatVisibility::Archived {
|
||||
transaction.execute(
|
||||
"UPDATE msgs SET state=? WHERE chat_id=? AND state=?;",
|
||||
paramsv![MessageState::InNoticed, self, MessageState::InFresh],
|
||||
(MessageState::InNoticed, self, MessageState::InFresh),
|
||||
)?;
|
||||
}
|
||||
transaction.execute(
|
||||
"UPDATE chats SET archived=? WHERE id=?;",
|
||||
paramsv![visibility, self],
|
||||
(visibility, self),
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -557,7 +555,7 @@ impl ChatId {
|
||||
.execute(
|
||||
"UPDATE chats SET archived=0 WHERE id=? AND archived=1 \
|
||||
AND NOT(muted_until=-1 OR muted_until>?)",
|
||||
paramsv![self, time()],
|
||||
(self, time()),
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
@@ -575,7 +573,7 @@ impl ChatId {
|
||||
WHERE state=?
|
||||
AND hidden=0
|
||||
AND chat_id=?",
|
||||
paramsv![MessageState::InFresh, self],
|
||||
(MessageState::InFresh, self),
|
||||
)
|
||||
.await?;
|
||||
if unread_cnt == 1 {
|
||||
@@ -586,7 +584,7 @@ impl ChatId {
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE chats SET archived=0 WHERE id=?", paramsv![self])
|
||||
.execute("UPDATE chats SET archived=0 WHERE id=?", (self,))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -615,26 +613,23 @@ impl ChatId {
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?);",
|
||||
paramsv![self],
|
||||
(self,),
|
||||
)
|
||||
.await?;
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM msgs WHERE chat_id=?;", paramsv![self])
|
||||
.execute("DELETE FROM msgs WHERE chat_id=?;", (self,))
|
||||
.await?;
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM chats_contacts WHERE chat_id=?;",
|
||||
paramsv![self],
|
||||
)
|
||||
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (self,))
|
||||
.await?;
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM chats WHERE id=?;", paramsv![self])
|
||||
.execute("DELETE FROM chats WHERE id=?;", (self,))
|
||||
.await?;
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
@@ -690,7 +685,7 @@ impl ChatId {
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT id FROM msgs WHERE chat_id=? AND state=?;",
|
||||
paramsv![self, MessageState::OutDraft],
|
||||
(self, MessageState::OutDraft),
|
||||
)
|
||||
.await?;
|
||||
Ok(msg_id)
|
||||
@@ -772,14 +767,14 @@ impl ChatId {
|
||||
"UPDATE msgs
|
||||
SET timestamp=?,type=?,txt=?, param=?,mime_in_reply_to=?
|
||||
WHERE id=?;",
|
||||
paramsv![
|
||||
(
|
||||
time(),
|
||||
msg.viewtype,
|
||||
msg.text.as_deref().unwrap_or(""),
|
||||
msg.param.to_string(),
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
msg.id
|
||||
],
|
||||
msg.id,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
return Ok(true);
|
||||
@@ -803,7 +798,7 @@ impl ChatId {
|
||||
hidden,
|
||||
mime_in_reply_to)
|
||||
VALUES (?,?,?, ?,?,?,?,?,?);",
|
||||
paramsv![
|
||||
(
|
||||
self,
|
||||
ContactId::SELF,
|
||||
time(),
|
||||
@@ -813,7 +808,7 @@ impl ChatId {
|
||||
msg.param.to_string(),
|
||||
1,
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
],
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
msg.id = MsgId::new(row_id.try_into()?);
|
||||
@@ -826,7 +821,7 @@ impl ChatId {
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM msgs WHERE hidden=0 AND chat_id=?",
|
||||
paramsv![self],
|
||||
(self,),
|
||||
)
|
||||
.await?;
|
||||
Ok(count)
|
||||
@@ -869,7 +864,7 @@ impl ChatId {
|
||||
WHERE state=?
|
||||
AND hidden=0
|
||||
AND chat_id=?;",
|
||||
paramsv![MessageState::InFresh, self],
|
||||
(MessageState::InFresh, self),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
@@ -879,7 +874,7 @@ impl ChatId {
|
||||
pub(crate) async fn get_param(self, context: &Context) -> Result<Params> {
|
||||
let res: Option<String> = context
|
||||
.sql
|
||||
.query_get_value("SELECT param FROM chats WHERE id=?", paramsv![self])
|
||||
.query_get_value("SELECT param FROM chats WHERE id=?", (self,))
|
||||
.await?;
|
||||
Ok(res
|
||||
.map(|s| s.parse().unwrap_or_default())
|
||||
@@ -924,13 +919,13 @@ impl ChatId {
|
||||
let row = sql
|
||||
.query_row_optional(
|
||||
&query,
|
||||
paramsv![
|
||||
(
|
||||
self,
|
||||
MessageState::OutPreparing,
|
||||
MessageState::OutDraft,
|
||||
MessageState::OutPending,
|
||||
MessageState::OutFailed
|
||||
],
|
||||
MessageState::OutFailed,
|
||||
),
|
||||
f,
|
||||
)
|
||||
.await?;
|
||||
@@ -1052,10 +1047,7 @@ impl ChatId {
|
||||
pub async fn get_gossiped_timestamp(self, context: &Context) -> Result<i64> {
|
||||
let timestamp: Option<i64> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT gossiped_timestamp FROM chats WHERE id=?;",
|
||||
paramsv![self],
|
||||
)
|
||||
.query_get_value("SELECT gossiped_timestamp FROM chats WHERE id=?;", (self,))
|
||||
.await?;
|
||||
Ok(timestamp.unwrap_or_default())
|
||||
}
|
||||
@@ -1078,7 +1070,7 @@ impl ChatId {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
|
||||
paramsv![timestamp, self],
|
||||
(timestamp, self),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1174,7 +1166,7 @@ impl Chat {
|
||||
c.blocked, c.locations_send_until, c.muted_until, c.protected
|
||||
FROM chats c
|
||||
WHERE c.id=?;",
|
||||
paramsv![chat_id],
|
||||
(chat_id,),
|
||||
|row| {
|
||||
let c = Chat {
|
||||
id: chat_id,
|
||||
@@ -1288,7 +1280,7 @@ impl Chat {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET param=? WHERE id=?",
|
||||
paramsv![self.param.to_string(), self.id],
|
||||
(self.param.to_string(), self.id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -1470,7 +1462,7 @@ impl Chat {
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT contact_id FROM chats_contacts WHERE chat_id=?;",
|
||||
paramsv![self.id],
|
||||
(self.id,),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
@@ -1551,13 +1543,13 @@ impl Chat {
|
||||
"INSERT INTO locations \
|
||||
(timestamp,from_id,chat_id, latitude,longitude,independent)\
|
||||
VALUES (?,?,?, ?,?,1);",
|
||||
paramsv![
|
||||
(
|
||||
timestamp,
|
||||
ContactId::SELF,
|
||||
self.id,
|
||||
msg.param.get_float(Param::SetLatitude).unwrap_or_default(),
|
||||
msg.param.get_float(Param::SetLongitude).unwrap_or_default(),
|
||||
],
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1603,7 +1595,7 @@ impl Chat {
|
||||
mime_headers=?, mime_compressed=1, location_id=?, ephemeral_timer=?,
|
||||
ephemeral_timestamp=?
|
||||
WHERE id=?;",
|
||||
paramsv![
|
||||
params_slice![
|
||||
new_rfc724_mid,
|
||||
self.id,
|
||||
ContactId::SELF,
|
||||
@@ -1652,7 +1644,7 @@ impl Chat {
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
|
||||
paramsv![
|
||||
params_slice![
|
||||
new_rfc724_mid,
|
||||
self.id,
|
||||
ContactId::SELF,
|
||||
@@ -1674,6 +1666,7 @@ impl Chat {
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
context.new_msgs_notify.notify_one();
|
||||
msg.id = MsgId::new(u32::try_from(raw_id)?);
|
||||
|
||||
maybe_set_logging_xdc(context, msg, self.id).await?;
|
||||
@@ -1863,7 +1856,7 @@ async fn update_special_chat_name(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET name=? WHERE id=? AND name!=?",
|
||||
paramsv![name, chat_id, name],
|
||||
(&name, chat_id, &name),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -1926,7 +1919,7 @@ impl ChatIdBlocked {
|
||||
WHERE c.type=100 -- 100 = Chattype::Single
|
||||
AND c.id>9 -- 9 = DC_CHAT_ID_LAST_SPECIAL
|
||||
AND j.contact_id=?;",
|
||||
paramsv![contact_id],
|
||||
(contact_id,),
|
||||
|row| {
|
||||
let id: ChatId = row.get(0)?;
|
||||
let blocked: Blocked = row.get(1)?;
|
||||
@@ -1977,13 +1970,13 @@ impl ChatIdBlocked {
|
||||
"INSERT INTO chats
|
||||
(type, name, param, blocked, created_timestamp)
|
||||
VALUES(?, ?, ?, ?, ?)",
|
||||
params![
|
||||
(
|
||||
Chattype::Single,
|
||||
chat_name,
|
||||
params.to_string(),
|
||||
create_blocked as u8,
|
||||
create_smeared_timestamp(context)
|
||||
],
|
||||
create_smeared_timestamp(context),
|
||||
),
|
||||
)?;
|
||||
let chat_id = ChatId::new(
|
||||
transaction
|
||||
@@ -1996,7 +1989,7 @@ impl ChatIdBlocked {
|
||||
"INSERT INTO chats_contacts
|
||||
(chat_id, contact_id)
|
||||
VALUES((SELECT last_insert_rowid()), ?)",
|
||||
params![contact_id],
|
||||
(contact_id,),
|
||||
)?;
|
||||
|
||||
Ok(chat_id)
|
||||
@@ -2156,7 +2149,7 @@ pub async fn is_contact_in_chat(
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=?;",
|
||||
paramsv![chat_id, contact_id],
|
||||
(chat_id, contact_id),
|
||||
)
|
||||
.await?;
|
||||
Ok(exists)
|
||||
@@ -2208,6 +2201,13 @@ pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message
|
||||
}
|
||||
|
||||
async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
|
||||
// protect all system messages againts RTLO attacks
|
||||
if msg.is_system_message() {
|
||||
if let Some(text) = &msg.text {
|
||||
msg.text = Some(strip_rtlo_characters(text.as_ref()));
|
||||
}
|
||||
}
|
||||
|
||||
if prepare_send_msg(context, chat_id, msg).await?.is_some() {
|
||||
context.emit_msgs_changed(msg.chat_id, msg.id);
|
||||
|
||||
@@ -2370,12 +2370,12 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
.insert(
|
||||
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
paramsv![
|
||||
(
|
||||
&rendered_msg.rfc724_mid,
|
||||
recipients,
|
||||
&rendered_msg.message,
|
||||
msg_id
|
||||
],
|
||||
msg_id,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
Ok(Some(row_id))
|
||||
@@ -2541,7 +2541,7 @@ pub async fn get_chat_msgs_ex(
|
||||
OR m.from_id == ?
|
||||
OR m.to_id == ?
|
||||
);",
|
||||
paramsv![chat_id, ContactId::INFO, ContactId::INFO],
|
||||
(chat_id, ContactId::INFO, ContactId::INFO),
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
@@ -2554,7 +2554,7 @@ pub async fn get_chat_msgs_ex(
|
||||
FROM msgs m
|
||||
WHERE m.chat_id=?
|
||||
AND m.hidden=0;",
|
||||
paramsv![chat_id],
|
||||
(chat_id,),
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
@@ -2572,7 +2572,7 @@ pub(crate) async fn marknoticed_chat_if_older_than(
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=?",
|
||||
paramsv![chat_id],
|
||||
(chat_id,),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
@@ -2622,7 +2622,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;",
|
||||
paramsv![MessageState::InFresh, chat_id],
|
||||
(MessageState::InFresh, chat_id),
|
||||
)
|
||||
.await?;
|
||||
if !exists {
|
||||
@@ -2637,7 +2637,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
|
||||
WHERE state=?
|
||||
AND hidden=0
|
||||
AND chat_id=?;",
|
||||
paramsv![MessageState::InNoticed, MessageState::InFresh, chat_id],
|
||||
(MessageState::InNoticed, MessageState::InFresh, chat_id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -2686,12 +2686,12 @@ pub(crate) async fn mark_old_messages_as_noticed(
|
||||
AND hidden=0
|
||||
AND chat_id=?
|
||||
AND timestamp<=?;",
|
||||
paramsv![
|
||||
(
|
||||
MessageState::InNoticed,
|
||||
MessageState::InFresh,
|
||||
msg.chat_id,
|
||||
msg.sort_timestamp
|
||||
],
|
||||
msg.sort_timestamp,
|
||||
),
|
||||
)?;
|
||||
if changed_rows > 0 {
|
||||
changed_chats.push(msg.chat_id);
|
||||
@@ -2739,7 +2739,7 @@ pub async fn get_chat_media(
|
||||
AND (type=? OR type=? OR type=?)
|
||||
AND hidden=0
|
||||
ORDER BY timestamp, id;",
|
||||
paramsv![
|
||||
(
|
||||
chat_id.is_none(),
|
||||
chat_id.unwrap_or_else(|| ChatId::new(0)),
|
||||
DC_CHAT_ID_TRASH,
|
||||
@@ -2754,7 +2754,7 @@ pub async fn get_chat_media(
|
||||
} else {
|
||||
msg_type
|
||||
},
|
||||
],
|
||||
),
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|ids| Ok(ids.flatten().collect()),
|
||||
)
|
||||
@@ -2832,7 +2832,7 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec
|
||||
ON c.id=cc.contact_id
|
||||
WHERE cc.chat_id=?
|
||||
ORDER BY c.id=1, c.last_seen DESC, c.id DESC;",
|
||||
paramsv![chat_id],
|
||||
(chat_id,),
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
@@ -2858,12 +2858,12 @@ pub async fn create_group_chat(
|
||||
"INSERT INTO chats
|
||||
(type, name, grpid, param, created_timestamp)
|
||||
VALUES(?, ?, ?, \'U=1\', ?);",
|
||||
paramsv![
|
||||
(
|
||||
Chattype::Group,
|
||||
chat_name,
|
||||
grpid,
|
||||
create_smeared_timestamp(context),
|
||||
],
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -2896,7 +2896,7 @@ async fn find_unused_broadcast_list_name(context: &Context) -> Result<String> {
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM chats WHERE type=? AND name=?;",
|
||||
paramsv![Chattype::Broadcast, better_name],
|
||||
(Chattype::Broadcast, &better_name),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
@@ -2916,12 +2916,12 @@ pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
|
||||
"INSERT INTO chats
|
||||
(type, name, grpid, param, created_timestamp)
|
||||
VALUES(?, ?, ?, \'U=1\', ?);",
|
||||
paramsv![
|
||||
(
|
||||
Chattype::Broadcast,
|
||||
chat_name,
|
||||
grpid,
|
||||
create_smeared_timestamp(context),
|
||||
],
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
let chat_id = ChatId::new(u32::try_from(row_id)?);
|
||||
@@ -2942,7 +2942,7 @@ pub(crate) async fn add_to_chat_contacts_table(
|
||||
for contact_id in contact_ids {
|
||||
transaction.execute(
|
||||
"INSERT OR IGNORE INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
|
||||
paramsv![chat_id, contact_id],
|
||||
(chat_id, contact_id),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -2962,7 +2962,7 @@ pub(crate) async fn remove_from_chat_contacts_table(
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?",
|
||||
paramsv![chat_id, contact_id],
|
||||
(chat_id, contact_id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -3080,7 +3080,7 @@ pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId)
|
||||
FROM chats_contacts cc
|
||||
LEFT JOIN contacts c ON c.id=cc.contact_id
|
||||
WHERE cc.chat_id=? AND cc.contact_id!=?;",
|
||||
paramsv![chat_id, ContactId::SELF],
|
||||
(chat_id, ContactId::SELF),
|
||||
|row| Ok(row.get::<_, i64>(0)),
|
||||
|rows| {
|
||||
let mut needs_attach = false;
|
||||
@@ -3153,7 +3153,7 @@ pub async fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuratio
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||
paramsv![duration, chat_id],
|
||||
(duration, chat_id),
|
||||
)
|
||||
.await
|
||||
.context(format!("Failed to set mute duration for {chat_id}"))?;
|
||||
@@ -3240,10 +3240,7 @@ async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()>
|
||||
if !is_group_explicitly_left(context, grpid).await? {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO leftgrps (grpid) VALUES(?);",
|
||||
paramsv![grpid.to_string()],
|
||||
)
|
||||
.execute("INSERT INTO leftgrps (grpid) VALUES(?);", (grpid,))
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -3253,10 +3250,7 @@ async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()>
|
||||
pub(crate) async fn is_group_explicitly_left(context: &Context, grpid: &str) -> Result<bool> {
|
||||
let exists = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM leftgrps WHERE grpid=?;",
|
||||
paramsv![grpid],
|
||||
)
|
||||
.exists("SELECT COUNT(*) FROM leftgrps WHERE grpid=?;", (grpid,))
|
||||
.await?;
|
||||
Ok(exists)
|
||||
}
|
||||
@@ -3288,7 +3282,7 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET name=? WHERE id=?;",
|
||||
paramsv![new_name.to_string(), chat_id],
|
||||
(new_name.to_string(), chat_id),
|
||||
)
|
||||
.await?;
|
||||
if chat.is_promoted()
|
||||
@@ -3541,7 +3535,7 @@ pub(crate) async fn get_chat_id_by_grpid(
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id, blocked, protected FROM chats WHERE grpid=?;",
|
||||
paramsv![grpid],
|
||||
(grpid,),
|
||||
|row| {
|
||||
let chat_id = row.get::<_, ChatId>(0)?;
|
||||
|
||||
@@ -3595,7 +3589,7 @@ pub async fn add_device_msg_with_importance(
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=?",
|
||||
paramsv![chat_id],
|
||||
(chat_id,),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
@@ -3620,7 +3614,7 @@ pub async fn add_device_msg_with_importance(
|
||||
param,
|
||||
rfc724_mid)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?);",
|
||||
paramsv![
|
||||
(
|
||||
chat_id,
|
||||
ContactId::DEVICE,
|
||||
ContactId::SELF,
|
||||
@@ -3632,9 +3626,10 @@ pub async fn add_device_msg_with_importance(
|
||||
msg.text.as_ref().cloned().unwrap_or_default(),
|
||||
msg.param.to_string(),
|
||||
rfc724_mid,
|
||||
],
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
context.new_msgs_notify.notify_one();
|
||||
|
||||
msg_id = MsgId::new(u32::try_from(row_id)?);
|
||||
if !msg.hidden {
|
||||
@@ -3645,10 +3640,7 @@ pub async fn add_device_msg_with_importance(
|
||||
if let Some(label) = label {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO devmsglabels (label) VALUES (?);",
|
||||
paramsv![label.to_string()],
|
||||
)
|
||||
.execute("INSERT INTO devmsglabels (label) VALUES (?);", (label,))
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -3675,7 +3667,7 @@ pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(label) FROM devmsglabels WHERE label=?",
|
||||
paramsv![label],
|
||||
(label,),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -3692,10 +3684,7 @@ pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result
|
||||
pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM msgs WHERE from_id=?;",
|
||||
paramsv![ContactId::DEVICE],
|
||||
)
|
||||
.execute("DELETE FROM msgs WHERE from_id=?;", (ContactId::DEVICE,))
|
||||
.await?;
|
||||
context.sql.execute("DELETE FROM devmsglabels;", ()).await?;
|
||||
|
||||
@@ -3738,7 +3727,7 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
context.sql.insert(
|
||||
"INSERT INTO msgs (chat_id,from_id,to_id,timestamp,timestamp_sent,timestamp_rcvd,type,state,txt,rfc724_mid,ephemeral_timer, param,mime_in_reply_to)
|
||||
VALUES (?,?,?, ?,?,?,?,?, ?,?,?, ?,?);",
|
||||
paramsv![
|
||||
(
|
||||
chat_id,
|
||||
from_id.unwrap_or(ContactId::INFO),
|
||||
ContactId::INFO,
|
||||
@@ -3752,8 +3741,9 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
ephemeral_timer,
|
||||
param.to_string(),
|
||||
parent.map(|msg|msg.rfc724_mid.clone()).unwrap_or_default()
|
||||
]
|
||||
)
|
||||
).await?;
|
||||
context.new_msgs_notify.notify_one();
|
||||
|
||||
let msg_id = MsgId::new(row_id.try_into()?);
|
||||
context.emit_msgs_changed(chat_id, msg_id);
|
||||
@@ -3792,7 +3782,7 @@ pub(crate) async fn update_msg_text_and_timestamp(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET txt=?, timestamp=? WHERE id=?;",
|
||||
paramsv![text, timestamp, msg_id],
|
||||
(text, timestamp, msg_id),
|
||||
)
|
||||
.await?;
|
||||
context.emit_msgs_changed(chat_id, msg_id);
|
||||
@@ -3808,6 +3798,7 @@ mod tests {
|
||||
use crate::message::delete_msgs;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::TestContext;
|
||||
use tokio::fs;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_chat_info() {
|
||||
@@ -6107,4 +6098,30 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_blob_renaming() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
chat_id,
|
||||
Contact::create(&alice, "bob", "bob@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
let dir = tempfile::tempdir()?;
|
||||
let file = dir.path().join("harmless_file.\u{202e}txt.exe");
|
||||
fs::write(&file, "aaa").await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let msg = bob.recv_msg(&alice.send_msg(chat_id, &mut msg).await).await;
|
||||
|
||||
// the file bob receives should not contain BIDI-control characters
|
||||
assert_eq!(
|
||||
Some("$BLOBDIR/harmless_file.txt.exe"),
|
||||
msg.param.get(Param::File),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ impl Chatlist {
|
||||
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
|
||||
GROUP BY c.id
|
||||
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned],
|
||||
(MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?
|
||||
@@ -164,7 +164,7 @@ impl Chatlist {
|
||||
AND c.archived=1
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft],
|
||||
(MessageState::OutDraft,),
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
@@ -198,7 +198,7 @@ impl Chatlist {
|
||||
AND c.name LIKE ?3
|
||||
GROUP BY c.id
|
||||
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft, skip_id, str_like_cmd],
|
||||
(MessageState::OutDraft, skip_id, str_like_cmd),
|
||||
process_row,
|
||||
process_rows,
|
||||
)
|
||||
@@ -228,7 +228,7 @@ impl Chatlist {
|
||||
AND NOT c.archived=?4
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=?5 DESC, c.archived=?6 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
paramsv![MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
|
||||
(MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?;
|
||||
@@ -356,7 +356,7 @@ pub async fn get_archived_cnt(context: &Context) -> Result<usize> {
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM chats WHERE blocked!=? AND archived=?;",
|
||||
paramsv![Blocked::Yes, ChatVisibility::Archived],
|
||||
(Blocked::Yes, ChatVisibility::Archived),
|
||||
)
|
||||
.await?;
|
||||
Ok(count)
|
||||
|
||||
@@ -308,6 +308,9 @@ pub enum Config {
|
||||
/// This value is used internally to remember the MsgId of the logging xdc
|
||||
#[strum(props(default = "0"))]
|
||||
DebugLogging,
|
||||
|
||||
/// Last message processed by the bot.
|
||||
LastMsgId,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@@ -358,6 +361,11 @@ impl Context {
|
||||
Ok(self.get_config_parsed(key).await?.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Returns 32-bit unsigned integer configuration value for the given key.
|
||||
pub async fn get_config_u32(&self, key: Config) -> Result<u32> {
|
||||
Ok(self.get_config_parsed(key).await?.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Returns 64-bit signed integer configuration value for the given key.
|
||||
pub async fn get_config_i64(&self, key: Config) -> Result<i64> {
|
||||
Ok(self.get_config_parsed(key).await?.unwrap_or_default())
|
||||
@@ -459,6 +467,12 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the given config to an unsigned 32-bit integer value.
|
||||
pub async fn set_config_u32(&self, key: Config, value: u32) -> Result<()> {
|
||||
self.set_config(key, Some(&value.to_string())).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the given config to a boolean value.
|
||||
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
|
||||
self.set_config(key, if value { Some("1") } else { Some("0") })
|
||||
|
||||
@@ -32,7 +32,10 @@ use crate::mimeparser::AvatarAction;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::tools::{duration_to_str, get_abs_path, improve_single_line_input, time, EmailAddress};
|
||||
use crate::tools::{
|
||||
duration_to_str, get_abs_path, improve_single_line_input, strip_rtlo_characters, time,
|
||||
EmailAddress,
|
||||
};
|
||||
use crate::{chat, stock_str};
|
||||
|
||||
/// Time during which a contact is considered as seen recently.
|
||||
@@ -344,7 +347,7 @@ impl Contact {
|
||||
c.authname, c.param, c.status
|
||||
FROM contacts c
|
||||
WHERE c.id=?;",
|
||||
paramsv![contact_id],
|
||||
(contact_id,),
|
||||
|row| {
|
||||
let name: String = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
@@ -457,7 +460,7 @@ impl Contact {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
|
||||
paramsv![MessageState::InNoticed, id, MessageState::InFresh],
|
||||
(MessageState::InNoticed, id, MessageState::InFresh),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -490,7 +493,7 @@ impl Contact {
|
||||
"SELECT id FROM contacts \
|
||||
WHERE addr=?1 COLLATE NOCASE \
|
||||
AND id>?2 AND origin>=?3 AND blocked=0;",
|
||||
paramsv![addr_normalized, ContactId::LAST_SPECIAL, min_origin as u32,],
|
||||
(&addr_normalized, ContactId::LAST_SPECIAL, min_origin as u32),
|
||||
)
|
||||
.await?;
|
||||
Ok(id)
|
||||
@@ -536,7 +539,7 @@ impl Contact {
|
||||
return Ok((ContactId::SELF, sth_modified));
|
||||
}
|
||||
|
||||
let mut name = name;
|
||||
let mut name = strip_rtlo_characters(name);
|
||||
#[allow(clippy::collapsible_if)]
|
||||
if origin <= Origin::OutgoingTo {
|
||||
// The user may accidentally have written to a "noreply" address with another MUA:
|
||||
@@ -551,7 +554,7 @@ impl Contact {
|
||||
// For these kind of email addresses, sender and address often don't belong together
|
||||
// (like hocuri <notifications@github.com>). In this example, hocuri shouldn't
|
||||
// be saved as the displayname for notifications@github.com.
|
||||
name = "";
|
||||
name = "".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,7 +608,7 @@ impl Contact {
|
||||
transaction
|
||||
.execute(
|
||||
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
|
||||
paramsv![
|
||||
(
|
||||
new_name,
|
||||
if update_addr {
|
||||
addr.to_string()
|
||||
@@ -623,7 +626,7 @@ impl Contact {
|
||||
row_authname
|
||||
},
|
||||
row_id
|
||||
],
|
||||
),
|
||||
)?;
|
||||
|
||||
if update_name || update_authname {
|
||||
@@ -631,7 +634,7 @@ impl Contact {
|
||||
// This is one of the few duplicated data, however, getting the chat list is easier this way.
|
||||
let chat_id: Option<ChatId> = transaction.query_row(
|
||||
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
|
||||
params![Chattype::Single, isize::try_from(row_id)?],
|
||||
(Chattype::Single, isize::try_from(row_id)?),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
Ok(chat_id)
|
||||
@@ -645,7 +648,7 @@ impl Contact {
|
||||
"SELECT addr, name, authname
|
||||
FROM contacts
|
||||
WHERE id=?",
|
||||
params![contact_id],
|
||||
(contact_id,),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
let name: String = row.get(1)?;
|
||||
@@ -663,7 +666,7 @@ impl Contact {
|
||||
|
||||
let count = transaction.execute(
|
||||
"UPDATE chats SET name=?1 WHERE id=?2 AND name!=?1",
|
||||
params![chat_name, chat_id])?;
|
||||
(chat_name, chat_id))?;
|
||||
|
||||
if count > 0 {
|
||||
// Chat name updated
|
||||
@@ -681,7 +684,7 @@ impl Contact {
|
||||
.execute(
|
||||
"INSERT INTO contacts (name, addr, origin, authname)
|
||||
VALUES (?, ?, ?, ?);",
|
||||
params![
|
||||
(
|
||||
if update_name {
|
||||
name.to_string()
|
||||
} else {
|
||||
@@ -694,7 +697,7 @@ impl Contact {
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
],
|
||||
),
|
||||
)?;
|
||||
|
||||
sth_modified = Modifier::Created;
|
||||
@@ -795,12 +798,12 @@ impl Contact {
|
||||
ORDER BY c.last_seen DESC, c.id DESC;",
|
||||
sql::repeat_vars(self_addrs.len())
|
||||
),
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
|
||||
ContactId::LAST_SPECIAL,
|
||||
Origin::IncomingReplyTo,
|
||||
s3str_like_cmd,
|
||||
s3str_like_cmd,
|
||||
if flag_verified_only { 0i32 } else { 1i32 },
|
||||
if flag_verified_only { 0i32 } else { 1i32 }
|
||||
])),
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| {
|
||||
@@ -847,7 +850,7 @@ impl Contact {
|
||||
ORDER BY last_seen DESC, id DESC;",
|
||||
sql::repeat_vars(self_addrs.len())
|
||||
),
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
|
||||
ContactId::LAST_SPECIAL,
|
||||
Origin::IncomingReplyTo
|
||||
])),
|
||||
@@ -880,7 +883,7 @@ impl Contact {
|
||||
.transaction(move |transaction| {
|
||||
let mut stmt = transaction
|
||||
.prepare("SELECT name, grpid FROM chats WHERE type=? AND blocked=?")?;
|
||||
let rows = stmt.query_map(params![Chattype::Mailinglist, Blocked::Yes], |row| {
|
||||
let rows = stmt.query_map((Chattype::Mailinglist, Blocked::Yes), |row| {
|
||||
let name: String = row.get(0)?;
|
||||
let grpid: String = row.get(1)?;
|
||||
Ok((name, grpid))
|
||||
@@ -902,7 +905,7 @@ impl Contact {
|
||||
// Always do an update in case the blocking is reset or name is changed.
|
||||
transaction.execute(
|
||||
"UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?",
|
||||
params![&name, Origin::MailinglistAddress, &grpid],
|
||||
(&name, Origin::MailinglistAddress, &grpid),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -917,7 +920,7 @@ impl Contact {
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0",
|
||||
paramsv![ContactId::LAST_SPECIAL],
|
||||
(ContactId::LAST_SPECIAL,),
|
||||
)
|
||||
.await?;
|
||||
Ok(count)
|
||||
@@ -933,7 +936,7 @@ impl Contact {
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY last_seen DESC, id DESC;",
|
||||
paramsv![ContactId::LAST_SPECIAL],
|
||||
(ContactId::LAST_SPECIAL,),
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| {
|
||||
ids.collect::<std::result::Result<Vec<_>, _>>()
|
||||
@@ -1027,12 +1030,12 @@ impl Contact {
|
||||
let deleted_contacts = transaction.execute(
|
||||
"DELETE FROM contacts WHERE id=?
|
||||
AND (SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?)=0;",
|
||||
paramsv![contact_id, contact_id],
|
||||
(contact_id, contact_id),
|
||||
)?;
|
||||
if deleted_contacts == 0 {
|
||||
transaction.execute(
|
||||
"UPDATE contacts SET origin=? WHERE id=?;",
|
||||
paramsv![Origin::Hidden, contact_id],
|
||||
(Origin::Hidden, contact_id),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -1060,7 +1063,7 @@ impl Contact {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET param=? WHERE id=?",
|
||||
paramsv![self.param.to_string(), self.id],
|
||||
(self.param.to_string(), self.id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -1072,7 +1075,7 @@ impl Contact {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET status=? WHERE id=?",
|
||||
paramsv![self.status, self.id],
|
||||
(&self.status, self.id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -1192,9 +1195,7 @@ impl Contact {
|
||||
if peerstate.verified_key.is_some() {
|
||||
return Ok(VerifiedStatus::BidirectVerified);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
|
||||
} else if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
|
||||
if peerstate.verified_key.is_some() {
|
||||
return Ok(VerifiedStatus::BidirectVerified);
|
||||
}
|
||||
@@ -1230,7 +1231,7 @@ impl Contact {
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM contacts WHERE id>?;",
|
||||
paramsv![ContactId::LAST_SPECIAL],
|
||||
(ContactId::LAST_SPECIAL,),
|
||||
)
|
||||
.await?;
|
||||
Ok(count)
|
||||
@@ -1244,10 +1245,7 @@ impl Contact {
|
||||
|
||||
let exists = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM contacts WHERE id=?;",
|
||||
paramsv![contact_id],
|
||||
)
|
||||
.exists("SELECT COUNT(*) FROM contacts WHERE id=?;", (contact_id,))
|
||||
.await?;
|
||||
Ok(exists)
|
||||
}
|
||||
@@ -1262,7 +1260,7 @@ impl Contact {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
|
||||
paramsv![origin, contact_id, origin],
|
||||
(origin, contact_id, origin),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -1291,18 +1289,20 @@ fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||
(
|
||||
if name.is_empty() {
|
||||
captures
|
||||
.get(1)
|
||||
.map_or("".to_string(), |m| normalize_name(m.as_str()))
|
||||
strip_rtlo_characters(
|
||||
&captures
|
||||
.get(1)
|
||||
.map_or("".to_string(), |m| normalize_name(m.as_str())),
|
||||
)
|
||||
} else {
|
||||
name.to_string()
|
||||
strip_rtlo_characters(name)
|
||||
},
|
||||
captures
|
||||
.get(2)
|
||||
.map_or("".to_string(), |m| m.as_str().to_string()),
|
||||
)
|
||||
} else {
|
||||
(name.to_string(), addr.to_string())
|
||||
(strip_rtlo_characters(name), addr.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1324,7 +1324,7 @@ async fn set_block_contact(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET blocked=? WHERE id=?;",
|
||||
paramsv![i32::from(new_blocking), contact_id],
|
||||
(i32::from(new_blocking), contact_id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1343,7 +1343,7 @@ WHERE type=? AND id IN (
|
||||
SELECT chat_id FROM chats_contacts WHERE contact_id=?
|
||||
);
|
||||
"#,
|
||||
paramsv![new_blocking, Chattype::Single, contact_id],
|
||||
(new_blocking, Chattype::Single, contact_id),
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -1460,7 +1460,7 @@ pub(crate) async fn update_last_seen(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET last_seen = ?1 WHERE last_seen < ?1 AND id = ?2",
|
||||
paramsv![timestamp, contact_id],
|
||||
(timestamp, contact_id),
|
||||
)
|
||||
.await?
|
||||
> 0
|
||||
@@ -1489,7 +1489,7 @@ pub fn normalize_name(full_name: &str) -> String {
|
||||
match full_name.as_bytes() {
|
||||
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name
|
||||
.get(1..full_name.len() - 1)
|
||||
.map_or("".to_string(), |s| s.trim().into()),
|
||||
.map_or("".to_string(), |s| s.trim().to_string()),
|
||||
_ => full_name.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -1575,7 +1575,7 @@ impl RecentlySeenLoop {
|
||||
.query_map(
|
||||
"SELECT id, last_seen FROM contacts
|
||||
WHERE last_seen > ?",
|
||||
paramsv![time() - SEEN_RECENTLY_SECONDS],
|
||||
(time() - SEEN_RECENTLY_SECONDS,),
|
||||
|row| {
|
||||
let contact_id: ContactId = row.get("id")?;
|
||||
let last_seen: i64 = row.get("last_seen")?;
|
||||
|
||||
177
src/context.rs
177
src/context.rs
@@ -14,7 +14,7 @@ use std::time::{Duration, Instant, SystemTime};
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_channel::Sender;
|
||||
use ratelimit::Ratelimit;
|
||||
use tokio::sync::{oneshot, Mutex, RwLock};
|
||||
use tokio::sync::{oneshot, Mutex, Notify, RwLock};
|
||||
use tokio::task;
|
||||
|
||||
use crate::chat::{get_chat_cnt, ChatId};
|
||||
@@ -221,6 +221,11 @@ pub struct InnerContext {
|
||||
/// IMAP UID resync request.
|
||||
pub(crate) resync_request: AtomicBool,
|
||||
|
||||
/// Notify about new messages.
|
||||
///
|
||||
/// This causes [`Context::wait_next_msgs`] to wake up.
|
||||
pub(crate) new_msgs_notify: Notify,
|
||||
|
||||
/// Server ID response if ID capability is supported
|
||||
/// and the server returned non-NIL on the inbox connection.
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc2971>
|
||||
@@ -251,8 +256,8 @@ pub(crate) struct DebugLogging {
|
||||
pub(crate) msg_id: MsgId,
|
||||
/// Handle to the background task responsible for sending
|
||||
pub(crate) loop_handle: task::JoinHandle<()>,
|
||||
/// Channel that log events should be send to
|
||||
/// A background loop will receive and handle them
|
||||
/// Channel that log events should be sent to.
|
||||
/// A background loop will receive and handle them.
|
||||
pub(crate) sender: Sender<DebugEventLogData>,
|
||||
}
|
||||
|
||||
@@ -366,6 +371,11 @@ impl Context {
|
||||
blobdir.display()
|
||||
);
|
||||
|
||||
let new_msgs_notify = Notify::new();
|
||||
// Notify once immediately to allow processing old messages
|
||||
// without starting I/O.
|
||||
new_msgs_notify.notify_one();
|
||||
|
||||
let inner = InnerContext {
|
||||
id,
|
||||
blobdir,
|
||||
@@ -382,6 +392,7 @@ impl Context {
|
||||
quota: RwLock::new(None),
|
||||
quota_update_request: AtomicBool::new(false),
|
||||
resync_request: AtomicBool::new(false),
|
||||
new_msgs_notify,
|
||||
server_id: RwLock::new(None),
|
||||
creation_time: std::time::SystemTime::now(),
|
||||
last_full_folder_scan: Mutex::new(None),
|
||||
@@ -782,6 +793,10 @@ impl Context {
|
||||
"debug_logging",
|
||||
self.get_config_int(Config::DebugLogging).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"last_msg_id",
|
||||
self.get_config_int(Config::LastMsgId).await?.to_string(),
|
||||
);
|
||||
|
||||
let elapsed = self.creation_time.elapsed();
|
||||
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
|
||||
@@ -814,7 +829,7 @@ impl Context {
|
||||
" AND NOT(c.muted_until=-1 OR c.muted_until>?)",
|
||||
" ORDER BY m.timestamp DESC,m.id DESC;"
|
||||
),
|
||||
paramsv![MessageState::InFresh, time()],
|
||||
(MessageState::InFresh, time()),
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|rows| {
|
||||
let mut list = Vec::new();
|
||||
@@ -828,6 +843,66 @@ impl Context {
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Returns a list of messages with database ID higher than requested.
|
||||
///
|
||||
/// Blocked contacts and chats are excluded,
|
||||
/// but self-sent messages and contact requests are included in the results.
|
||||
pub async fn get_next_msgs(&self) -> Result<Vec<MsgId>> {
|
||||
let last_msg_id = match self.get_config(Config::LastMsgId).await? {
|
||||
Some(s) => MsgId::new(s.parse()?),
|
||||
None => MsgId::new_unset(),
|
||||
};
|
||||
|
||||
let list = self
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT m.id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
LEFT JOIN chats c
|
||||
ON m.chat_id=c.id
|
||||
WHERE m.id>?
|
||||
AND m.hidden=0
|
||||
AND m.chat_id>9
|
||||
AND ct.blocked=0
|
||||
AND c.blocked!=1
|
||||
ORDER BY m.id ASC",
|
||||
(
|
||||
last_msg_id.to_u32(), // Explicitly convert to u32 because 0 is allowed.
|
||||
),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
},
|
||||
|rows| {
|
||||
let mut list = Vec::new();
|
||||
for row in rows {
|
||||
list.push(row?);
|
||||
}
|
||||
Ok(list)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Returns a list of messages with database ID higher than last marked as seen.
|
||||
///
|
||||
/// This function is supposed to be used by bot to request messages
|
||||
/// that are not processed yet.
|
||||
///
|
||||
/// Waits for notification and returns a result.
|
||||
/// Note that the result may be empty if the message is deleted
|
||||
/// shortly after notification or notification is manually triggered
|
||||
/// to interrupt waiting.
|
||||
/// Notification may be manually triggered by calling [`Self::stop_io`].
|
||||
pub async fn wait_next_msgs(&self) -> Result<Vec<MsgId>> {
|
||||
self.new_msgs_notify.notified().await;
|
||||
let list = self.get_next_msgs().await?;
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Searches for messages containing the query string.
|
||||
///
|
||||
/// If `chat_id` is provided this searches only for messages in this chat, if `chat_id`
|
||||
@@ -839,24 +914,10 @@ impl Context {
|
||||
}
|
||||
let str_like_in_text = format!("%{real_query}%");
|
||||
|
||||
let do_query = |query, params| {
|
||||
self.sql.query_map(
|
||||
query,
|
||||
params,
|
||||
|row| row.get::<_, MsgId>("id"),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
for id in rows {
|
||||
ret.push(id?);
|
||||
}
|
||||
Ok(ret)
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let list = if let Some(chat_id) = chat_id {
|
||||
do_query(
|
||||
"SELECT m.id AS id
|
||||
self.sql
|
||||
.query_map(
|
||||
"SELECT m.id AS id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
@@ -865,9 +926,17 @@ impl Context {
|
||||
AND ct.blocked=0
|
||||
AND txt LIKE ?
|
||||
ORDER BY m.timestamp,m.id;",
|
||||
paramsv![chat_id, str_like_in_text],
|
||||
)
|
||||
.await?
|
||||
(chat_id, str_like_in_text),
|
||||
|row| row.get::<_, MsgId>("id"),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
for id in rows {
|
||||
ret.push(id?);
|
||||
}
|
||||
Ok(ret)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
// For performance reasons results are sorted only by `id`, that is in the order of
|
||||
// message reception.
|
||||
@@ -879,8 +948,9 @@ impl Context {
|
||||
// of unwanted results that are discarded moments later, we added `LIMIT 1000`.
|
||||
// According to some tests, this limit speeds up eg. 2 character searches by factor 10.
|
||||
// The limit is documented and UI may add a hint when getting 1000 results.
|
||||
do_query(
|
||||
"SELECT m.id AS id
|
||||
self.sql
|
||||
.query_map(
|
||||
"SELECT m.id AS id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
@@ -892,9 +962,17 @@ impl Context {
|
||||
AND ct.blocked=0
|
||||
AND m.txt LIKE ?
|
||||
ORDER BY m.id DESC LIMIT 1000",
|
||||
paramsv![str_like_in_text],
|
||||
)
|
||||
.await?
|
||||
(str_like_in_text,),
|
||||
|row| row.get::<_, MsgId>("id"),
|
||||
|rows| {
|
||||
let mut ret = Vec::new();
|
||||
for id in rows {
|
||||
ret.push(id?);
|
||||
}
|
||||
Ok(ret)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
Ok(list)
|
||||
@@ -1152,7 +1230,7 @@ mod tests {
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE chats SET muted_until=? WHERE id=?;",
|
||||
paramsv![time() - 3600, bob.id],
|
||||
(time() - 3600, bob.id),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1169,10 +1247,7 @@ mod tests {
|
||||
// to test get_fresh_msgs() with invalid mute_until (everything < -1),
|
||||
// that results in "muted forever" by definition.
|
||||
t.sql
|
||||
.execute(
|
||||
"UPDATE chats SET muted_until=-2 WHERE id=?;",
|
||||
paramsv![bob.id],
|
||||
)
|
||||
.execute("UPDATE chats SET muted_until=-2 WHERE id=?;", (bob.id,))
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
|
||||
@@ -1521,4 +1596,38 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_next_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
|
||||
assert!(alice.get_next_msgs().await?.is_empty());
|
||||
assert!(bob.get_next_msgs().await?.is_empty());
|
||||
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
let bob_next_msg_ids = bob.get_next_msgs().await?;
|
||||
assert_eq!(bob_next_msg_ids.len(), 1);
|
||||
assert_eq!(bob_next_msg_ids.get(0), Some(&received_msg.id));
|
||||
|
||||
bob.set_config_u32(Config::LastMsgId, received_msg.id.to_u32())
|
||||
.await?;
|
||||
assert!(bob.get_next_msgs().await?.is_empty());
|
||||
|
||||
// Next messages include self-sent messages.
|
||||
let alice_next_msg_ids = alice.get_next_msgs().await?;
|
||||
assert_eq!(alice_next_msg_ids.len(), 1);
|
||||
assert_eq!(alice_next_msg_ids.get(0), Some(&sent_msg.sender_msg_id));
|
||||
|
||||
alice
|
||||
.set_config_u32(Config::LastMsgId, sent_msg.sender_msg_id.to_u32())
|
||||
.await?;
|
||||
assert!(alice.get_next_msgs().await?.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ impl MsgId {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET download_state=? WHERE id=?;",
|
||||
paramsv![download_state, self],
|
||||
(download_state, self),
|
||||
)
|
||||
.await?;
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
@@ -134,7 +134,7 @@ impl Job {
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target=folder",
|
||||
paramsv![msg.rfc724_mid],
|
||||
(&msg.rfc724_mid,),
|
||||
|row| {
|
||||
let server_uid: u32 = row.get(0)?;
|
||||
let server_folder: String = row.get(1)?;
|
||||
|
||||
@@ -174,10 +174,7 @@ impl ChatId {
|
||||
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer> {
|
||||
let timer = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT ephemeral_timer FROM chats WHERE id=?;",
|
||||
paramsv![self],
|
||||
)
|
||||
.query_get_value("SELECT ephemeral_timer FROM chats WHERE id=?;", (self,))
|
||||
.await?;
|
||||
Ok(timer.unwrap_or_default())
|
||||
}
|
||||
@@ -199,7 +196,7 @@ impl ChatId {
|
||||
"UPDATE chats
|
||||
SET ephemeral_timer=?
|
||||
WHERE id=?;",
|
||||
paramsv![timer, self],
|
||||
(timer, self),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -291,10 +288,7 @@ impl MsgId {
|
||||
pub(crate) async fn ephemeral_timer(self, context: &Context) -> Result<Timer> {
|
||||
let res = match context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT ephemeral_timer FROM msgs WHERE id=?",
|
||||
paramsv![self],
|
||||
)
|
||||
.query_get_value("SELECT ephemeral_timer FROM msgs WHERE id=?", (self,))
|
||||
.await?
|
||||
{
|
||||
None | Some(0) => Timer::Disabled,
|
||||
@@ -314,7 +308,7 @@ impl MsgId {
|
||||
"UPDATE msgs SET ephemeral_timestamp = ? \
|
||||
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \
|
||||
AND id = ?",
|
||||
paramsv![ephemeral_timestamp, ephemeral_timestamp, self],
|
||||
(ephemeral_timestamp, ephemeral_timestamp, self),
|
||||
)
|
||||
.await?;
|
||||
context.scheduler.interrupt_ephemeral_task().await;
|
||||
@@ -338,8 +332,8 @@ pub(crate) async fn start_ephemeral_timers_msgids(
|
||||
sql::repeat_vars(msg_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(
|
||||
std::iter::once(&now as &dyn crate::ToSql)
|
||||
.chain(std::iter::once(&now as &dyn crate::ToSql))
|
||||
std::iter::once(&now as &dyn crate::sql::ToSql)
|
||||
.chain(std::iter::once(&now as &dyn crate::sql::ToSql))
|
||||
.chain(params_iter(msg_ids)),
|
||||
),
|
||||
)
|
||||
@@ -369,7 +363,7 @@ WHERE
|
||||
AND ephemeral_timestamp <= ?
|
||||
AND chat_id != ?
|
||||
"#,
|
||||
paramsv![now, DC_CHAT_ID_TRASH],
|
||||
(now, DC_CHAT_ID_TRASH),
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
@@ -402,12 +396,12 @@ WHERE
|
||||
AND chat_id != ?
|
||||
AND chat_id != ?
|
||||
"#,
|
||||
paramsv![
|
||||
(
|
||||
threshold_timestamp,
|
||||
DC_CHAT_ID_LAST_SPECIAL,
|
||||
self_chat_id,
|
||||
device_chat_id
|
||||
],
|
||||
device_chat_id,
|
||||
),
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
@@ -449,7 +443,7 @@ pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Resu
|
||||
SET chat_id=?, txt='', subject='', txt_raw='',
|
||||
mime_headers='', from_id=0, to_id=0, param=''
|
||||
WHERE id=?",
|
||||
params![DC_CHAT_ID_TRASH, msg_id],
|
||||
(DC_CHAT_ID_TRASH, msg_id),
|
||||
)?;
|
||||
|
||||
msgs_changed.push((chat_id, msg_id));
|
||||
@@ -494,7 +488,7 @@ async fn next_delete_device_after_timestamp(context: &Context) -> Result<Option<
|
||||
AND chat_id != ?
|
||||
AND chat_id != ?;
|
||||
"#,
|
||||
paramsv![DC_CHAT_ID_TRASH, self_chat_id, device_chat_id],
|
||||
(DC_CHAT_ID_TRASH, self_chat_id, device_chat_id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -518,7 +512,7 @@ async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
|
||||
WHERE ephemeral_timestamp != 0
|
||||
AND chat_id != ?;
|
||||
"#,
|
||||
paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them
|
||||
(DC_CHAT_ID_TRASH,), // Trash contains already deleted messages, skip them
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -605,12 +599,12 @@ pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()
|
||||
(download_state != 0 AND timestamp < ?) OR
|
||||
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
|
||||
)",
|
||||
paramsv![
|
||||
target,
|
||||
(
|
||||
&target,
|
||||
threshold_timestamp,
|
||||
threshold_timestamp_extended,
|
||||
now,
|
||||
],
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -635,12 +629,12 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
|
||||
WHERE ephemeral_timer > 0 \
|
||||
AND ephemeral_timestamp = 0 \
|
||||
AND state NOT IN (?, ?, ?)",
|
||||
paramsv![
|
||||
(
|
||||
time(),
|
||||
MessageState::InFresh,
|
||||
MessageState::InNoticed,
|
||||
MessageState::OutDraft
|
||||
],
|
||||
MessageState::OutDraft,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1106,7 +1100,7 @@ mod tests {
|
||||
assert!(msg.text.is_none_or_empty(), "{:?}", msg.text);
|
||||
let rawtxt: Option<String> = t
|
||||
.sql
|
||||
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id])
|
||||
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(rawtxt.is_none_or_empty(), "{rawtxt:?}");
|
||||
@@ -1131,13 +1125,13 @@ mod tests {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
|
||||
paramsv![id, message_id, timestamp, ephemeral_timestamp],
|
||||
(id, &message_id, timestamp, ephemeral_timestamp),
|
||||
)
|
||||
.await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
|
||||
paramsv![message_id, id],
|
||||
(&message_id, id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -1148,7 +1142,7 @@ mod tests {
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
|
||||
paramsv![id.to_string()],
|
||||
(id.to_string(),),
|
||||
)
|
||||
.await?,
|
||||
1
|
||||
@@ -1159,10 +1153,7 @@ mod tests {
|
||||
async fn remove_uid(context: &Context, id: u32) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM imap WHERE rfc724_mid=?",
|
||||
paramsv![id.to_string()],
|
||||
)
|
||||
.execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ impl Events {
|
||||
Self { receiver, sender }
|
||||
}
|
||||
|
||||
/// Emits an event.
|
||||
/// Emits an event into event channel.
|
||||
///
|
||||
/// If the channel is full, deletes the oldest event first.
|
||||
pub fn emit(&self, event: Event) {
|
||||
match self.sender.try_send(event) {
|
||||
Ok(()) => {}
|
||||
@@ -49,7 +51,7 @@ impl Events {
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve the event emitter.
|
||||
/// Creates an event emitter.
|
||||
pub fn get_emitter(&self) -> EventEmitter {
|
||||
EventEmitter(self.receiver.clone())
|
||||
}
|
||||
|
||||
62
src/imap.rs
62
src/imap.rs
@@ -550,7 +550,7 @@ impl Imap {
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
transaction.execute("DELETE FROM imap WHERE folder=?", params![folder])?;
|
||||
transaction.execute("DELETE FROM imap WHERE folder=?", (folder,))?;
|
||||
for (uid, (rfc724_mid, target)) in &msgs {
|
||||
// This may detect previously undetected moved
|
||||
// messages, so we update server_folder too.
|
||||
@@ -560,7 +560,7 @@ impl Imap {
|
||||
ON CONFLICT(folder, uid, uidvalidity)
|
||||
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
|
||||
target=excluded.target",
|
||||
params![rfc724_mid, folder, uid, uid_validity, target],
|
||||
(rfc724_mid, folder, uid, uid_validity, target),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -676,7 +676,7 @@ impl Imap {
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM imap WHERE folder=? AND uidvalidity!=?",
|
||||
paramsv![folder, new_uid_validity],
|
||||
(&folder, new_uid_validity),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -759,7 +759,7 @@ impl Imap {
|
||||
ON CONFLICT(folder, uid, uidvalidity)
|
||||
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
|
||||
target=excluded.target",
|
||||
paramsv![message_id, folder, uid, uid_validity, &target],
|
||||
(&message_id, &folder, uid, uid_validity, &target),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1050,7 +1050,7 @@ impl Session {
|
||||
WHERE folder = ?
|
||||
AND target != folder
|
||||
ORDER BY target, uid",
|
||||
paramsv![folder],
|
||||
(folder,),
|
||||
|row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
let uid: u32 = row.get(1)?;
|
||||
@@ -1740,17 +1740,37 @@ impl Session {
|
||||
/// If this returns `true`, this means that new emails arrived and you should
|
||||
/// fetch again, even if you just fetched.
|
||||
fn server_sent_unsolicited_exists(&self, context: &Context) -> Result<bool> {
|
||||
use async_imap::imap_proto::Response;
|
||||
use async_imap::imap_proto::ResponseCode;
|
||||
use UnsolicitedResponse::*;
|
||||
|
||||
let mut unsolicited_exists = false;
|
||||
while let Ok(response) = self.unsolicited_responses.try_recv() {
|
||||
match response {
|
||||
UnsolicitedResponse::Exists(_) => {
|
||||
Exists(_) => {
|
||||
info!(
|
||||
context,
|
||||
"Need to fetch again, got unsolicited EXISTS {:?}", response
|
||||
);
|
||||
unsolicited_exists = true;
|
||||
}
|
||||
_ => info!(context, "ignoring unsolicited response {:?}", response),
|
||||
|
||||
// We are not interested in the following responses and they are are
|
||||
// sent quite frequently, so, we ignore them without logging them
|
||||
Expunge(_) | Recent(_) => {}
|
||||
Other(response_data)
|
||||
if matches!(
|
||||
response_data.parsed(),
|
||||
Response::Fetch { .. }
|
||||
| Response::Done {
|
||||
code: Some(ResponseCode::CopyUid(_, _, _)),
|
||||
..
|
||||
}
|
||||
) => {}
|
||||
|
||||
_ => {
|
||||
info!(context, "got unsolicited response {:?}", response)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(unsolicited_exists)
|
||||
@@ -2168,7 +2188,7 @@ async fn mark_seen_by_uid(
|
||||
AND uid=?3
|
||||
LIMIT 1
|
||||
)",
|
||||
paramsv![&folder, uid_validity, uid],
|
||||
(&folder, uid_validity, uid),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let chat_id: ChatId = row.get(1)?;
|
||||
@@ -2184,12 +2204,12 @@ async fn mark_seen_by_uid(
|
||||
"UPDATE msgs SET state=?1
|
||||
WHERE (state=?2 OR state=?3)
|
||||
AND id=?4",
|
||||
paramsv![
|
||||
(
|
||||
MessageState::InSeen,
|
||||
MessageState::InFresh,
|
||||
MessageState::InNoticed,
|
||||
msg_id
|
||||
],
|
||||
msg_id,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to update msg {msg_id} state"))?
|
||||
@@ -2219,7 +2239,7 @@ pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str)
|
||||
.execute(
|
||||
"INSERT OR IGNORE INTO imap_markseen (id)
|
||||
SELECT id FROM imap WHERE rfc724_mid=?",
|
||||
paramsv![message_id],
|
||||
(message_id,),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
@@ -2239,7 +2259,7 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
|
||||
.execute(
|
||||
"INSERT INTO imap_sync (folder, uid_next) VALUES (?,?)
|
||||
ON CONFLICT(folder) DO UPDATE SET uid_next=excluded.uid_next",
|
||||
paramsv![folder, uid_next],
|
||||
(folder, uid_next),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -2253,10 +2273,7 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
|
||||
async fn get_uid_next(context: &Context, folder: &str) -> Result<u32> {
|
||||
Ok(context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT uid_next FROM imap_sync WHERE folder=?;",
|
||||
paramsv![folder],
|
||||
)
|
||||
.query_get_value("SELECT uid_next FROM imap_sync WHERE folder=?;", (folder,))
|
||||
.await?
|
||||
.unwrap_or(0))
|
||||
}
|
||||
@@ -2271,7 +2288,7 @@ pub(crate) async fn set_uidvalidity(
|
||||
.execute(
|
||||
"INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?)
|
||||
ON CONFLICT(folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
|
||||
paramsv![folder, uidvalidity],
|
||||
(folder, uidvalidity),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -2282,7 +2299,7 @@ async fn get_uidvalidity(context: &Context, folder: &str) -> Result<u32> {
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT uidvalidity FROM imap_sync WHERE folder=?;",
|
||||
paramsv![folder],
|
||||
(folder,),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or(0))
|
||||
@@ -2294,7 +2311,7 @@ pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) ->
|
||||
.execute(
|
||||
"INSERT INTO imap_sync (folder, modseq) VALUES (?,?)
|
||||
ON CONFLICT(folder) DO UPDATE SET modseq=excluded.modseq",
|
||||
paramsv![folder, modseq],
|
||||
(folder, modseq),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -2303,10 +2320,7 @@ pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) ->
|
||||
async fn get_modseq(context: &Context, folder: &str) -> Result<u64> {
|
||||
Ok(context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT modseq FROM imap_sync WHERE folder=?;",
|
||||
paramsv![folder],
|
||||
)
|
||||
.query_get_value("SELECT modseq FROM imap_sync WHERE folder=?;", (folder,))
|
||||
.await?
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
@@ -762,14 +762,11 @@ async fn export_database(context: &Context, dest: &Path, passphrase: String) ->
|
||||
context
|
||||
.sql
|
||||
.call_write(|conn| {
|
||||
conn.execute("VACUUM;", params![])
|
||||
conn.execute("VACUUM;", ())
|
||||
.map_err(|err| warn!(context, "Vacuum failed, exporting anyway {err}"))
|
||||
.ok();
|
||||
conn.execute(
|
||||
"ATTACH DATABASE ? AS backup KEY ?",
|
||||
paramsv![dest, passphrase],
|
||||
)
|
||||
.context("failed to attach backup database")?;
|
||||
conn.execute("ATTACH DATABASE ? AS backup KEY ?", (dest, passphrase))
|
||||
.context("failed to attach backup database")?;
|
||||
let res = conn
|
||||
.query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(()))
|
||||
.context("failed to export to attached backup database");
|
||||
|
||||
@@ -46,9 +46,11 @@ use tokio_stream::wrappers::ReadDirStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::blob::BlobDirContents;
|
||||
use crate::chat::delete_and_reset_all_device_msgs;
|
||||
use crate::chat::{add_device_msg, delete_and_reset_all_device_msgs};
|
||||
use crate::context::{Context, OngoingGuard};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::qr::Qr;
|
||||
use crate::stock_str::backup_transfer_msg_body;
|
||||
use crate::{e2ee, EventType};
|
||||
|
||||
use super::{export_database, DBFILE_BACKUP_NAME};
|
||||
@@ -261,7 +263,12 @@ impl BackupProvider {
|
||||
}
|
||||
};
|
||||
match &res {
|
||||
Ok(_) => context.emit_event(SendProgress::Completed.into()),
|
||||
Ok(_) => {
|
||||
context.emit_event(SendProgress::Completed.into());
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(backup_transfer_msg_body(context).await);
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "Backup transfer failure: {err:#}");
|
||||
context.emit_event(SendProgress::Failed.into())
|
||||
|
||||
28
src/job.rs
28
src/job.rs
@@ -99,7 +99,7 @@ impl Job {
|
||||
if self.job_id != 0 {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32])
|
||||
.execute("DELETE FROM jobs WHERE id=?;", (self.job_id as i32,))
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -117,22 +117,22 @@ impl Job {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE jobs SET desired_timestamp=?, tries=? WHERE id=?;",
|
||||
paramsv![
|
||||
(
|
||||
self.desired_timestamp,
|
||||
i64::from(self.tries),
|
||||
self.job_id as i32,
|
||||
],
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
context.sql.execute(
|
||||
"INSERT INTO jobs (added_timestamp, action, foreign_id, desired_timestamp) VALUES (?,?,?,?);",
|
||||
paramsv![
|
||||
(
|
||||
self.added_timestamp,
|
||||
self.action,
|
||||
self.foreign_id,
|
||||
self.desired_timestamp
|
||||
]
|
||||
)
|
||||
).await?;
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ WHERE desired_timestamp<=?
|
||||
ORDER BY action DESC, added_timestamp
|
||||
LIMIT 1;
|
||||
"#;
|
||||
params = paramsv![t];
|
||||
params = vec![t];
|
||||
} else {
|
||||
// processing after call to dc_maybe_network():
|
||||
// process _all_ pending jobs that failed before
|
||||
@@ -289,13 +289,13 @@ WHERE tries>0
|
||||
ORDER BY desired_timestamp, action DESC
|
||||
LIMIT 1;
|
||||
"#;
|
||||
params = paramsv![];
|
||||
params = vec![];
|
||||
};
|
||||
|
||||
loop {
|
||||
let job_res = context
|
||||
.sql
|
||||
.query_row_optional(query, params.clone(), |row| {
|
||||
.query_row_optional(query, rusqlite::params_from_iter(params.clone()), |row| {
|
||||
let job = Job {
|
||||
job_id: row.get("id")?,
|
||||
action: row.get("action")?,
|
||||
@@ -318,12 +318,14 @@ LIMIT 1;
|
||||
// TODO: improve by only doing a single query
|
||||
let id = context
|
||||
.sql
|
||||
.query_row(query, params.clone(), |row| row.get::<_, i32>(0))
|
||||
.query_row(query, rusqlite::params_from_iter(params.clone()), |row| {
|
||||
row.get::<_, i32>(0)
|
||||
})
|
||||
.await
|
||||
.context("failed to retrieve invalid job ID from the database")?;
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
|
||||
.execute("DELETE FROM jobs WHERE id=?;", (id,))
|
||||
.await
|
||||
.with_context(|| format!("failed to delete invalid job {id}"))?;
|
||||
}
|
||||
@@ -344,7 +346,7 @@ mod tests {
|
||||
"INSERT INTO jobs
|
||||
(added_timestamp, action, foreign_id, desired_timestamp)
|
||||
VALUES (?, ?, ?, ?);",
|
||||
paramsv![
|
||||
(
|
||||
now,
|
||||
if valid {
|
||||
Action::DownloadMsg as i32
|
||||
@@ -352,8 +354,8 @@ mod tests {
|
||||
-1
|
||||
},
|
||||
foreign_id,
|
||||
now
|
||||
],
|
||||
now,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -100,7 +100,7 @@ impl DcKey for SignedPublicKey {
|
||||
WHERE addr=?
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![addr],
|
||||
(addr,),
|
||||
|row| {
|
||||
let bytes: Vec<u8> = row.get(0)?;
|
||||
Ok(bytes)
|
||||
@@ -240,7 +240,7 @@ pub(crate) async fn load_keypair(
|
||||
WHERE addr=?1
|
||||
AND is_default=1;
|
||||
"#,
|
||||
paramsv![addr],
|
||||
(addr,),
|
||||
|row| {
|
||||
let pub_bytes: Vec<u8> = row.get(0)?;
|
||||
let sec_bytes: Vec<u8> = row.get(1)?;
|
||||
@@ -297,7 +297,7 @@ pub async fn store_self_keypair(
|
||||
transaction
|
||||
.execute(
|
||||
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
|
||||
paramsv![public_key, secret_key],
|
||||
(&public_key, &secret_key),
|
||||
)
|
||||
.context("failed to remove old use of key")?;
|
||||
if default == KeyPairUse::Default {
|
||||
@@ -317,7 +317,7 @@ pub async fn store_self_keypair(
|
||||
.execute(
|
||||
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
|
||||
VALUES (?,?,?,?,?);",
|
||||
paramsv![addr, is_default, public_key, secret_key, t],
|
||||
(addr, is_default, &public_key, &secret_key, t),
|
||||
)
|
||||
.context("failed to insert keypair")?;
|
||||
|
||||
|
||||
@@ -35,11 +35,6 @@ extern crate rusqlite;
|
||||
#[macro_use]
|
||||
extern crate strum_macros;
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub trait ToSql: rusqlite::ToSql + Send + Sync {}
|
||||
|
||||
impl<T: rusqlite::ToSql + Send + Sync> ToSql for T {}
|
||||
|
||||
#[macro_use]
|
||||
pub mod log;
|
||||
|
||||
@@ -71,6 +66,7 @@ pub mod ephemeral;
|
||||
mod http;
|
||||
mod imap;
|
||||
pub mod imex;
|
||||
pub mod release;
|
||||
mod scheduler;
|
||||
#[macro_use]
|
||||
mod job;
|
||||
|
||||
@@ -271,11 +271,11 @@ pub async fn send_locations_to_chat(
|
||||
SET locations_send_begin=?, \
|
||||
locations_send_until=? \
|
||||
WHERE id=?",
|
||||
paramsv![
|
||||
(
|
||||
if 0 != seconds { now } else { 0 },
|
||||
if 0 != seconds { now + seconds } else { 0 },
|
||||
chat_id,
|
||||
],
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
if 0 != seconds && !is_sending_locations_before {
|
||||
@@ -310,7 +310,7 @@ pub async fn is_sending_locations_to_chat(
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;",
|
||||
paramsv![chat_id, time()],
|
||||
(chat_id, time()),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
@@ -319,7 +319,7 @@ pub async fn is_sending_locations_to_chat(
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(id) FROM chats WHERE locations_send_until>?;",
|
||||
paramsv![time()],
|
||||
(time(),),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
@@ -338,7 +338,7 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM chats WHERE locations_send_until>?;",
|
||||
paramsv![time()],
|
||||
(time(),),
|
||||
|row| row.get::<_, i32>(0),
|
||||
|chats| {
|
||||
chats
|
||||
@@ -352,14 +352,14 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
|
||||
if let Err(err) = context.sql.execute(
|
||||
"INSERT INTO locations \
|
||||
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
|
||||
paramsv![
|
||||
(
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
time(),
|
||||
chat_id,
|
||||
ContactId::SELF,
|
||||
]
|
||||
)
|
||||
).await {
|
||||
warn!(context, "failed to store location {:#}", err);
|
||||
} else {
|
||||
@@ -404,14 +404,14 @@ pub async fn get_range(
|
||||
AND (? OR l.from_id=?) \
|
||||
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
|
||||
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
|
||||
paramsv![
|
||||
(
|
||||
disable_chat_id,
|
||||
chat_id,
|
||||
disable_contact_id,
|
||||
contact_id as i32,
|
||||
timestamp_from,
|
||||
timestamp_to,
|
||||
],
|
||||
),
|
||||
|row| {
|
||||
let msg_id = row.get(6)?;
|
||||
let txt: String = row.get(9)?;
|
||||
@@ -471,7 +471,7 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
|
||||
|
||||
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
|
||||
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
|
||||
paramsv![chat_id], |row| {
|
||||
(chat_id,), |row| {
|
||||
let send_begin: i64 = row.get(0)?;
|
||||
let send_until: i64 = row.get(1)?;
|
||||
let last_sent: i64 = row.get(2)?;
|
||||
@@ -500,12 +500,12 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
|
||||
AND independent=0 \
|
||||
GROUP BY timestamp \
|
||||
ORDER BY timestamp;",
|
||||
paramsv![
|
||||
(
|
||||
ContactId::SELF,
|
||||
locations_send_begin,
|
||||
locations_last_sent,
|
||||
ContactId::SELF
|
||||
],
|
||||
),
|
||||
|row| {
|
||||
let location_id: i32 = row.get(0)?;
|
||||
let latitude: f64 = row.get(1)?;
|
||||
@@ -575,7 +575,7 @@ pub async fn set_kml_sent_timestamp(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
|
||||
paramsv![timestamp, chat_id],
|
||||
(timestamp, chat_id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -587,7 +587,7 @@ pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id:
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET location_id=? WHERE id=?;",
|
||||
paramsv![location_id, msg_id],
|
||||
(location_id, msg_id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -629,10 +629,10 @@ pub(crate) async fn save(
|
||||
.prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
|
||||
let mut stmt_insert = conn.prepare_cached(stmt_insert)?;
|
||||
|
||||
let exists = stmt_test.exists(paramsv![timestamp, contact_id])?;
|
||||
let exists = stmt_test.exists((timestamp, contact_id))?;
|
||||
|
||||
if independent || !exists {
|
||||
stmt_insert.execute(paramsv![
|
||||
stmt_insert.execute((
|
||||
timestamp,
|
||||
contact_id,
|
||||
chat_id,
|
||||
@@ -640,7 +640,7 @@ pub(crate) async fn save(
|
||||
longitude,
|
||||
accuracy,
|
||||
independent,
|
||||
])?;
|
||||
))?;
|
||||
|
||||
if timestamp > newest_timestamp {
|
||||
// okay to drop, as we use cached prepared statements
|
||||
@@ -729,7 +729,7 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
|
||||
AND timestamp>=? \
|
||||
AND timestamp>? \
|
||||
AND independent=0",
|
||||
paramsv![ContactId::SELF, locations_send_begin, locations_last_sent,],
|
||||
(ContactId::SELF, locations_send_begin, locations_last_sent),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -781,7 +781,7 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
|
||||
"UPDATE chats \
|
||||
SET locations_send_begin=0, locations_send_until=0 \
|
||||
WHERE id=?",
|
||||
paramsv![chat_id],
|
||||
(chat_id,),
|
||||
)
|
||||
.await
|
||||
.context("failed to disable location streaming")?;
|
||||
|
||||
@@ -77,7 +77,7 @@ impl MsgId {
|
||||
pub async fn get_state(self, context: &Context) -> Result<MessageState> {
|
||||
let result = context
|
||||
.sql
|
||||
.query_get_value("SELECT state FROM msgs WHERE id=?", paramsv![self])
|
||||
.query_get_value("SELECT state FROM msgs WHERE id=?", (self,))
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
Ok(result)
|
||||
@@ -106,7 +106,7 @@ SET
|
||||
param=''
|
||||
WHERE id=?;
|
||||
"#,
|
||||
paramsv![chat_id, self],
|
||||
(chat_id, self),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -119,22 +119,19 @@ WHERE id=?;
|
||||
// sure they are not left while the message is deleted.
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE msg_id=?", paramsv![self])
|
||||
.execute("DELETE FROM smtp WHERE msg_id=?", (self,))
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM msgs_mdns WHERE msg_id=?;", paramsv![self])
|
||||
.execute("DELETE FROM msgs_mdns WHERE msg_id=?;", (self,))
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM msgs_status_updates WHERE msg_id=?;",
|
||||
paramsv![self],
|
||||
)
|
||||
.execute("DELETE FROM msgs_status_updates WHERE msg_id=?;", (self,))
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM msgs WHERE id=?;", paramsv![self])
|
||||
.execute("DELETE FROM msgs WHERE id=?;", (self,))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -143,7 +140,7 @@ WHERE id=?;
|
||||
update_msg_state(context, self, MessageState::OutDelivered).await?;
|
||||
let chat_id: ChatId = context
|
||||
.sql
|
||||
.query_get_value("SELECT chat_id FROM msgs WHERE id=?", paramsv![self])
|
||||
.query_get_value("SELECT chat_id FROM msgs WHERE id=?", (self,))
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
context.emit_event(EventType::MsgDelivered {
|
||||
@@ -328,7 +325,7 @@ impl Message {
|
||||
" FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id",
|
||||
" WHERE m.id=?;"
|
||||
),
|
||||
paramsv![id],
|
||||
(id,),
|
||||
|row| {
|
||||
let text = match row.get_ref("txt")? {
|
||||
rusqlite::types::ValueRef::Text(buf) => {
|
||||
@@ -949,7 +946,7 @@ impl Message {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET param=? WHERE id=?;",
|
||||
paramsv![self.param.to_string(), self.id],
|
||||
(self.param.to_string(), self.id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -960,7 +957,7 @@ impl Message {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET subject=? WHERE id=?;",
|
||||
paramsv![self.subject, self.id],
|
||||
(&self.subject, self.id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -1094,7 +1091,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
let rawtxt: Option<String> = context
|
||||
.sql
|
||||
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id])
|
||||
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,))
|
||||
.await?;
|
||||
|
||||
let mut ret = String::new();
|
||||
@@ -1144,7 +1141,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;",
|
||||
paramsv![msg_id],
|
||||
(msg_id,),
|
||||
|row| {
|
||||
let contact_id: ContactId = row.get(0)?;
|
||||
let ts: i64 = row.get(1)?;
|
||||
@@ -1225,7 +1222,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT folder, uid FROM imap WHERE rfc724_mid=?",
|
||||
paramsv![msg.rfc724_mid],
|
||||
(msg.rfc724_mid,),
|
||||
|row| {
|
||||
let folder: String = row.get("folder")?;
|
||||
let uid: u32 = row.get("uid")?;
|
||||
@@ -1245,7 +1242,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
|
||||
}
|
||||
let hop_info: Option<String> = context
|
||||
.sql
|
||||
.query_get_value("SELECT hop_info FROM msgs WHERE id=?;", paramsv![msg_id])
|
||||
.query_get_value("SELECT hop_info FROM msgs WHERE id=?;", (msg_id,))
|
||||
.await?;
|
||||
|
||||
ret += "\n\n";
|
||||
@@ -1353,7 +1350,7 @@ pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT mime_headers, mime_compressed FROM msgs WHERE id=?",
|
||||
paramsv![msg_id],
|
||||
(msg_id,),
|
||||
|row| {
|
||||
let headers = sql::row_get_vec(row, 0)?;
|
||||
let compressed: bool = row.get(1)?;
|
||||
@@ -1378,7 +1375,7 @@ pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8
|
||||
"\
|
||||
UPDATE msgs SET mime_headers=?, mime_compressed=1 \
|
||||
WHERE id=? AND mime_headers!='' AND mime_compressed=0",
|
||||
params![compressed, msg_id],
|
||||
(compressed, msg_id),
|
||||
) {
|
||||
Ok(rows_updated) => ensure!(rows_updated <= 1),
|
||||
Err(e) => {
|
||||
@@ -1421,7 +1418,7 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap SET target=? WHERE rfc724_mid=?",
|
||||
paramsv![target, msg.rfc724_mid],
|
||||
(target, msg.rfc724_mid),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1459,7 +1456,7 @@ async fn delete_poi_location(context: &Context, location_id: u32) -> Result<()>
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM locations WHERE independent = 1 AND id=?;",
|
||||
paramsv![location_id as i32],
|
||||
(location_id as i32,),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -1471,6 +1468,12 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let old_last_msg_id = MsgId::new(context.get_config_u32(Config::LastMsgId).await?);
|
||||
let last_msg_id = msg_ids.iter().fold(&old_last_msg_id, std::cmp::max);
|
||||
context
|
||||
.set_config_u32(Config::LastMsgId, last_msg_id.to_u32())
|
||||
.await?;
|
||||
|
||||
let msgs = context
|
||||
.sql
|
||||
.query_map(
|
||||
@@ -1558,7 +1561,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
|
||||
paramsv![id, curr_from_id, curr_rfc724_mid],
|
||||
(id, curr_from_id, curr_rfc724_mid),
|
||||
)
|
||||
.await
|
||||
.context("failed to insert into smtp_mdns")?;
|
||||
@@ -1586,10 +1589,7 @@ pub(crate) async fn update_msg_state(
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET state=? WHERE id=?;",
|
||||
paramsv![state, msg_id],
|
||||
)
|
||||
.execute("UPDATE msgs SET state=? WHERE id=?;", (state, msg_id))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1609,7 +1609,7 @@ pub(crate) async fn exists(context: &Context, msg_id: MsgId) -> Result<bool> {
|
||||
|
||||
let chat_id: Option<ChatId> = context
|
||||
.sql
|
||||
.query_get_value("SELECT chat_id FROM msgs WHERE id=?;", paramsv![msg_id])
|
||||
.query_get_value("SELECT chat_id FROM msgs WHERE id=?;", (msg_id,))
|
||||
.await?;
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
@@ -1635,7 +1635,7 @@ pub(crate) async fn set_msg_failed(context: &Context, msg_id: MsgId, error: &str
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET state=?, error=? WHERE id=?;",
|
||||
paramsv![msg.state, error, msg_id],
|
||||
(msg.state, error, msg_id),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1680,7 +1680,7 @@ pub async fn handle_mdn(
|
||||
" WHERE rfc724_mid=? AND from_id=1",
|
||||
" ORDER BY m.id;"
|
||||
),
|
||||
paramsv![rfc724_mid],
|
||||
(&rfc724_mid,),
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, MsgId>("msg_id")?,
|
||||
@@ -1706,7 +1706,7 @@ pub async fn handle_mdn(
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=? AND contact_id=?;",
|
||||
paramsv![msg_id, from_id],
|
||||
(msg_id, from_id),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
@@ -1714,7 +1714,7 @@ pub async fn handle_mdn(
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);",
|
||||
paramsv![msg_id, from_id, timestamp_sent],
|
||||
(msg_id, from_id, timestamp_sent),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -1754,7 +1754,7 @@ pub(crate) async fn handle_ndn(
|
||||
" FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id",
|
||||
" WHERE rfc724_mid=? AND from_id=1",
|
||||
),
|
||||
paramsv![failed.rfc724_mid],
|
||||
(&failed.rfc724_mid,),
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, MsgId>("msg_id")?,
|
||||
@@ -1894,7 +1894,7 @@ pub async fn estimate_deletion_cnt(
|
||||
AND timestamp < ?
|
||||
AND chat_id != ?
|
||||
AND EXISTS (SELECT * FROM imap WHERE rfc724_mid=m.rfc724_mid);",
|
||||
paramsv![DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id],
|
||||
(DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
@@ -1907,12 +1907,12 @@ pub async fn estimate_deletion_cnt(
|
||||
AND timestamp < ?
|
||||
AND chat_id != ?
|
||||
AND chat_id != ? AND hidden = 0;",
|
||||
paramsv![
|
||||
(
|
||||
DC_MSG_ID_LAST_SPECIAL,
|
||||
threshold_timestamp,
|
||||
self_chat_id,
|
||||
DC_CHAT_ID_TRASH
|
||||
],
|
||||
DC_CHAT_ID_TRASH,
|
||||
),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
@@ -1933,7 +1933,7 @@ pub(crate) async fn rfc724_mid_exists(
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id FROM msgs WHERE rfc724_mid=?",
|
||||
paramsv![rfc724_mid],
|
||||
(rfc724_mid,),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ impl<'a> MimeFactory<'a> {
|
||||
FROM chats_contacts cc \
|
||||
LEFT JOIN contacts c ON cc.contact_id=c.id \
|
||||
WHERE cc.chat_id=? AND cc.contact_id>9;",
|
||||
paramsv![msg.chat_id],
|
||||
(msg.chat_id,),
|
||||
|row| {
|
||||
let authname: String = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
@@ -195,7 +195,7 @@ impl<'a> MimeFactory<'a> {
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?",
|
||||
paramsv![msg.id],
|
||||
(msg.id,),
|
||||
|row| {
|
||||
let in_reply_to: String = row.get(0)?;
|
||||
let references: String = row.get(1)?;
|
||||
|
||||
@@ -33,7 +33,7 @@ use crate::peerstate::Peerstate;
|
||||
use crate::simplify::{simplify, SimplifiedText};
|
||||
use crate::stock_str;
|
||||
use crate::sync::SyncItems;
|
||||
use crate::tools::{get_filemeta, parse_receive_headers, truncate_by_lines};
|
||||
use crate::tools::{get_filemeta, parse_receive_headers, strip_rtlo_characters, truncate_by_lines};
|
||||
use crate::{location, tools};
|
||||
|
||||
/// A parsed MIME message.
|
||||
@@ -1668,10 +1668,7 @@ impl MimeMessage {
|
||||
{
|
||||
context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT timestamp FROM msgs WHERE rfc724_mid=?",
|
||||
paramsv![field],
|
||||
)
|
||||
.query_get_value("SELECT timestamp FROM msgs WHERE rfc724_mid=?", (field,))
|
||||
.await?
|
||||
} else {
|
||||
None
|
||||
@@ -1952,6 +1949,8 @@ fn get_attachment_filename(
|
||||
};
|
||||
}
|
||||
|
||||
let desired_filename = desired_filename.map(|filename| strip_rtlo_characters(&filename));
|
||||
|
||||
Ok(desired_filename)
|
||||
}
|
||||
|
||||
@@ -2350,7 +2349,7 @@ mod tests {
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)",
|
||||
paramsv!["Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org", timestamp],
|
||||
("Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org", timestamp),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to write to the database");
|
||||
|
||||
@@ -75,7 +75,7 @@ async fn lookup_host_with_cache(
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT (hostname, address)
|
||||
DO UPDATE SET timestamp=excluded.timestamp",
|
||||
paramsv![hostname, ip_string, now],
|
||||
(hostname, ip_string, now),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -89,7 +89,7 @@ async fn lookup_host_with_cache(
|
||||
WHERE hostname = ?
|
||||
AND ? < timestamp + 30 * 24 * 3600
|
||||
ORDER BY timestamp DESC",
|
||||
paramsv![hostname, now],
|
||||
(hostname, now),
|
||||
|row| {
|
||||
let address: String = row.get(0)?;
|
||||
Ok(address)
|
||||
@@ -157,7 +157,7 @@ pub(crate) async fn connect_tcp(
|
||||
"UPDATE dns_cache
|
||||
SET timestamp = ?
|
||||
WHERE address = ?",
|
||||
paramsv![time(), resolved_addr.ip().to_string()],
|
||||
(time(), resolved_addr.ip().to_string()),
|
||||
)
|
||||
.await?;
|
||||
break;
|
||||
|
||||
@@ -144,7 +144,7 @@ impl Peerstate {
|
||||
verified_key, verified_key_fingerprint, verifier \
|
||||
FROM acpeerstates \
|
||||
WHERE addr=? COLLATE NOCASE LIMIT 1;";
|
||||
Self::from_stmt(context, query, paramsv![addr]).await
|
||||
Self::from_stmt(context, query, (addr,)).await
|
||||
}
|
||||
|
||||
/// Loads peerstate corresponding to the given fingerprint from the database.
|
||||
@@ -160,7 +160,7 @@ impl Peerstate {
|
||||
OR gossip_key_fingerprint=? \
|
||||
ORDER BY public_key_fingerprint=? DESC LIMIT 1;";
|
||||
let fp = fingerprint.hex();
|
||||
Self::from_stmt(context, query, paramsv![fp, fp, fp]).await
|
||||
Self::from_stmt(context, query, (&fp, &fp, &fp)).await
|
||||
}
|
||||
|
||||
/// Loads peerstate by address or verified fingerprint.
|
||||
@@ -180,7 +180,7 @@ impl Peerstate {
|
||||
OR addr=? COLLATE NOCASE \
|
||||
ORDER BY verified_key_fingerprint=? DESC, last_seen DESC LIMIT 1;";
|
||||
let fp = fingerprint.hex();
|
||||
Self::from_stmt(context, query, paramsv![fp, addr, fp]).await
|
||||
Self::from_stmt(context, query, (&fp, &addr, &fp)).await
|
||||
}
|
||||
|
||||
async fn from_stmt(
|
||||
@@ -471,7 +471,7 @@ impl Peerstate {
|
||||
verified_key = excluded.verified_key,
|
||||
verified_key_fingerprint = excluded.verified_key_fingerprint,
|
||||
verifier = excluded.verifier",
|
||||
paramsv![
|
||||
(
|
||||
self.last_seen,
|
||||
self.last_seen_autocrypt,
|
||||
self.prefer_encrypt as i64,
|
||||
@@ -482,9 +482,9 @@ impl Peerstate {
|
||||
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.verified_key.as_ref().map(|k| k.to_bytes()),
|
||||
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.addr,
|
||||
&self.addr,
|
||||
self.verifier.as_deref().unwrap_or(""),
|
||||
],
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -524,7 +524,7 @@ impl Peerstate {
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT id FROM contacts WHERE addr=? COLLATE NOCASE;",
|
||||
paramsv![self.addr],
|
||||
(&self.addr,),
|
||||
)
|
||||
.await?
|
||||
.with_context(|| format!("contact with peerstate.addr {:?} not found", &self.addr))?;
|
||||
@@ -554,7 +554,7 @@ impl Peerstate {
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT created_timestamp FROM chats WHERE id=?;",
|
||||
paramsv![chat_id],
|
||||
(chat_id,),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or(0)
|
||||
@@ -851,7 +851,7 @@ mod tests {
|
||||
// can be loaded without errors.
|
||||
ctx.ctx
|
||||
.sql
|
||||
.execute("INSERT INTO acpeerstates (addr) VALUES(?)", paramsv![addr])
|
||||
.execute("INSERT INTO acpeerstates (addr) VALUES(?)", (addr,))
|
||||
.await
|
||||
.expect("Failed to write to the database");
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
mod data;
|
||||
|
||||
use anyhow::Result;
|
||||
use chrono::{NaiveDateTime, NaiveTime};
|
||||
use trust_dns_resolver::{config, AsyncResolver, TokioAsyncResolver};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS, PROVIDER_UPDATED};
|
||||
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS};
|
||||
|
||||
/// Provider status according to manual testing.
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
@@ -258,22 +257,12 @@ pub fn get_provider_by_id(id: &str) -> Option<&'static Provider> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns update timestamp as a unix timestamp compatible for comparison with time() and database times.
|
||||
pub fn get_provider_update_timestamp() -> i64 {
|
||||
NaiveDateTime::new(*PROVIDER_UPDATED, NaiveTime::from_hms_opt(0, 0, 0).unwrap())
|
||||
.timestamp_millis()
|
||||
/ 1_000
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::tools::time;
|
||||
|
||||
#[test]
|
||||
fn test_get_provider_by_domain_unexistant() {
|
||||
@@ -336,18 +325,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_provider_update_timestamp() {
|
||||
let timestamp_past = NaiveDateTime::new(
|
||||
NaiveDate::from_ymd_opt(2020, 9, 9).unwrap(),
|
||||
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
|
||||
)
|
||||
.timestamp_millis()
|
||||
/ 1_000;
|
||||
assert!(get_provider_update_timestamp() <= time());
|
||||
assert!(get_provider_update_timestamp() > timestamp_past);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_resolver() -> Result<()> {
|
||||
assert!(get_resolver().is_ok());
|
||||
|
||||
@@ -1945,5 +1945,5 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
])
|
||||
});
|
||||
|
||||
pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
|
||||
pub static _PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
|
||||
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2023, 2, 20).unwrap());
|
||||
|
||||
12
src/qr.rs
12
src/qr.rs
@@ -10,7 +10,7 @@ use percent_encoding::percent_decode_str;
|
||||
use serde::Deserialize;
|
||||
|
||||
use self::dclogin_scheme::configure_from_login_qr;
|
||||
use crate::chat::{self, get_chat_id_by_grpid, ChatIdBlocked};
|
||||
use crate::chat::{get_chat_id_by_grpid, ChatIdBlocked};
|
||||
use crate::config::Config;
|
||||
use crate::constants::Blocked;
|
||||
use crate::contact::{
|
||||
@@ -21,7 +21,6 @@ use crate::key::Fingerprint;
|
||||
use crate::message::Message;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::socks::Socks5Config;
|
||||
use crate::tools::time;
|
||||
use crate::{token, EventType};
|
||||
|
||||
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
|
||||
@@ -435,16 +434,9 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
Contact::add_or_lookup(context, &name, peerstate_addr, Origin::UnhandledQrScan)
|
||||
.await
|
||||
.context("add_or_lookup")?;
|
||||
let chat = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Request)
|
||||
ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Request)
|
||||
.await
|
||||
.context("Failed to create (new) chat for contact")?;
|
||||
chat::add_info_msg(
|
||||
context,
|
||||
chat.id,
|
||||
&format!("{} verified.", peerstate.addr),
|
||||
time(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Qr::FprOk { contact_id })
|
||||
} else {
|
||||
let contact_id = Contact::lookup_id_by_addr(context, &addr, Origin::Unknown)
|
||||
|
||||
@@ -156,7 +156,7 @@ async fn set_msg_id_reaction(
|
||||
"DELETE FROM reactions
|
||||
WHERE msg_id = ?1
|
||||
AND contact_id = ?2",
|
||||
paramsv![msg_id, contact_id],
|
||||
(msg_id, contact_id),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
@@ -167,7 +167,7 @@ async fn set_msg_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()],
|
||||
(msg_id, contact_id, reaction.as_str()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -247,7 +247,7 @@ async fn get_self_reaction(context: &Context, msg_id: MsgId) -> Result<Reaction>
|
||||
"SELECT reaction
|
||||
FROM reactions
|
||||
WHERE msg_id=? AND contact_id=?",
|
||||
paramsv![msg_id, ContactId::SELF],
|
||||
(msg_id, ContactId::SELF),
|
||||
)
|
||||
.await?;
|
||||
Ok(reaction_str
|
||||
@@ -262,7 +262,7 @@ pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result<React
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT contact_id, reaction FROM reactions WHERE msg_id=?",
|
||||
paramsv![msg_id],
|
||||
(msg_id,),
|
||||
|row| {
|
||||
let contact_id: ContactId = row.get(0)?;
|
||||
let reaction: String = row.get(1)?;
|
||||
|
||||
@@ -37,7 +37,9 @@ 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;
|
||||
use crate::tools::{buf_compress, extract_grpid_from_rfc724_mid, smeared_time};
|
||||
use crate::tools::{
|
||||
buf_compress, extract_grpid_from_rfc724_mid, smeared_time, strip_rtlo_characters,
|
||||
};
|
||||
use crate::{contact, imap};
|
||||
|
||||
/// This is the struct that is returned after receiving one email (aka MIME message).
|
||||
@@ -116,7 +118,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
|
||||
paramsv![rfc724_mid, DC_CHAT_ID_TRASH],
|
||||
(rfc724_mid, DC_CHAT_ID_TRASH),
|
||||
)
|
||||
.await?;
|
||||
msg_ids = vec![MsgId::new(u32::try_from(row_id)?)];
|
||||
@@ -344,7 +346,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap SET target=? WHERE rfc724_mid=?",
|
||||
paramsv![target, rfc724_mid],
|
||||
(target, rfc724_mid),
|
||||
)
|
||||
.await?;
|
||||
} else if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() {
|
||||
@@ -361,6 +363,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
chat_id.emit_msg_event(context, *msg_id, incoming && fresh);
|
||||
}
|
||||
}
|
||||
context.new_msgs_notify.notify_one();
|
||||
|
||||
mime_parser
|
||||
.handle_reports(context, from_id, sent_timestamp, &mime_parser.parts)
|
||||
@@ -551,18 +554,18 @@ async fn add_parts(
|
||||
// signals whether the current user is a bot
|
||||
let is_bot = context.get_config_bool(Config::Bot).await?;
|
||||
|
||||
let create_blocked = match test_normal_chat {
|
||||
Some(ChatIdBlocked {
|
||||
id: _,
|
||||
blocked: Blocked::Request,
|
||||
}) if is_bot => Blocked::Not,
|
||||
Some(ChatIdBlocked { id: _, blocked }) => blocked,
|
||||
None => Blocked::Request,
|
||||
};
|
||||
|
||||
if chat_id.is_none() {
|
||||
// try to create a group
|
||||
|
||||
let create_blocked = match test_normal_chat {
|
||||
Some(ChatIdBlocked {
|
||||
id: _,
|
||||
blocked: Blocked::Not,
|
||||
}) => Blocked::Not,
|
||||
_ if is_bot => Blocked::Not,
|
||||
_ => Blocked::Request,
|
||||
};
|
||||
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
|
||||
context,
|
||||
mime_parser,
|
||||
@@ -579,13 +582,15 @@ async fn add_parts(
|
||||
{
|
||||
chat_id = Some(new_chat_id);
|
||||
chat_id_blocked = new_chat_id_blocked;
|
||||
}
|
||||
}
|
||||
|
||||
// if the chat is somehow blocked but we want to create a non-blocked chat,
|
||||
// unblock the chat
|
||||
if chat_id_blocked != Blocked::Not && create_blocked == Blocked::Not {
|
||||
new_chat_id.unblock(context).await?;
|
||||
chat_id_blocked = Blocked::Not;
|
||||
}
|
||||
// if the chat is somehow blocked but we want to create a non-blocked chat,
|
||||
// unblock the chat
|
||||
if chat_id_blocked != Blocked::Not && create_blocked != Blocked::Yes {
|
||||
if let Some(chat_id) = chat_id {
|
||||
chat_id.set_blocked(context, create_blocked).await?;
|
||||
chat_id_blocked = create_blocked;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1077,7 +1082,7 @@ async fn add_parts(
|
||||
|
||||
let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
|
||||
|
||||
for part in &mime_parser.parts {
|
||||
for part in &mut mime_parser.parts {
|
||||
if part.is_reaction {
|
||||
set_msg_reaction(
|
||||
context,
|
||||
@@ -1093,6 +1098,7 @@ async fn add_parts(
|
||||
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 [
|
||||
@@ -1362,7 +1368,7 @@ async fn calc_sort_timestamp(
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND state>?",
|
||||
paramsv![chat_id, MessageState::InFresh],
|
||||
(chat_id, MessageState::InFresh),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1681,7 +1687,7 @@ async fn apply_group_changes(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET name=? WHERE id=?;",
|
||||
paramsv![grpname.to_string(), chat_id],
|
||||
(strip_rtlo_characters(grpname), chat_id),
|
||||
)
|
||||
.await?;
|
||||
send_event_chat_modified = true;
|
||||
@@ -1751,10 +1757,7 @@ async fn apply_group_changes(
|
||||
// start from scratch.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM chats_contacts WHERE chat_id=?;",
|
||||
paramsv![chat_id],
|
||||
)
|
||||
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (chat_id,))
|
||||
.await?;
|
||||
|
||||
members_to_add.push(ContactId::SELF);
|
||||
|
||||
@@ -374,7 +374,7 @@ async fn test_no_message_id_header() {
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM msgs WHERE chat_id=?;",
|
||||
paramsv![DC_CHAT_ID_TRASH],
|
||||
(DC_CHAT_ID_TRASH,),
|
||||
)
|
||||
.await
|
||||
.unwrap());
|
||||
@@ -3046,6 +3046,54 @@ async fn test_no_private_reply_to_blocked_account() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for two bugs:
|
||||
///
|
||||
/// 1. If you blocked some spammer using DC, the 1:1 messages with that contact
|
||||
/// are not received, but they could easily bypass this restriction creating
|
||||
/// a new group with only you two as member.
|
||||
/// 2. A blocked group was sometimes not unblocked when when an unblocked
|
||||
/// contact sent a message into it.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_blocked_contact_creates_group() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let fiona = tcm.fiona().await;
|
||||
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
chat.id.block(&alice).await?;
|
||||
|
||||
let group_id = bob
|
||||
.create_group_with_members(
|
||||
ProtectionStatus::Unprotected,
|
||||
"group name",
|
||||
&[&alice, &fiona],
|
||||
)
|
||||
.await;
|
||||
|
||||
let sent = bob.send_text(group_id, "Heyho, I'm a spammer!").await;
|
||||
let rcvd = alice.recv_msg(&sent).await;
|
||||
// Alice blocked Bob, so she shouldn't get the message
|
||||
assert_eq!(rcvd.chat_blocked, Blocked::Yes);
|
||||
|
||||
// Fiona didn't block Bob, though, so she gets the message
|
||||
let rcvd = fiona.recv_msg(&sent).await;
|
||||
assert_eq!(rcvd.chat_blocked, Blocked::Request);
|
||||
|
||||
// Fiona writes to the group
|
||||
rcvd.chat_id.accept(&fiona).await?;
|
||||
let sent = fiona.send_text(rcvd.chat_id, "Hello from Fiona").await;
|
||||
|
||||
// The group is unblocked now that Fiona sent a message to it
|
||||
let rcvd = alice.recv_msg(&sent).await;
|
||||
assert_eq!(rcvd.chat_blocked, Blocked::Request);
|
||||
// In order not to lose context, Bob's message should also be shown in the group
|
||||
let msgs = chat::get_chat_msgs(&alice, rcvd.chat_id).await?;
|
||||
assert_eq!(msgs.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_thunderbird_autocrypt() -> Result<()> {
|
||||
let t = TestContext::new_bob().await;
|
||||
|
||||
10
src/release.rs
Normal file
10
src/release.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! DC release info.
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
const DATE_STR: &str = include_str!("../release-date.in");
|
||||
|
||||
/// Last release date.
|
||||
pub static DATE: Lazy<NaiveDate> =
|
||||
Lazy::new(|| NaiveDate::parse_from_str(DATE_STR, "%Y-%m-%d").unwrap());
|
||||
@@ -62,6 +62,11 @@ impl SchedulerState {
|
||||
/// Starts the scheduler if it is not yet started.
|
||||
async fn do_start(mut inner: RwLockWriteGuard<'_, InnerSchedulerState>, context: Context) {
|
||||
info!(context, "starting IO");
|
||||
|
||||
// Notify message processing loop
|
||||
// to allow processing old messages after restart.
|
||||
context.new_msgs_notify.notify_one();
|
||||
|
||||
let ctx = context.clone();
|
||||
match Scheduler::start(context).await {
|
||||
Ok(scheduler) => *inner = InnerSchedulerState::Started(scheduler),
|
||||
@@ -95,6 +100,11 @@ impl SchedulerState {
|
||||
// to terminate on receiving the next event and then call stop_io()
|
||||
// which will emit the below event(s)
|
||||
info!(context, "stopping IO");
|
||||
|
||||
// Wake up message processing loop even if there are no messages
|
||||
// to allow for clean shutdown.
|
||||
context.new_msgs_notify.notify_one();
|
||||
|
||||
if let Some(debug_logging) = context.debug_logging.read().await.as_ref() {
|
||||
debug_logging.loop_handle.abort();
|
||||
}
|
||||
|
||||
@@ -135,21 +135,21 @@ impl BobState {
|
||||
// rows that we will delete. So start with a dummy UPDATE.
|
||||
transaction.execute(
|
||||
r#"UPDATE bobstate SET next_step=?;"#,
|
||||
params![SecureJoinStep::Terminated],
|
||||
(SecureJoinStep::Terminated,),
|
||||
)?;
|
||||
let mut stmt = transaction.prepare("SELECT id FROM bobstate;")?;
|
||||
let mut aborted = Vec::new();
|
||||
for id in stmt.query_map(params![], |row| row.get::<_, i64>(0))? {
|
||||
for id in stmt.query_map((), |row| row.get::<_, i64>(0))? {
|
||||
let id = id?;
|
||||
let state = BobState::from_db_id(transaction, id)?;
|
||||
aborted.push(state);
|
||||
}
|
||||
|
||||
// Finally delete everything and insert new row.
|
||||
transaction.execute("DELETE FROM bobstate;", params![])?;
|
||||
transaction.execute("DELETE FROM bobstate;", ())?;
|
||||
transaction.execute(
|
||||
"INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);",
|
||||
params![invite, next, chat_id],
|
||||
(invite, next, chat_id),
|
||||
)?;
|
||||
let id = transaction.last_insert_rowid();
|
||||
Ok((id, aborted))
|
||||
@@ -180,7 +180,7 @@ impl BobState {
|
||||
fn from_db_id(connection: &Connection, id: i64) -> rusqlite::Result<Self> {
|
||||
connection.query_row(
|
||||
"SELECT invite, next_step, chat_id FROM bobstate WHERE id=?;",
|
||||
params![id],
|
||||
(id,),
|
||||
|row| {
|
||||
let s = BobState {
|
||||
id,
|
||||
@@ -217,12 +217,12 @@ impl BobState {
|
||||
SecureJoinStep::AuthRequired | SecureJoinStep::ContactConfirm => {
|
||||
sql.execute(
|
||||
"UPDATE bobstate SET next_step=? WHERE id=?;",
|
||||
paramsv![next, self.id],
|
||||
(next, self.id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
SecureJoinStep::Terminated | SecureJoinStep::Completed => {
|
||||
sql.execute("DELETE FROM bobstate WHERE id=?;", paramsv!(self.id))
|
||||
sql.execute("DELETE FROM bobstate WHERE id=?;", (self.id,))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
21
src/smtp.rs
21
src/smtp.rs
@@ -520,10 +520,7 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
// database.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE smtp SET retries=retries+1 WHERE id=?",
|
||||
paramsv![rowid],
|
||||
)
|
||||
.execute("UPDATE smtp SET retries=retries+1 WHERE id=?", (rowid,))
|
||||
.await
|
||||
.context("failed to update retries count")?;
|
||||
|
||||
@@ -531,7 +528,7 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT mime, recipients, msg_id, retries FROM smtp WHERE id=?",
|
||||
paramsv![rowid],
|
||||
(rowid,),
|
||||
|row| {
|
||||
let mime: String = row.get(0)?;
|
||||
let recipients: String = row.get(1)?;
|
||||
@@ -545,7 +542,7 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
message::set_msg_failed(context, msg_id, "Number of retries exceeded the limit.").await;
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?", paramsv![rowid])
|
||||
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
|
||||
.await
|
||||
.context("failed to remove message with exceeded retry limit from smtp table")?;
|
||||
bail!("Number of retries exceeded the limit");
|
||||
@@ -588,7 +585,7 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
SendResult::Success | SendResult::Failure(_) => {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?", paramsv![rowid])
|
||||
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
|
||||
.await?;
|
||||
}
|
||||
};
|
||||
@@ -637,7 +634,7 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp)
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM smtp ORDER BY id ASC",
|
||||
paramsv![],
|
||||
(),
|
||||
|row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
Ok(rowid)
|
||||
@@ -691,7 +688,7 @@ async fn send_mdn_msg_id(
|
||||
"SELECT msg_id, rfc724_mid
|
||||
FROM smtp_mdns
|
||||
WHERE from_id=? AND msg_id!=?",
|
||||
paramsv![contact_id, msg_id],
|
||||
(contact_id, msg_id),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let rfc724_mid: String = row.get(1)?;
|
||||
@@ -718,7 +715,7 @@ async fn send_mdn_msg_id(
|
||||
info!(context, "Successfully sent MDN for {}", msg_id);
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp_mdns WHERE msg_id = ?", paramsv![msg_id])
|
||||
.execute("DELETE FROM smtp_mdns WHERE msg_id = ?", (msg_id,))
|
||||
.await?;
|
||||
if !additional_msg_ids.is_empty() {
|
||||
let q = format!(
|
||||
@@ -779,7 +776,7 @@ async fn send_mdn(context: &Context, smtp: &mut Smtp) -> Result<bool> {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE smtp_mdns SET retries=retries+1 WHERE msg_id=?",
|
||||
paramsv![msg_id],
|
||||
(msg_id,),
|
||||
)
|
||||
.await
|
||||
.context("failed to update MDN retries count")?;
|
||||
@@ -789,7 +786,7 @@ async fn send_mdn(context: &Context, smtp: &mut Smtp) -> Result<bool> {
|
||||
// database, do not try to send this MDN again.
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp_mdns WHERE msg_id = ?", paramsv![msg_id])
|
||||
.execute("DELETE FROM smtp_mdns WHERE msg_id = ?", (msg_id,))
|
||||
.await?;
|
||||
Err(err)
|
||||
} else {
|
||||
|
||||
66
src/sql.rs
66
src/sql.rs
@@ -23,27 +23,28 @@ use crate::peerstate::{deduplicate_peerstates, Peerstate};
|
||||
use crate::stock_str;
|
||||
use crate::tools::{delete_file, time};
|
||||
|
||||
#[allow(missing_docs)]
|
||||
/// Extension to [`rusqlite::ToSql`] trait
|
||||
/// which also includes [`Send`] and [`Sync`].
|
||||
pub trait ToSql: rusqlite::ToSql + Send + Sync {}
|
||||
|
||||
impl<T: rusqlite::ToSql + Send + Sync> ToSql for T {}
|
||||
|
||||
/// Constructs a slice of trait object references `&dyn ToSql`.
|
||||
///
|
||||
/// One of the uses is passing more than 16 parameters
|
||||
/// to a query, because [`rusqlite::Params`] is only implemented
|
||||
/// for tuples of up to 16 elements.
|
||||
#[macro_export]
|
||||
macro_rules! paramsv {
|
||||
() => {
|
||||
rusqlite::params_from_iter(Vec::<&dyn $crate::ToSql>::new())
|
||||
};
|
||||
($($param:expr),+ $(,)?) => {
|
||||
rusqlite::params_from_iter(vec![$(&$param as &dyn $crate::ToSql),+])
|
||||
macro_rules! params_slice {
|
||||
($($param:expr),+) => {
|
||||
[$(&$param as &dyn $crate::sql::ToSql),+]
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[macro_export]
|
||||
macro_rules! params_iterv {
|
||||
($($param:expr),+ $(,)?) => {
|
||||
vec![$(&$param as &dyn $crate::ToSql),+]
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn params_iter(iter: &[impl crate::ToSql]) -> impl Iterator<Item = &dyn crate::ToSql> {
|
||||
iter.iter().map(|item| item as &dyn crate::ToSql)
|
||||
pub(crate) fn params_iter(
|
||||
iter: &[impl crate::sql::ToSql],
|
||||
) -> impl Iterator<Item = &dyn crate::sql::ToSql> {
|
||||
iter.iter().map(|item| item as &dyn crate::sql::ToSql)
|
||||
}
|
||||
|
||||
mod migrations;
|
||||
@@ -139,11 +140,8 @@ impl Sql {
|
||||
let res = self
|
||||
.call_write(move |conn| {
|
||||
// Check that backup passphrase is correct before resetting our database.
|
||||
conn.execute(
|
||||
"ATTACH DATABASE ? AS backup KEY ?",
|
||||
paramsv![path_str, passphrase],
|
||||
)
|
||||
.context("failed to attach backup database")?;
|
||||
conn.execute("ATTACH DATABASE ? AS backup KEY ?", (path_str, passphrase))
|
||||
.context("failed to attach backup database")?;
|
||||
if let Err(err) = conn
|
||||
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
|
||||
.context("backup passphrase is not correct")
|
||||
@@ -556,27 +554,21 @@ impl Sql {
|
||||
let mut lock = self.config_cache.write().await;
|
||||
if let Some(value) = value {
|
||||
let exists = self
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM config WHERE keyname=?;",
|
||||
paramsv![key],
|
||||
)
|
||||
.exists("SELECT COUNT(*) FROM config WHERE keyname=?;", (key,))
|
||||
.await?;
|
||||
|
||||
if exists {
|
||||
self.execute(
|
||||
"UPDATE config SET value=? WHERE keyname=?;",
|
||||
paramsv![value, key],
|
||||
)
|
||||
.await?;
|
||||
self.execute("UPDATE config SET value=? WHERE keyname=?;", (value, key))
|
||||
.await?;
|
||||
} else {
|
||||
self.execute(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?);",
|
||||
paramsv![key, value],
|
||||
(key, value),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
self.execute("DELETE FROM config WHERE keyname=?;", paramsv![key])
|
||||
self.execute("DELETE FROM config WHERE keyname=?;", (key,))
|
||||
.await?;
|
||||
}
|
||||
lock.insert(key.to_string(), value.map(|s| s.to_string()));
|
||||
@@ -597,7 +589,7 @@ impl Sql {
|
||||
|
||||
let mut lock = self.config_cache.write().await;
|
||||
let value = self
|
||||
.query_get_value("SELECT value FROM config WHERE keyname=?;", paramsv![key])
|
||||
.query_get_value("SELECT value FROM config WHERE keyname=?;", (key,))
|
||||
.await
|
||||
.context(format!("failed to fetch raw config: {key}"))?;
|
||||
lock.insert(key.to_string(), value.clone());
|
||||
@@ -980,7 +972,7 @@ async fn prune_tombstones(sql: &Sql) -> Result<()> {
|
||||
AND NOT EXISTS (
|
||||
SELECT * FROM imap WHERE msgs.rfc724_mid=rfc724_mid AND target!=''
|
||||
)",
|
||||
paramsv![DC_CHAT_ID_TRASH],
|
||||
(DC_CHAT_ID_TRASH,),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -1163,12 +1155,12 @@ mod tests {
|
||||
sql.open(&t, "".to_string()).await?;
|
||||
sql.execute(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?);",
|
||||
paramsv!("foo", "bar"),
|
||||
("foo", "bar"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let value: Option<String> = sql
|
||||
.query_get_value("SELECT value FROM config WHERE keyname=?;", paramsv!("foo"))
|
||||
.query_get_value("SELECT value FROM config WHERE keyname=?;", ("foo",))
|
||||
.await?;
|
||||
assert_eq!(value.unwrap(), "bar");
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool, bool
|
||||
// set raw config inside the transaction
|
||||
transaction.execute(
|
||||
"INSERT INTO config (keyname, value) VALUES (?, ?);",
|
||||
paramsv![VERSION_CFG, format!("{dbversion_before_update}")],
|
||||
(VERSION_CFG, format!("{dbversion_before_update}")),
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -746,7 +746,7 @@ impl Sql {
|
||||
fn set_db_version_trans(transaction: &mut rusqlite::Transaction, version: i32) -> Result<()> {
|
||||
transaction.execute(
|
||||
"UPDATE config SET value=? WHERE keyname=?;",
|
||||
params![format!("{version}"), VERSION_CFG],
|
||||
(format!("{version}"), VERSION_CFG),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -407,6 +407,9 @@ pub enum StockMessage {
|
||||
|
||||
#[strum(props(fallback = "Scan to set up second device for %1$s"))]
|
||||
BackupTransferQr = 162,
|
||||
|
||||
#[strum(props(fallback = "ℹ️ Account transferred to your second device."))]
|
||||
BackupTransferMsgBody = 163,
|
||||
}
|
||||
|
||||
impl StockMessage {
|
||||
@@ -1261,6 +1264,10 @@ pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
|
||||
.replace1(&full_name))
|
||||
}
|
||||
|
||||
pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String {
|
||||
translated(context, StockMessage::BackupTransferMsgBody).await
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Set the stock string for the [StockMessage].
|
||||
///
|
||||
|
||||
@@ -65,10 +65,7 @@ impl Context {
|
||||
let item = SyncItem { timestamp, data };
|
||||
let item = serde_json::to_string(&item)?;
|
||||
self.sql
|
||||
.execute(
|
||||
"INSERT INTO multi_device_sync (item) VALUES(?);",
|
||||
paramsv![item],
|
||||
)
|
||||
.execute("INSERT INTO multi_device_sync (item) VALUES(?);", (item,))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -18,7 +18,10 @@ use tokio::runtime::Handle;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::task;
|
||||
|
||||
use crate::chat::{self, Chat, ChatId, MessageListOptions};
|
||||
use crate::chat::{
|
||||
self, add_to_chat_contacts_table, create_group_chat, Chat, ChatId, MessageListOptions,
|
||||
ProtectionStatus,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::constants::Chattype;
|
||||
@@ -425,7 +428,7 @@ impl TestContext {
|
||||
};
|
||||
self.ctx
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?;", paramsv![rowid])
|
||||
.execute("DELETE FROM smtp WHERE id=?;", (rowid,))
|
||||
.await
|
||||
.expect("failed to remove job");
|
||||
update_msg_state(&self.ctx, msg_id, MessageState::OutDelivered)
|
||||
@@ -702,6 +705,25 @@ impl TestContext {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_group_with_members(
|
||||
&self,
|
||||
protect: ProtectionStatus,
|
||||
chat_name: &str,
|
||||
members: &[&TestContext],
|
||||
) -> ChatId {
|
||||
let chat_id = create_group_chat(self, protect, chat_name).await.unwrap();
|
||||
let mut to_add = vec![];
|
||||
for member in members {
|
||||
let contact = self.add_or_lookup_contact(member).await;
|
||||
to_add.push(contact.id);
|
||||
}
|
||||
add_to_chat_contacts_table(self, chat_id, &to_add)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
chat_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TestContext {
|
||||
|
||||
12
src/token.rs
12
src/token.rs
@@ -35,7 +35,7 @@ pub async fn save(
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);",
|
||||
paramsv![namespace, foreign_id, token, time()],
|
||||
(namespace, foreign_id, token, time()),
|
||||
)
|
||||
.await?,
|
||||
None => {
|
||||
@@ -43,7 +43,7 @@ pub async fn save(
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO tokens (namespc, token, timestamp) VALUES (?, ?, ?);",
|
||||
paramsv![namespace, token, time()],
|
||||
(namespace, token, time()),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
@@ -71,7 +71,7 @@ pub async fn lookup(
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT token FROM tokens WHERE namespc=? AND foreign_id=? ORDER BY timestamp DESC LIMIT 1;",
|
||||
paramsv![namespace, chat_id],
|
||||
(namespace, chat_id),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
@@ -81,7 +81,7 @@ pub async fn lookup(
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT token FROM tokens WHERE namespc=? AND foreign_id=0 ORDER BY timestamp DESC LIMIT 1;",
|
||||
paramsv![namespace],
|
||||
(namespace,),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
@@ -108,7 +108,7 @@ pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> boo
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM tokens WHERE namespc=? AND token=?;",
|
||||
paramsv![namespace, token],
|
||||
(namespace, token),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -119,7 +119,7 @@ pub async fn delete(context: &Context, namespace: Namespace, token: &str) -> Res
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM tokens WHERE namespc=? AND token=?;",
|
||||
paramsv![namespace, token],
|
||||
(namespace, token),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
|
||||
75
src/tools.rs
75
src/tools.rs
@@ -13,7 +13,7 @@ use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use chrono::{Local, TimeZone};
|
||||
use chrono::{Local, NaiveDateTime, NaiveTime, TimeZone};
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use mailparse::dateparse;
|
||||
use mailparse::headers::Headers;
|
||||
@@ -26,7 +26,6 @@ use crate::constants::{DC_ELLIPSIS, DC_OUTDATED_WARNING_DAYS};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::provider::get_provider_update_timestamp;
|
||||
use crate::stock_str;
|
||||
|
||||
/// Shortens a string to a specified length and adds "[...]" to the
|
||||
@@ -163,12 +162,23 @@ pub(crate) fn create_smeared_timestamps(context: &Context, count: usize) -> i64
|
||||
context.smeared_timestamp.create_n(now, count as i64)
|
||||
}
|
||||
|
||||
/// Returns the last release timestamp as a unix timestamp compatible for comparison with time() and
|
||||
/// database times.
|
||||
pub fn get_release_timestamp() -> i64 {
|
||||
NaiveDateTime::new(
|
||||
*crate::release::DATE,
|
||||
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
|
||||
)
|
||||
.timestamp_millis()
|
||||
/ 1_000
|
||||
}
|
||||
|
||||
// if the system time is not plausible, once a day, add a device message.
|
||||
// for testing we're using time() as that is also used for message timestamps.
|
||||
// moreover, add a warning if the app is outdated.
|
||||
pub(crate) async fn maybe_add_time_based_warnings(context: &Context) {
|
||||
if !maybe_warn_on_bad_time(context, time(), get_provider_update_timestamp()).await {
|
||||
maybe_warn_on_outdated(context, time(), get_provider_update_timestamp()).await;
|
||||
if !maybe_warn_on_bad_time(context, time(), get_release_timestamp()).await {
|
||||
maybe_warn_on_outdated(context, time(), get_release_timestamp()).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,9 +560,11 @@ impl rusqlite::types::ToSql for EmailAddress {
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes sure that a user input that is not supposed to contain newlines does not contain newlines.
|
||||
/// Sanitizes user input
|
||||
/// - strip newlines
|
||||
/// - strip malicious bidi characters
|
||||
pub(crate) fn improve_single_line_input(input: &str) -> String {
|
||||
input.replace(['\n', '\r'], " ").trim().to_string()
|
||||
strip_rtlo_characters(input.replace(['\n', '\r'], " ").trim())
|
||||
}
|
||||
|
||||
pub(crate) trait IsNoneOrEmpty<T> {
|
||||
@@ -691,6 +703,13 @@ pub(crate) fn buf_decompress(buf: &[u8]) -> Result<Vec<u8>> {
|
||||
Ok(mem::take(decompressor.get_mut()))
|
||||
}
|
||||
|
||||
const RTLO_CHARACTERS: [char; 5] = ['\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}'];
|
||||
/// This method strips all occurances of the RTLO Unicode character.
|
||||
/// [Why is this needed](https://github.com/deltachat/deltachat-core-rust/issues/3479)?
|
||||
pub(crate) fn strip_rtlo_characters(input_str: &str) -> String {
|
||||
input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
@@ -1140,17 +1159,17 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
|
||||
/ 1_000;
|
||||
|
||||
// a correct time must not add a device message
|
||||
maybe_warn_on_bad_time(&t, timestamp_now, get_provider_update_timestamp()).await;
|
||||
maybe_warn_on_bad_time(&t, timestamp_now, get_release_timestamp()).await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
// we cannot find out if a date in the future is wrong - a device message is not added
|
||||
maybe_warn_on_bad_time(&t, timestamp_future, get_provider_update_timestamp()).await;
|
||||
maybe_warn_on_bad_time(&t, timestamp_future, get_release_timestamp()).await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
// a date in the past must add a device message
|
||||
maybe_warn_on_bad_time(&t, timestamp_past, get_provider_update_timestamp()).await;
|
||||
maybe_warn_on_bad_time(&t, timestamp_past, get_release_timestamp()).await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let device_chat_id = chats.get_chat_id(0).unwrap();
|
||||
@@ -1158,31 +1177,21 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
// the message should be added only once a day - test that an hour later and nearly a day later
|
||||
maybe_warn_on_bad_time(
|
||||
&t,
|
||||
timestamp_past + 60 * 60,
|
||||
get_provider_update_timestamp(),
|
||||
)
|
||||
.await;
|
||||
maybe_warn_on_bad_time(&t, timestamp_past + 60 * 60, get_release_timestamp()).await;
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
maybe_warn_on_bad_time(
|
||||
&t,
|
||||
timestamp_past + 60 * 60 * 24 - 1,
|
||||
get_provider_update_timestamp(),
|
||||
get_release_timestamp(),
|
||||
)
|
||||
.await;
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id).await.unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
// next day, there should be another device message
|
||||
maybe_warn_on_bad_time(
|
||||
&t,
|
||||
timestamp_past + 60 * 60 * 24,
|
||||
get_provider_update_timestamp(),
|
||||
)
|
||||
.await;
|
||||
maybe_warn_on_bad_time(&t, timestamp_past + 60 * 60 * 24, get_release_timestamp()).await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(device_chat_id, chats.get_chat_id(0).unwrap());
|
||||
@@ -1200,7 +1209,7 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
|
||||
maybe_warn_on_outdated(
|
||||
&t,
|
||||
timestamp_now + 180 * 24 * 60 * 60,
|
||||
get_provider_update_timestamp(),
|
||||
get_release_timestamp(),
|
||||
)
|
||||
.await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
@@ -1210,7 +1219,7 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
|
||||
maybe_warn_on_outdated(
|
||||
&t,
|
||||
timestamp_now + 365 * 24 * 60 * 60,
|
||||
get_provider_update_timestamp(),
|
||||
get_release_timestamp(),
|
||||
)
|
||||
.await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
@@ -1224,13 +1233,13 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
|
||||
maybe_warn_on_outdated(
|
||||
&t,
|
||||
timestamp_now + (365 + 1) * 24 * 60 * 60,
|
||||
get_provider_update_timestamp(),
|
||||
get_release_timestamp(),
|
||||
)
|
||||
.await;
|
||||
maybe_warn_on_outdated(
|
||||
&t,
|
||||
timestamp_now + (365 + 2) * 24 * 60 * 60,
|
||||
get_provider_update_timestamp(),
|
||||
get_release_timestamp(),
|
||||
)
|
||||
.await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
@@ -1245,7 +1254,7 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
|
||||
maybe_warn_on_outdated(
|
||||
&t,
|
||||
timestamp_now + (365 + 33) * 24 * 60 * 60,
|
||||
get_provider_update_timestamp(),
|
||||
get_release_timestamp(),
|
||||
)
|
||||
.await;
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
@@ -1255,6 +1264,18 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
|
||||
assert_eq!(msgs.len(), test_len + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_release_timestamp() {
|
||||
let timestamp_past = NaiveDateTime::new(
|
||||
NaiveDate::from_ymd_opt(2020, 9, 9).unwrap(),
|
||||
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
|
||||
)
|
||||
.timestamp_millis()
|
||||
/ 1_000;
|
||||
assert!(get_release_timestamp() <= time());
|
||||
assert!(get_release_timestamp() > timestamp_past);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_subject_prefix() {
|
||||
assert_eq!(remove_subject_prefix("Subject"), "Subject");
|
||||
|
||||
@@ -31,7 +31,7 @@ impl Context {
|
||||
if update {
|
||||
transaction.execute(
|
||||
"UPDATE contacts SET param=? WHERE id=?",
|
||||
params![param.to_string(), contact_id],
|
||||
(param.to_string(), contact_id),
|
||||
)?;
|
||||
}
|
||||
Ok(update)
|
||||
@@ -61,7 +61,7 @@ impl ChatId {
|
||||
if update {
|
||||
transaction.execute(
|
||||
"UPDATE chats SET param=? WHERE id=?",
|
||||
params![param.to_string(), self],
|
||||
(param.to_string(), self),
|
||||
)?;
|
||||
}
|
||||
Ok(update)
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::param::Params;
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::tools::strip_rtlo_characters;
|
||||
use crate::tools::{create_smeared_timestamp, get_abs_path};
|
||||
use crate::{chat, EventType};
|
||||
|
||||
@@ -262,7 +263,7 @@ impl Context {
|
||||
FROM msgs
|
||||
WHERE chat_id=?1 AND hidden=0
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1"#,
|
||||
paramsv![instance.chat_id],
|
||||
(instance.chat_id,),
|
||||
|row| {
|
||||
let last_msg_id: MsgId = row.get(0)?;
|
||||
let last_from_id: ContactId = row.get(1)?;
|
||||
@@ -293,13 +294,13 @@ impl Context {
|
||||
can_info_msg: bool,
|
||||
from_id: ContactId,
|
||||
) -> Result<StatusUpdateSerial> {
|
||||
let update_str = update_str.trim();
|
||||
let update_str = strip_rtlo_characters(update_str.trim());
|
||||
if update_str.is_empty() {
|
||||
bail!("create_status_update_record: empty update.");
|
||||
}
|
||||
|
||||
let status_update_item: StatusUpdateItem =
|
||||
if let Ok(item) = serde_json::from_str::<StatusUpdateItem>(update_str) {
|
||||
if let Ok(item) = serde_json::from_str::<StatusUpdateItem>(&update_str) {
|
||||
item
|
||||
} else {
|
||||
bail!("create_status_update_record: no valid update item.");
|
||||
@@ -351,7 +352,9 @@ impl Context {
|
||||
.param
|
||||
.update_timestamp(Param::WebxdcSummaryTimestamp, timestamp)?
|
||||
{
|
||||
instance.param.set(Param::WebxdcSummary, summary);
|
||||
instance
|
||||
.param
|
||||
.set(Param::WebxdcSummary, strip_rtlo_characters(summary));
|
||||
param_changed = true;
|
||||
}
|
||||
}
|
||||
@@ -384,7 +387,7 @@ impl Context {
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT INTO msgs_status_updates (msg_id, update_item) VALUES(?, ?);",
|
||||
paramsv![instance_id, serde_json::to_string(&status_update_item)?],
|
||||
(instance_id, serde_json::to_string(&status_update_item)?),
|
||||
)
|
||||
.await?;
|
||||
let status_update_serial = StatusUpdateSerial(u32::try_from(rowid)?);
|
||||
@@ -432,7 +435,7 @@ impl Context {
|
||||
"INSERT INTO smtp_status_updates (msg_id, first_serial, last_serial, descr) VALUES(?, ?, ?, ?)
|
||||
ON CONFLICT(msg_id)
|
||||
DO UPDATE SET last_serial=excluded.last_serial, descr=excluded.descr",
|
||||
paramsv![instance.id, status_update_serial, status_update_serial, descr],
|
||||
(instance.id, status_update_serial, status_update_serial, descr),
|
||||
).await?;
|
||||
self.scheduler
|
||||
.interrupt_smtp(InterruptInfo::new(false))
|
||||
@@ -576,7 +579,7 @@ impl Context {
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT update_item, id FROM msgs_status_updates WHERE msg_id=? AND id>? ORDER BY id",
|
||||
paramsv![instance_msg_id, last_known_serial],
|
||||
(instance_msg_id, last_known_serial),
|
||||
|row| {
|
||||
let update_item_str = row.get::<_, String>(0)?;
|
||||
let serial = row.get::<_, StatusUpdateSerial>(1)?;
|
||||
@@ -628,11 +631,11 @@ impl Context {
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT update_item FROM msgs_status_updates WHERE msg_id=? AND id>=? AND id<=? ORDER BY id",
|
||||
paramsv![
|
||||
(
|
||||
instance_msg_id,
|
||||
range.map(|r|r.0).unwrap_or(StatusUpdateSerial(0)),
|
||||
range.map(|r|r.1).unwrap_or(StatusUpdateSerial(u32::MAX)),
|
||||
],
|
||||
),
|
||||
|row| row.get::<_, String>(0),
|
||||
|rows| {
|
||||
let mut json = String::default();
|
||||
|
||||
Reference in New Issue
Block a user