mirror of
https://github.com/chatmail/core.git
synced 2026-06-29 11:06:35 +03:00
Compare commits
53 Commits
webxdc_upd
...
fix3782
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf52a77f98 | ||
|
|
d850813ef0 | ||
|
|
7f4cebc08b | ||
|
|
d422ab8105 | ||
|
|
6529cfff03 | ||
|
|
061fd1dd2d | ||
|
|
41f45bd680 | ||
|
|
18dce04608 | ||
|
|
fe11198dbc | ||
|
|
fac7b064b4 | ||
|
|
ef6f252842 | ||
|
|
b8da19e49f | ||
|
|
a483df8b20 | ||
|
|
41ccc13394 | ||
|
|
0978357c5f | ||
|
|
7935085e74 | ||
|
|
7a47c9e38b | ||
|
|
20124bfca0 | ||
|
|
eaeaa297c7 | ||
|
|
9adb9ab5f4 | ||
|
|
c4c4c977a6 | ||
|
|
7d508dcb52 | ||
|
|
773754d74f | ||
|
|
ed20a23297 | ||
|
|
4615c84f31 | ||
|
|
677136f4ab | ||
|
|
3c3710420b | ||
|
|
de268b8225 | ||
|
|
42c709e7b1 | ||
|
|
cf0349acc8 | ||
|
|
e43b36b61f | ||
|
|
ed24867309 | ||
|
|
3cf78749df | ||
|
|
badbf766bb | ||
|
|
57ba2387e8 | ||
|
|
8e0626e995 | ||
|
|
7273c917a6 | ||
|
|
d0871d3bd7 | ||
|
|
c8fe830c33 | ||
|
|
12dbf41116 | ||
|
|
f7bf31dbea | ||
|
|
0215046c76 | ||
|
|
b5fc9aedc5 | ||
|
|
0250b6f421 | ||
|
|
7415eadb78 | ||
|
|
8d077b964c | ||
|
|
99923fd816 | ||
|
|
f3614536d9 | ||
|
|
a9a485d19e | ||
|
|
32d54c8b93 | ||
|
|
f9a5cf9b11 | ||
|
|
71ada54703 | ||
|
|
81695d6b80 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -144,7 +144,7 @@ jobs:
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
working-directory: deltachat-rpc-client
|
||||
run: tox -e py3
|
||||
run: tox -e py3,lint
|
||||
|
||||
- name: install pypy
|
||||
if: ${{ matrix.python }}
|
||||
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -2,13 +2,24 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Changes
|
||||
- Pipeline SMTP commands #3924
|
||||
- Cache DNS results #3970
|
||||
|
||||
### Fixes
|
||||
- Securejoin: Fix adding and handling Autocrypt-Gossip headers #3914
|
||||
- fix verifier-by addr was empty string intead of None #3961
|
||||
- Emit DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK when the number of archived chats with
|
||||
unread messages increases #3959
|
||||
- Fix Peerstate comparison #3962
|
||||
- Log SOCKS5 configuration for IMAP like already done for SMTP #3964
|
||||
- Fix SOCKS5 usage for IMAP #3965
|
||||
- Exit from recently seen loop on interrupt channel errors to avoid busy looping #3966
|
||||
|
||||
### API-Changes
|
||||
- jsonrpc: add verified-by information to `Contact`-Object
|
||||
- Remove `attach_selfavatar` config #3951
|
||||
- `DC_EVENT_WEBXDC_UPDATE_STATE_CHANGED` is emitted when webxdc update state changes #3320
|
||||
|
||||
|
||||
## 1.106.0
|
||||
|
||||
@@ -46,13 +57,13 @@
|
||||
### Fixes
|
||||
- Do not add an error if the message is encrypted but not signed #3860
|
||||
- Do not strip leading spaces from message lines #3867
|
||||
- Don't always rebuild group member lists #3872
|
||||
- Fix uncaught exception in JSON-RPC tests #3884
|
||||
- Fix STARTTLS connection and add a test for it #3907
|
||||
- Trigger reconnection when failing to fetch existing messages #3911
|
||||
- Do not retry fetching existing messages after failure, prevents infinite reconnection loop #3913
|
||||
- Ensure format=flowed formatting is always reversible on the receiver side #3880
|
||||
|
||||
|
||||
## 1.104.0
|
||||
|
||||
### Changes
|
||||
|
||||
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -165,9 +165,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-smtp"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6da21e1dd19fbad3e095ad519fb1558ab77fd82e5c4778dca8f9be0464589e1e"
|
||||
checksum = "2ade89127f9e0d44f9e83cf574d499060005cd45b7dc76be89c0167487fe8edd"
|
||||
dependencies = [
|
||||
"async-native-tls",
|
||||
"async-trait",
|
||||
|
||||
@@ -25,7 +25,7 @@ ansi_term = { version = "0.12.1", optional = true }
|
||||
anyhow = "1"
|
||||
async-imap = { git = "https://github.com/async-email/async-imap", branch = "master", default-features = false, features = ["runtime-tokio"] }
|
||||
async-native-tls = { version = "0.4", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.5", default-features = false, features = ["smtp-transport", "socks5", "runtime-tokio"] }
|
||||
async-smtp = { version = "0.6", default-features = false, features = ["smtp-transport", "socks5", "runtime-tokio"] }
|
||||
trust-dns-resolver = "0.22"
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use deltachat::accounts::Accounts;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::tempdir;
|
||||
|
||||
async fn create_accounts(n: u32) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
use deltachat::chat::{self, ChatId};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::context::Context;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::Events;
|
||||
use std::path::Path;
|
||||
|
||||
async fn search_benchmark(dbfile: impl AsRef<Path>) {
|
||||
let id = 100;
|
||||
|
||||
@@ -1082,17 +1082,6 @@ int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const
|
||||
*/
|
||||
char* dc_get_webxdc_status_updates (dc_context_t* context, uint32_t msg_id, uint32_t serial);
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a webxdc has pending updates to be sent out.
|
||||
|
||||
* @param context The context object.
|
||||
* @param msg_id The ID of the message with the webxdc instance.
|
||||
* @return 1 = webxdc is updating , 0 = webxdc is not updating
|
||||
*/
|
||||
int dc_is_webxdc_updating (dc_context_t* context, uint32_t msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* Save a draft for a chat in the database.
|
||||
*
|
||||
@@ -5883,17 +5872,10 @@ void dc_event_unref(dc_event_t* event);
|
||||
*
|
||||
* @param data1 (int) msg_id
|
||||
*/
|
||||
|
||||
#define DC_EVENT_WEBXDC_INSTANCE_DELETED 2121
|
||||
|
||||
|
||||
/**
|
||||
* Webxdc changed it's update sending state
|
||||
*
|
||||
* @param data1 (int) msg_id
|
||||
* @param data1 (int) is_sending
|
||||
*/
|
||||
#define DC_EVENT_WEBXDC_UPDATE_STATE_CHANGED 2122
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -23,13 +23,6 @@ use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::Rng;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::{Contact, ContactId, Origin};
|
||||
@@ -37,21 +30,28 @@ use deltachat::context::Context;
|
||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::key::DcKey;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
use deltachat::{accounts::Accounts, log::LogExt};
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::Rng;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
mod dc_array;
|
||||
mod lot;
|
||||
|
||||
mod string;
|
||||
use self::string::*;
|
||||
use deltachat::chatlist::Chatlist;
|
||||
|
||||
use self::string::*;
|
||||
|
||||
// as C lacks a good and portable error handling,
|
||||
// in general, the C Interface is forgiving wrt to bad parameters.
|
||||
// - objects returned by some functions
|
||||
@@ -521,7 +521,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::SelfavatarChanged => 2110,
|
||||
EventType::WebxdcStatusUpdate { .. } => 2120,
|
||||
EventType::WebxdcInstanceDeleted { .. } => 2121,
|
||||
EventType::WebxdcUpdateStateChanged { .. } => 2122,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,7 +570,6 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
}
|
||||
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::WebxdcUpdateStateChanged { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,10 +618,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
status_update_serial,
|
||||
..
|
||||
} => status_update_serial.to_u32() as libc::c_int,
|
||||
EventType::WebxdcUpdateStateChanged {
|
||||
has_pending_updates: is_send,
|
||||
..
|
||||
} => *is_send as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,7 +662,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::WebxdcStatusUpdate { .. }
|
||||
| EventType::WebxdcInstanceDeleted { .. }
|
||||
| EventType::WebxdcUpdateStateChanged { .. }
|
||||
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
@@ -3292,24 +3285,6 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_info(msg: *mut dc_msg_t) -> *mut libc
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_is_webxdc_updating(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_is_webxdc_updating()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(async move {
|
||||
webxdc::get_busy_webxdc_instances(ctx)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.contains(&MsgId::new(msg_id)) as libc::c_int
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_filemime(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
@@ -4602,11 +4577,12 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||
|
||||
#[cfg(feature = "jsonrpc")]
|
||||
mod jsonrpc {
|
||||
use super::*;
|
||||
use deltachat_jsonrpc::api::CommandApi;
|
||||
use deltachat_jsonrpc::events::event_to_json_rpc_notification;
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct dc_jsonrpc_instance_t {
|
||||
receiver: OutReceiver,
|
||||
handle: RpcSession<CommandApi>,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
//! # Legacy generic return values for C API.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use anyhow::Error;
|
||||
|
||||
use crate::message::MessageState;
|
||||
use crate::qr::Qr;
|
||||
use crate::summary::{Summary, SummaryPrefix};
|
||||
use anyhow::Error;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// An object containing a set of values.
|
||||
/// The meaning of the values is defined by the function returning the object.
|
||||
|
||||
@@ -287,9 +287,10 @@ fn as_path_unicode<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use libc::{free, strcmp};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_os_str_to_c_string_cwd() {
|
||||
let some_dir = std::env::current_dir().unwrap();
|
||||
|
||||
@@ -281,7 +281,6 @@ pub enum JSONRPCEventType {
|
||||
WebxdcInstanceDeleted {
|
||||
msg_id: u32,
|
||||
},
|
||||
WebxdcUpdateStateChanged,
|
||||
}
|
||||
|
||||
impl From<EventType> for JSONRPCEventType {
|
||||
@@ -382,7 +381,6 @@ impl From<EventType> for JSONRPCEventType {
|
||||
EventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
EventType::WebxdcUpdateStateChanged { .. } => WebxdcUpdateStateChanged,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
pub use deltachat::accounts::Accounts;
|
||||
use deltachat::{
|
||||
chat::{
|
||||
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, marknoticed_chat,
|
||||
@@ -23,21 +28,14 @@ use deltachat::{
|
||||
webxdc::StatusUpdateSerial,
|
||||
};
|
||||
use sanitize_filename::is_sanitized;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
use tokio::{fs, sync::RwLock};
|
||||
use walkdir::WalkDir;
|
||||
use yerpc::rpc;
|
||||
|
||||
pub use deltachat::accounts::Accounts;
|
||||
|
||||
pub mod events;
|
||||
pub mod types;
|
||||
|
||||
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
|
||||
use crate::api::types::qr::QrObject;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
use types::account::Account;
|
||||
use types::chat::FullChat;
|
||||
use types::chat_list::ChatListEntry;
|
||||
@@ -53,8 +51,8 @@ use self::types::{
|
||||
JSONRPCMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
|
||||
},
|
||||
};
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
|
||||
use crate::api::types::qr::QrObject;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CommandApi {
|
||||
|
||||
@@ -4,12 +4,13 @@ pub use yerpc;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::api::{Accounts, CommandApi};
|
||||
use async_channel::unbounded;
|
||||
use futures::StreamExt;
|
||||
use tempfile::TempDir;
|
||||
use yerpc::{RpcClient, RpcSession};
|
||||
|
||||
use super::api::{Accounts, CommandApi};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new().unwrap().path().into();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use axum::{extract::ws::WebSocketUpgrade, response::Response, routing::get, Extension, Router};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use axum::{extract::ws::WebSocketUpgrade, response::Response, routing::get, Extension, Router};
|
||||
use yerpc::axum::handle_ws_rpc;
|
||||
use yerpc::{RpcClient, RpcSession};
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// AUTO-GENERATED by typescript-type-def
|
||||
|
||||
export type EventTypeName=("Info"|"SmtpConnected"|"ImapConnected"|"SmtpMessageSent"|"ImapMessageDeleted"|"ImapMessageMoved"|"NewBlobFile"|"DeletedBlobFile"|"Warning"|"Error"|"ErrorSelfNotInGroup"|"MsgsChanged"|"IncomingMsg"|"MsgsNoticed"|"MsgDelivered"|"MsgFailed"|"MsgRead"|"ChatModified"|"ChatEphemeralTimerModified"|"ContactsChanged"|"LocationChanged"|"ConfigureProgress"|"ImexProgress"|"ImexFileWritten"|"SecurejoinInviterProgress"|"SecurejoinJoinerProgress"|"ConnectivityChanged"|"SelfavatarChanged"|"WebxdcStatusUpdate"|"WebxdcInstanceDeleted"|"WebxdcUpdateStateChanged");
|
||||
@@ -27,9 +27,7 @@ async def log_error(event):
|
||||
|
||||
@hooks.on(events.MemberListChanged)
|
||||
async def on_memberlist_changed(event):
|
||||
logging.info(
|
||||
"member %s was %s", event.member, "added" if event.member_added else "removed"
|
||||
)
|
||||
logging.info("member %s was %s", event.member, "added" if event.member_added else "removed")
|
||||
|
||||
|
||||
@hooks.on(events.GroupImageChanged)
|
||||
|
||||
@@ -27,3 +27,13 @@ deltachat_rpc_client = [
|
||||
|
||||
[project.entry-points.pytest11]
|
||||
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
[tool.ruff]
|
||||
select = ["E", "F", "W", "N", "YTT", "B", "C4", "ISC", "ICN", "PT", "RET", "SIM", "TID", "ARG", "DTZ", "ERA", "PLC", "PLE", "PLW", "PIE", "COM"]
|
||||
line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
|
||||
@@ -8,3 +8,18 @@ from .contact import Contact
|
||||
from .deltachat import DeltaChat
|
||||
from .message import Message
|
||||
from .rpc import Rpc
|
||||
|
||||
__all__ = [
|
||||
"Account",
|
||||
"AttrDict",
|
||||
"Bot",
|
||||
"Chat",
|
||||
"Client",
|
||||
"Contact",
|
||||
"DeltaChat",
|
||||
"EventType",
|
||||
"Message",
|
||||
"Rpc",
|
||||
"run_bot_cli",
|
||||
"run_client_cli",
|
||||
]
|
||||
|
||||
@@ -30,12 +30,7 @@ class AttrDict(dict):
|
||||
"""Dictionary that allows accessing values usin the "dot notation" as attributes."""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(
|
||||
{
|
||||
_camel_to_snake(key): _to_attrdict(value)
|
||||
for key, value in dict(*args, **kwargs).items()
|
||||
}
|
||||
)
|
||||
super().__init__({_camel_to_snake(key): _to_attrdict(value) for key, value in dict(*args, **kwargs).items()})
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if attr in self:
|
||||
@@ -51,7 +46,7 @@ class AttrDict(dict):
|
||||
async def run_client_cli(
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Run a simple command line app, using the given hooks.
|
||||
|
||||
@@ -65,7 +60,7 @@ async def run_client_cli(
|
||||
async def run_bot_cli(
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Run a simple bot command line using the given hooks.
|
||||
|
||||
@@ -80,7 +75,7 @@ async def _run_cli(
|
||||
client_type: Type["Client"],
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> None:
|
||||
from .deltachat import DeltaChat
|
||||
from .rpc import Rpc
|
||||
@@ -107,12 +102,9 @@ async def _run_cli(
|
||||
client = client_type(account, hooks)
|
||||
client.logger.debug("Running deltachat core %s", core_version)
|
||||
if not await client.is_configured():
|
||||
assert (
|
||||
args.email and args.password
|
||||
), "Account is not configured and email and password must be provided"
|
||||
asyncio.create_task(
|
||||
client.configure(email=args.email, password=args.password)
|
||||
)
|
||||
assert args.email, "Account is not configured and email must be provided"
|
||||
assert args.password, "Account is not configured and password must be provided"
|
||||
asyncio.create_task(client.configure(email=args.email, password=args.password))
|
||||
await client.run_forever()
|
||||
|
||||
|
||||
|
||||
@@ -89,9 +89,7 @@ class Account:
|
||||
"""Configure an account."""
|
||||
await self._rpc.configure(self.id)
|
||||
|
||||
async def create_contact(
|
||||
self, obj: Union[int, str, Contact], name: Optional[str] = None
|
||||
) -> Contact:
|
||||
async def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
||||
"""Create a new Contact or return an existing one.
|
||||
|
||||
Calling this method will always result in the same
|
||||
@@ -120,10 +118,7 @@ class Account:
|
||||
async def get_blocked_contacts(self) -> List[AttrDict]:
|
||||
"""Return a list with snapshots of all blocked contacts."""
|
||||
contacts = await self._rpc.get_blocked_contacts(self.id)
|
||||
return [
|
||||
AttrDict(contact=Contact(self, contact["id"]), **contact)
|
||||
for contact in contacts
|
||||
]
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
|
||||
async def get_contacts(
|
||||
self,
|
||||
@@ -148,10 +143,7 @@ class Account:
|
||||
|
||||
if snapshot:
|
||||
contacts = await self._rpc.get_contacts(self.id, flags, query)
|
||||
return [
|
||||
AttrDict(contact=Contact(self, contact["id"]), **contact)
|
||||
for contact in contacts
|
||||
]
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
contacts = await self._rpc.get_contact_ids(self.id, flags, query)
|
||||
return [Contact(self, contact_id) for contact_id in contacts]
|
||||
|
||||
@@ -192,9 +184,7 @@ class Account:
|
||||
if alldone_hint:
|
||||
flags |= ChatlistFlag.ADD_ALLDONE_HINT
|
||||
|
||||
entries = await self._rpc.get_chatlist_entries(
|
||||
self.id, flags, query, contact and contact.id
|
||||
)
|
||||
entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
|
||||
if not snapshot:
|
||||
return [Chat(self, entry[0]) for entry in entries]
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class Chat:
|
||||
"""
|
||||
if duration is not None:
|
||||
assert duration > 0, "Invalid duration"
|
||||
dur: Union[str, dict] = dict(Until=duration)
|
||||
dur: Union[str, dict] = {"Until": duration}
|
||||
else:
|
||||
dur = "Forever"
|
||||
await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
|
||||
@@ -74,27 +74,19 @@ class Chat:
|
||||
|
||||
async def pin(self) -> None:
|
||||
"""Pin this chat."""
|
||||
await self._rpc.set_chat_visibility(
|
||||
self.account.id, self.id, ChatVisibility.PINNED
|
||||
)
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
|
||||
|
||||
async def unpin(self) -> None:
|
||||
"""Unpin this chat."""
|
||||
await self._rpc.set_chat_visibility(
|
||||
self.account.id, self.id, ChatVisibility.NORMAL
|
||||
)
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||
|
||||
async def archive(self) -> None:
|
||||
"""Archive this chat."""
|
||||
await self._rpc.set_chat_visibility(
|
||||
self.account.id, self.id, ChatVisibility.ARCHIVED
|
||||
)
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
|
||||
|
||||
async def unarchive(self) -> None:
|
||||
"""Unarchive this chat."""
|
||||
await self._rpc.set_chat_visibility(
|
||||
self.account.id, self.id, ChatVisibility.NORMAL
|
||||
)
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||
|
||||
async def set_name(self, name: str) -> None:
|
||||
"""Set name of this chat."""
|
||||
@@ -133,9 +125,7 @@ class Chat:
|
||||
if isinstance(quoted_msg, Message):
|
||||
quoted_msg = quoted_msg.id
|
||||
|
||||
msg_id, _ = await self._rpc.misc_send_msg(
|
||||
self.account.id, self.id, text, file, location, quoted_msg
|
||||
)
|
||||
msg_id, _ = await self._rpc.misc_send_msg(self.account.id, self.id, text, file, location, quoted_msg)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
async def send_text(self, text: str) -> Message:
|
||||
@@ -241,23 +231,17 @@ class Chat:
|
||||
timestamp_to: Optional[datetime] = None,
|
||||
) -> List[AttrDict]:
|
||||
"""Get list of location snapshots for the given contact in the given timespan."""
|
||||
time_from = (
|
||||
calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
|
||||
)
|
||||
time_from = calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
|
||||
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
|
||||
contact_id = contact.id if contact else 0
|
||||
|
||||
result = await self._rpc.get_locations(
|
||||
self.account.id, self.id, contact_id, time_from, time_to
|
||||
)
|
||||
result = await self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
|
||||
locations = []
|
||||
contacts: Dict[int, Contact] = {}
|
||||
for loc in result:
|
||||
loc = AttrDict(loc)
|
||||
loc["chat"] = self
|
||||
loc["contact"] = contacts.setdefault(
|
||||
loc.contact_id, Contact(self.account, loc.contact_id)
|
||||
)
|
||||
loc["contact"] = contacts.setdefault(loc.contact_id, Contact(self.account, loc.contact_id))
|
||||
loc["message"] = Message(self.account, loc.msg_id)
|
||||
locations.append(loc)
|
||||
return locations
|
||||
|
||||
@@ -47,15 +47,11 @@ class Client:
|
||||
self._should_process_messages = 0
|
||||
self.add_hooks(hooks or [])
|
||||
|
||||
def add_hooks(
|
||||
self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]
|
||||
) -> None:
|
||||
def add_hooks(self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]) -> None:
|
||||
for hook, event in hooks:
|
||||
self.add_hook(hook, event)
|
||||
|
||||
def add_hook(
|
||||
self, hook: Callable, event: Union[type, EventFilter] = RawEvent
|
||||
) -> None:
|
||||
def add_hook(self, hook: Callable, event: Union[type, EventFilter] = RawEvent) -> None:
|
||||
"""Register hook for the given event filter."""
|
||||
if isinstance(event, type):
|
||||
event = event()
|
||||
@@ -64,7 +60,7 @@ class Client:
|
||||
isinstance(
|
||||
event,
|
||||
(NewMessage, MemberListChanged, GroupImageChanged, GroupNameChanged),
|
||||
)
|
||||
),
|
||||
)
|
||||
self._hooks.setdefault(type(event), set()).add((hook, event))
|
||||
|
||||
@@ -76,7 +72,7 @@ class Client:
|
||||
isinstance(
|
||||
event,
|
||||
(NewMessage, MemberListChanged, GroupImageChanged, GroupNameChanged),
|
||||
)
|
||||
),
|
||||
)
|
||||
self._hooks.get(type(event), set()).remove((hook, event))
|
||||
|
||||
@@ -95,9 +91,7 @@ class Client:
|
||||
"""Process events forever."""
|
||||
await self.run_until(lambda _: False)
|
||||
|
||||
async def run_until(
|
||||
self, func: Callable[[AttrDict], Union[bool, Coroutine]]
|
||||
) -> AttrDict:
|
||||
async def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
|
||||
"""Process events until the given callable evaluates to True.
|
||||
|
||||
The callable should accept an AttrDict object representing the
|
||||
@@ -122,9 +116,7 @@ class Client:
|
||||
if stop:
|
||||
return event
|
||||
|
||||
async def _on_event(
|
||||
self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent
|
||||
) -> None:
|
||||
async def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
|
||||
for hook, evfilter in self._hooks.get(filter_type, []):
|
||||
if await evfilter.filter(event):
|
||||
try:
|
||||
@@ -133,11 +125,7 @@ class Client:
|
||||
self.logger.exception(ex)
|
||||
|
||||
async def _parse_command(self, event: AttrDict) -> None:
|
||||
cmds = [
|
||||
hook[1].command
|
||||
for hook in self._hooks.get(NewMessage, [])
|
||||
if hook[1].command
|
||||
]
|
||||
cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
|
||||
parts = event.message_snapshot.text.split(maxsplit=1)
|
||||
payload = parts[1] if len(parts) > 1 else ""
|
||||
cmd = parts.pop(0)
|
||||
@@ -202,11 +190,7 @@ class Client:
|
||||
for message in await self.account.get_fresh_messages_in_arrival_order():
|
||||
snapshot = await message.get_snapshot()
|
||||
await self._on_new_msg(snapshot)
|
||||
if (
|
||||
snapshot.is_info
|
||||
and snapshot.system_message_type
|
||||
!= SystemMessageType.WEBXDC_INFO_MESSAGE
|
||||
):
|
||||
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
|
||||
await self._handle_info_msg(snapshot)
|
||||
await snapshot.message.mark_seen()
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from .const import EventType
|
||||
|
||||
def _tuple_of(obj, type_: type) -> tuple:
|
||||
if not obj:
|
||||
return tuple()
|
||||
return ()
|
||||
if isinstance(obj, type_):
|
||||
obj = (obj,)
|
||||
|
||||
@@ -39,7 +39,7 @@ class EventFilter(ABC):
|
||||
"""Return True if two event filters are equal."""
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
return not self == other
|
||||
|
||||
async def _call_func(self, event) -> bool:
|
||||
if not self.func:
|
||||
@@ -65,9 +65,7 @@ class RawEvent(EventFilter):
|
||||
should be dispatched or not.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, types: Union[None, EventType, Iterable[EventType]] = None, **kwargs
|
||||
):
|
||||
def __init__(self, types: Union[None, EventType, Iterable[EventType]] = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
try:
|
||||
self.types = _tuple_of(types, EventType)
|
||||
|
||||
@@ -49,22 +49,14 @@ class Message:
|
||||
"""Mark the message as seen."""
|
||||
await self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||
|
||||
async def send_webxdc_status_update(
|
||||
self, update: Union[dict, str], description: str
|
||||
) -> None:
|
||||
async def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
||||
"""Send a webxdc status update. This message must be a webxdc."""
|
||||
if not isinstance(update, str):
|
||||
update = json.dumps(update)
|
||||
await self._rpc.send_webxdc_status_update(
|
||||
self.account.id, self.id, update, description
|
||||
)
|
||||
await self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
|
||||
|
||||
async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
|
||||
return json.loads(
|
||||
await self._rpc.get_webxdc_status_updates(
|
||||
self.account.id, self.id, last_known_serial
|
||||
)
|
||||
)
|
||||
return json.loads(await self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
|
||||
|
||||
async def get_webxdc_info(self) -> dict:
|
||||
return await self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||
|
||||
@@ -67,9 +67,7 @@ class ACFactory:
|
||||
) -> Message:
|
||||
if not from_account:
|
||||
from_account = (await self.get_online_accounts(1))[0]
|
||||
to_contact = await from_account.create_contact(
|
||||
await to_account.get_config("addr")
|
||||
)
|
||||
to_contact = await from_account.create_contact(await to_account.get_config("addr"))
|
||||
if group:
|
||||
to_chat = await from_account.create_group(group)
|
||||
await to_chat.add_contact(to_contact)
|
||||
|
||||
@@ -30,7 +30,7 @@ class Rpc:
|
||||
"deltachat-rpc-server",
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
**self._kwargs
|
||||
**self._kwargs,
|
||||
)
|
||||
self.id = 0
|
||||
self.event_queues = {}
|
||||
@@ -46,7 +46,7 @@ class Rpc:
|
||||
await self.start()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
async def __aexit__(self, _exc_type, _exc, _tb):
|
||||
await self.close()
|
||||
|
||||
async def reader_loop(self) -> None:
|
||||
@@ -97,5 +97,6 @@ class Rpc:
|
||||
raise JsonRpcError(response["error"])
|
||||
if "result" in response:
|
||||
return response["result"]
|
||||
return None
|
||||
|
||||
return method
|
||||
|
||||
@@ -6,14 +6,14 @@ from deltachat_rpc_client import EventType, events
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_system_info(rpc) -> None:
|
||||
system_info = await rpc.get_system_info()
|
||||
assert "arch" in system_info
|
||||
assert "deltachat_core_version" in system_info
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_email_address_validity(rpc) -> None:
|
||||
valid_addresses = [
|
||||
"email@example.com",
|
||||
@@ -27,7 +27,7 @@ async def test_email_address_validity(rpc) -> None:
|
||||
assert not await rpc.check_email_validity(addr)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_acfactory(acfactory) -> None:
|
||||
account = await acfactory.new_configured_account()
|
||||
while True:
|
||||
@@ -41,7 +41,7 @@ async def test_acfactory(acfactory) -> None:
|
||||
print("Successful configuration")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_configure_starttls(acfactory) -> None:
|
||||
account = await acfactory.new_preconfigured_account()
|
||||
|
||||
@@ -51,7 +51,7 @@ async def test_configure_starttls(acfactory) -> None:
|
||||
assert await account.is_configured()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_account(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -111,7 +111,7 @@ async def test_account(acfactory) -> None:
|
||||
await alice.stop_io()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_chat(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -177,7 +177,7 @@ async def test_chat(acfactory) -> None:
|
||||
await group.get_locations()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_contact(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -195,7 +195,7 @@ async def test_contact(acfactory) -> None:
|
||||
await alice_contact_bob.create_chat()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_message(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -226,7 +226,7 @@ async def test_message(acfactory) -> None:
|
||||
await message.send_reaction("😎")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_bot(acfactory) -> None:
|
||||
mock = MagicMock()
|
||||
user = (await acfactory.get_online_accounts(1))[0]
|
||||
@@ -237,25 +237,20 @@ async def test_bot(acfactory) -> None:
|
||||
|
||||
hook = lambda e: mock.hook(e.msg_id), events.RawEvent(EventType.INCOMING_MSG)
|
||||
bot.add_hook(*hook)
|
||||
event = await acfactory.process_message(
|
||||
from_account=user, to_client=bot, text="Hello!"
|
||||
)
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
|
||||
mock.hook.assert_called_once_with(event.msg_id)
|
||||
bot.remove_hook(*hook)
|
||||
|
||||
track = lambda e: mock.hook(e.message_snapshot.id)
|
||||
def track(e):
|
||||
mock.hook(e.message_snapshot.id)
|
||||
|
||||
mock.hook.reset_mock()
|
||||
hook = track, events.NewMessage(r"hello")
|
||||
bot.add_hook(*hook)
|
||||
bot.add_hook(track, events.NewMessage(command="/help"))
|
||||
event = await acfactory.process_message(
|
||||
from_account=user, to_client=bot, text="hello"
|
||||
)
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
mock.hook.assert_called_with(event.msg_id)
|
||||
event = await acfactory.process_message(
|
||||
from_account=user, to_client=bot, text="hello!"
|
||||
)
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!")
|
||||
mock.hook.assert_called_with(event.msg_id)
|
||||
await acfactory.process_message(from_account=user, to_client=bot, text="hey!")
|
||||
assert len(mock.hook.mock_calls) == 2
|
||||
@@ -263,7 +258,5 @@ async def test_bot(acfactory) -> None:
|
||||
|
||||
mock.hook.reset_mock()
|
||||
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"
|
||||
)
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="/help")
|
||||
mock.hook.assert_called_once_with(event.msg_id)
|
||||
|
||||
@@ -3,16 +3,14 @@ import pytest
|
||||
from deltachat_rpc_client import EventType
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio()
|
||||
async def test_webxdc(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = await bob.get_config("addr")
|
||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
||||
await alice_chat_bob.send_message(
|
||||
text="Let's play chess!", file="../test-data/webxdc/chess.xdc"
|
||||
)
|
||||
await alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
isolated_build = true
|
||||
envlist =
|
||||
py3
|
||||
lint
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
@@ -16,3 +17,13 @@ deps =
|
||||
pytest-asyncio
|
||||
aiohttp
|
||||
aiodns
|
||||
|
||||
[testenv:lint]
|
||||
skipsdist = True
|
||||
skip_install = True
|
||||
deps =
|
||||
ruff
|
||||
black
|
||||
commands =
|
||||
black --check src/ examples/ tests/
|
||||
ruff src/ examples/ tests/
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#![recursion_limit = "128"]
|
||||
extern crate proc_macro;
|
||||
|
||||
use crate::proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
|
||||
use crate::proc_macro::TokenStream;
|
||||
|
||||
// For now, assume (not check) that these macroses are applied to enum without
|
||||
// data. If this assumption is violated, compiler error will point to
|
||||
// generated code, which is not very user-friendly.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use tempfile::tempdir;
|
||||
|
||||
use deltachat::chat::{self, ChatId};
|
||||
use deltachat::chatlist::*;
|
||||
use deltachat::config;
|
||||
@@ -8,6 +6,7 @@ use deltachat::context::*;
|
||||
use deltachat::message::Message;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::{EventType, Events};
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn cb(event: EventType) {
|
||||
match event {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
import sys
|
||||
import os
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
|
||||
@@ -34,8 +34,10 @@ class GroupTrackingPlugin:
|
||||
def ac_member_added(self, chat, contact, actor, message):
|
||||
print(
|
||||
"ac_member_added {} to chat {} from {}".format(
|
||||
contact.addr, chat.id, actor or message.get_sender_contact().addr
|
||||
)
|
||||
contact.addr,
|
||||
chat.id,
|
||||
actor or message.get_sender_contact().addr,
|
||||
),
|
||||
)
|
||||
for member in chat.get_contacts():
|
||||
print("chat member: {}".format(member.addr))
|
||||
@@ -44,8 +46,10 @@ class GroupTrackingPlugin:
|
||||
def ac_member_removed(self, chat, contact, actor, message):
|
||||
print(
|
||||
"ac_member_removed {} from chat {} by {}".format(
|
||||
contact.addr, chat.id, actor or message.get_sender_contact().addr
|
||||
)
|
||||
contact.addr,
|
||||
chat.id,
|
||||
actor or message.get_sender_contact().addr,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ def datadir():
|
||||
datadir = path.join("test-data")
|
||||
if datadir.isdir():
|
||||
return datadir
|
||||
else:
|
||||
pytest.skip("test-data directory not found")
|
||||
pytest.skip("test-data directory not found")
|
||||
return None
|
||||
|
||||
|
||||
def test_echo_quit_plugin(acfactory, lp):
|
||||
@@ -47,7 +47,7 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
botproc.fnmatch_lines(
|
||||
"""
|
||||
*ac_configure_completed*
|
||||
"""
|
||||
""",
|
||||
)
|
||||
ac1.add_account_plugin(FFIEventLogger(ac1))
|
||||
ac2.add_account_plugin(FFIEventLogger(ac2))
|
||||
@@ -61,7 +61,7 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
botproc.fnmatch_lines(
|
||||
"""
|
||||
*ac_chat_modified*bot test group*
|
||||
"""
|
||||
""",
|
||||
)
|
||||
|
||||
lp.sec("adding third member {}".format(ac2.get_config("addr")))
|
||||
@@ -76,8 +76,9 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
"""
|
||||
*ac_member_added {}*from*{}*
|
||||
""".format(
|
||||
contact3.addr, ac1.get_config("addr")
|
||||
)
|
||||
contact3.addr,
|
||||
ac1.get_config("addr"),
|
||||
),
|
||||
)
|
||||
|
||||
lp.sec("contact successfully added, now removing")
|
||||
@@ -86,6 +87,7 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
"""
|
||||
*ac_member_removed {}*from*{}*
|
||||
""".format(
|
||||
contact3.addr, ac1.get_config("addr")
|
||||
)
|
||||
contact3.addr,
|
||||
ac1.get_config("addr"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -44,5 +44,9 @@ git_describe_command = "git describe --dirty --tags --long --match py-*.*"
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
[tool.ruff]
|
||||
select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM"]
|
||||
line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
profile = "black"
|
||||
|
||||
@@ -36,7 +36,8 @@ register_global_plugin(events)
|
||||
|
||||
def run_cmdline(argv=None, account_plugins=None):
|
||||
"""Run a simple default command line app, registering the specified
|
||||
account plugins."""
|
||||
account plugins.
|
||||
"""
|
||||
import argparse
|
||||
|
||||
if argv is None:
|
||||
|
||||
@@ -102,8 +102,8 @@ def find_header(flags):
|
||||
printf("%s", _dc_header_file_location());
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
)
|
||||
""",
|
||||
),
|
||||
)
|
||||
cwd = os.getcwd()
|
||||
try:
|
||||
@@ -198,7 +198,7 @@ def ffibuilder():
|
||||
typedef int... time_t;
|
||||
void free(void *ptr);
|
||||
extern int dc_event_has_string_data(int);
|
||||
"""
|
||||
""",
|
||||
)
|
||||
function_defs = extract_functions(flags)
|
||||
defines = extract_defines(flags)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" Account class implementation. """
|
||||
"""Account class implementation."""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
@@ -39,7 +39,7 @@ def get_core_info():
|
||||
ffi.gc(
|
||||
lib.dc_context_new(as_dc_charpointer(""), as_dc_charpointer(path.name), ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -172,10 +172,7 @@ class Account(object):
|
||||
namebytes = name.encode("utf8")
|
||||
if isinstance(value, (int, bool)):
|
||||
value = str(int(value))
|
||||
if value is not None:
|
||||
valuebytes = value.encode("utf8")
|
||||
else:
|
||||
valuebytes = ffi.NULL
|
||||
valuebytes = value.encode("utf8") if value is not None else ffi.NULL
|
||||
lib.dc_set_config(self._dc_context, namebytes, valuebytes)
|
||||
|
||||
def get_config(self, name: str) -> str:
|
||||
@@ -225,9 +222,10 @@ class Account(object):
|
||||
return bool(lib.dc_is_configured(self._dc_context))
|
||||
|
||||
def is_open(self) -> bool:
|
||||
"""Determine if account is open
|
||||
"""Determine if account is open.
|
||||
|
||||
:returns True if account is open."""
|
||||
:returns True if account is open.
|
||||
"""
|
||||
return bool(lib.dc_context_is_open(self._dc_context))
|
||||
|
||||
def set_avatar(self, img_path: Optional[str]) -> None:
|
||||
@@ -543,7 +541,7 @@ class Account(object):
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def check_qr(self, qr):
|
||||
"""check qr code and return :class:`ScannedQRCode` instance representing the result"""
|
||||
"""check qr code and return :class:`ScannedQRCode` instance representing the result."""
|
||||
res = ffi.gc(lib.dc_check_qr(self._dc_context, as_dc_charpointer(qr)), lib.dc_lot_unref)
|
||||
lot = DCLot(res)
|
||||
if lot.state() == const.DC_QR_ERROR:
|
||||
@@ -662,7 +660,7 @@ class Account(object):
|
||||
return lib.dc_all_work_done(self._dc_context)
|
||||
|
||||
def start_io(self):
|
||||
"""start this account's IO scheduling (Rust-core async scheduler)
|
||||
"""start this account's IO scheduling (Rust-core async scheduler).
|
||||
|
||||
If this account is not configured an Exception is raised.
|
||||
You need to call account.configure() and account.wait_configure_finish()
|
||||
@@ -705,12 +703,10 @@ class Account(object):
|
||||
"""
|
||||
lib.dc_maybe_network(self._dc_context)
|
||||
|
||||
def configure(self, reconfigure: bool = False) -> ConfigureTracker:
|
||||
def configure(self) -> ConfigureTracker:
|
||||
"""Start configuration process and return a Configtracker instance
|
||||
on which you can block with wait_finish() to get a True/False success
|
||||
value for the configuration process.
|
||||
|
||||
:param reconfigure: deprecated, doesn't need to be checked anymore.
|
||||
"""
|
||||
if not self.get_config("addr") or not self.get_config("mail_pw"):
|
||||
raise MissingCredentials("addr or mail_pwd not set in config")
|
||||
@@ -733,7 +729,8 @@ class Account(object):
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""shutdown and destroy account (stop callback thread, close and remove
|
||||
underlying dc_context)."""
|
||||
underlying dc_context).
|
||||
"""
|
||||
if self._dc_context is None:
|
||||
return
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" Chat and Location related API. """
|
||||
"""Chat and Location related API."""
|
||||
|
||||
import calendar
|
||||
import json
|
||||
@@ -37,7 +37,7 @@ class Chat(object):
|
||||
return self.id == getattr(other, "id", None) and self.account._dc_context == other.account._dc_context
|
||||
|
||||
def __ne__(self, other) -> bool:
|
||||
return not (self == other)
|
||||
return not self == other
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<Chat id={} name={}>".format(self.id, self.get_name())
|
||||
@@ -74,19 +74,19 @@ class Chat(object):
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
|
||||
|
||||
def is_single(self) -> bool:
|
||||
"""Return True if this chat is a single/direct chat, False otherwise"""
|
||||
"""Return True if this chat is a single/direct chat, False otherwise."""
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_SINGLE
|
||||
|
||||
def is_mailinglist(self) -> bool:
|
||||
"""Return True if this chat is a mailing list, False otherwise"""
|
||||
"""Return True if this chat is a mailing list, False otherwise."""
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_MAILINGLIST
|
||||
|
||||
def is_broadcast(self) -> bool:
|
||||
"""Return True if this chat is a broadcast list, False otherwise"""
|
||||
"""Return True if this chat is a broadcast list, False otherwise."""
|
||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_BROADCAST
|
||||
|
||||
def is_multiuser(self) -> bool:
|
||||
"""Return True if this chat is a multi-user chat (group, mailing list or broadcast), False otherwise"""
|
||||
"""Return True if this chat is a multi-user chat (group, mailing list or broadcast), False otherwise."""
|
||||
return lib.dc_chat_get_type(self._dc_chat) in (
|
||||
const.DC_CHAT_TYPE_GROUP,
|
||||
const.DC_CHAT_TYPE_MAILINGLIST,
|
||||
@@ -94,11 +94,11 @@ class Chat(object):
|
||||
)
|
||||
|
||||
def is_self_talk(self) -> bool:
|
||||
"""Return True if this chat is the self-chat (a.k.a. "Saved Messages"), False otherwise"""
|
||||
"""Return True if this chat is the self-chat (a.k.a. "Saved Messages"), False otherwise."""
|
||||
return bool(lib.dc_chat_is_self_talk(self._dc_chat))
|
||||
|
||||
def is_device_talk(self) -> bool:
|
||||
"""Returns True if this chat is the "Device Messages" chat, False otherwise"""
|
||||
"""Returns True if this chat is the "Device Messages" chat, False otherwise."""
|
||||
return bool(lib.dc_chat_is_device_talk(self._dc_chat))
|
||||
|
||||
def is_muted(self) -> bool:
|
||||
@@ -109,12 +109,12 @@ class Chat(object):
|
||||
return bool(lib.dc_chat_is_muted(self._dc_chat))
|
||||
|
||||
def is_pinned(self) -> bool:
|
||||
"""Return True if this chat is pinned, False otherwise"""
|
||||
"""Return True if this chat is pinned, False otherwise."""
|
||||
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_PINNED
|
||||
|
||||
def is_archived(self) -> bool:
|
||||
"""Return True if this chat is archived, False otherwise.
|
||||
:returns: True if archived, False otherwise
|
||||
:returns: True if archived, False otherwise.
|
||||
"""
|
||||
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_ARCHIVED
|
||||
|
||||
@@ -136,7 +136,7 @@ class Chat(object):
|
||||
|
||||
def can_send(self) -> bool:
|
||||
"""Check if messages can be sent to a give chat.
|
||||
This is not true eg. for the contact requests or for the device-talk
|
||||
This is not true eg. for the contact requests or for the device-talk.
|
||||
|
||||
:returns: True if the chat is writable, False otherwise
|
||||
"""
|
||||
@@ -167,7 +167,7 @@ class Chat(object):
|
||||
|
||||
def get_color(self):
|
||||
"""return the color of the chat.
|
||||
:returns: color as 0x00rrggbb
|
||||
:returns: color as 0x00rrggbb.
|
||||
"""
|
||||
return lib.dc_chat_get_color(self._dc_chat)
|
||||
|
||||
@@ -178,21 +178,18 @@ class Chat(object):
|
||||
return json.loads(s)
|
||||
|
||||
def mute(self, duration: Optional[int] = None) -> None:
|
||||
"""mutes the chat
|
||||
"""mutes the chat.
|
||||
|
||||
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
|
||||
:returns: None
|
||||
"""
|
||||
if duration is None:
|
||||
mute_duration = -1
|
||||
else:
|
||||
mute_duration = duration
|
||||
mute_duration = -1 if duration is None else duration
|
||||
ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, mute_duration)
|
||||
if not bool(ret):
|
||||
raise ValueError("Call to dc_set_chat_mute_duration failed")
|
||||
|
||||
def unmute(self) -> None:
|
||||
"""unmutes the chat
|
||||
"""unmutes the chat.
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
@@ -252,7 +249,8 @@ class Chat(object):
|
||||
def get_encryption_info(self) -> Optional[str]:
|
||||
"""Return encryption info for this chat.
|
||||
|
||||
:returns: a string with encryption preferences of all chat members"""
|
||||
:returns: a string with encryption preferences of all chat members
|
||||
"""
|
||||
res = lib.dc_get_chat_encrinfo(self.account._dc_context, self.id)
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
@@ -463,7 +461,7 @@ class Chat(object):
|
||||
|
||||
def get_contacts(self):
|
||||
"""get all contacts for this chat.
|
||||
:returns: list of :class:`deltachat.contact.Contact` objects for this chat
|
||||
:returns: list of :class:`deltachat.contact.Contact` objects for this chat.
|
||||
"""
|
||||
from .contact import Contact
|
||||
|
||||
@@ -547,19 +545,10 @@ class Chat(object):
|
||||
:param timespan_to: a datetime object or None (indicating up till now)
|
||||
:returns: list of :class:`deltachat.chat.Location` objects.
|
||||
"""
|
||||
if timestamp_from is None:
|
||||
time_from = 0
|
||||
else:
|
||||
time_from = calendar.timegm(timestamp_from.utctimetuple())
|
||||
if timestamp_to is None:
|
||||
time_to = 0
|
||||
else:
|
||||
time_to = calendar.timegm(timestamp_to.utctimetuple())
|
||||
time_from = 0 if timestamp_from is None else calendar.timegm(timestamp_from.utctimetuple())
|
||||
time_to = 0 if timestamp_to is None else calendar.timegm(timestamp_to.utctimetuple())
|
||||
|
||||
if contact is None:
|
||||
contact_id = 0
|
||||
else:
|
||||
contact_id = contact.id
|
||||
contact_id = 0 if contact is None else contact.id
|
||||
|
||||
dc_array = lib.dc_get_locations(self.account._dc_context, self.id, contact_id, time_from, time_to)
|
||||
return [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" Contact object. """
|
||||
"""Contact object."""
|
||||
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import Optional
|
||||
@@ -28,7 +28,7 @@ class Contact(object):
|
||||
return self.account._dc_context == other.account._dc_context and self.id == other.id
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
return not self == other
|
||||
|
||||
def __repr__(self):
|
||||
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self.account._dc_context)
|
||||
@@ -76,7 +76,7 @@ class Contact(object):
|
||||
return lib.dc_contact_is_verified(self._dc_contact)
|
||||
|
||||
def get_verifier(self, contact):
|
||||
"""Return the address of the contact that verified the contact"""
|
||||
"""Return the address of the contact that verified the contact."""
|
||||
return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact))
|
||||
|
||||
def get_profile_image(self) -> Optional[str]:
|
||||
|
||||
@@ -79,15 +79,17 @@ class DirectImap:
|
||||
|
||||
def select_config_folder(self, config_name: str):
|
||||
"""Return info about selected folder if it is
|
||||
configured, otherwise None."""
|
||||
configured, otherwise None.
|
||||
"""
|
||||
if "_" not in config_name:
|
||||
config_name = "configured_{}_folder".format(config_name)
|
||||
foldername = self.account.get_config(config_name)
|
||||
if foldername:
|
||||
return self.select_folder(foldername)
|
||||
return None
|
||||
|
||||
def list_folders(self) -> List[str]:
|
||||
"""return list of all existing folder names"""
|
||||
"""return list of all existing folder names."""
|
||||
assert not self._idling
|
||||
return [folder.name for folder in self.conn.folder.list()]
|
||||
|
||||
@@ -103,7 +105,7 @@ class DirectImap:
|
||||
|
||||
def get_all_messages(self) -> List[MailMessage]:
|
||||
assert not self._idling
|
||||
return [mail for mail in self.conn.fetch()]
|
||||
return list(self.conn.fetch())
|
||||
|
||||
def get_unread_messages(self) -> List[str]:
|
||||
assert not self._idling
|
||||
@@ -221,5 +223,4 @@ class IdleManager:
|
||||
|
||||
def done(self):
|
||||
"""send idle-done to server if we are currently in idle mode."""
|
||||
res = self.direct_imap.conn.idle.stop()
|
||||
return res
|
||||
return self.direct_imap.conn.idle.stop()
|
||||
|
||||
@@ -32,12 +32,11 @@ class FFIEvent:
|
||||
def __str__(self):
|
||||
if self.name == "DC_EVENT_INFO":
|
||||
return "INFO {data2}".format(data2=self.data2)
|
||||
elif self.name == "DC_EVENT_WARNING":
|
||||
if self.name == "DC_EVENT_WARNING":
|
||||
return "WARNING {data2}".format(data2=self.data2)
|
||||
elif self.name == "DC_EVENT_ERROR":
|
||||
if self.name == "DC_EVENT_ERROR":
|
||||
return "ERROR {data2}".format(data2=self.data2)
|
||||
else:
|
||||
return "{name} data1={data1} data2={data2}".format(**self.__dict__)
|
||||
return "{name} data1={data1} data2={data2}".format(**self.__dict__)
|
||||
|
||||
|
||||
class FFIEventLogger:
|
||||
@@ -135,7 +134,8 @@ class FFIEventTracker:
|
||||
def wait_for_connectivity(self, connectivity):
|
||||
"""Wait for the specified connectivity.
|
||||
This only works reliably if the connectivity doesn't change
|
||||
again too quickly, otherwise we might miss it."""
|
||||
again too quickly, otherwise we might miss it.
|
||||
"""
|
||||
while 1:
|
||||
if self.account.get_connectivity() == connectivity:
|
||||
return
|
||||
@@ -143,12 +143,13 @@ class FFIEventTracker:
|
||||
|
||||
def wait_for_connectivity_change(self, previous, expected_next):
|
||||
"""Wait until the connectivity changes to `expected_next`.
|
||||
Fails the test if it changes to something else."""
|
||||
Fails the test if it changes to something else.
|
||||
"""
|
||||
while 1:
|
||||
current = self.account.get_connectivity()
|
||||
if current == expected_next:
|
||||
return
|
||||
elif current != previous:
|
||||
if current != previous:
|
||||
raise Exception("Expected connectivity " + str(expected_next) + " but got " + str(current))
|
||||
|
||||
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
@@ -183,7 +184,8 @@ class FFIEventTracker:
|
||||
- ac1 and ac2 are created
|
||||
- ac1 sends a message to ac2
|
||||
- ac2 is still running FetchExsistingMsgs job and thinks it's an existing, old message
|
||||
- therefore no DC_EVENT_INCOMING_MSG is sent"""
|
||||
- therefore no DC_EVENT_INCOMING_MSG is sent
|
||||
"""
|
||||
self.get_info_contains("INBOX: Idle entering")
|
||||
|
||||
def wait_next_incoming_message(self):
|
||||
@@ -193,14 +195,15 @@ class FFIEventTracker:
|
||||
|
||||
def wait_next_messages_changed(self):
|
||||
"""wait for and return next message-changed message or None
|
||||
if the event contains no msgid"""
|
||||
if the event contains no msgid
|
||||
"""
|
||||
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
if ev.data2 > 0:
|
||||
return self.account.get_message_by_id(ev.data2)
|
||||
return None
|
||||
|
||||
def wait_next_reactions_changed(self):
|
||||
"""wait for and return next reactions-changed message"""
|
||||
"""wait for and return next reactions-changed message."""
|
||||
ev = self.get_matching("DC_EVENT_REACTIONS_CHANGED")
|
||||
assert ev.data1 > 0
|
||||
return self.account.get_message_by_id(ev.data2)
|
||||
@@ -292,10 +295,10 @@ class EventThread(threading.Thread):
|
||||
if data1 == 0 or data1 == 1000:
|
||||
success = data1 == 1000
|
||||
comment = ffi_event.data2
|
||||
yield "ac_configure_completed", dict(success=success, comment=comment)
|
||||
yield "ac_configure_completed", {"success": success, "comment": comment}
|
||||
elif name == "DC_EVENT_INCOMING_MSG":
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
|
||||
yield map_system_message(msg) or ("ac_incoming_message", {"message": msg})
|
||||
elif name == "DC_EVENT_MSGS_CHANGED":
|
||||
if ffi_event.data2 != 0:
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
@@ -303,19 +306,19 @@ class EventThread(threading.Thread):
|
||||
res = map_system_message(msg)
|
||||
if res and res[0].startswith("ac_member"):
|
||||
yield res
|
||||
yield "ac_outgoing_message", dict(message=msg)
|
||||
yield "ac_outgoing_message", {"message": msg}
|
||||
elif msg.is_in_fresh():
|
||||
yield map_system_message(msg) or (
|
||||
"ac_incoming_message",
|
||||
dict(message=msg),
|
||||
{"message": msg},
|
||||
)
|
||||
elif name == "DC_EVENT_REACTIONS_CHANGED":
|
||||
assert ffi_event.data1 > 0
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
yield "ac_reactions_changed", dict(message=msg)
|
||||
yield "ac_reactions_changed", {"message": msg}
|
||||
elif name == "DC_EVENT_MSG_DELIVERED":
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
yield "ac_message_delivered", dict(message=msg)
|
||||
yield "ac_message_delivered", {"message": msg}
|
||||
elif name == "DC_EVENT_CHAT_MODIFIED":
|
||||
chat = account.get_chat_by_id(ffi_event.data1)
|
||||
yield "ac_chat_modified", dict(chat=chat)
|
||||
yield "ac_chat_modified", {"chat": chat}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" Hooks for Python bindings to Delta Chat Core Rust CFFI"""
|
||||
"""Hooks for Python bindings to Delta Chat Core Rust CFFI."""
|
||||
|
||||
import pluggy
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" The Message object. """
|
||||
"""The Message object."""
|
||||
|
||||
import json
|
||||
import os
|
||||
@@ -59,10 +59,7 @@ class Message(object):
|
||||
:param view_type: the message type code or one of the strings:
|
||||
"text", "audio", "video", "file", "sticker", "videochat", "webxdc"
|
||||
"""
|
||||
if isinstance(view_type, int):
|
||||
view_type_code = view_type
|
||||
else:
|
||||
view_type_code = get_viewtype_code_from_name(view_type)
|
||||
view_type_code = view_type if isinstance(view_type, int) else get_viewtype_code_from_name(view_type)
|
||||
return Message(
|
||||
account,
|
||||
ffi.gc(lib.dc_msg_new(account._dc_context, view_type_code), lib.dc_msg_unref),
|
||||
@@ -129,7 +126,7 @@ class Message(object):
|
||||
|
||||
@props.with_doc
|
||||
def filemime(self) -> str:
|
||||
"""mime type of the file (if it exists)"""
|
||||
"""mime type of the file (if it exists)."""
|
||||
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
|
||||
|
||||
def get_status_updates(self, serial: int = 0) -> list:
|
||||
@@ -141,7 +138,7 @@ class Message(object):
|
||||
:param serial: The last known serial. Pass 0 if there are no known serials to receive all updates.
|
||||
"""
|
||||
return json.loads(
|
||||
from_dc_charpointer(lib.dc_get_webxdc_status_updates(self.account._dc_context, self.id, serial))
|
||||
from_dc_charpointer(lib.dc_get_webxdc_status_updates(self.account._dc_context, self.id, serial)),
|
||||
)
|
||||
|
||||
def send_status_update(self, json_data: Union[str, dict], description: str) -> bool:
|
||||
@@ -158,8 +155,11 @@ class Message(object):
|
||||
json_data = json.dumps(json_data, default=str)
|
||||
return bool(
|
||||
lib.dc_send_webxdc_status_update(
|
||||
self.account._dc_context, self.id, as_dc_charpointer(json_data), as_dc_charpointer(description)
|
||||
)
|
||||
self.account._dc_context,
|
||||
self.id,
|
||||
as_dc_charpointer(json_data),
|
||||
as_dc_charpointer(description),
|
||||
),
|
||||
)
|
||||
|
||||
def send_reaction(self, reaction: str):
|
||||
@@ -232,16 +232,18 @@ class Message(object):
|
||||
ts = lib.dc_msg_get_received_timestamp(self._dc_msg)
|
||||
if ts:
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
return None
|
||||
|
||||
@props.with_doc
|
||||
def ephemeral_timer(self):
|
||||
"""Ephemeral timer in seconds
|
||||
"""Ephemeral timer in seconds.
|
||||
|
||||
:returns: timer in seconds or None if there is no timer
|
||||
"""
|
||||
timer = lib.dc_msg_get_ephemeral_timer(self._dc_msg)
|
||||
if timer:
|
||||
return timer
|
||||
return None
|
||||
|
||||
@props.with_doc
|
||||
def ephemeral_timestamp(self):
|
||||
@@ -255,23 +257,25 @@ class Message(object):
|
||||
|
||||
@property
|
||||
def quoted_text(self) -> Optional[str]:
|
||||
"""Text inside the quote
|
||||
"""Text inside the quote.
|
||||
|
||||
:returns: Quoted text"""
|
||||
:returns: Quoted text
|
||||
"""
|
||||
return from_optional_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
|
||||
|
||||
@property
|
||||
def quote(self):
|
||||
"""Quote getter
|
||||
"""Quote getter.
|
||||
|
||||
:returns: Quoted message, if found in the database"""
|
||||
:returns: Quoted message, if found in the database
|
||||
"""
|
||||
msg = lib.dc_msg_get_quoted_msg(self._dc_msg)
|
||||
if msg:
|
||||
return Message(self.account, ffi.gc(msg, lib.dc_msg_unref))
|
||||
|
||||
@quote.setter
|
||||
def quote(self, quoted_message):
|
||||
"""Quote setter"""
|
||||
"""Quote setter."""
|
||||
lib.dc_msg_set_quote(self._dc_msg, quoted_message._dc_msg)
|
||||
|
||||
def force_plaintext(self) -> None:
|
||||
@@ -286,7 +290,7 @@ class Message(object):
|
||||
|
||||
:returns: email-mime message object (with headers only, no body).
|
||||
"""
|
||||
import email.parser
|
||||
import email
|
||||
|
||||
mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id)
|
||||
if mime_headers:
|
||||
@@ -297,7 +301,7 @@ class Message(object):
|
||||
|
||||
@property
|
||||
def error(self) -> Optional[str]:
|
||||
"""Error message"""
|
||||
"""Error message."""
|
||||
return from_optional_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
|
||||
|
||||
@property
|
||||
@@ -493,7 +497,8 @@ def get_viewtype_code_from_name(view_type_name):
|
||||
if code is not None:
|
||||
return code
|
||||
raise ValueError(
|
||||
"message typecode not found for {!r}, " "available {!r}".format(view_type_name, list(_view_type_mapping.keys()))
|
||||
"message typecode not found for {!r}, "
|
||||
"available {!r}".format(view_type_name, list(_view_type_mapping.keys())),
|
||||
)
|
||||
|
||||
|
||||
@@ -506,14 +511,11 @@ def map_system_message(msg):
|
||||
if msg.is_system_message():
|
||||
res = parse_system_add_remove(msg.text)
|
||||
if not res:
|
||||
return
|
||||
return None
|
||||
action, affected, actor = res
|
||||
affected = msg.account.get_contact_by_addr(affected)
|
||||
if actor == "me":
|
||||
actor = None
|
||||
else:
|
||||
actor = msg.account.get_contact_by_addr(actor)
|
||||
d = dict(chat=msg.chat, contact=affected, actor=actor, message=msg)
|
||||
actor = None if actor == "me" else msg.account.get_contact_by_addr(actor)
|
||||
d = {"chat": msg.chat, "contact": affected, "actor": actor, "message": msg}
|
||||
return "ac_member_" + res[0], d
|
||||
|
||||
|
||||
@@ -528,8 +530,8 @@ def extract_addr(text):
|
||||
def parse_system_add_remove(text):
|
||||
"""return add/remove info from parsing the given system message text.
|
||||
|
||||
returns a (action, affected, actor) triple"""
|
||||
|
||||
returns a (action, affected, actor) triple
|
||||
"""
|
||||
# You removed member a@b.
|
||||
# You added member a@b.
|
||||
# Member Me (x@y) removed by a@b.
|
||||
|
||||
@@ -8,7 +8,7 @@ def with_doc(f):
|
||||
# copied over unmodified from
|
||||
# https://github.com/devpi/devpi/blob/master/common/devpi_common/types.py
|
||||
def cached(f):
|
||||
"""returns a cached property that is calculated by function f"""
|
||||
"""returns a cached property that is calculated by function f."""
|
||||
|
||||
def get(self):
|
||||
try:
|
||||
@@ -17,8 +17,9 @@ def cached(f):
|
||||
self._property_cache = {}
|
||||
except KeyError:
|
||||
pass
|
||||
x = self._property_cache[f] = f(self)
|
||||
return x
|
||||
res = f(self)
|
||||
self._property_cache[f] = res
|
||||
return res
|
||||
|
||||
def set(self, val):
|
||||
propcache = self.__dict__.setdefault("_property_cache", {})
|
||||
|
||||
@@ -9,7 +9,8 @@ class ProviderNotFoundError(Exception):
|
||||
|
||||
|
||||
class Provider(object):
|
||||
"""Provider information.
|
||||
"""
|
||||
Provider information.
|
||||
|
||||
:param domain: The email to get the provider info for.
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" The Reactions object. """
|
||||
"""The Reactions object."""
|
||||
|
||||
from .capi import ffi, lib
|
||||
from .cutil import from_dc_charpointer, iter_array
|
||||
|
||||
@@ -29,7 +29,7 @@ def pytest_addoption(parser):
|
||||
"--liveconfig",
|
||||
action="store",
|
||||
default=None,
|
||||
help="a file with >=2 lines where each line " "contains NAME=VALUE config settings for one account",
|
||||
help="a file with >=2 lines where each line contains NAME=VALUE config settings for one account",
|
||||
)
|
||||
group.addoption(
|
||||
"--ignored",
|
||||
@@ -124,7 +124,7 @@ def pytest_report_header(config, startdir):
|
||||
info["deltachat_core_version"],
|
||||
info["sqlite_version"],
|
||||
info["journal_mode"],
|
||||
)
|
||||
),
|
||||
]
|
||||
|
||||
cfg = config.option.liveconfig
|
||||
@@ -180,7 +180,7 @@ class TestProcess:
|
||||
if res.status_code != 200:
|
||||
pytest.fail("newtmpuser count={} code={}: '{}'".format(index, res.status_code, res.text))
|
||||
d = res.json()
|
||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
||||
config = {"addr": d["email"], "mail_pw": d["password"]}
|
||||
print("newtmpuser {}: addr={}".format(index, config["addr"]))
|
||||
self._configlist.append(config)
|
||||
yield config
|
||||
@@ -229,7 +229,7 @@ def write_dict_to_dir(dic, target_dir):
|
||||
path.write_bytes(content)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.fixture()
|
||||
def data(request):
|
||||
class Data:
|
||||
def __init__(self) -> None:
|
||||
@@ -253,6 +253,7 @@ def data(request):
|
||||
if os.path.exists(fn):
|
||||
return fn
|
||||
print("WARNING: path does not exist: {!r}".format(fn))
|
||||
return None
|
||||
|
||||
def read_path(self, bn, mode="r"):
|
||||
fn = self.get_path(bn)
|
||||
@@ -264,8 +265,11 @@ def data(request):
|
||||
|
||||
|
||||
class ACSetup:
|
||||
"""accounts setup helper to deal with multiple configure-process
|
||||
and io & imap initialization phases. From tests, use the higher level
|
||||
"""
|
||||
Accounts setup helper to deal with multiple configure-process
|
||||
and io & imap initialization phases.
|
||||
|
||||
From tests, use the higher level
|
||||
public ACFactory methods instead of its private helper class.
|
||||
"""
|
||||
|
||||
@@ -289,7 +293,7 @@ class ACSetup:
|
||||
self._account2state[account] = self.CONFIGURED
|
||||
self.log("added already configured account", account, account.get_config("addr"))
|
||||
|
||||
def start_configure(self, account, reconfigure=False):
|
||||
def start_configure(self, account):
|
||||
"""add an account and start its configure process."""
|
||||
|
||||
class PendingTracker:
|
||||
@@ -299,7 +303,7 @@ class ACSetup:
|
||||
|
||||
account.add_account_plugin(PendingTracker(), name="pending_tracker")
|
||||
self._account2state[account] = self.CONFIGURING
|
||||
account.configure(reconfigure=reconfigure)
|
||||
account.configure()
|
||||
self.log("started configure on", account)
|
||||
|
||||
def wait_one_configured(self, account):
|
||||
@@ -411,7 +415,8 @@ class ACFactory:
|
||||
acc.disable_logging()
|
||||
|
||||
def get_next_liveconfig(self):
|
||||
"""Base function to get functional online configurations
|
||||
"""
|
||||
Base function to get functional online configurations
|
||||
where we can make valid SMTP and IMAP connections with.
|
||||
"""
|
||||
configdict = next(self._liveconfig_producer).copy()
|
||||
@@ -465,8 +470,7 @@ class ACFactory:
|
||||
if fname_pub and fname_sec:
|
||||
account._preconfigure_keypair(addr, fname_pub, fname_sec)
|
||||
return True
|
||||
else:
|
||||
print("WARN: could not use preconfigured keys for {!r}".format(addr))
|
||||
print("WARN: could not use preconfigured keys for {!r}".format(addr))
|
||||
|
||||
def get_pseudo_configured_account(self, passphrase: Optional[str] = None) -> Account:
|
||||
# do a pseudo-configured account
|
||||
@@ -476,14 +480,14 @@ class ACFactory:
|
||||
acname = ac._logid
|
||||
addr = "{}@offline.org".format(acname)
|
||||
ac.update_config(
|
||||
dict(
|
||||
addr=addr,
|
||||
displayname=acname,
|
||||
mail_pw="123",
|
||||
configured_addr=addr,
|
||||
configured_mail_pw="123",
|
||||
configured="1",
|
||||
)
|
||||
{
|
||||
"addr": addr,
|
||||
"displayname": acname,
|
||||
"mail_pw": "123",
|
||||
"configured_addr": addr,
|
||||
"configured_mail_pw": "123",
|
||||
"configured": "1",
|
||||
},
|
||||
)
|
||||
self._preconfigure_key(ac, addr)
|
||||
self._acsetup.init_logging(ac)
|
||||
@@ -494,12 +498,12 @@ class ACFactory:
|
||||
configdict = self.get_next_liveconfig()
|
||||
else:
|
||||
# XXX we might want to transfer the key to the new account
|
||||
configdict = dict(
|
||||
addr=cloned_from.get_config("addr"),
|
||||
mail_pw=cloned_from.get_config("mail_pw"),
|
||||
imap_certificate_checks=cloned_from.get_config("imap_certificate_checks"),
|
||||
smtp_certificate_checks=cloned_from.get_config("smtp_certificate_checks"),
|
||||
)
|
||||
configdict = {
|
||||
"addr": cloned_from.get_config("addr"),
|
||||
"mail_pw": cloned_from.get_config("mail_pw"),
|
||||
"imap_certificate_checks": cloned_from.get_config("imap_certificate_checks"),
|
||||
"smtp_certificate_checks": cloned_from.get_config("smtp_certificate_checks"),
|
||||
}
|
||||
configdict.update(kwargs)
|
||||
ac = self._get_cached_account(addr=configdict["addr"]) if cache else None
|
||||
if ac is not None:
|
||||
@@ -600,7 +604,7 @@ class ACFactory:
|
||||
acc._evtracker.wait_next_incoming_message()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.fixture()
|
||||
def acfactory(request, tmpdir, testprocess, data):
|
||||
am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testprocess, data=data)
|
||||
yield am
|
||||
@@ -665,12 +669,12 @@ class BotProcess:
|
||||
ignored.append(line)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.fixture()
|
||||
def tmp_db_path(tmpdir):
|
||||
return tmpdir.join("test.db").strpath
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.fixture()
|
||||
def lp():
|
||||
class Printer:
|
||||
def sec(self, msg: str) -> None:
|
||||
|
||||
@@ -77,11 +77,11 @@ class ConfigureTracker:
|
||||
self.account.remove_account_plugin(self)
|
||||
|
||||
def wait_smtp_connected(self):
|
||||
"""wait until smtp is configured."""
|
||||
"""Wait until SMTP is configured."""
|
||||
self._smtp_finished.wait()
|
||||
|
||||
def wait_imap_connected(self):
|
||||
"""wait until smtp is configured."""
|
||||
"""Wait until IMAP is configured."""
|
||||
self._imap_finished.wait()
|
||||
|
||||
def wait_progress(self, data1=None):
|
||||
@@ -91,7 +91,8 @@ class ConfigureTracker:
|
||||
break
|
||||
|
||||
def wait_finish(self, timeout=None):
|
||||
"""wait until configure is completed.
|
||||
"""
|
||||
Wait until configure is completed.
|
||||
|
||||
Raise Exception if Configure failed
|
||||
"""
|
||||
|
||||
@@ -15,5 +15,5 @@ if __name__ == "__main__":
|
||||
p,
|
||||
"-w",
|
||||
workspacedir,
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
@@ -216,7 +216,7 @@ def test_fetch_existing(acfactory, lp, mvbox_move):
|
||||
# would also find the "Sent" folder, but it would be too late:
|
||||
# The sentbox thread, started by `start_io()`, would have seen that there is no
|
||||
# ConfiguredSentboxFolder and do nothing.
|
||||
acfactory._acsetup.start_configure(ac1, reconfigure=True)
|
||||
acfactory._acsetup.start_configure(ac1)
|
||||
acfactory.bring_accounts_online()
|
||||
assert_folders_configured(ac1)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ def test_basic_imap_api(acfactory, tmpdir):
|
||||
imap2.shutdown()
|
||||
|
||||
|
||||
@pytest.mark.ignored
|
||||
@pytest.mark.ignored()
|
||||
def test_configure_generate_key(acfactory, lp):
|
||||
# A slow test which will generate new keys.
|
||||
acfactory.remove_preconfigured_keys()
|
||||
@@ -510,7 +510,7 @@ def test_send_and_receive_message_markseen(acfactory, lp):
|
||||
idle2.wait_for_seen()
|
||||
|
||||
lp.step("1")
|
||||
for i in range(2):
|
||||
for _i in range(2):
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
|
||||
assert ev.data1 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev.data2 > const.DC_MSG_ID_LAST_SPECIAL
|
||||
@@ -529,7 +529,7 @@ def test_send_and_receive_message_markseen(acfactory, lp):
|
||||
pass # mark_seen_messages() has generated events before it returns
|
||||
|
||||
|
||||
def test_moved_markseen(acfactory, lp):
|
||||
def test_moved_markseen(acfactory):
|
||||
"""Test that message already moved to DeltaChat folder is marked as seen."""
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
@@ -553,7 +553,7 @@ def test_moved_markseen(acfactory, lp):
|
||||
ac2.mark_seen_messages([msg])
|
||||
uid = idle2.wait_for_seen()
|
||||
|
||||
assert len([a for a in ac2.direct_imap.conn.fetch(AND(seen=True, uid=U(uid, "*")))]) == 1
|
||||
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True, uid=U(uid, "*"))))) == 1
|
||||
|
||||
|
||||
def test_message_override_sender_name(acfactory, lp):
|
||||
@@ -832,7 +832,7 @@ def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
|
||||
lp.sec("sending multi-line non-unicode message from ac1 to ac2")
|
||||
text1 = (
|
||||
"hello\nworld\nthis is a very long message that should be"
|
||||
+ " wrapped using format=flowed and unwrapped on the receiver"
|
||||
" wrapped using format=flowed and unwrapped on the receiver"
|
||||
)
|
||||
msg_out = chat.send_text(text1)
|
||||
assert not msg_out.is_encrypted()
|
||||
@@ -894,7 +894,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
|
||||
message in Drafts that is moved to Sent later
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
@@ -908,7 +908,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
|
||||
message in Sent
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
@@ -922,7 +922,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
|
||||
Unknown message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
@@ -936,7 +936,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
|
||||
Unknown & malformed message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
@@ -950,7 +950,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
|
||||
Unknown & malformed message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
@@ -964,7 +964,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
|
||||
Actually interesting message in Spam
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
ac1.direct_imap.append(
|
||||
@@ -978,7 +978,7 @@ def test_dont_show_emails(acfactory, lp):
|
||||
|
||||
Unknown message in Junk
|
||||
""".format(
|
||||
ac1.get_config("configured_addr")
|
||||
ac1.get_config("configured_addr"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1710,7 +1710,7 @@ def test_system_group_msg_from_blocked_user(acfactory, lp):
|
||||
assert contact.is_blocked()
|
||||
chat_on_ac2.remove_contact(ac1)
|
||||
ac1._evtracker.get_matching("DC_EVENT_CHAT_MODIFIED")
|
||||
assert not ac1.get_self_contact() in chat_on_ac1.get_contacts()
|
||||
assert ac1.get_self_contact() not in chat_on_ac1.get_contacts()
|
||||
|
||||
|
||||
def test_set_get_group_image(acfactory, data, lp):
|
||||
@@ -1784,7 +1784,7 @@ def test_connectivity(acfactory, lp):
|
||||
|
||||
lp.sec(
|
||||
"Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, "
|
||||
+ "all messages are fetched"
|
||||
"all messages are fetched",
|
||||
)
|
||||
|
||||
ac1.direct_imap.select_config_folder("inbox")
|
||||
@@ -2146,7 +2146,7 @@ def test_group_quote(acfactory, lp):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"folder,move,expected_destination,",
|
||||
("folder", "move", "expected_destination"),
|
||||
[
|
||||
(
|
||||
"xyz",
|
||||
@@ -2261,11 +2261,44 @@ def test_aeap_flow_verified(acfactory, lp):
|
||||
assert ac1new.get_config("addr") in [contact.addr for contact in msg_in_2.chat.get_contacts()]
|
||||
|
||||
|
||||
def test_archived_muted_chat(acfactory, lp):
|
||||
"""If an archived and muted chat receives a new message, DC_EVENT_MSGS_CHANGED for
|
||||
DC_CHAT_ID_ARCHIVED_LINK must be generated if the chat had only seen messages previously.
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: send message to ac2")
|
||||
chat.send_text("message0")
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message0"
|
||||
msg2.mark_seen()
|
||||
|
||||
chat2 = msg2.chat
|
||||
chat2.archive()
|
||||
chat2.mute()
|
||||
|
||||
lp.sec("ac1: send another message to ac2")
|
||||
chat.send_text("message1")
|
||||
|
||||
lp.sec("wait for ac2 to receive DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK")
|
||||
while 1:
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
if ev.data1 == const.DC_CHAT_ID_ARCHIVED_LINK:
|
||||
assert ev.data2 == 0
|
||||
archive = ac2.get_chat_by_id(const.DC_CHAT_ID_ARCHIVED_LINK)
|
||||
assert archive.count_fresh_messages() == 1
|
||||
assert chat2.count_fresh_messages() == 1
|
||||
break
|
||||
|
||||
|
||||
class TestOnlineConfigureFails:
|
||||
def test_invalid_password(self, acfactory):
|
||||
configdict = acfactory.get_next_liveconfig()
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
ac1.update_config(dict(addr=configdict["addr"], mail_pw="123"))
|
||||
ac1.update_config({"addr": configdict["addr"], "mail_pw": "123"})
|
||||
configtracker = ac1.configure()
|
||||
configtracker.wait_progress(500)
|
||||
configtracker.wait_progress(0)
|
||||
|
||||
@@ -15,7 +15,7 @@ from deltachat.tracker import ImexFailed
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"msgtext,res",
|
||||
("msgtext", "res"),
|
||||
[
|
||||
(
|
||||
"Member Me (tmp1@x.org) removed by tmp2@x.org.",
|
||||
@@ -108,7 +108,7 @@ class TestOfflineAccountBasic:
|
||||
|
||||
def test_update_config(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
ac1.update_config(dict(mvbox_move=False))
|
||||
ac1.update_config({"mvbox_move": False})
|
||||
assert ac1.get_config("mvbox_move") == "0"
|
||||
|
||||
def test_has_savemime(self, acfactory):
|
||||
@@ -229,11 +229,11 @@ class TestOfflineContact:
|
||||
|
||||
|
||||
class TestOfflineChat:
|
||||
@pytest.fixture
|
||||
@pytest.fixture()
|
||||
def ac1(self, acfactory):
|
||||
return acfactory.get_pseudo_configured_account()
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.fixture()
|
||||
def chat1(self, ac1):
|
||||
return ac1.create_contact("some1@example.org", name="some1").create_chat()
|
||||
|
||||
@@ -257,7 +257,7 @@ class TestOfflineChat:
|
||||
assert chat2.id == chat1.id
|
||||
assert chat2.get_name() == chat1.get_name()
|
||||
assert chat1 == chat2
|
||||
assert not (chat1 != chat2)
|
||||
assert not chat1.__ne__(chat2)
|
||||
assert chat1 != chat3
|
||||
|
||||
for ichat in ac1.get_chats():
|
||||
@@ -450,7 +450,7 @@ class TestOfflineChat:
|
||||
assert msg.filemime == "image/png"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"fn,typein,typeout",
|
||||
("fn", "typein", "typeout"),
|
||||
[
|
||||
("r", None, "application/octet-stream"),
|
||||
("r.txt", None, "text/plain"),
|
||||
@@ -458,7 +458,7 @@ class TestOfflineChat:
|
||||
("r.txt", "image/png", "image/png"),
|
||||
],
|
||||
)
|
||||
def test_message_file(self, ac1, chat1, data, lp, fn, typein, typeout):
|
||||
def test_message_file(self, chat1, data, lp, fn, typein, typeout):
|
||||
lp.sec("sending file")
|
||||
fp = data.get_path(fn)
|
||||
msg = chat1.send_file(fp, typein)
|
||||
@@ -694,7 +694,7 @@ class TestOfflineChat:
|
||||
chat1.set_draft(None)
|
||||
assert chat1.get_draft() is None
|
||||
|
||||
def test_qr_setup_contact(self, acfactory, lp):
|
||||
def test_qr_setup_contact(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
qr = ac1.get_setup_contact_qr()
|
||||
|
||||
@@ -93,7 +93,7 @@ def test_empty_context():
|
||||
capi.lib.dc_context_unref(ctx)
|
||||
|
||||
|
||||
def test_dc_close_events(tmpdir, acfactory):
|
||||
def test_dc_close_events(acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
|
||||
# register after_shutdown function
|
||||
|
||||
@@ -50,18 +50,14 @@ commands =
|
||||
skipsdist = True
|
||||
skip_install = True
|
||||
deps =
|
||||
flake8
|
||||
# isort 5.11.0 is broken: https://github.com/PyCQA/isort/issues/2031
|
||||
isort<5.11.0
|
||||
ruff
|
||||
black
|
||||
# pygments required by rst-lint
|
||||
pygments
|
||||
restructuredtext_lint
|
||||
commands =
|
||||
isort --check setup.py install_python_bindings.py src/deltachat examples/ tests/
|
||||
black --check setup.py install_python_bindings.py src/deltachat examples/ tests/
|
||||
flake8 src/deltachat
|
||||
flake8 tests/ examples/
|
||||
ruff src/deltachat tests/ examples/
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
|
||||
[testenv:mypy]
|
||||
@@ -102,7 +98,3 @@ timeout = 150
|
||||
timeout_func_only = True
|
||||
markers =
|
||||
ignored: ignore this test in default test runs, use --ignored to run.
|
||||
|
||||
[flake8]
|
||||
max-line-length = 120
|
||||
ignore = E203, E266, E501, W503
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
Remove old "dc" indices except for master which always stays.
|
||||
|
||||
"""
|
||||
from requests import Session
|
||||
import datetime
|
||||
import sys
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
MAXDAYS=7
|
||||
from requests import Session
|
||||
|
||||
MAXDAYS = 7
|
||||
|
||||
session = Session()
|
||||
session.headers["Accept"] = "application/json"
|
||||
@@ -54,7 +55,8 @@ def run():
|
||||
if not dates:
|
||||
print(
|
||||
"%s has no releases" % (baseurl + username + "/" + indexname),
|
||||
file=sys.stderr)
|
||||
file=sys.stderr,
|
||||
)
|
||||
date = datetime.datetime.now()
|
||||
else:
|
||||
date = datetime.datetime(*max(dates))
|
||||
@@ -67,6 +69,5 @@ def run():
|
||||
subprocess.check_call(["devpi", "index", "-y", "--delete", url])
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
from argparse import ArgumentParser
|
||||
|
||||
@@ -23,7 +23,7 @@ def read_toml_version(relpath):
|
||||
res = regex_matches(relpath, rex)
|
||||
if res is not None:
|
||||
return res.group(1)
|
||||
raise ValueError("no version found in {}".format(relpath))
|
||||
raise ValueError(f"no version found in {relpath}")
|
||||
|
||||
|
||||
def replace_toml_version(relpath, newversion):
|
||||
@@ -34,8 +34,8 @@ def replace_toml_version(relpath, newversion):
|
||||
for line in open(str(p)):
|
||||
m = rex.match(line)
|
||||
if m is not None:
|
||||
print("{}: set version={}".format(relpath, newversion))
|
||||
f.write('version = "{}"\n'.format(newversion))
|
||||
print(f"{relpath}: set version={newversion}")
|
||||
f.write(f'version = "{newversion}"\n')
|
||||
else:
|
||||
f.write(line)
|
||||
os.rename(tmp_path, str(p))
|
||||
@@ -44,7 +44,7 @@ def replace_toml_version(relpath, newversion):
|
||||
def read_json_version(relpath):
|
||||
p = pathlib.Path(relpath)
|
||||
assert p.exists()
|
||||
with open(p, "r") as f:
|
||||
with open(p) as f:
|
||||
json_data = json.loads(f.read())
|
||||
return json_data["version"]
|
||||
|
||||
@@ -52,7 +52,7 @@ def read_json_version(relpath):
|
||||
def update_package_json(relpath, newversion):
|
||||
p = pathlib.Path(relpath)
|
||||
assert p.exists()
|
||||
with open(p, "r") as f:
|
||||
with open(p) as f:
|
||||
json_data = json.loads(f.read())
|
||||
json_data["version"] = newversion
|
||||
with open(p, "w") as f:
|
||||
@@ -63,7 +63,7 @@ def main():
|
||||
parser = ArgumentParser(prog="set_core_version")
|
||||
parser.add_argument("newversion")
|
||||
|
||||
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
|
||||
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
|
||||
toml_list = [
|
||||
"Cargo.toml",
|
||||
"deltachat-ffi/Cargo.toml",
|
||||
@@ -75,9 +75,9 @@ def main():
|
||||
except SystemExit:
|
||||
print()
|
||||
for x in toml_list:
|
||||
print("{}: {}".format(x, read_toml_version(x)))
|
||||
print(f"{x}: {read_toml_version(x)}")
|
||||
for x in json_list:
|
||||
print("{}: {}".format(x, read_json_version(x)))
|
||||
print(f"{x}: {read_json_version(x)}")
|
||||
print()
|
||||
raise SystemExit("need argument: new version, example: 1.25.0")
|
||||
|
||||
@@ -92,19 +92,19 @@ def main():
|
||||
if "alpha" not in newversion:
|
||||
for line in open("CHANGELOG.md"):
|
||||
## 1.25.0
|
||||
if line.startswith("## "):
|
||||
if line[2:].strip().startswith(newversion):
|
||||
break
|
||||
if line.startswith("## ") and line[2:].strip().startswith(newversion):
|
||||
break
|
||||
else:
|
||||
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
|
||||
raise SystemExit(
|
||||
f"CHANGELOG.md contains no entry for version: {newversion}"
|
||||
)
|
||||
|
||||
for toml_filename in toml_list:
|
||||
replace_toml_version(toml_filename, newversion)
|
||||
|
||||
|
||||
for json_filename in json_list:
|
||||
update_package_json(json_filename, newversion)
|
||||
|
||||
|
||||
print("running cargo check")
|
||||
subprocess.call(["cargo", "check"])
|
||||
|
||||
@@ -114,13 +114,12 @@ def main():
|
||||
|
||||
print("after commit, on master make sure to: ")
|
||||
print("")
|
||||
print(" git tag -a {}".format(newversion))
|
||||
print(" git push origin {}".format(newversion))
|
||||
print(" git tag -a py-{}".format(newversion))
|
||||
print(" git push origin py-{}".format(newversion))
|
||||
print(f" git tag -a {newversion}")
|
||||
print(f" git push origin {newversion}")
|
||||
print(f" git tag -a py-{newversion}")
|
||||
print(f" git push origin py-{newversion}")
|
||||
print("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
@@ -509,7 +509,6 @@ impl AccountConfig {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::stock_str::{self, StockMessage};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
//!
|
||||
//! Parse and create [Autocrypt-headers](https://autocrypt.org/en/latest/level1.html#the-autocrypt-header).
|
||||
|
||||
use anyhow::{bail, Context as _, Error, Result};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, Context as _, Error, Result};
|
||||
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
|
||||
/// Possible values for encryption preference
|
||||
|
||||
@@ -355,7 +355,6 @@ mod tests {
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::e2ee;
|
||||
use crate::message;
|
||||
|
||||
@@ -499,17 +499,15 @@ fn encoded_img_exceeds_bytes(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fs::File;
|
||||
|
||||
use anyhow::Result;
|
||||
use fs::File;
|
||||
use image::{GenericImageView, Pixel};
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{self, create_group_chat, ProtectionStatus};
|
||||
use crate::message::Message;
|
||||
use crate::test_utils::{self, TestContext};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
|
||||
tokio::task::block_in_place(move || {
|
||||
let img = image::open(path).expect("failed to open image");
|
||||
|
||||
175
src/chat.rs
175
src/chat.rs
@@ -531,20 +531,68 @@ impl ChatId {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Unarchives a chat that is archived and not muted.
|
||||
// Needed when a message is added to a chat so that the chat gets a normal visibility again.
|
||||
// Sending an appropriate event is up to the caller.
|
||||
pub async fn unarchive_if_not_muted(self, context: &Context) -> Result<()> {
|
||||
/// Unarchives a chat that is archived and not muted.
|
||||
/// Needed after a message is added to a chat so that the chat gets a normal visibility again.
|
||||
/// `msg_state` is the state of the message. Matters only for incoming messages currently. For
|
||||
/// multiple outgoing messages the function may be called once with MessageState::Undefined.
|
||||
/// Sending an appropriate event is up to the caller.
|
||||
/// Also emits DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK when the number of archived
|
||||
/// chats with unread messages increases (which is possible if the chat is muted).
|
||||
pub async fn unarchive_if_not_muted(
|
||||
self,
|
||||
context: &Context,
|
||||
msg_state: MessageState,
|
||||
) -> Result<()> {
|
||||
if msg_state != MessageState::InFresh {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET archived=0 WHERE id=? AND archived=1 \
|
||||
AND NOT(muted_until=-1 OR muted_until>?)",
|
||||
paramsv![self, time()],
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
if chat.visibility != ChatVisibility::Archived {
|
||||
return Ok(());
|
||||
}
|
||||
if chat.is_muted() {
|
||||
let unread_cnt = context
|
||||
.sql
|
||||
.count(
|
||||
"SELECT COUNT(*)
|
||||
FROM msgs
|
||||
WHERE state=?
|
||||
AND hidden=0
|
||||
AND chat_id=?",
|
||||
paramsv![MessageState::InFresh, self],
|
||||
)
|
||||
.await?;
|
||||
if unread_cnt == 1 {
|
||||
// Added the first unread message in the chat.
|
||||
context.emit_msgs_changed(DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET archived=0 WHERE id=? AND archived=1 AND NOT(muted_until=-1 OR muted_until>?)",
|
||||
paramsv![self, time()],
|
||||
)
|
||||
.execute("UPDATE chats SET archived=0 WHERE id=?", paramsv![self])
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Emits an appropriate event for a message. `important` is whether a notification should be
|
||||
/// shown.
|
||||
pub(crate) fn emit_msg_event(self, context: &Context, msg_id: MsgId, important: bool) {
|
||||
if important {
|
||||
context.emit_incoming_msg(self, msg_id);
|
||||
} else {
|
||||
context.emit_msgs_changed(self, msg_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a chat.
|
||||
pub async fn delete(self, context: &Context) -> Result<()> {
|
||||
ensure!(
|
||||
@@ -1315,6 +1363,8 @@ impl Chat {
|
||||
self.param.get_bool(Param::Unpromoted).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns whether the chat is promoted which means that a message has been
|
||||
/// send to it and it not only exists on the users device.
|
||||
pub fn is_promoted(&self) -> bool {
|
||||
!self.is_unpromoted()
|
||||
}
|
||||
@@ -2009,7 +2059,9 @@ async fn prepare_msg_common(
|
||||
msg.state = change_state_to;
|
||||
|
||||
prepare_msg_blob(context, msg).await?;
|
||||
chat_id.unarchive_if_not_muted(context).await?;
|
||||
if !msg.hidden {
|
||||
chat_id.unarchive_if_not_muted(context, msg.state).await?;
|
||||
}
|
||||
msg.id = chat
|
||||
.prepare_msg_raw(
|
||||
context,
|
||||
@@ -2805,6 +2857,7 @@ pub(crate) async fn remove_from_chat_contacts_table(
|
||||
}
|
||||
|
||||
/// Adds a contact to the chat.
|
||||
/// If the group is promoted, also sends out a system message to all group members
|
||||
pub async fn add_contact_to_chat(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
@@ -2826,7 +2879,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
|
||||
chat_id.reset_gossiped_timestamp(context).await?;
|
||||
|
||||
/*this also makes sure, not contacts are added to special or normal chats*/
|
||||
// this also makes sure, no contacts are added to special or normal chats
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
ensure!(
|
||||
chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast,
|
||||
@@ -2848,7 +2901,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
"Cannot add contact to group; self not in group.".into(),
|
||||
));
|
||||
bail!("can not add contact because our account is not part of it");
|
||||
bail!("can not add contact because the account is not part of the group/broadcast");
|
||||
}
|
||||
|
||||
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
|
||||
@@ -3201,7 +3254,9 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
let mut created_msgs: Vec<MsgId> = Vec::new();
|
||||
let mut curr_timestamp: i64;
|
||||
|
||||
chat_id.unarchive_if_not_muted(context).await?;
|
||||
chat_id
|
||||
.unarchive_if_not_muted(context, MessageState::Undefined)
|
||||
.await?;
|
||||
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
|
||||
if let Some(reason) = chat.why_cant_send(context).await? {
|
||||
bail!("cannot send to {}: {}", chat_id, reason);
|
||||
@@ -3404,7 +3459,6 @@ pub async fn add_device_msg_with_importance(
|
||||
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
|
||||
msg.try_calc_and_set_dimensions(context).await.ok();
|
||||
prepare_msg_blob(context, msg).await?;
|
||||
chat_id.unarchive_if_not_muted(context).await?;
|
||||
|
||||
let timestamp_sent = create_smeared_timestamp(context).await;
|
||||
|
||||
@@ -3424,6 +3478,7 @@ pub async fn add_device_msg_with_importance(
|
||||
}
|
||||
}
|
||||
|
||||
let state = MessageState::InFresh;
|
||||
let row_id = context
|
||||
.sql
|
||||
.insert(
|
||||
@@ -3447,7 +3502,7 @@ pub async fn add_device_msg_with_importance(
|
||||
timestamp_sent,
|
||||
timestamp_sent, // timestamp_sent equals timestamp_rcvd
|
||||
msg.viewtype,
|
||||
MessageState::InFresh,
|
||||
state,
|
||||
msg.text.as_ref().cloned().unwrap_or_default(),
|
||||
msg.param.to_string(),
|
||||
rfc724_mid,
|
||||
@@ -3456,6 +3511,9 @@ pub async fn add_device_msg_with_importance(
|
||||
.await?;
|
||||
|
||||
msg_id = MsgId::new(u32::try_from(row_id)?);
|
||||
if !msg.hidden {
|
||||
chat_id.unarchive_if_not_muted(context, state).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(label) = label {
|
||||
@@ -3469,11 +3527,7 @@ pub async fn add_device_msg_with_importance(
|
||||
}
|
||||
|
||||
if !msg_id.is_unset() {
|
||||
if important {
|
||||
context.emit_incoming_msg(chat_id, msg_id);
|
||||
} else {
|
||||
context.emit_msgs_changed(chat_id, msg_id);
|
||||
}
|
||||
chat_id.emit_msg_event(context, msg_id, important);
|
||||
}
|
||||
|
||||
Ok(msg_id)
|
||||
@@ -3624,13 +3678,11 @@ pub(crate) async fn update_msg_text_and_timestamp(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chatlist::{get_archived_cnt, Chatlist};
|
||||
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
|
||||
use crate::contact::{Contact, ContactAddress};
|
||||
use crate::receive_imf::receive_imf;
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_chat_info() {
|
||||
@@ -4025,6 +4077,45 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_member_list_on_rejoin() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
|
||||
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
|
||||
let claire_id = Contact::create(&alice, "", "claire@example.de").await?;
|
||||
|
||||
let alice_chat_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "foos").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
let add = alice.pop_sent_msg().await;
|
||||
let bob = tcm.bob().await;
|
||||
bob.recv_msg(&add).await;
|
||||
let bob_chat_id = bob.get_last_msg().await.chat_id;
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 3);
|
||||
|
||||
// remove bob from chat
|
||||
remove_contact_from_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
let remove_bob = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&remove_bob).await;
|
||||
|
||||
// remove any other member
|
||||
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
|
||||
// readd bob
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
let add2 = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&add2).await;
|
||||
|
||||
// number of members in chat should have updated
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_leave_group() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
@@ -4643,6 +4734,46 @@ mod tests {
|
||||
assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_pinned_after_new_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat_id = alice.create_chat(&bob).await.id;
|
||||
let bob_chat_id = bob.create_chat(&alice).await.id;
|
||||
|
||||
assert!(alice_chat_id
|
||||
.set_visibility(&alice, ChatVisibility::Pinned)
|
||||
.await
|
||||
.is_ok());
|
||||
assert_eq!(
|
||||
Chat::load_from_db(&alice, alice_chat_id)
|
||||
.await?
|
||||
.get_visibility(),
|
||||
ChatVisibility::Pinned,
|
||||
);
|
||||
|
||||
send_text_msg(&alice, alice_chat_id, "hi!".into()).await?;
|
||||
assert_eq!(
|
||||
Chat::load_from_db(&alice, alice_chat_id)
|
||||
.await?
|
||||
.get_visibility(),
|
||||
ChatVisibility::Pinned,
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some("hi!".into()));
|
||||
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, alice_chat_id);
|
||||
assert_eq!(
|
||||
Chat::load_from_db(&alice, alice_chat_id)
|
||||
.await?
|
||||
.get_visibility(),
|
||||
ChatVisibility::Pinned,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_chat_name() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
@@ -365,7 +365,6 @@ pub async fn get_archived_cnt(context: &Context) -> Result<usize> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
|
||||
use crate::message::Viewtype;
|
||||
use crate::receive_imf::receive_imf;
|
||||
|
||||
@@ -440,16 +440,15 @@ fn get_config_keys_string() -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use super::*;
|
||||
use crate::constants;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
#[test]
|
||||
fn test_to_string() {
|
||||
assert_eq!(Config::MailServer.to_string(), "mail_server");
|
||||
|
||||
@@ -6,9 +6,12 @@ mod read_url;
|
||||
mod server_params;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
use auto_outlook::outlk_autodiscover;
|
||||
use futures::FutureExt;
|
||||
use futures_lite::FutureExt as _;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use server_params::{expand_param_vector, ServerParams};
|
||||
use tokio::task;
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -28,10 +31,6 @@ use crate::stock_str;
|
||||
use crate::tools::{time, EmailAddress};
|
||||
use crate::{chat, e2ee, provider};
|
||||
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
use auto_outlook::outlk_autodiscover;
|
||||
use server_params::{expand_param_vector, ServerParams};
|
||||
|
||||
macro_rules! progress {
|
||||
($context:tt, $progress:expr, $comment:expr) => {
|
||||
assert!(
|
||||
@@ -565,13 +564,18 @@ async fn try_imap_one_param(
|
||||
provider_strict_tls: bool,
|
||||
) -> Result<Imap, ConfigurationError> {
|
||||
let inf = format!(
|
||||
"imap: {}@{}:{} security={} certificate_checks={} oauth2={}",
|
||||
"imap: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
|
||||
param.user,
|
||||
param.server,
|
||||
param.port,
|
||||
param.security,
|
||||
param.certificate_checks,
|
||||
param.oauth2
|
||||
param.oauth2,
|
||||
if let Some(socks5_config) = socks5_config {
|
||||
socks5_config.to_string()
|
||||
} else {
|
||||
"None".to_string()
|
||||
}
|
||||
);
|
||||
info!(context, "Trying: {}", inf);
|
||||
|
||||
@@ -661,6 +665,7 @@ async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationE
|
||||
|
||||
if errors.iter().all(|e| {
|
||||
e.msg.to_lowercase().contains("could not resolve")
|
||||
|| e.msg.to_lowercase().contains("no dns resolution results")
|
||||
|| e.msg
|
||||
.to_lowercase()
|
||||
.contains("temporary failure in name resolution")
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
//! # Thunderbird's Autoconfiguration implementation
|
||||
//!
|
||||
//! Documentation: <https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration>
|
||||
use quick_xml::events::{BytesStart, Event};
|
||||
|
||||
use std::io::BufRead;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
use quick_xml::events::{BytesStart, Event};
|
||||
|
||||
use super::read_url::read_url;
|
||||
use super::{Error, ServerParams};
|
||||
use crate::context::Context;
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Server {
|
||||
|
||||
@@ -3,15 +3,14 @@
|
||||
//! This module implements autoconfiguration via POX (Plain Old XML) interface to Autodiscover
|
||||
//! Service. Newer SOAP interface, introduced in Exchange 2010, is not used.
|
||||
|
||||
use quick_xml::events::Event;
|
||||
|
||||
use std::io::BufRead;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
use quick_xml::events::Event;
|
||||
|
||||
use super::read_url::read_url;
|
||||
use super::{Error, ServerParams};
|
||||
use crate::context::Context;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
/// Result of parsing a single `Protocol` tag.
|
||||
///
|
||||
|
||||
@@ -1612,6 +1612,9 @@ impl RecentlySeenLoop {
|
||||
context,
|
||||
"Error receiving an interruption in recently seen loop: {}", err
|
||||
);
|
||||
// Maybe the sender side is closed.
|
||||
// Terminate the loop to avoid looping indefinitely.
|
||||
return;
|
||||
}
|
||||
Ok(Ok(RecentlySeenInterrupt {
|
||||
contact_id,
|
||||
@@ -1653,7 +1656,6 @@ impl RecentlySeenLoop {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::receive_imf::receive_imf;
|
||||
@@ -2628,4 +2630,27 @@ Hi."#;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_verified_by_none() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await?;
|
||||
assert!(contact.get_verifier_addr(&alice).await?.is_none());
|
||||
assert!(contact.get_verifier_id(&alice).await?.is_none());
|
||||
|
||||
// Receive a message from Bob to create a peerstate.
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let sent_msg = bob.send_text(chat.id, "moin").await;
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await?;
|
||||
assert!(contact.get_verifier_addr(&alice).await?.is_none());
|
||||
assert!(contact.get_verifier_id(&alice).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -861,8 +861,13 @@ pub fn get_version_str() -> &'static str {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use strum::IntoEnumIterator;
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{
|
||||
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
|
||||
};
|
||||
@@ -873,10 +878,6 @@ mod tests {
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::tools::create_outgoing_rfc724_mid;
|
||||
use anyhow::Context as _;
|
||||
use std::time::Duration;
|
||||
use strum::IntoEnumIterator;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_wrong_db() -> Result<()> {
|
||||
|
||||
@@ -344,11 +344,10 @@ pub(crate) async fn get_autocrypt_peerstate(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_has_decrypted_pgp_armor() {
|
||||
let data = b" -----BEGIN PGP MESSAGE-----";
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//! # Download large messages manually.
|
||||
|
||||
use std::cmp::max;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
@@ -14,7 +16,6 @@ use crate::mimeparser::{MimeMessage, Part};
|
||||
use crate::param::Params;
|
||||
use crate::tools::time;
|
||||
use crate::{job_try, stock_str, EventType};
|
||||
use std::cmp::max;
|
||||
|
||||
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
|
||||
///
|
||||
@@ -264,14 +265,13 @@ impl MimeMessage {
|
||||
mod tests {
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{get_chat_msgs, send_msg};
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::message::Viewtype;
|
||||
use crate::receive_imf::receive_imf_inner;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_downloadstate_values() {
|
||||
// values may be written to disk and must not change
|
||||
|
||||
@@ -144,13 +144,12 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::Param;
|
||||
use crate::test_utils::{bob_keypair, TestContext};
|
||||
|
||||
use super::*;
|
||||
|
||||
mod ensure_secret_key_exists {
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::cmp::max;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::num::ParseIntError;
|
||||
use std::str::FromStr;
|
||||
@@ -86,7 +87,6 @@ use crate::mimeparser::SystemMessage;
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::stock_str;
|
||||
use crate::tools::{duration_to_str, time};
|
||||
use std::cmp::max;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
|
||||
pub enum Timer {
|
||||
|
||||
@@ -316,14 +316,8 @@ pub enum EventType {
|
||||
status_update_serial: StatusUpdateSerial,
|
||||
},
|
||||
|
||||
/// Informs that a message containing a webxdc instance has been deleted
|
||||
/// Inform that a message containing a webxdc instance has been deleted
|
||||
WebxdcInstanceDeleted {
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// Informs that the webxdc changed its update sending state
|
||||
WebxdcUpdateStateChanged {
|
||||
msg_id: MsgId,
|
||||
has_pending_updates: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
//! `MsgId.get_html()` will return HTML -
|
||||
//! this allows nice quoting, handling linebreaks properly etc.
|
||||
|
||||
use futures::future::FutureExt;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use futures::future::FutureExt;
|
||||
use lettre_email::mime::{self, Mime};
|
||||
use lettre_email::PartBuilder;
|
||||
use mailparse::ParsedContentType;
|
||||
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::message::{Message, MsgId};
|
||||
@@ -20,8 +22,6 @@ use crate::mimeparser::parse_message_id;
|
||||
use crate::param::Param::SendHtml;
|
||||
use crate::plaintext::PlainText;
|
||||
use crate::{context::Context, message};
|
||||
use lettre_email::PartBuilder;
|
||||
use mailparse::ParsedContentType;
|
||||
|
||||
impl Message {
|
||||
/// Check if the message can be retrieved as HTML.
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! # HTTP module.
|
||||
|
||||
use anyhow::Result;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
const HTTP_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
pub(crate) fn get_client() -> Result<reqwest::Client> {
|
||||
|
||||
17
src/imap.rs
17
src/imap.rs
@@ -308,6 +308,7 @@ impl Imap {
|
||||
if let Some(socks5_config) = &config.socks5_config {
|
||||
if config.lp.security == Socket::Starttls {
|
||||
Client::connect_starttls_socks5(
|
||||
context,
|
||||
imap_server,
|
||||
imap_port,
|
||||
socks5_config.clone(),
|
||||
@@ -315,13 +316,18 @@ impl Imap {
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Client::connect_insecure_socks5((imap_server, imap_port), socks5_config.clone())
|
||||
.await
|
||||
Client::connect_insecure_socks5(
|
||||
context,
|
||||
imap_server,
|
||||
imap_port,
|
||||
socks5_config.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
} else if config.lp.security == Socket::Starttls {
|
||||
Client::connect_starttls(imap_server, imap_port, config.strict_tls).await
|
||||
Client::connect_starttls(context, imap_server, imap_port, config.strict_tls).await
|
||||
} else {
|
||||
Client::connect_insecure((imap_server, imap_port)).await
|
||||
Client::connect_insecure(context, imap_server, imap_port).await
|
||||
}
|
||||
} else {
|
||||
let config = &self.config;
|
||||
@@ -330,6 +336,7 @@ impl Imap {
|
||||
|
||||
if let Some(socks5_config) = &config.socks5_config {
|
||||
Client::connect_secure_socks5(
|
||||
context,
|
||||
imap_server,
|
||||
imap_port,
|
||||
config.strict_tls,
|
||||
@@ -337,7 +344,7 @@ impl Imap {
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Client::connect_secure(imap_server, imap_port, config.strict_tls).await
|
||||
Client::connect_secure(context, imap_server, imap_port, config.strict_tls).await
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,21 +4,18 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
use async_imap::Client as ImapClient;
|
||||
use async_imap::Session as ImapSession;
|
||||
|
||||
use tokio::io::BufWriter;
|
||||
use tokio::net::ToSocketAddrs;
|
||||
|
||||
use super::capabilities::Capabilities;
|
||||
use super::session::Session;
|
||||
use super::session::SessionStream;
|
||||
use crate::context::Context;
|
||||
use crate::login_param::build_tls;
|
||||
use crate::net::connect_tcp;
|
||||
use crate::socks::Socks5Config;
|
||||
|
||||
use super::session::SessionStream;
|
||||
|
||||
/// IMAP write and read timeout in seconds.
|
||||
pub(crate) const IMAP_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
@@ -91,8 +88,13 @@ impl Client {
|
||||
Ok(Session::new(session, capabilities))
|
||||
}
|
||||
|
||||
pub async fn connect_secure(hostname: &str, port: u16, strict_tls: bool) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp((hostname, port), IMAP_TIMEOUT).await?;
|
||||
pub async fn connect_secure(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
strict_tls: bool,
|
||||
) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, strict_tls).await?;
|
||||
let tls = build_tls(strict_tls);
|
||||
let tls_stream = tls.connect(hostname, tcp_stream).await?;
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
@@ -107,8 +109,8 @@ impl Client {
|
||||
Ok(Client { inner: client })
|
||||
}
|
||||
|
||||
pub async fn connect_insecure(addr: impl ToSocketAddrs) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp(addr, IMAP_TIMEOUT).await?;
|
||||
pub async fn connect_insecure(context: &Context, hostname: &str, port: u16) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, false).await?;
|
||||
let buffered_stream = BufWriter::new(tcp_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = ImapClient::new(session_stream);
|
||||
@@ -120,12 +122,16 @@ impl Client {
|
||||
Ok(Client { inner: client })
|
||||
}
|
||||
|
||||
pub async fn connect_starttls(hostname: &str, port: u16, strict_tls: bool) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp((hostname, port), IMAP_TIMEOUT).await?;
|
||||
pub async fn connect_starttls(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
strict_tls: bool,
|
||||
) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, strict_tls).await?;
|
||||
|
||||
// Run STARTTLS command and convert the client back into a stream.
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(tcp_stream);
|
||||
let mut client = ImapClient::new(session_stream);
|
||||
let mut client = ImapClient::new(tcp_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
@@ -150,12 +156,15 @@ impl Client {
|
||||
}
|
||||
|
||||
pub async fn connect_secure_socks5(
|
||||
context: &Context,
|
||||
domain: &str,
|
||||
port: u16,
|
||||
strict_tls: bool,
|
||||
socks5_config: Socks5Config,
|
||||
) -> Result<Self> {
|
||||
let socks5_stream = socks5_config.connect((domain, port), IMAP_TIMEOUT).await?;
|
||||
let socks5_stream = socks5_config
|
||||
.connect(context, domain, port, IMAP_TIMEOUT, strict_tls)
|
||||
.await?;
|
||||
let tls = build_tls(strict_tls);
|
||||
let tls_stream = tls.connect(domain, socks5_stream).await?;
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
@@ -170,10 +179,14 @@ impl Client {
|
||||
}
|
||||
|
||||
pub async fn connect_insecure_socks5(
|
||||
target_addr: impl ToSocketAddrs,
|
||||
context: &Context,
|
||||
domain: &str,
|
||||
port: u16,
|
||||
socks5_config: Socks5Config,
|
||||
) -> Result<Self> {
|
||||
let socks5_stream = socks5_config.connect(target_addr, IMAP_TIMEOUT).await?;
|
||||
let socks5_stream = socks5_config
|
||||
.connect(context, domain, port, IMAP_TIMEOUT, false)
|
||||
.await?;
|
||||
let buffered_stream = BufWriter::new(socks5_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = ImapClient::new(session_stream);
|
||||
@@ -186,18 +199,18 @@ impl Client {
|
||||
}
|
||||
|
||||
pub async fn connect_starttls_socks5(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
socks5_config: Socks5Config,
|
||||
strict_tls: bool,
|
||||
) -> Result<Self> {
|
||||
let socks5_stream = socks5_config
|
||||
.connect((hostname, port), IMAP_TIMEOUT)
|
||||
.connect(context, hostname, port, IMAP_TIMEOUT, strict_tls)
|
||||
.await?;
|
||||
|
||||
// Run STARTTLS command and convert the client back into a stream.
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(socks5_stream);
|
||||
let mut client = ImapClient::new(session_stream);
|
||||
let mut client = ImapClient::new(socks5_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use super::Imap;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use async_channel::Receiver;
|
||||
use async_imap::extensions::idle::IdleResponse;
|
||||
use futures_lite::FutureExt;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use super::session::Session;
|
||||
use super::Imap;
|
||||
use crate::imap::client::IMAP_TIMEOUT;
|
||||
use crate::{context::Context, scheduler::InterruptInfo};
|
||||
|
||||
|
||||
@@ -3,13 +3,12 @@ use std::{collections::BTreeMap, time::Instant};
|
||||
use anyhow::{Context as _, Result};
|
||||
use futures::stream::StreamExt;
|
||||
|
||||
use super::{get_folder_meaning, get_folder_meaning_by_name};
|
||||
use crate::config::Config;
|
||||
use crate::imap::Imap;
|
||||
use crate::log::LogExt;
|
||||
use crate::{context::Context, imap::FolderMeaning};
|
||||
|
||||
use super::{get_folder_meaning, get_folder_meaning_by_name};
|
||||
|
||||
impl Imap {
|
||||
/// Returns true if folders were scanned, false if scanning was postponed.
|
||||
pub(crate) async fn scan_folders(&mut self, context: &Context) -> Result<bool> {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::session::Session as ImapSession;
|
||||
|
||||
use crate::context::Context;
|
||||
use anyhow::Context as _;
|
||||
|
||||
use super::session::Session as ImapSession;
|
||||
use crate::context::Context;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
||||
@@ -769,14 +769,13 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ::pgp::armor::BlockType;
|
||||
|
||||
use super::*;
|
||||
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
|
||||
use crate::stock_str::StockMessage;
|
||||
use crate::test_utils::{alice_keypair, TestContext};
|
||||
|
||||
use ::pgp::armor::BlockType;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_render_setup_file() {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
@@ -424,7 +424,6 @@ LIMIT 1;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
async fn insert_job(context: &Context, foreign_id: i64, valid: bool) {
|
||||
|
||||
12
src/key.rs
12
src/key.rs
@@ -11,6 +11,7 @@ use anyhow::{ensure, Context as _, Result};
|
||||
use futures::Future;
|
||||
use num_traits::FromPrimitive;
|
||||
use pgp::composed::Deserializable;
|
||||
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
|
||||
use pgp::ser::Serialize;
|
||||
use pgp::types::{KeyTrait, SecretKeyTrait};
|
||||
use tokio::runtime::Handle;
|
||||
@@ -18,11 +19,9 @@ use tokio::runtime::Handle;
|
||||
use crate::config::Config;
|
||||
use crate::constants::KeyGenType;
|
||||
use crate::context::Context;
|
||||
use crate::tools::{time, EmailAddress};
|
||||
|
||||
// Re-export key types
|
||||
pub use crate::pgp::KeyPair;
|
||||
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
|
||||
use crate::tools::{time, EmailAddress};
|
||||
|
||||
/// Convenience trait for working with keys.
|
||||
///
|
||||
@@ -390,11 +389,12 @@ impl std::str::FromStr for Fingerprint {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::{alice_keypair, TestContext};
|
||||
use std::sync::Arc;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::{alice_keypair, TestContext};
|
||||
|
||||
static KEYPAIR: Lazy<KeyPair> = Lazy::new(alice_keypair);
|
||||
|
||||
|
||||
@@ -155,9 +155,10 @@ impl<T, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test_utils::TestContext;
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_last_error() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
@@ -332,7 +332,6 @@ pub fn build_tls(strict_tls: bool) -> async_native_tls::TlsConnector {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1982,14 +1982,13 @@ impl Viewtype {
|
||||
mod tests {
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{marknoticed_chat, ChatItem};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils as test;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_guess_msgtype_from_suffix() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -1508,6 +1508,7 @@ fn maybe_encode_words(words: &str) -> String {
|
||||
mod tests {
|
||||
use mailparse::{addrparse_header, MailHeaderMap};
|
||||
|
||||
use super::*;
|
||||
use crate::chat::ChatId;
|
||||
use crate::chat::{
|
||||
self, add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg,
|
||||
@@ -1518,8 +1519,6 @@ mod tests {
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{get_chat_msg, TestContext};
|
||||
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_render_email_address() {
|
||||
let display_name = "ä space";
|
||||
|
||||
@@ -57,6 +57,8 @@ pub struct MimeMessage {
|
||||
/// Whether the From address was repeated in the signed part
|
||||
/// (and we know that the signer intended to send from this address)
|
||||
pub from_is_signed: bool,
|
||||
/// The List-Post address is only set for mailing lists. Users can send
|
||||
/// messages to this address to post them to the list.
|
||||
pub list_post: Option<String>,
|
||||
pub chat_disposition_notification_to: Option<SingleInfo>,
|
||||
pub decryption_info: DecryptionInfo,
|
||||
@@ -721,6 +723,8 @@ impl MimeMessage {
|
||||
!self.signatures.is_empty()
|
||||
}
|
||||
|
||||
/// Returns whether the email contains a `chat-version` header.
|
||||
/// This indicates that the email is a DC-email.
|
||||
pub(crate) fn has_chat_version(&self) -> bool {
|
||||
self.header.contains_key("chat-version")
|
||||
}
|
||||
@@ -1953,6 +1957,8 @@ where
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use mailparse::ParsedMail;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
chatlist::Chatlist,
|
||||
@@ -1962,7 +1968,6 @@ mod tests {
|
||||
receive_imf::receive_imf,
|
||||
test_utils::TestContext,
|
||||
};
|
||||
use mailparse::ParsedMail;
|
||||
|
||||
impl AvatarAction {
|
||||
pub fn is_change(&self) -> bool {
|
||||
|
||||
169
src/net.rs
169
src/net.rs
@@ -1,25 +1,180 @@
|
||||
///! # Common network utilities.
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use tokio::net::{TcpStream, ToSocketAddrs};
|
||||
use anyhow::{Context as _, Error, Result};
|
||||
use tokio::net::{lookup_host, TcpStream};
|
||||
use tokio::time::timeout;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::tools::time;
|
||||
|
||||
async fn connect_tcp_inner(addr: SocketAddr, timeout_val: Duration) -> Result<TcpStream> {
|
||||
let tcp_stream = timeout(timeout_val, TcpStream::connect(addr))
|
||||
.await
|
||||
.context("connection timeout")?
|
||||
.context("connection failure")?;
|
||||
Ok(tcp_stream)
|
||||
}
|
||||
|
||||
async fn lookup_host_with_timeout(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
timeout_val: Duration,
|
||||
) -> Result<Vec<SocketAddr>> {
|
||||
let res = timeout(timeout_val, lookup_host((hostname, port)))
|
||||
.await
|
||||
.context("DNS lookup timeout")?
|
||||
.context("DNS lookup failure")?;
|
||||
Ok(res.collect())
|
||||
}
|
||||
|
||||
/// Looks up hostname and port using DNS and updates the address resolution cache.
|
||||
///
|
||||
/// If `load_cache` is true, appends cached results not older than 30 days to the end.
|
||||
async fn lookup_host_with_cache(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
timeout_val: Duration,
|
||||
load_cache: bool,
|
||||
) -> Result<Vec<SocketAddr>> {
|
||||
let now = time();
|
||||
let mut resolved_addrs = match lookup_host_with_timeout(hostname, port, timeout_val).await {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"DNS resolution for {}:{} failed: {:#}.", hostname, port, err
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
for addr in resolved_addrs.iter() {
|
||||
let ip_string = addr.ip().to_string();
|
||||
if ip_string == hostname {
|
||||
// IP address resolved into itself, not interesting to cache.
|
||||
continue;
|
||||
}
|
||||
|
||||
info!(context, "Resolved {}:{} into {}.", hostname, port, &addr);
|
||||
|
||||
// Update the cache.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO dns_cache
|
||||
(hostname, address, timestamp)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT (hostname, address)
|
||||
DO UPDATE SET timestamp=excluded.timestamp",
|
||||
paramsv![hostname, ip_string, now],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if load_cache {
|
||||
for cached_address in context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT address
|
||||
FROM dns_cache
|
||||
WHERE hostname = ?
|
||||
AND ? < timestamp + 30 * 24 * 3600
|
||||
ORDER BY timestamp DESC",
|
||||
paramsv![hostname, now],
|
||||
|row| {
|
||||
let address: String = row.get(0)?;
|
||||
Ok(address)
|
||||
},
|
||||
|rows| {
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
match IpAddr::from_str(&cached_address) {
|
||||
Ok(ip_addr) => {
|
||||
let addr = SocketAddr::new(ip_addr, port);
|
||||
if !resolved_addrs.contains(&addr) {
|
||||
resolved_addrs.push(addr);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to parse cached address {:?}: {:#}.", cached_address, err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resolved_addrs)
|
||||
}
|
||||
|
||||
/// Returns a TCP connection stream with read/write timeouts set
|
||||
/// and Nagle's algorithm disabled with `TCP_NODELAY`.
|
||||
///
|
||||
/// `TCP_NODELAY` ensures writing to the stream always results in immediate sending of the packet
|
||||
/// to the network, which is important to reduce the latency of interactive protocols such as IMAP.
|
||||
///
|
||||
/// If `load_cache` is true, may use cached DNS results.
|
||||
/// Because the cache may be poisoned with incorrect results by networks hijacking DNS requests,
|
||||
/// this option should only be used when connection is authenticated,
|
||||
/// for example using TLS.
|
||||
/// If TLS is not used or invalid TLS certificates are allowed,
|
||||
/// this option should be disabled.
|
||||
pub(crate) async fn connect_tcp(
|
||||
addr: impl ToSocketAddrs,
|
||||
context: &Context,
|
||||
host: &str,
|
||||
port: u16,
|
||||
timeout_val: Duration,
|
||||
load_cache: bool,
|
||||
) -> Result<Pin<Box<TimeoutStream<TcpStream>>>> {
|
||||
let tcp_stream = timeout(timeout_val, TcpStream::connect(addr))
|
||||
.await
|
||||
.context("connection timeout")?
|
||||
.context("connection failure")?;
|
||||
let mut tcp_stream = None;
|
||||
let mut last_error = None;
|
||||
|
||||
for resolved_addr in
|
||||
lookup_host_with_cache(context, host, port, timeout_val, load_cache).await?
|
||||
{
|
||||
match connect_tcp_inner(resolved_addr, timeout_val).await {
|
||||
Ok(stream) => {
|
||||
tcp_stream = Some(stream);
|
||||
|
||||
// Maximize priority of this cached entry.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE dns_cache
|
||||
SET timestamp = ?
|
||||
WHERE address = ?",
|
||||
paramsv![time(), resolved_addr.ip().to_string()],
|
||||
)
|
||||
.await?;
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to connect to {}: {:#}.", resolved_addr, err
|
||||
);
|
||||
last_error = Some(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tcp_stream = match tcp_stream {
|
||||
Some(tcp_stream) => tcp_stream,
|
||||
None => {
|
||||
return Err(last_error.unwrap_or_else(|| Error::msg("no DNS resolution results")));
|
||||
}
|
||||
};
|
||||
|
||||
// Disable Nagle's algorithm.
|
||||
tcp_stream.set_nodelay(true)?;
|
||||
|
||||
@@ -352,7 +352,6 @@ fn normalize_addr(addr: &str) -> &str {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -435,14 +435,13 @@ impl<'a> ParamsFile<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::fs;
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{Context as _, Error, Result};
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::chat::{self, Chat};
|
||||
@@ -17,8 +19,6 @@ use crate::message::Message;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::sql::Sql;
|
||||
use crate::stock_str;
|
||||
use anyhow::{Context as _, Error, Result};
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PeerstateKeyType {
|
||||
@@ -35,6 +35,7 @@ pub enum PeerstateVerifiedStatus {
|
||||
}
|
||||
|
||||
/// Peerstate represents the state of an Autocrypt peer.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Peerstate {
|
||||
pub addr: String,
|
||||
pub last_seen: i64,
|
||||
@@ -52,44 +53,6 @@ pub struct Peerstate {
|
||||
pub verifier: Option<String>,
|
||||
}
|
||||
|
||||
impl PartialEq for Peerstate {
|
||||
fn eq(&self, other: &Peerstate) -> bool {
|
||||
self.addr == other.addr
|
||||
&& self.last_seen == other.last_seen
|
||||
&& self.last_seen_autocrypt == other.last_seen_autocrypt
|
||||
&& self.prefer_encrypt == other.prefer_encrypt
|
||||
&& self.public_key == other.public_key
|
||||
&& self.public_key_fingerprint == other.public_key_fingerprint
|
||||
&& self.gossip_key == other.gossip_key
|
||||
&& self.gossip_timestamp == other.gossip_timestamp
|
||||
&& self.gossip_key_fingerprint == other.gossip_key_fingerprint
|
||||
&& self.verified_key == other.verified_key
|
||||
&& self.verified_key_fingerprint == other.verified_key_fingerprint
|
||||
&& self.fingerprint_changed == other.fingerprint_changed
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Peerstate {}
|
||||
|
||||
impl fmt::Debug for Peerstate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_struct("Peerstate")
|
||||
.field("addr", &self.addr)
|
||||
.field("last_seen", &self.last_seen)
|
||||
.field("last_seen_autocrypt", &self.last_seen_autocrypt)
|
||||
.field("prefer_encrypt", &self.prefer_encrypt)
|
||||
.field("public_key", &self.public_key)
|
||||
.field("public_key_fingerprint", &self.public_key_fingerprint)
|
||||
.field("gossip_key", &self.gossip_key)
|
||||
.field("gossip_timestamp", &self.gossip_timestamp)
|
||||
.field("gossip_key_fingerprint", &self.gossip_key_fingerprint)
|
||||
.field("verified_key", &self.verified_key)
|
||||
.field("verified_key_fingerprint", &self.verified_key_fingerprint)
|
||||
.field("fingerprint_changed", &self.fingerprint_changed)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Peerstate {
|
||||
pub fn from_header(header: &Aheader, message_time: i64) -> Self {
|
||||
Peerstate {
|
||||
@@ -223,7 +186,10 @@ impl Peerstate {
|
||||
.transpose()
|
||||
.unwrap_or_default(),
|
||||
fingerprint_changed: false,
|
||||
verifier: row.get("verifier")?,
|
||||
verifier: {
|
||||
let verifier: Option<String> = row.get("verifier")?;
|
||||
verifier.filter(|verifier| !verifier.is_empty())
|
||||
},
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
@@ -455,7 +421,7 @@ impl Peerstate {
|
||||
self.verified_key.as_ref().map(|k| k.to_bytes()),
|
||||
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.addr,
|
||||
self.verifier,
|
||||
self.verifier.as_deref().unwrap_or(""),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -382,11 +382,12 @@ pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::{alice_keypair, bob_keypair};
|
||||
use once_cell::sync::Lazy;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::{alice_keypair, bob_keypair};
|
||||
|
||||
#[test]
|
||||
fn test_split_armored_data_1() {
|
||||
let (typ, _headers, base64) = split_armored_data(
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use crate::simplify::split_lines;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::simplify::split_lines;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PlainText {
|
||||
pub text: String,
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
|
||||
mod data;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS, PROVIDER_UPDATED};
|
||||
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};
|
||||
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum Status {
|
||||
@@ -195,10 +196,11 @@ pub fn get_provider_update_timestamp() -> i64 {
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::tools::time;
|
||||
use chrono::NaiveDate;
|
||||
|
||||
#[test]
|
||||
fn test_get_provider_by_domain_unexistant() {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// file generated by src/provider/update.py
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::provider::Protocol::*;
|
||||
use crate::provider::Socket::*;
|
||||
use crate::provider::UsernamePattern::*;
|
||||
use crate::provider::{Config, ConfigDefault, Oauth2Authorizer, Provider, Server, Status};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
// 163.md: 163.com
|
||||
static P_163: Lazy<Provider> = Lazy::new(|| Provider {
|
||||
|
||||
11
src/qr.rs
11
src/qr.rs
@@ -3,14 +3,15 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
mod dclogin_scheme;
|
||||
pub use dclogin_scheme::LoginOptions;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context as _, Error, Result};
|
||||
pub use dclogin_scheme::LoginOptions;
|
||||
use once_cell::sync::Lazy;
|
||||
use percent_encoding::percent_decode_str;
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use self::dclogin_scheme::configure_from_login_qr;
|
||||
use crate::chat::{self, get_chat_id_by_grpid, ChatIdBlocked};
|
||||
use crate::config::Config;
|
||||
use crate::constants::Blocked;
|
||||
@@ -24,8 +25,6 @@ use crate::peerstate::Peerstate;
|
||||
use crate::tools::time;
|
||||
use crate::{token, EventType};
|
||||
|
||||
use self::dclogin_scheme::configure_from_login_qr;
|
||||
|
||||
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
|
||||
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
|
||||
pub(super) const DCLOGIN_SCHEME: &str = "DCLOGIN:";
|
||||
@@ -642,14 +641,14 @@ fn normalize_address(addr: &str) -> Result<String, Error> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::Result;
|
||||
|
||||
use super::*;
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{create_group_chat, ProtectionStatus};
|
||||
use crate::key::DcKey;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{alice_keypair, TestContext};
|
||||
use anyhow::Result;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_http() -> Result<()> {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user