Introduce a ContactId newtype

This makes the contact ID its own newtype instead of being a plain
u32.  The change purposefully does not yet try and reap any benefits
from this yet, instead aiming for a boring change that's easy to
review.  Only exception is the ToSql/FromSql as not doing that yet
would also have created churn in the database code and it is easier to
go straight for the right solution here.
This commit is contained in:
Floris Bruynooghe
2022-03-02 22:06:48 +01:00
parent f28fcec81d
commit 438940219e
20 changed files with 372 additions and 261 deletions

View File

@@ -1,12 +1,14 @@
//! Contacts module
use std::convert::{TryFrom, TryInto};
use std::fmt;
use anyhow::{bail, ensure, Context as _, Result};
use async_std::path::PathBuf;
use deltachat_derive::{FromSql, ToSql};
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::aheader::EncryptPreference;
use crate::chat::ChatId;
@@ -27,6 +29,67 @@ use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::{chat, stock_str};
/// Contact ID, including reserved IDs.
///
/// Some contact IDs are reserved to identify special contacts. This
/// type can represent both the special as well as normal contacts.
#[derive(
Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
)]
pub struct ContactId(u32);
impl ContactId {
/// Creates a new [`ContactId`].
pub const fn new(id: u32) -> ContactId {
ContactId(id)
}
/// Bad evil escape hatch, do not use.
pub const fn to_u32(&self) -> u32 {
self.0
}
}
impl fmt::Display for ContactId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
// TODO: Something like this
// if self == DC_CONTACT_ID_UNDEFINED {
// write!(f, "Contact#Undefined")
// } else if self == DC_CONTACT_ID_SELF {
// write!(f, "Contact#Self")
// } else if self == DC_CONTACT_ID_INFO {
// write!(f, "Contact#Info")
// } else if self == DC_CONTACT_ID_DEVICE {
// write!(f, "Contact#Device")
// } else if self <= DC_CONTACT_ID_LAST_SPECIAL {
// write!(f, "Contact#Special{}", self.0)
// } else {
// write!(f, "Contact#{}", self.0)
// }
}
}
/// Allow converting [`ContactId`] to an SQLite type.
impl rusqlite::types::ToSql for ContactId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Integer(i64::from(self.0));
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
/// Allow converting an SQLite integer directly into [`ContactId`].
impl rusqlite::types::FromSql for ContactId {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
i64::column_result(value).and_then(|val| {
val.try_into()
.map(ContactId::new)
.map_err(|_| rusqlite::types::FromSqlError::OutOfRange(val))
})
}
}
/// An object representing a single contact in memory.
///
/// The contact object is not updated.
@@ -48,7 +111,7 @@ pub struct Contact {
/// `dc_set_config` using "addr".
///
/// Normal contact IDs are larger than these special ones (larger than DC_CONTACT_ID_LAST_SPECIAL).
pub id: u32,
pub id: ContactId,
/// Contact name. It is recommended to use `Contact::get_name`,
/// `Contact::get_display_name` or `Contact::get_name_n_addr` to access this field.
@@ -183,7 +246,7 @@ impl Default for VerifiedStatus {
}
impl Contact {
pub async fn load_from_db(context: &Context, contact_id: u32) -> Result<Self> {
pub async fn load_from_db(context: &Context, contact_id: ContactId) -> Result<Self> {
let mut contact = context
.sql
.query_row(
@@ -191,7 +254,7 @@ impl Contact {
c.authname, c.param, c.status
FROM contacts c
WHERE c.id=?;",
paramsv![contact_id as i32],
paramsv![contact_id],
|row| {
let name: String = row.get(0)?;
let addr: String = row.get(1)?;
@@ -245,18 +308,18 @@ impl Contact {
}
/// Check if a contact is blocked.
pub async fn is_blocked_load(context: &Context, id: u32) -> Result<bool> {
pub async fn is_blocked_load(context: &Context, id: ContactId) -> Result<bool> {
let blocked = Self::load_from_db(context, id).await?.blocked;
Ok(blocked)
}
/// Block the given contact.
pub async fn block(context: &Context, id: u32) -> Result<()> {
pub async fn block(context: &Context, id: ContactId) -> Result<()> {
set_block_contact(context, id, true).await
}
/// Unblock the given contact.
pub async fn unblock(context: &Context, id: u32) -> Result<()> {
pub async fn unblock(context: &Context, id: ContactId) -> Result<()> {
set_block_contact(context, id, false).await
}
@@ -269,7 +332,7 @@ impl Contact {
/// a bunch of addresses.
///
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
pub async fn create(context: &Context, name: &str, addr: &str) -> Result<u32> {
pub async fn create(context: &Context, name: &str, addr: &str) -> Result<ContactId> {
let name = improve_single_line_input(name);
ensure!(!addr.is_empty(), "Cannot create contact with empty address");
@@ -292,12 +355,12 @@ impl Contact {
}
/// Mark messages from a contact as noticed.
pub async fn mark_noticed(context: &Context, id: u32) -> Result<()> {
pub async fn mark_noticed(context: &Context, id: ContactId) -> Result<()> {
context
.sql
.execute(
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh],
paramsv![MessageState::InNoticed, id, MessageState::InFresh],
)
.await?;
Ok(())
@@ -313,7 +376,7 @@ impl Contact {
context: &Context,
addr: &str,
min_origin: Origin,
) -> Result<Option<u32>> {
) -> Result<Option<ContactId>> {
if addr.is_empty() {
bail!("lookup_id_by_addr: empty address");
}
@@ -333,7 +396,7 @@ impl Contact {
AND id>?2 AND origin>=?3 AND blocked=0;",
paramsv![
addr_normalized,
DC_CONTACT_ID_LAST_SPECIAL as i32,
DC_CONTACT_ID_LAST_SPECIAL,
min_origin as u32,
],
)
@@ -371,7 +434,7 @@ impl Contact {
name: &str,
addr: &str,
mut origin: Origin,
) -> Result<(u32, Modifier)> {
) -> Result<(ContactId, Modifier)> {
let mut sth_modified = Modifier::None;
ensure!(!addr.is_empty(), "Can not add_or_lookup empty address");
@@ -500,7 +563,7 @@ impl Contact {
paramsv![Chattype::Single, isize::try_from(row_id)?]
).await?;
if let Some(chat_id) = chat_id {
let contact = Contact::get_by_id(context, row_id as u32).await?;
let contact = Contact::get_by_id(context, ContactId::new(row_id)).await?;
let chat_name = contact.get_display_name();
match context
.sql
@@ -557,7 +620,7 @@ impl Contact {
}
}
Ok((row_id, sth_modified))
Ok((ContactId::new(row_id), sth_modified))
}
/// Add a number of contacts.
@@ -617,7 +680,7 @@ impl Contact {
context: &Context,
listflags: u32,
query: Option<impl AsRef<str>>,
) -> Result<Vec<u32>> {
) -> Result<Vec<ContactId>> {
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
@@ -644,16 +707,16 @@ impl Contact {
ORDER BY LOWER(iif(c.name='',c.authname,c.name)||c.addr),c.id;",
paramsv![
self_addr,
DC_CONTACT_ID_LAST_SPECIAL as i32,
DC_CONTACT_ID_LAST_SPECIAL,
Origin::IncomingReplyTo,
s3str_like_cmd,
s3str_like_cmd,
if flag_verified_only { 0i32 } else { 1i32 },
],
|row| row.get::<_, i32>(0),
|row| row.get::<_, ContactId>(0),
|ids| {
for id in ids {
ret.push(id? as u32);
ret.push(id?);
}
Ok(())
},
@@ -690,13 +753,13 @@ impl Contact {
ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
paramsv![
self_addr,
DC_CONTACT_ID_LAST_SPECIAL as i32,
DC_CONTACT_ID_LAST_SPECIAL,
Origin::IncomingReplyTo
],
|row| row.get::<_, i32>(0),
|row| row.get::<_, ContactId>(0),
|ids| {
for id in ids {
ret.push(id? as u32);
ret.push(id?);
}
Ok(())
},
@@ -767,7 +830,7 @@ impl Contact {
}
/// Get blocked contacts.
pub async fn get_all_blocked(context: &Context) -> Result<Vec<u32>> {
pub async fn get_all_blocked(context: &Context) -> Result<Vec<ContactId>> {
Contact::update_blocked_mailinglist_contacts(context)
.await
.context("cannot update blocked mailinglist contacts")?;
@@ -776,8 +839,8 @@ impl Contact {
.sql
.query_map(
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|row| row.get::<_, u32>(0),
paramsv![DC_CONTACT_ID_LAST_SPECIAL],
|row| row.get::<_, ContactId>(0),
|ids| {
ids.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
@@ -792,7 +855,7 @@ impl Contact {
/// This function returns a string explaining the encryption state
/// of the contact and if the connection is encrypted the
/// fingerprints of the keys involved.
pub async fn get_encrinfo(context: &Context, contact_id: u32) -> Result<String> {
pub async fn get_encrinfo(context: &Context, contact_id: ContactId) -> Result<String> {
ensure!(
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
"Can not provide encryption info for special contact"
@@ -861,7 +924,7 @@ impl Contact {
/// possible as the contact is in use. In this case, the contact can be blocked.
///
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
pub async fn delete(context: &Context, contact_id: u32) -> Result<()> {
pub async fn delete(context: &Context, contact_id: ContactId) -> Result<()> {
ensure!(
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
"Can not delete special contact"
@@ -871,17 +934,14 @@ impl Contact {
.sql
.count(
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
paramsv![contact_id as i32],
paramsv![contact_id],
)
.await?;
if count_chats == 0 {
match context
.sql
.execute(
"DELETE FROM contacts WHERE id=?;",
paramsv![contact_id as i32],
)
.execute("DELETE FROM contacts WHERE id=?;", paramsv![contact_id])
.await
{
Ok(_) => {
@@ -907,7 +967,7 @@ impl Contact {
/// For contact DC_CONTACT_ID_SELF (1), the function returns sth.
/// like "Me" in the selected language and the email address
/// defined by dc_set_config().
pub async fn get_by_id(context: &Context, contact_id: u32) -> Result<Contact> {
pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result<Contact> {
let contact = Contact::load_from_db(context, contact_id).await?;
Ok(contact)
@@ -919,7 +979,7 @@ impl Contact {
.sql
.execute(
"UPDATE contacts SET param=? WHERE id=?",
paramsv![self.param.to_string(), self.id as i32],
paramsv![self.param.to_string(), self.id],
)
.await?;
Ok(())
@@ -931,14 +991,14 @@ impl Contact {
.sql
.execute(
"UPDATE contacts SET status=? WHERE id=?",
paramsv![self.status, self.id as i32],
paramsv![self.status, self.id],
)
.await?;
Ok(())
}
/// Get the ID of the contact.
pub fn get_id(&self) -> u32 {
pub fn get_id(&self) -> ContactId {
self.id
}
@@ -1065,7 +1125,7 @@ impl Contact {
pub async fn addr_equals_contact(
context: &Context,
addr: &str,
contact_id: u32,
contact_id: ContactId,
) -> Result<bool> {
if addr.is_empty() {
return Ok(false);
@@ -1091,13 +1151,13 @@ impl Contact {
.sql
.count(
"SELECT COUNT(*) FROM contacts WHERE id>?;",
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
paramsv![DC_CONTACT_ID_LAST_SPECIAL],
)
.await?;
Ok(count)
}
pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> Result<bool> {
pub async fn real_exists_by_id(context: &Context, contact_id: ContactId) -> Result<bool> {
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
return Ok(false);
}
@@ -1106,7 +1166,7 @@ impl Contact {
.sql
.exists(
"SELECT COUNT(*) FROM contacts WHERE id=?;",
paramsv![contact_id as i32],
paramsv![contact_id],
)
.await?;
Ok(exists)
@@ -1114,14 +1174,14 @@ impl Contact {
pub async fn scaleup_origin_by_id(
context: &Context,
contact_id: u32,
contact_id: ContactId,
origin: Origin,
) -> Result<()> {
context
.sql
.execute(
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
paramsv![origin, contact_id as i32, origin],
paramsv![origin, contact_id, origin],
)
.await?;
Ok(())
@@ -1165,7 +1225,11 @@ fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
}
}
async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) -> Result<()> {
async fn set_block_contact(
context: &Context,
contact_id: ContactId,
new_blocking: bool,
) -> Result<()> {
ensure!(
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
"Can't block special contact {}",
@@ -1179,7 +1243,7 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
.sql
.execute(
"UPDATE contacts SET blocked=? WHERE id=?;",
paramsv![i32::from(new_blocking), contact_id as i32],
paramsv![i32::from(new_blocking), contact_id],
)
.await?;
@@ -1230,7 +1294,7 @@ WHERE type=? AND id IN (
/// this typically happens if we see message with our own profile image, sent from another device.
pub(crate) async fn set_profile_image(
context: &Context,
contact_id: u32,
contact_id: ContactId,
profile_image: &AvatarAction,
was_encrypted: bool,
) -> Result<()> {
@@ -1277,7 +1341,7 @@ pub(crate) async fn set_profile_image(
/// between Delta Chat devices.
pub(crate) async fn set_status(
context: &Context,
contact_id: u32,
contact_id: ContactId,
status: String,
encrypted: bool,
has_chat_version: bool,
@@ -1303,7 +1367,7 @@ pub(crate) async fn set_status(
/// Updates last seen timestamp of the contact if it is earlier than the given `timestamp`.
pub(crate) async fn update_last_seen(
context: &Context,
contact_id: u32,
contact_id: ContactId,
timestamp: i64,
) -> Result<()> {
ensure!(
@@ -1474,7 +1538,7 @@ mod tests {
Origin::IncomingReplyTo,
)
.await?;
assert_ne!(id, 0);
assert_ne!(id, ContactId::new(0));
let contact = Contact::load_from_db(&context.ctx, id).await.unwrap();
assert_eq!(contact.get_name(), "");
@@ -1629,7 +1693,7 @@ mod tests {
// check SELF
let contact = Contact::load_from_db(&t, DC_CONTACT_ID_SELF).await.unwrap();
assert_eq!(DC_CONTACT_ID_SELF, 1);
assert_eq!(DC_CONTACT_ID_SELF, ContactId::new(1));
assert_eq!(contact.get_name(), stock_str::self_msg(&t).await);
assert_eq!(contact.get_addr(), ""); // we're not configured
assert!(!contact.is_blocked());