diff --git a/CHANGELOG.md b/CHANGELOG.md index b8f54fe29..6d76ac2a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,19 @@ ### Changes - deltachat-rpc-client: use `dataclass` for `Account`, `Chat`, `Contact` and `Message` #4042 +- python: mark bindings as supporting typing according to PEP 561 #4045 +- retry filesystem operations during account migration #4043 - ability to send backup over network and QR code to setup second device #4007 ### Fixes - deltachat-rpc-server: do not block stdin while processing the request. #4041 deltachat-rpc-server now reads the next request as soon as previous request handler is spawned. +- enable `auto_vacuum` on all SQL connections #2955 ### API-Changes + - Remove `MimeMessage::from_bytes()` public interface. #4033 +- BREAKING Types: jsonrpc: `get_messages` now returns a map with `MessageLoadResult` instead of failing completely if one of the requested messages could not be loaded. #4038 ## 1.108.0 diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 987e0d3e4..2a28020b1 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -5777,7 +5777,7 @@ void dc_event_unref(dc_event_t* event); #define DC_EVENT_INCOMING_MSG 2005 /** - * Downloading a bunch of messages just finished. This is an experimental + * Downloading a bunch of messages just finished. This is an * event to allow the UI to only show one notification per message bunch, * instead of cluttering the user with many notifications. * For each of the msg_ids, an additional #DC_EVENT_INCOMING_MSG event was emitted before. diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index e0baf9a3b..047c285cc 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -45,6 +45,7 @@ use types::message::MessageObject; use types::provider_info::ProviderInfo; use types::webxdc::WebxdcMessageInfo; +use self::types::message::MessageLoadResult; use self::types::{ chat::{BasicChat, JSONRPCChatVisibility, MuteDuration}, location::JsonrpcLocation, @@ -465,7 +466,7 @@ impl CommandApi { Ok(res) => res, Err(err) => ChatListItemFetchResult::Error { id: entry.0, - error: format!("{err:?}"), + error: format!("{err:#}"), }, }, ); @@ -945,17 +946,27 @@ impl CommandApi { MsgId::new(message_id).get_html(&ctx).await } + /// get multiple messages in one call, + /// if loading one message fails the error is stored in the result object in it's place. + /// + /// this is the batch variant of [get_message] async fn get_messages( &self, account_id: u32, message_ids: Vec, - ) -> Result> { + ) -> Result> { let ctx = self.get_context(account_id).await?; - let mut messages: HashMap = HashMap::new(); + let mut messages: HashMap = HashMap::new(); for message_id in message_ids { + let message_result = MessageObject::from_message_id(&ctx, message_id).await; messages.insert( message_id, - MessageObject::from_message_id(&ctx, message_id).await?, + match message_result { + Ok(message) => MessageLoadResult::Message(message), + Err(error) => MessageLoadResult::LoadingError { + error: format!("{error:#}"), + }, + }, ); } Ok(messages) diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index 9030467f6..04a850715 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -19,6 +19,13 @@ use super::contact::ContactObject; use super::reactions::JSONRPCReactions; use super::webxdc::WebxdcMessageInfo; +#[derive(Serialize, TypeDef)] +#[serde(rename_all = "camelCase", tag = "variant")] +pub enum MessageLoadResult { + Message(MessageObject), + LoadingError { error: String }, +} + #[derive(Serialize, TypeDef)] #[serde(rename = "Message", rename_all = "camelCase")] pub struct MessageObject { diff --git a/deltachat-jsonrpc/typescript/example/example.ts b/deltachat-jsonrpc/typescript/example/example.ts index c180a9e31..0caa2de06 100644 --- a/deltachat-jsonrpc/typescript/example/example.ts +++ b/deltachat-jsonrpc/typescript/example/example.ts @@ -81,7 +81,9 @@ async function run() { messageIds ); for (const [_messageId, message] of Object.entries(messages)) { - write($main, `

${message.text}

`); + if (message.variant === "message") + write($main, `

${message.text}

`); + else write($main, `

loading error: ${message.error}

`); } } } diff --git a/deltachat-rpc-client/pyproject.toml b/deltachat-rpc-client/pyproject.toml index 7797ac903..ef76fa82b 100644 --- a/deltachat-rpc-client/pyproject.toml +++ b/deltachat-rpc-client/pyproject.toml @@ -13,13 +13,6 @@ dynamic = [ "version" ] -[tool.setuptools] -# We declare the package not-zip-safe so that our type hints are also available -# when checking client code that uses our (installed) package. -# Ref: -# https://mypy.readthedocs.io/en/stable/installed_packages.html?highlight=zip#using-installed-packages-with-mypy-pep-561 -zip-safe = false - [tool.setuptools.package-data] deltachat_rpc_client = [ "py.typed" diff --git a/python/pyproject.toml b/python/pyproject.toml index 569e849db..4aa6666db 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -36,6 +36,11 @@ dynamic = [ [project.entry-points.pytest11] "deltachat.testplugin" = "deltachat.testplugin" +[tool.setuptools.package-data] +deltachat = [ + "py.typed" +] + [tool.setuptools_scm] root = ".." tag_regex = '^(?Ppy-)?(?P[^\+]+)(?P.*)?$' @@ -45,7 +50,7 @@ git_describe_command = "git describe --dirty --tags --long --match py-*.*" line-length = 120 [tool.ruff] -select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM", "UP032"] +select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM", "UP004", "UP010", "UP031", "UP032"] line-length = 120 [tool.isort] diff --git a/python/src/deltachat/account.py b/python/src/deltachat/account.py index e9ec5e311..61d74d793 100644 --- a/python/src/deltachat/account.py +++ b/python/src/deltachat/account.py @@ -1,6 +1,5 @@ """Account class implementation.""" -from __future__ import print_function import os from array import array @@ -53,7 +52,7 @@ def get_dc_info_as_dict(dc_context): return info_dict -class Account(object): +class Account: """Each account is tied to a sqlite database file which is fully managed by the underlying deltachat core library. All public Account methods are meant to be memory-safe and return memory-safe objects. @@ -302,7 +301,7 @@ class Account(object): elif isinstance(obj, str): displayname, addr = parseaddr(obj) else: - raise TypeError("don't know how to create chat for %r" % (obj,)) + raise TypeError(f"don't know how to create chat for {obj!r}") if name is None and displayname: name = displayname diff --git a/python/src/deltachat/chat.py b/python/src/deltachat/chat.py index 1548829ae..77fac073d 100644 --- a/python/src/deltachat/chat.py +++ b/python/src/deltachat/chat.py @@ -18,7 +18,7 @@ from .cutil import ( from .message import Message -class Chat(object): +class Chat: """Chat object which manages members and through which you can send and retrieve messages. You obtain instances of it through :class:`deltachat.account.Account`. diff --git a/python/src/deltachat/contact.py b/python/src/deltachat/contact.py index fc38cdd73..f317eb315 100644 --- a/python/src/deltachat/contact.py +++ b/python/src/deltachat/contact.py @@ -9,7 +9,7 @@ from .chat import Chat from .cutil import from_dc_charpointer, from_optional_dc_charpointer -class Contact(object): +class Contact: """Delta-Chat Contact. You obtain instances of it through :class:`deltachat.account.Account`. diff --git a/python/src/deltachat/message.py b/python/src/deltachat/message.py index d84713bf8..f0a1cd08b 100644 --- a/python/src/deltachat/message.py +++ b/python/src/deltachat/message.py @@ -12,7 +12,7 @@ from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_char from .reactions import Reactions -class Message(object): +class Message: """Message object. You obtain instances of it through :class:`deltachat.account.Account` or diff --git a/python/src/deltachat/provider.py b/python/src/deltachat/provider.py index d760dbabb..29313a0fb 100644 --- a/python/src/deltachat/provider.py +++ b/python/src/deltachat/provider.py @@ -8,7 +8,7 @@ class ProviderNotFoundError(Exception): """The provider information was not found.""" -class Provider(object): +class Provider: """ Provider information. diff --git a/python/src/deltachat/py.typed b/python/src/deltachat/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/python/src/deltachat/reactions.py b/python/src/deltachat/reactions.py index 968bcb0d5..1ab2744d0 100644 --- a/python/src/deltachat/reactions.py +++ b/python/src/deltachat/reactions.py @@ -4,7 +4,7 @@ from .capi import ffi, lib from .cutil import from_dc_charpointer, iter_array -class Reactions(object): +class Reactions: """Reactions object. You obtain instances of it through :class:`deltachat.message.Message`. diff --git a/python/src/deltachat/testplugin.py b/python/src/deltachat/testplugin.py index fe1d85436..d9d3e4ebb 100644 --- a/python/src/deltachat/testplugin.py +++ b/python/src/deltachat/testplugin.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import fnmatch import io import os diff --git a/python/tests/stress_test_db.py b/python/tests/stress_test_db.py index f46bcdf65..a8b942a09 100644 --- a/python/tests/stress_test_db.py +++ b/python/tests/stress_test_db.py @@ -14,7 +14,7 @@ def test_db_busy_error(acfactory, tmpdir): def log(string): with log_lock: - print("%3.2f %s" % (time.time() - starttime, string)) + print(f"{time.time() - starttime:3.2f} {string}") # make a number of accounts accounts = acfactory.get_many_online_accounts(3) diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py index 1141fe4c7..e3decc7d6 100644 --- a/python/tests/test_0_complex_or_slow.py +++ b/python/tests/test_0_complex_or_slow.py @@ -239,7 +239,7 @@ def test_fetch_existing(acfactory, lp, mvbox_move): ac1_clone.start_io() assert_folders_configured(ac1_clone) - lp.sec("check that ac2 contact was fetchted during configure") + lp.sec("check that ac2 contact was fetched during configure") ac1_clone._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED") ac2_addr = ac2.get_config("addr") assert any(c.addr == ac2_addr for c in ac1_clone.get_contacts()) diff --git a/python/tests/test_2_increation.py b/python/tests/test_2_increation.py index 33c264ef1..c7e35119a 100644 --- a/python/tests/test_2_increation.py +++ b/python/tests/test_2_increation.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import os.path import shutil from filecmp import cmp diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index 8a01eb2a2..e7a1a6231 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import os import time from datetime import datetime, timedelta, timezone diff --git a/scripts/run_all.sh b/scripts/run_all.sh index 2a81a2c09..7b08f0c9a 100755 --- a/scripts/run_all.sh +++ b/scripts/run_all.sh @@ -31,7 +31,7 @@ unset DCC_NEW_TMP_EMAIL # Try to build wheels for a range of interpreters, but don't fail if they are not available. # E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10 -tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,pypy37,pypy38,pypy39,auditwheels --skip-missing-interpreters true +tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39,auditwheels --skip-missing-interpreters true echo ----------------------- diff --git a/src/accounts.rs b/src/accounts.rs index 5ea6965e0..e01cc49c2 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -1,6 +1,7 @@ //! # Account manager module. use std::collections::BTreeMap; +use std::future::Future; use std::path::{Path, PathBuf}; use anyhow::{ensure, Context as _, Result}; @@ -150,27 +151,9 @@ impl Accounts { if let Some(cfg) = self.config.get_account(id) { let account_path = self.dir.join(cfg.dir); - // Spend up to 1 minute trying to remove the files. - // Files may remain locked up to 30 seconds due to r2d2 bug: - // https://github.com/sfackler/r2d2/issues/99 - let mut counter = 0; - loop { - counter += 1; - - if let Err(err) = fs::remove_dir_all(&account_path) - .await - .context("failed to remove account data") - { - if counter > 60 { - return Err(err); - } - - // Wait 1 second and try again. - tokio::time::sleep(std::time::Duration::from_millis(1000)).await; - } else { - break; - } - } + try_many_times(|| fs::remove_dir_all(&account_path)) + .await + .context("failed to remove account data")?; } self.config.remove_account(id).await?; @@ -178,6 +161,8 @@ impl Accounts { } /// Migrate an existing account into this structure. + /// + /// Returns the ID of new account. pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result { let blobdir = Context::derive_blobdir(&dbfile); let walfile = Context::derive_walfile(&dbfile); @@ -202,10 +187,10 @@ impl Accounts { fs::create_dir_all(self.dir.join(&account_config.dir)) .await .context("failed to create dir")?; - fs::rename(&dbfile, &new_dbfile) + try_many_times(|| fs::rename(&dbfile, &new_dbfile)) .await .context("failed to rename dbfile")?; - fs::rename(&blobdir, &new_blobdir) + try_many_times(|| fs::rename(&blobdir, &new_blobdir)) .await .context("failed to rename blobdir")?; if walfile.exists() { @@ -229,11 +214,10 @@ impl Accounts { Ok(account_config.id) } Err(err) => { - // remove temp account - fs::remove_dir_all(std::path::PathBuf::from(&account_config.dir)) + let account_path = std::path::PathBuf::from(&account_config.dir); + try_many_times(|| fs::remove_dir_all(&account_path)) .await .context("failed to remove account data")?; - self.config.remove_account(account_config.id).await?; // set selection back @@ -488,6 +472,33 @@ impl Config { } } +/// Spend up to 1 minute trying to do the operation. +/// +/// Files may remain locked up to 30 seconds due to r2d2 bug: +/// +async fn try_many_times(f: F) -> std::result::Result<(), T> +where + F: Fn() -> Fut, + Fut: Future>, +{ + let mut counter = 0; + loop { + counter += 1; + + if let Err(err) = f().await { + if counter > 60 { + return Err(err); + } + + // Wait 1 second and try again. + tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + } else { + break; + } + } + Ok(()) +} + /// Configuration of a single account. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] struct AccountConfig { diff --git a/src/chat.rs b/src/chat.rs index 0017eeabc..cc217af15 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2075,8 +2075,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> { Ok(()) } -/// Prepares a message to be send out -/// - Checks if chat can be sent to +/// Prepares a message to be sent out. async fn prepare_msg_common( context: &Context, chat_id: ChatId, @@ -2084,6 +2083,8 @@ async fn prepare_msg_common( change_state_to: MessageState, ) -> Result { let mut chat = Chat::load_from_db(context, chat_id).await?; + + // Check if the chat can be sent to. if let Some(reason) = chat.why_cant_send(context).await? { bail!("cannot send to {}: {}", chat_id, reason); } @@ -2141,7 +2142,7 @@ pub async fn is_contact_in_chat( Ok(exists) } -/// Send a message defined by a dc_msg_t object to a chat. +/// Sends a message object to a chat. /// /// Sends the event #DC_EVENT_MSGS_CHANGED on succcess. /// However, this does not imply, the message really reached the recipient - @@ -3294,7 +3295,7 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) - Ok(()) } -/// Set a new profile image for the chat. +/// Sets a new profile image for the chat. /// /// The profile image can only be set when you are a member of the /// chat. To remove the profile image pass an empty string for the diff --git a/src/config.rs b/src/config.rs index 91cc37e94..67d77811f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,5 @@ //! # Key-value configuration management. -#![allow(missing_docs)] - use anyhow::{ensure, Context as _, Result}; use strum::{EnumProperty, IntoEnumIterator}; use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString}; @@ -195,6 +193,8 @@ pub enum Config { /// Configured IMAP server security (e.g. TLS, STARTTLS). ConfiguredMailSecurity, + + /// How to check IMAP server TLS certificates. ConfiguredImapCertificateChecks, /// Configured SMTP server hostname. @@ -208,14 +208,26 @@ pub enum Config { /// Configured SMTP server port. ConfiguredSendPort, + + /// How to check SMTP server TLS certificates. ConfiguredSmtpCertificateChecks, /// Whether OAuth 2 is used with configured provider. ConfiguredServerFlags, + + /// Configured SMTP server security (e.g. TLS, STARTTLS). ConfiguredSendSecurity, + + /// Configured folder for incoming messages. ConfiguredInboxFolder, + + /// Configured folder for chat messages. ConfiguredMvboxFolder, + + /// Configured "Sent" folder. ConfiguredSentboxFolder, + + /// Unix timestamp of the last successful configuration. ConfiguredTimestamp, /// ID of the configured provider from the provider database. @@ -228,12 +240,15 @@ pub enum Config { /// (`addr1@example.org addr2@exapmle.org addr3@example.org`) SecondaryAddrs, + /// Read-only core version string. #[strum(serialize = "sys.version")] SysVersion, + /// Maximal recommended attachment size in bytes. #[strum(serialize = "sys.msgsize_max_recommended")] SysMsgsizeMaxRecommended, + /// Space separated list of all config keys available. #[strum(serialize = "sys.config_keys")] SysConfigKeys, @@ -419,6 +434,7 @@ impl Context { Ok(()) } + /// Set the given config to a boolean value. pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> { self.set_config(key, if value { Some("1") } else { Some("0") }) .await?; diff --git a/src/constants.rs b/src/constants.rs index f83f7d519..2775f97ef 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -66,14 +66,20 @@ pub enum KeyGenType { Ed25519 = 2, } +/// Video chat URL type. #[derive( Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, )] #[repr(i8)] pub enum VideochatType { + /// Unknown type. #[default] Unknown = 0, + + /// [basicWebRTC](https://github.com/cracker0dks/basicwebrtc) instance. BasicWebrtc = 1, + + /// [Jitsi Meet](https://jitsi.org/jitsi-meet/) instance. Jitsi = 2, } @@ -109,6 +115,7 @@ pub const DC_CHAT_ID_ALLDONE_HINT: ChatId = ChatId::new(7); /// larger chat IDs are "real" chats, their messages are "real" messages. pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9); +/// Chat type. #[derive( Debug, Default, @@ -127,11 +134,20 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9); )] #[repr(u32)] pub enum Chattype { + /// Undefined chat type. #[default] Undefined = 0, + + /// 1:1 chat. Single = 100, + + /// Group chat. Group = 120, + + /// Mailing list. Mailinglist = 140, + + /// Broadcast list. Broadcast = 160, } diff --git a/src/events.rs b/src/events.rs index 0e8dbed45..fd7a29c03 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,7 +1,5 @@ //! # Events specification. -#![allow(missing_docs)] - use std::path::PathBuf; use async_channel::{self as channel, Receiver, Sender, TrySendError}; @@ -111,6 +109,7 @@ pub struct Event { pub typ: EventType, } +/// Event payload. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub enum EventType { /// The library-user may write an informational string to the log. @@ -171,17 +170,23 @@ pub enum EventType { /// - Chats created, deleted or archived /// - A draft has been set /// - /// `chat_id` is set if only a single chat is affected by the changes, otherwise 0. - /// `msg_id` is set if only a single message is affected by the changes, otherwise 0. MsgsChanged { + /// Set if only a single chat is affected by the changes, otherwise 0. chat_id: ChatId, + + /// Set if only a single message is affected by the changes, otherwise 0. msg_id: MsgId, }, /// Reactions for the message changed. ReactionsChanged { + /// ID of the chat which the message belongs to. chat_id: ChatId, + + /// ID of the message for which reactions were changed. msg_id: MsgId, + + /// ID of the contact whose reaction set is changed. contact_id: ContactId, }, @@ -190,11 +195,16 @@ pub enum EventType { /// /// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event. IncomingMsg { + /// ID of the chat where the message is assigned. chat_id: ChatId, + + /// ID of the message. msg_id: MsgId, }, + /// Downloading a bunch of messages just finished. IncomingMsgBunch { + /// List of incoming message IDs. msg_ids: Vec, }, @@ -205,21 +215,30 @@ pub enum EventType { /// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to /// DC_STATE_OUT_DELIVERED, see dc_msg_get_state(). MsgDelivered { + /// ID of the chat which the message belongs to. chat_id: ChatId, + + /// ID of the message that was successfully sent. msg_id: MsgId, }, /// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to /// DC_STATE_OUT_FAILED, see dc_msg_get_state(). MsgFailed { + /// ID of the chat which the message belongs to. chat_id: ChatId, + + /// ID of the message that could not be sent. msg_id: MsgId, }, /// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to /// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state(). MsgRead { + /// ID of the chat which the message belongs to. chat_id: ChatId, + + /// ID of the message that was read. msg_id: MsgId, }, @@ -234,7 +253,10 @@ pub enum EventType { /// Chat ephemeral timer changed. ChatEphemeralTimerModified { + /// Chat ID. chat_id: ChatId, + + /// New ephemeral timer value. timer: EphemeralTimer, }, @@ -281,15 +303,15 @@ pub enum EventType { /// /// These events are typically sent after a joiner has scanned the QR code /// generated by dc_get_securejoin_qr(). - /// - /// @param data1 (int) ID of the contact that wants to join. - /// @param data2 (int) Progress as: - /// 300=vg-/vc-request received, typically shown as "bob@addr joins". - /// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified". - /// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol. - /// 1000=Protocol finished for this contact. SecurejoinInviterProgress { + /// ID of the contact that wants to join. contact_id: ContactId, + + /// Progress as: + /// 300=vg-/vc-request received, typically shown as "bob@addr joins". + /// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified". + /// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol. + /// 1000=Protocol finished for this contact. progress: usize, }, @@ -297,12 +319,13 @@ pub enum EventType { /// (Bob, the person who scans the QR code). /// The events are typically sent while dc_join_securejoin(), which /// may take some time, is executed. - /// @param data1 (int) ID of the inviting contact. - /// @param data2 (int) Progress as: - /// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself." - /// (Bob has verified alice and waits until Alice does the same for him) SecurejoinJoinerProgress { + /// ID of the inviting contact. contact_id: ContactId, + + /// Progress as: + /// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself." + /// (Bob has verified alice and waits until Alice does the same for him) progress: usize, }, @@ -312,15 +335,21 @@ pub enum EventType { /// dc_get_connectivity_html() for details. ConnectivityChanged, + /// The user's avatar changed. SelfavatarChanged, + /// Webxdc status update received. WebxdcStatusUpdate { + /// Message ID. msg_id: MsgId, + + /// Status update ID. status_update_serial: StatusUpdateSerial, }, - /// Inform that a message containing a webxdc instance has been deleted + /// Inform that a message containing a webxdc instance has been deleted. WebxdcInstanceDeleted { + /// ID of the deleted message. msg_id: MsgId, }, } diff --git a/src/headerdef.rs b/src/headerdef.rs index 6f9dd495a..ca282a81b 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -1,17 +1,18 @@ //! # List of email headers. -#![allow(missing_docs)] - use mailparse::{MailHeader, MailHeaderMap}; #[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, IntoStaticStr)] #[strum(serialize_all = "kebab_case")] +#[allow(missing_docs)] pub enum HeaderDef { MessageId, Subject, Date, From_, To, + + /// Carbon copy. Cc, Disposition, @@ -34,11 +35,18 @@ pub enum HeaderDef { /// header, so it can be used to ignore such messages. XMozillaDraftInfo, + /// Mailing list ID defined in [RFC 2919](https://tools.ietf.org/html/rfc2919). ListId, ListPost, References, + + /// In-Reply-To header containing Message-ID of the parent message. InReplyTo, + + /// Used to detect mailing lists if contains "list" value + /// as described in [RFC 3834](https://tools.ietf.org/html/rfc3834) Precedence, + ContentType, ContentId, ChatVersion, @@ -52,9 +60,14 @@ pub enum HeaderDef { ChatGroupMemberRemoved, ChatGroupMemberAdded, ChatContent, + + /// Duration of the attached media file. ChatDuration, + ChatDispositionNotificationTo, ChatWebrtcRoom, + + /// [Autocrypt](https://autocrypt.org/) header. Autocrypt, AutocryptSetupMessage, SecureJoin, @@ -63,6 +76,8 @@ pub enum HeaderDef { SecureJoinInvitenumber, SecureJoinAuth, Sender, + + /// Ephemeral message timer. EphemeralTimer, Received, @@ -81,8 +96,12 @@ impl HeaderDef { } } +#[allow(missing_docs)] pub trait HeaderDefMap { + /// Returns requested header value if it exists. fn get_header_value(&self, headerdef: HeaderDef) -> Option; + + /// Returns requested header if it exists. fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader>; } diff --git a/src/lib.rs b/src/lib.rs index 6ca085a32..9feb6329b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//! # Delta Chat Core Library. +//! # Delta Chat Core Library #![recursion_limit = "256"] #![forbid(unsafe_code)] @@ -111,7 +111,7 @@ pub mod tools; pub mod accounts; pub mod reaction; -/// if set imap/incoming and smtp/outgoing MIME messages will be printed +/// If set IMAP/incoming and SMTP/outgoing MIME messages will be printed. pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG"; #[cfg(test)] diff --git a/src/mimefactory.rs b/src/mimefactory.rs index e67b0aace..2672c203d 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -98,6 +98,8 @@ pub struct RenderedEmail { /// Message ID (Message in the sense of Email) pub rfc724_mid: String, + + /// Message subject. pub subject: String, } diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 4f5f2f1bf..cf03a0100 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1,7 +1,5 @@ //! # MIME message parsing module. -#![allow(missing_docs)] - use std::collections::{HashMap, HashSet}; use std::future::Future; use std::pin::Pin; @@ -130,11 +128,13 @@ pub(crate) enum MailinglistType { None, } +/// System message type. #[derive( Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql, )] #[repr(u32)] pub enum SystemMessage { + /// Unknown type of system message. #[default] Unknown = 0, @@ -152,8 +152,14 @@ pub enum SystemMessage { /// Autocrypt Setup Message. AutocryptSetupMessage = 6, + + /// Secure-join message. SecurejoinMessage = 7, + + /// Location streaming is enabled. LocationStreamingEnabled = 8, + + /// Location-only message. LocationOnly = 9, /// Chat ephemeral message timer is changed. @@ -1792,6 +1798,8 @@ pub struct Part { /// Size of the MIME part in bytes. pub bytes: usize, + + /// Parameters. pub param: Params, /// Attachment filename. diff --git a/src/oauth2.rs b/src/oauth2.rs index 70c58c1d2..2fe8795b4 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -1,7 +1,5 @@ //! OAuth 2 module. -#![allow(missing_docs)] - use std::collections::HashMap; use anyhow::Result; @@ -56,6 +54,8 @@ struct Response { scope: Option, } +/// Returns URL that should be opened in the browser +/// if OAuth 2 is supported for this address. pub async fn get_oauth2_url( context: &Context, addr: &str, @@ -76,7 +76,7 @@ pub async fn get_oauth2_url( } } -pub async fn get_oauth2_access_token( +pub(crate) async fn get_oauth2_access_token( context: &Context, addr: &str, code: &str, @@ -228,7 +228,11 @@ pub async fn get_oauth2_access_token( } } -pub async fn get_oauth2_addr(context: &Context, addr: &str, code: &str) -> Result> { +pub(crate) async fn get_oauth2_addr( + context: &Context, + addr: &str, + code: &str, +) -> Result> { let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?; let oauth2 = match Oauth2::from_address(context, addr, socks5_enabled).await { Some(o) => o, diff --git a/src/pgp.rs b/src/pgp.rs index c25826cd5..6bc4bface 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -1,7 +1,5 @@ //! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp). -#![allow(missing_docs)] - use std::collections::{BTreeMap, HashSet}; use std::io; use std::io::Cursor; @@ -24,7 +22,10 @@ use crate::key::{DcKey, Fingerprint}; use crate::keyring::Keyring; use crate::tools::EmailAddress; +#[allow(missing_docs)] pub const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt"; + +#[allow(missing_docs)] pub const HEADER_SETUPCODE: &str = "passphrase-begin"; /// A wrapper for rPGP public key types diff --git a/src/qr.rs b/src/qr.rs index e3625b8e9..dd5c84e75 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -1,7 +1,5 @@ //! # QR code module. -#![allow(missing_docs)] - mod dclogin_scheme; use std::collections::BTreeMap; @@ -38,34 +36,73 @@ const HTTP_SCHEME: &str = "http://"; const HTTPS_SCHEME: &str = "https://"; pub(crate) const DCBACKUP_SCHEME: &str = "DCBACKUP:"; +/// Scanned QR code. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Qr { + /// Ask the user whether to verify the contact. + /// + /// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`]. AskVerifyContact { + /// ID of the contact. contact_id: ContactId, + + /// Fingerprint of the contact key as scanned from the QR code. fingerprint: Fingerprint, + + /// Invite number. invitenumber: String, + + /// Authentication code. authcode: String, }, + + /// Ask the user whether to join the group. AskVerifyGroup { + /// Group name. grpname: String, + + /// Group ID. grpid: String, + + /// ID of the contact. contact_id: ContactId, + + /// Fingerprint of the contact key as scanned from the QR code. fingerprint: Fingerprint, + + /// Invite number. invitenumber: String, + + /// Authentication code. authcode: String, }, + + /// Contact fingerprint is verified. + /// + /// Ask the user if they want to start chatting. FprOk { + /// Contact ID. contact_id: ContactId, }, + + /// Scanned fingerprint does not match the last seen fingerprint. FprMismatch { + /// Contact ID. contact_id: Option, }, + + /// The scanned QR code contains a fingerprint but no e-mail address. FprWithoutAddr { + /// Key fingerprint. fingerprint: String, }, + + /// Ask the user if they want to create an account on the given domain. Account { + /// Server domain name. domain: String, }, + /// Provides a backup that can be retrieve. /// /// This contains all the data needed to connect to a device and download a backup from @@ -79,52 +116,124 @@ pub enum Qr { /// The format is somewhat opaque, but `sendme` can deserialise this. ticket: sendme::provider::Ticket, }, + + /// Ask the user if they want to use the given service for video chats. WebrtcInstance { + /// Server domain name. domain: String, + + /// URL pattern for video chat rooms. instance_pattern: String, }, + + /// Contact address is scanned. + /// + /// Optionally, a draft message could be provided. + /// Ask the user if they want to start chatting. Addr { + /// Contact ID. contact_id: ContactId, + + /// Draft message. draft: Option, }, + + /// URL scanned. + /// + /// Ask the user if they want to open a browser or copy the URL to clipboard. Url { + /// URL. url: String, }, + + /// Text scanned. + /// + /// Ask the user if they want to copy the text to clipboard. Text { + /// Scanned text. text: String, }, + + /// Ask the user if they want to withdraw their own QR code. WithdrawVerifyContact { + /// Contact ID. contact_id: ContactId, + + /// Fingerprint of the contact key as scanned from the QR code. fingerprint: Fingerprint, + + /// Invite number. invitenumber: String, + + /// Authentication code. authcode: String, }, + + /// Ask the user if they want to withdraw their own group invite QR code. WithdrawVerifyGroup { + /// Group name. grpname: String, + + /// Group ID. grpid: String, + + /// Contact ID. contact_id: ContactId, + + /// Fingerprint of the contact key as scanned from the QR code. fingerprint: Fingerprint, + + /// Invite number. invitenumber: String, + + /// Authentication code. authcode: String, }, + + /// Ask the user if they want to revive their own QR code. ReviveVerifyContact { + /// Contact ID. contact_id: ContactId, + + /// Fingerprint of the contact key as scanned from the QR code. fingerprint: Fingerprint, + + /// Invite number. invitenumber: String, + + /// Authentication code. authcode: String, }, + + /// Ask the user if they want to revive their own group invite QR code. ReviveVerifyGroup { + /// Group name. grpname: String, + + /// Group ID. grpid: String, + + /// Contact ID. contact_id: ContactId, + + /// Fingerprint of the contact key as scanned from the QR code. fingerprint: Fingerprint, + + /// Invite number. invitenumber: String, + + /// Authentication code. authcode: String, }, /// `dclogin:` scheme parameters. + /// + /// Ask the user if they want to login with the email address. Login { + /// Email address. address: String, + + /// Login parameters. options: LoginOptions, }, } @@ -133,7 +242,8 @@ fn starts_with_ignore_case(string: &str, pattern: &str) -> bool { string.to_lowercase().starts_with(&pattern.to_lowercase()) } -/// Check a scanned QR code. +/// Checks a scanned QR code. +/// /// The function should be called after a QR code is scanned. /// The function takes the raw text scanned and checks what can be done with it. pub async fn check_qr(context: &Context, qr: &str) -> Result { @@ -456,6 +566,7 @@ async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> { } } +/// Sets configuration values from a QR code. pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> { match check_qr(context, qr).await? { Qr::Account { .. } => set_account_from_qr(context, qr).await?, @@ -658,6 +769,9 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result { } impl Qr { + /// Creates a new scanned QR code of a contact address. + /// + /// May contain a message draft. pub async fn from_address( context: &Context, name: &str, diff --git a/src/qr/dclogin_scheme.rs b/src/qr/dclogin_scheme.rs index 75c084043..36118e386 100644 --- a/src/qr/dclogin_scheme.rs +++ b/src/qr/dclogin_scheme.rs @@ -9,22 +9,53 @@ use crate::context::Context; use crate::provider::Socket; use crate::{contact, login_param::CertificateChecks}; +/// Options for `dclogin:` scheme. #[derive(Debug, Clone, PartialEq, Eq)] pub enum LoginOptions { + /// Unsupported version. UnsuportedVersion(u32), + + /// Version 1. V1 { + /// IMAP server password. + /// + /// Used for SMTP if separate SMTP password is not provided. mail_pw: String, + + /// IMAP host. imap_host: Option, + + /// IMAP port. imap_port: Option, + + /// IMAP username. imap_username: Option, + + /// IMAP password. imap_password: Option, + + /// IMAP socket security. imap_security: Option, + + /// IMAP certificate checks. imap_certificate_checks: Option, + + /// SMTP host. smtp_host: Option, + + /// SMTP port. smtp_port: Option, + + /// SMTP username. smtp_username: Option, + + /// SMTP password. smtp_password: Option, + + /// SMTP socket security. smtp_security: Option, + + /// SMTP certificate checks. smtp_certificate_checks: Option, }, } diff --git a/src/qr_code_generator.rs b/src/qr_code_generator.rs index 3f4b2dba2..fddcce826 100644 --- a/src/qr_code_generator.rs +++ b/src/qr_code_generator.rs @@ -1,4 +1,4 @@ -#![allow(missing_docs)] +//! # QR code generation module. use anyhow::{bail, Result}; use base64::Engine as _; @@ -16,6 +16,10 @@ use crate::{ stock_str::{self, backup_transfer_qr}, }; +/// Returns SVG of the QR code to join the group or verify contact. +/// +/// If `chat_id` is `None`, returns verification QR code. +/// Otherwise, returns secure join QR code. pub async fn get_securejoin_qr_svg(context: &Context, chat_id: Option) -> Result { if let Some(chat_id) = chat_id { generate_join_group_qr_code(context, chat_id).await @@ -56,6 +60,7 @@ async fn generate_verification_qr(context: &Context) -> Result { ) } +/// Renders a [`Qr::Backup`] QR code as an SVG image. pub async fn generate_backup_qr(context: &Context, qr: &Qr) -> Result { let ticket = match qr { Qr::Backup { ticket } => ticket, diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 889cdfb73..95db04d9c 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1,7 +1,5 @@ //! Internet Message Format reception pipeline. -#![allow(missing_docs)] - use std::cmp::min; use std::collections::HashSet; use std::convert::TryFrom; @@ -48,8 +46,13 @@ use crate::{contact, imap}; /// all have the same chat_id, state and sort_timestamp. #[derive(Debug)] pub struct ReceivedMsg { + /// Chat the message is assigned to. pub chat_id: ChatId, + + /// Received message state. pub state: MessageState, + + /// Message timestamp for sorting. pub sort_timestamp: i64, /// IDs of inserted rows in messages table. diff --git a/src/sql.rs b/src/sql.rs index c01322c8b..6b967de17 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -1,7 +1,5 @@ //! # SQLite wrapper. -#![allow(missing_docs)] - use std::collections::{HashMap, HashSet}; use std::convert::TryFrom; use std::path::Path; @@ -26,6 +24,7 @@ use crate::peerstate::{deduplicate_peerstates, Peerstate}; use crate::stock_str; use crate::tools::{delete_file, time}; +#[allow(missing_docs)] #[macro_export] macro_rules! paramsv { () => { @@ -36,6 +35,7 @@ macro_rules! paramsv { }; } +#[allow(missing_docs)] #[macro_export] macro_rules! params_iterv { ($($param:expr),+ $(,)?) => { @@ -55,16 +55,19 @@ pub struct Sql { /// Database file path pub(crate) dbfile: PathBuf, + /// SQL connection pool. pool: RwLock>>, /// None if the database is not open, true if it is open with passphrase and false if it is /// open without a passphrase. is_encrypted: RwLock>, + /// Cache of `config` table. pub(crate) config_cache: RwLock>>, } impl Sql { + /// Creates new SQL database. pub fn new(dbfile: PathBuf) -> Sql { Self { dbfile, @@ -213,6 +216,17 @@ impl Sql { Duration::from_secs(10).as_millis() ))?; c.pragma_update(None, "key", passphrase.clone())?; + // Try to enable auto_vacuum. This will only be + // applied if the database is new or after successful + // VACUUM, which usually happens before backup export. + // When auto_vacuum is INCREMENTAL, it is possible to + // use PRAGMA incremental_vacuum to return unused + // database pages to the filesystem. + c.pragma_update(None, "auto_vacuum", "INCREMENTAL".to_string())?; + + c.pragma_update(None, "journal_mode", "WAL".to_string())?; + // Default synchronous=FULL is much slower. NORMAL is sufficient for WAL mode. + c.pragma_update(None, "synchronous", "NORMAL".to_string())?; Ok(()) }); @@ -228,31 +242,12 @@ impl Sql { async fn try_open(&self, context: &Context, dbfile: &Path, passphrase: String) -> Result<()> { *self.pool.write().await = Some(Self::new_pool(dbfile, passphrase.to_string())?); - { - let conn = self.get_conn().await?; - tokio::task::block_in_place(move || -> Result<()> { - // Try to enable auto_vacuum. This will only be - // applied if the database is new or after successful - // VACUUM, which usually happens before backup export. - // When auto_vacuum is INCREMENTAL, it is possible to - // use PRAGMA incremental_vacuum to return unused - // database pages to the filesystem. - conn.pragma_update(None, "auto_vacuum", "INCREMENTAL".to_string())?; - - // journal_mode is persisted, it is sufficient to change it only for one handle. - conn.pragma_update(None, "journal_mode", "WAL".to_string())?; - - // Default synchronous=FULL is much slower. NORMAL is sufficient for WAL mode. - conn.pragma_update(None, "synchronous", "NORMAL".to_string())?; - Ok(()) - })?; - } - self.run_migrations(context).await?; Ok(()) } + /// Updates SQL schema to the latest version. pub async fn run_migrations(&self, context: &Context) -> Result<()> { // (1) update low-level database structure. // this should be done before updates that use high-level objects that @@ -397,6 +392,7 @@ impl Sql { }) } + /// Allocates a connection from the connection pool and returns it. pub async fn get_conn( &self, ) -> Result> { @@ -594,22 +590,26 @@ impl Sql { Ok(value) } + /// Sets configuration for the given key to 32-bit signed integer value. pub async fn set_raw_config_int(&self, key: &str, value: i32) -> Result<()> { self.set_raw_config(key, Some(&format!("{value}"))).await } + /// Returns 32-bit signed integer configuration value for the given key. pub async fn get_raw_config_int(&self, key: &str) -> Result> { self.get_raw_config(key) .await .map(|s| s.and_then(|s| s.parse().ok())) } + /// Returns 32-bit unsigned integer configuration value for the given key. pub async fn get_raw_config_u32(&self, key: &str) -> Result> { self.get_raw_config(key) .await .map(|s| s.and_then(|s| s.parse().ok())) } + /// Returns boolean configuration value for the given key. pub async fn get_raw_config_bool(&self, key: &str) -> Result { // Not the most obvious way to encode bool as string, but it is matter // of backward compatibility. @@ -617,27 +617,32 @@ impl Sql { Ok(res.unwrap_or_default() > 0) } + /// Sets configuration for the given key to boolean value. pub async fn set_raw_config_bool(&self, key: &str, value: bool) -> Result<()> { let value = if value { Some("1") } else { None }; self.set_raw_config(key, value).await } + /// Sets configuration for the given key to 64-bit signed integer value. pub async fn set_raw_config_int64(&self, key: &str, value: i64) -> Result<()> { self.set_raw_config(key, Some(&format!("{value}"))).await } + /// Returns 64-bit signed integer configuration value for the given key. pub async fn get_raw_config_int64(&self, key: &str) -> Result> { self.get_raw_config(key) .await .map(|s| s.and_then(|r| r.parse().ok())) } + /// Returns configuration cache. #[cfg(feature = "internals")] pub fn config_cache(&self) -> &RwLock>> { &self.config_cache } } +/// Cleanup the account to restore some storage and optimize the database. pub async fn housekeeping(context: &Context) -> Result<()> { if let Err(err) = remove_unused_files(context).await { warn!( @@ -696,6 +701,7 @@ pub async fn housekeeping(context: &Context) -> Result<()> { Ok(()) } +/// Enumerates used files in the blobdir and removes unused ones. pub async fn remove_unused_files(context: &Context) -> Result<()> { let mut files_in_use = HashSet::new(); let mut unreferenced_count = 0; diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 92f1c9551..02f13ecf3 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -723,14 +723,14 @@ impl Sql { async fn execute_migration(&self, query: &'static str, version: i32) -> Result<()> { self.transaction(move |transaction| { - transaction.execute_batch(query)?; - // set raw config inside the transaction transaction.execute( "UPDATE config SET value=? WHERE keyname=?;", paramsv![format!("{version}"), VERSION_CFG], )?; + transaction.execute_batch(query)?; + Ok(()) }) .await diff --git a/src/stock_str.rs b/src/stock_str.rs index 8b3b6162f..580ffc6f8 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -1,7 +1,5 @@ //! Module to work with translatable stock strings. -#![allow(missing_docs)] - use std::collections::HashMap; use std::sync::Arc; @@ -21,6 +19,7 @@ use crate::message::{Message, Viewtype}; use crate::param::Param; use crate::tools::timestamp_to_str; +/// Storage for string translations. #[derive(Debug, Clone)] pub struct StockStrings { /// Map from stock string ID to the translation. @@ -35,6 +34,7 @@ pub struct StockStrings { /// See the `stock_*` methods on [Context] to use these. /// /// [Context]: crate::context::Context +#[allow(missing_docs)] #[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, EnumProperty)] #[repr(u32)] pub enum StockMessage { @@ -425,6 +425,7 @@ impl Default for StockStrings { } impl StockStrings { + /// Creates a new translated string storage. pub fn new() -> Self { Self { translated_stockstrings: Arc::new(RwLock::new(Default::default())), diff --git a/src/tools.rs b/src/tools.rs index f62e4df67..8703a124f 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -124,6 +124,7 @@ pub fn timestamp_to_str(wanted: i64) -> String { } } +/// Converts duration to string representation suitable for logs. pub fn duration_to_str(duration: Duration) -> String { let secs = duration.as_secs(); let h = secs / 3600; @@ -442,6 +443,7 @@ pub(crate) async fn write_file( }) } +/// Reads the file and returns its context as a byte vector. pub async fn read_file(context: &Context, path: impl AsRef) -> Result> { let path_abs = get_abs_path(context, &path); @@ -530,7 +532,10 @@ pub(crate) fn time() -> i64 { /// ``` #[derive(Debug, PartialEq, Eq, Clone)] pub struct EmailAddress { + /// Local part of the email address. pub local: String, + + /// Email address domain. pub domain: String, } diff --git a/src/webxdc.rs b/src/webxdc.rs index eabb0684f..f4cf660a6 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -114,12 +114,12 @@ pub struct WebxdcInfo { pub struct StatusUpdateSerial(u32); impl StatusUpdateSerial { - /// Create a new [MsgId]. + /// Create a new [StatusUpdateSerial]. pub fn new(id: u32) -> StatusUpdateSerial { StatusUpdateSerial(id) } - /// Gets StatusUpdateId as untyped integer. + /// Gets StatusUpdateSerial as untyped integer. /// Avoid using this outside ffi. pub fn to_u32(self) -> u32 { self.0