| |
|
|
| use std::cmp::Reverse; |
| use std::collections::{BinaryHeap, HashSet}; |
| use std::fmt; |
| use std::path::{Path, PathBuf}; |
| use std::time::UNIX_EPOCH; |
|
|
| use anyhow::{Context as _, Result, bail, ensure}; |
| use async_channel::{self as channel, Receiver, Sender}; |
| use base64::Engine as _; |
| pub use deltachat_contact_tools::may_be_valid_addr; |
| use deltachat_contact_tools::{ |
| self as contact_tools, ContactAddress, VcardContact, addr_normalize, sanitize_name, |
| sanitize_name_and_addr, |
| }; |
| use deltachat_derive::{FromSql, ToSql}; |
| use rusqlite::OptionalExtension; |
| use serde::{Deserialize, Serialize}; |
| use tokio::task; |
| use tokio::time::{Duration, timeout}; |
|
|
| use crate::blob::BlobObject; |
| use crate::chat::ChatId; |
| use crate::color::str_to_color; |
| use crate::config::Config; |
| use crate::constants::{self, Blocked, Chattype}; |
| use crate::context::Context; |
| use crate::events::EventType; |
| use crate::key::{ |
| DcKey, Fingerprint, SignedPublicKey, load_self_public_key, self_fingerprint, |
| self_fingerprint_opt, |
| }; |
| use crate::log::{LogExt, warn}; |
| use crate::message::MessageState; |
| use crate::mimeparser::AvatarAction; |
| use crate::param::{Param, Params}; |
| use crate::sync::{self, Sync::*}; |
| use crate::tools::{SystemTime, duration_to_str, get_abs_path, normalize_text, time, to_lowercase}; |
| use crate::{chat, chatlist_events, ensure_and_debug_assert_ne, stock_str}; |
|
|
| |
| const SEEN_RECENTLY_SECONDS: i64 = 600; |
|
|
| |
| |
| |
| |
| #[derive( |
| Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, |
| )] |
| pub struct ContactId(u32); |
|
|
| impl ContactId { |
| |
| pub const UNDEFINED: ContactId = ContactId::new(0); |
|
|
| |
| |
| |
| pub const SELF: ContactId = ContactId::new(1); |
|
|
| |
| pub const INFO: ContactId = ContactId::new(2); |
|
|
| |
| pub const DEVICE: ContactId = ContactId::new(5); |
| pub(crate) const LAST_SPECIAL: ContactId = ContactId::new(9); |
|
|
| |
| |
| |
| pub const DEVICE_ADDR: &'static str = "device@localhost"; |
|
|
| |
| pub const fn new(id: u32) -> ContactId { |
| ContactId(id) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| pub fn is_special(&self) -> bool { |
| self.0 <= Self::LAST_SPECIAL.0 |
| } |
|
|
| |
| |
| |
| |
| |
| pub const fn to_u32(&self) -> u32 { |
| self.0 |
| } |
|
|
| |
| |
| |
| |
| |
| |
| pub async fn set_name(self, context: &Context, name: &str) -> Result<()> { |
| self.set_name_ex(context, Sync, name).await |
| } |
|
|
| pub(crate) async fn set_name_ex( |
| self, |
| context: &Context, |
| sync: sync::Sync, |
| name: &str, |
| ) -> Result<()> { |
| let row = context |
| .sql |
| .transaction(|transaction| { |
| let authname; |
| let name_or_authname = if !name.is_empty() { |
| name |
| } else { |
| authname = transaction.query_row( |
| "SELECT authname FROM contacts WHERE id=?", |
| (self,), |
| |row| { |
| let authname: String = row.get(0)?; |
| Ok(authname) |
| }, |
| )?; |
| &authname |
| }; |
| let is_changed = transaction.execute( |
| "UPDATE contacts SET name=?1, name_normalized=?2 WHERE id=?3 AND name!=?1", |
| (name, normalize_text(name_or_authname), self), |
| )? > 0; |
| if is_changed { |
| update_chat_names(context, transaction, self)?; |
| let (addr, fingerprint) = transaction.query_row( |
| "SELECT addr, fingerprint FROM contacts WHERE id=?", |
| (self,), |
| |row| { |
| let addr: String = row.get(0)?; |
| let fingerprint: String = row.get(1)?; |
| Ok((addr, fingerprint)) |
| }, |
| )?; |
| Ok(Some((addr, fingerprint))) |
| } else { |
| Ok(None) |
| } |
| }) |
| .await?; |
| if row.is_some() { |
| context.emit_event(EventType::ContactsChanged(Some(self))); |
| } |
|
|
| if sync.into() |
| && let Some((addr, fingerprint)) = row |
| { |
| if fingerprint.is_empty() { |
| chat::sync( |
| context, |
| chat::SyncId::ContactAddr(addr), |
| chat::SyncAction::Rename(name.to_string()), |
| ) |
| .await |
| .log_err(context) |
| .ok(); |
| } else { |
| chat::sync( |
| context, |
| chat::SyncId::ContactFingerprint(fingerprint), |
| chat::SyncAction::Rename(name.to_string()), |
| ) |
| .await |
| .log_err(context) |
| .ok(); |
| } |
| } |
| Ok(()) |
| } |
|
|
| |
| pub(crate) async fn mark_bot(&self, context: &Context, is_bot: bool) -> Result<()> { |
| context |
| .sql |
| .execute("UPDATE contacts SET is_bot=? WHERE id=?;", (is_bot, self.0)) |
| .await?; |
| Ok(()) |
| } |
|
|
| |
| pub(crate) async fn regossip_keys(&self, context: &Context) -> Result<()> { |
| context |
| .sql |
| .execute( |
| "UPDATE chats |
| SET gossiped_timestamp=0 |
| WHERE EXISTS (SELECT 1 FROM chats_contacts |
| WHERE chats_contacts.chat_id=chats.id |
| AND chats_contacts.contact_id=? |
| AND chats_contacts.add_timestamp >= chats_contacts.remove_timestamp)", |
| (self,), |
| ) |
| .await?; |
| Ok(()) |
| } |
|
|
| |
| pub(crate) async fn scaleup_origin( |
| context: &Context, |
| ids: &[Self], |
| origin: Origin, |
| ) -> Result<()> { |
| context |
| .sql |
| .transaction(|transaction| { |
| let mut stmt = transaction |
| .prepare("UPDATE contacts SET origin=?1 WHERE id = ?2 AND origin < ?1")?; |
| for id in ids { |
| stmt.execute((origin, id))?; |
| } |
| Ok(()) |
| }) |
| .await?; |
| Ok(()) |
| } |
|
|
| |
| pub async fn addr(&self, context: &Context) -> Result<String> { |
| let addr = context |
| .sql |
| .query_row("SELECT addr FROM contacts WHERE id=?", (self,), |row| { |
| let addr: String = row.get(0)?; |
| Ok(addr) |
| }) |
| .await?; |
| Ok(addr) |
| } |
| } |
|
|
| impl fmt::Display for ContactId { |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| if *self == ContactId::UNDEFINED { |
| write!(f, "Contact#Undefined") |
| } else if *self == ContactId::SELF { |
| write!(f, "Contact#Self") |
| } else if *self == ContactId::INFO { |
| write!(f, "Contact#Info") |
| } else if *self == ContactId::DEVICE { |
| write!(f, "Contact#Device") |
| } else if self.is_special() { |
| write!(f, "Contact#Special{}", self.0) |
| } else { |
| write!(f, "Contact#{}", self.0) |
| } |
| } |
| } |
|
|
| |
| 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) |
| } |
| } |
|
|
| |
| 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)) |
| }) |
| } |
| } |
|
|
| |
| pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result<String> { |
| let now = time(); |
| let mut vcard_contacts = Vec::with_capacity(contacts.len()); |
| for id in contacts { |
| let c = Contact::get_by_id(context, *id).await?; |
| let key = c.public_key(context).await?.map(|k| k.to_base64()); |
| let profile_image = match c.get_profile_image_ex(context, false).await? { |
| None => None, |
| Some(path) => tokio::fs::read(path) |
| .await |
| .log_err(context) |
| .ok() |
| .map(|data| base64::engine::general_purpose::STANDARD.encode(data)), |
| }; |
| vcard_contacts.push(VcardContact { |
| addr: c.addr, |
| authname: c.authname, |
| key, |
| profile_image, |
| biography: Some(c.status).filter(|s| !s.is_empty()), |
| |
| timestamp: Ok(now), |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| Ok(contact_tools::make_vcard(&vcard_contacts) |
| .trim_end() |
| .to_string()) |
| } |
|
|
| |
| |
| |
| |
| pub async fn import_vcard(context: &Context, vcard: &str) -> Result<Vec<ContactId>> { |
| let contacts = contact_tools::parse_vcard(vcard); |
| let mut contact_ids = Vec::with_capacity(contacts.len()); |
| for c in &contacts { |
| let Ok(id) = import_vcard_contact(context, c) |
| .await |
| .with_context(|| format!("import_vcard_contact() failed for {}", c.addr)) |
| .log_err(context) |
| else { |
| continue; |
| }; |
| contact_ids.push(id); |
| } |
| Ok(contact_ids) |
| } |
|
|
| async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Result<ContactId> { |
| let addr = ContactAddress::new(&contact.addr).context("Invalid address")?; |
| |
| |
| |
| let origin = Origin::CreateChat; |
| let key = contact.key.as_ref().and_then(|k| { |
| SignedPublicKey::from_base64(k) |
| .with_context(|| { |
| format!( |
| "import_vcard_contact: Cannot decode key for {}", |
| contact.addr |
| ) |
| }) |
| .log_err(context) |
| .ok() |
| }); |
|
|
| let fingerprint; |
| if let Some(public_key) = key { |
| fingerprint = public_key.dc_fingerprint().hex(); |
|
|
| context |
| .sql |
| .execute( |
| "INSERT INTO public_keys (fingerprint, public_key) |
| VALUES (?, ?) |
| ON CONFLICT (fingerprint) |
| DO NOTHING", |
| (&fingerprint, public_key.to_bytes()), |
| ) |
| .await?; |
| } else { |
| fingerprint = String::new(); |
| } |
|
|
| let (id, modified) = |
| match Contact::add_or_lookup_ex(context, &contact.authname, &addr, &fingerprint, origin) |
| .await |
| { |
| Err(e) => return Err(e).context("Contact::add_or_lookup() failed"), |
| Ok((ContactId::SELF, _)) => return Ok(ContactId::SELF), |
| Ok(val) => val, |
| }; |
| if modified != Modifier::None { |
| context.emit_event(EventType::ContactsChanged(Some(id))); |
| } |
| if modified != Modifier::Created { |
| return Ok(id); |
| } |
| let path = match &contact.profile_image { |
| Some(image) => match BlobObject::store_from_base64(context, image)? { |
| None => { |
| warn!( |
| context, |
| "import_vcard_contact: Could not decode avatar for {}.", contact.addr |
| ); |
| None |
| } |
| Some(path) => Some(path), |
| }, |
| None => None, |
| }; |
| if let Some(path) = path |
| && let Err(e) = set_profile_image(context, id, &AvatarAction::Change(path)).await |
| { |
| warn!( |
| context, |
| "import_vcard_contact: Could not set avatar for {}: {e:#}.", contact.addr |
| ); |
| } |
| if let Some(biography) = &contact.biography |
| && let Err(e) = set_status(context, id, biography.to_owned()).await |
| { |
| warn!( |
| context, |
| "import_vcard_contact: Could not set biography for {}: {e:#}.", contact.addr |
| ); |
| } |
| Ok(id) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| #[derive(Debug)] |
| pub struct Contact { |
| |
| pub id: ContactId, |
|
|
| |
| |
| |
| name: String, |
|
|
| |
| |
| |
| authname: String, |
|
|
| |
| addr: String, |
|
|
| |
| |
| |
| fingerprint: Option<String>, |
|
|
| |
| pub blocked: bool, |
|
|
| |
| last_seen: i64, |
|
|
| |
| pub origin: Origin, |
|
|
| |
| pub param: Params, |
|
|
| |
| status: String, |
|
|
| |
| is_bot: bool, |
| } |
|
|
| |
| #[derive( |
| Debug, |
| Default, |
| Clone, |
| Copy, |
| PartialEq, |
| Eq, |
| PartialOrd, |
| Ord, |
| FromPrimitive, |
| ToPrimitive, |
| FromSql, |
| ToSql, |
| )] |
| #[repr(u32)] |
| pub enum Origin { |
| |
| |
| #[default] |
| Unknown = 0, |
|
|
| |
| MailinglistAddress = 0x2, |
|
|
| |
| Hidden = 0x8, |
|
|
| |
| IncomingUnknownFrom = 0x10, |
|
|
| |
| IncomingUnknownCc = 0x20, |
|
|
| |
| IncomingUnknownTo = 0x40, |
|
|
| |
| UnhandledQrScan = 0x80, |
|
|
| |
| UnhandledSecurejoinQrScan = 0x81, |
|
|
| |
| |
| IncomingReplyTo = 0x100, |
|
|
| |
| IncomingCc = 0x200, |
|
|
| |
| IncomingTo = 0x400, |
|
|
| |
| CreateChat = 0x800, |
|
|
| |
| OutgoingBcc = 0x1000, |
|
|
| |
| OutgoingCc = 0x2000, |
|
|
| |
| OutgoingTo = 0x4000, |
|
|
| |
| Internal = 0x40000, |
|
|
| |
| AddressBook = 0x80000, |
|
|
| |
| SecurejoinInvited = 0x0100_0000, |
|
|
| |
| |
| |
| |
| |
| SecurejoinJoined = 0x0200_0000, |
|
|
| |
| ManuallyCreated = 0x0400_0000, |
| } |
|
|
| impl Origin { |
| |
| |
| |
| pub fn is_known(self) -> bool { |
| self >= Origin::IncomingReplyTo |
| } |
| } |
|
|
| #[derive(Debug, PartialEq, Eq, Clone, Copy)] |
| pub(crate) enum Modifier { |
| None, |
| Modified, |
| Created, |
| } |
|
|
| impl Contact { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result<Self> { |
| let contact = Self::get_by_id_optional(context, contact_id) |
| .await? |
| .with_context(|| format!("contact {contact_id} not found"))?; |
| Ok(contact) |
| } |
|
|
| |
| |
| |
| pub async fn get_by_id_optional( |
| context: &Context, |
| contact_id: ContactId, |
| ) -> Result<Option<Self>> { |
| if let Some(mut contact) = context |
| .sql |
| .query_row_optional( |
| "SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen, |
| c.authname, c.param, c.status, c.is_bot, c.fingerprint |
| FROM contacts c |
| WHERE c.id=?;", |
| (contact_id,), |
| |row| { |
| let name: String = row.get(0)?; |
| let addr: String = row.get(1)?; |
| let origin: Origin = row.get(2)?; |
| let blocked: Option<bool> = row.get(3)?; |
| let last_seen: i64 = row.get(4)?; |
| let authname: String = row.get(5)?; |
| let param: String = row.get(6)?; |
| let status: Option<String> = row.get(7)?; |
| let is_bot: bool = row.get(8)?; |
| let fingerprint: Option<String> = |
| Some(row.get(9)?).filter(|s: &String| !s.is_empty()); |
| let contact = Self { |
| id: contact_id, |
| name, |
| authname, |
| addr, |
| fingerprint, |
| blocked: blocked.unwrap_or_default(), |
| last_seen, |
| origin, |
| param: param.parse().unwrap_or_default(), |
| status: status.unwrap_or_default(), |
| is_bot, |
| }; |
| Ok(contact) |
| }, |
| ) |
| .await? |
| { |
| if contact_id == ContactId::SELF { |
| contact.name = stock_str::self_msg(context).await; |
| contact.authname = context |
| .get_config(Config::Displayname) |
| .await? |
| .unwrap_or_default(); |
| contact.addr = context |
| .get_config(Config::ConfiguredAddr) |
| .await? |
| .unwrap_or_default(); |
| if let Some(self_fp) = self_fingerprint_opt(context).await? { |
| contact.fingerprint = Some(self_fp.to_string()); |
| } |
| contact.status = context |
| .get_config(Config::Selfstatus) |
| .await? |
| .unwrap_or_default(); |
| } else if contact_id == ContactId::DEVICE { |
| contact.name = stock_str::device_messages(context).await; |
| contact.addr = ContactId::DEVICE_ADDR.to_string(); |
| contact.status = stock_str::device_messages_hint(context).await; |
| } |
| Ok(Some(contact)) |
| } else { |
| Ok(None) |
| } |
| } |
|
|
| |
| pub fn is_blocked(&self) -> bool { |
| self.blocked |
| } |
|
|
| |
| pub fn last_seen(&self) -> i64 { |
| self.last_seen |
| } |
|
|
| |
| pub fn was_seen_recently(&self) -> bool { |
| time() - self.last_seen <= SEEN_RECENTLY_SECONDS |
| } |
|
|
| |
| pub async fn is_blocked_load(context: &Context, id: ContactId) -> Result<bool> { |
| let blocked = context |
| .sql |
| .query_row("SELECT blocked FROM contacts WHERE id=?", (id,), |row| { |
| let blocked: bool = row.get(0)?; |
| Ok(blocked) |
| }) |
| .await?; |
| Ok(blocked) |
| } |
|
|
| |
| pub async fn block(context: &Context, id: ContactId) -> Result<()> { |
| set_blocked(context, Sync, id, true).await |
| } |
|
|
| |
| pub async fn unblock(context: &Context, id: ContactId) -> Result<()> { |
| set_blocked(context, Sync, id, false).await |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| pub async fn create(context: &Context, name: &str, addr: &str) -> Result<ContactId> { |
| Self::create_ex(context, Sync, name, addr).await |
| } |
|
|
| pub(crate) async fn create_ex( |
| context: &Context, |
| sync: sync::Sync, |
| name: &str, |
| addr: &str, |
| ) -> Result<ContactId> { |
| let (name, addr) = sanitize_name_and_addr(name, addr); |
| let addr = ContactAddress::new(&addr)?; |
|
|
| let (contact_id, sth_modified) = |
| Contact::add_or_lookup(context, &name, &addr, Origin::ManuallyCreated) |
| .await |
| .context("add_or_lookup")?; |
| let blocked = Contact::is_blocked_load(context, contact_id).await?; |
| match sth_modified { |
| Modifier::None => {} |
| Modifier::Modified | Modifier::Created => { |
| context.emit_event(EventType::ContactsChanged(Some(contact_id))) |
| } |
| } |
| if blocked { |
| set_blocked(context, Nosync, contact_id, false).await?; |
| } |
|
|
| if sync.into() && sth_modified != Modifier::None { |
| chat::sync( |
| context, |
| chat::SyncId::ContactAddr(addr.to_string()), |
| chat::SyncAction::Rename(name.to_string()), |
| ) |
| .await |
| .log_err(context) |
| .ok(); |
| } |
| Ok(contact_id) |
| } |
|
|
| |
| pub async fn mark_noticed(context: &Context, id: ContactId) -> Result<()> { |
| context |
| .sql |
| .execute( |
| "UPDATE msgs SET state=? WHERE from_id=? AND state=?;", |
| (MessageState::InNoticed, id, MessageState::InFresh), |
| ) |
| .await?; |
| Ok(()) |
| } |
|
|
| |
| pub fn is_bot(&self) -> bool { |
| self.is_bot |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| pub async fn lookup_id_by_addr( |
| context: &Context, |
| addr: &str, |
| min_origin: Origin, |
| ) -> Result<Option<ContactId>> { |
| Self::lookup_id_by_addr_ex(context, addr, min_origin, Some(Blocked::Not)).await |
| } |
|
|
| |
| |
| pub(crate) async fn lookup_id_by_addr_ex( |
| context: &Context, |
| addr: &str, |
| min_origin: Origin, |
| blocked: Option<Blocked>, |
| ) -> Result<Option<ContactId>> { |
| if addr.is_empty() { |
| bail!("lookup_id_by_addr: empty address"); |
| } |
|
|
| let addr_normalized = addr_normalize(addr); |
|
|
| if context.is_configured().await? && context.is_self_addr(addr).await? { |
| return Ok(Some(ContactId::SELF)); |
| } |
|
|
| let id = context |
| .sql |
| .query_get_value( |
| "SELECT id FROM contacts |
| WHERE addr=?1 COLLATE NOCASE |
| AND id>?2 AND origin>=?3 AND (? OR blocked=?) |
| ORDER BY |
| ( |
| SELECT COUNT(*) FROM chats c |
| INNER JOIN chats_contacts cc |
| ON c.id=cc.chat_id |
| WHERE c.type=? |
| AND c.id>? |
| AND c.blocked=? |
| AND cc.contact_id=contacts.id |
| ) DESC, |
| last_seen DESC, fingerprint DESC |
| LIMIT 1", |
| ( |
| &addr_normalized, |
| ContactId::LAST_SPECIAL, |
| min_origin as u32, |
| blocked.is_none(), |
| blocked.unwrap_or(Blocked::Not), |
| Chattype::Single, |
| constants::DC_CHAT_ID_LAST_SPECIAL, |
| blocked.unwrap_or(Blocked::Not), |
| ), |
| ) |
| .await?; |
| Ok(id) |
| } |
|
|
| pub(crate) async fn add_or_lookup( |
| context: &Context, |
| name: &str, |
| addr: &ContactAddress, |
| origin: Origin, |
| ) -> Result<(ContactId, Modifier)> { |
| Self::add_or_lookup_ex(context, name, addr, "", origin).await |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| pub(crate) async fn add_or_lookup_ex( |
| context: &Context, |
| name: &str, |
| addr: &str, |
| fingerprint: &str, |
| mut origin: Origin, |
| ) -> Result<(ContactId, Modifier)> { |
| let mut sth_modified = Modifier::None; |
|
|
| ensure!( |
| !addr.is_empty() || !fingerprint.is_empty(), |
| "Can not add_or_lookup empty address" |
| ); |
| ensure!(origin != Origin::Unknown, "Missing valid origin"); |
|
|
| if context.is_configured().await? && context.is_self_addr(addr).await? { |
| return Ok((ContactId::SELF, sth_modified)); |
| } |
|
|
| if !fingerprint.is_empty() && context.is_configured().await? { |
| let fingerprint_self = self_fingerprint(context) |
| .await |
| .context("self_fingerprint")?; |
| if fingerprint == fingerprint_self { |
| return Ok((ContactId::SELF, sth_modified)); |
| } |
| } |
|
|
| let mut name = sanitize_name(name); |
| if origin <= Origin::OutgoingTo { |
| |
| if addr.contains("noreply") |
| || addr.contains("no-reply") |
| || addr.starts_with("notifications@") |
| |
| || (addr.len() > 50 && addr.contains('+')) |
| { |
| info!(context, "hiding contact {}", addr); |
| origin = Origin::Hidden; |
| |
| |
| |
| name = "".to_string(); |
| } |
| } |
|
|
| |
| |
| |
| |
| let manual = matches!( |
| origin, |
| Origin::ManuallyCreated | Origin::AddressBook | Origin::UnhandledQrScan |
| ); |
|
|
| let mut update_addr = false; |
|
|
| let row_id = context |
| .sql |
| .transaction(|transaction| { |
| let row = transaction |
| .query_row( |
| "SELECT id, name, addr, origin, authname |
| FROM contacts |
| WHERE fingerprint=?1 AND |
| (?1<>'' OR addr=?2 COLLATE NOCASE)", |
| (fingerprint, addr), |
| |row| { |
| let row_id: u32 = row.get(0)?; |
| let row_name: String = row.get(1)?; |
| let row_addr: String = row.get(2)?; |
| let row_origin: Origin = row.get(3)?; |
| let row_authname: String = row.get(4)?; |
|
|
| Ok((row_id, row_name, row_addr, row_origin, row_authname)) |
| }, |
| ) |
| .optional()?; |
|
|
| let row_id; |
| if let Some((id, row_name, row_addr, row_origin, row_authname)) = row { |
| let update_name = manual && name != row_name; |
| let update_authname = !manual |
| && name != row_authname |
| && !name.is_empty() |
| && (origin >= row_origin |
| || origin == Origin::IncomingUnknownFrom |
| || row_authname.is_empty()); |
|
|
| row_id = id; |
| if origin >= row_origin && addr != row_addr { |
| update_addr = true; |
| } |
| if update_name || update_authname || update_addr || origin > row_origin { |
| let new_name = if update_name { |
| name.to_string() |
| } else { |
| row_name |
| }; |
| let new_authname = if update_authname { |
| name.to_string() |
| } else { |
| row_authname |
| }; |
|
|
| transaction.execute( |
| "UPDATE contacts SET name=?, name_normalized=?, addr=?, origin=?, authname=? WHERE id=?", |
| ( |
| &new_name, |
| normalize_text( |
| if !new_name.is_empty() { |
| &new_name |
| } else { |
| &new_authname |
| }), |
| if update_addr { |
| addr.to_string() |
| } else { |
| row_addr |
| }, |
| if origin > row_origin { |
| origin |
| } else { |
| row_origin |
| }, |
| &new_authname, |
| row_id, |
| ), |
| )?; |
|
|
| if update_name || update_authname { |
| let contact_id = ContactId::new(row_id); |
| update_chat_names(context, transaction, contact_id)?; |
| } |
| sth_modified = Modifier::Modified; |
| } |
| } else { |
| transaction.execute( |
| " |
| INSERT INTO contacts (name, name_normalized, addr, fingerprint, origin, authname) |
| VALUES (?, ?, ?, ?, ?, ?) |
| ", |
| ( |
| if manual { &name } else { "" }, |
| normalize_text(&name), |
| &addr, |
| fingerprint, |
| origin, |
| if manual { "" } else { &name }, |
| ), |
| )?; |
|
|
| sth_modified = Modifier::Created; |
| row_id = u32::try_from(transaction.last_insert_rowid())?; |
| if fingerprint.is_empty() { |
| info!(context, "Added contact id={row_id} addr={addr}."); |
| } else { |
| info!( |
| context, |
| "Added contact id={row_id} fpr={fingerprint} addr={addr}." |
| ); |
| } |
| } |
| Ok(row_id) |
| }) |
| .await?; |
|
|
| let contact_id = ContactId::new(row_id); |
|
|
| Ok((contact_id, sth_modified)) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| pub async fn add_address_book(context: &Context, addr_book: &str) -> Result<usize> { |
| let mut modify_cnt = 0; |
|
|
| for (name, addr) in split_address_book(addr_book) { |
| let (name, addr) = sanitize_name_and_addr(name, addr); |
| match ContactAddress::new(&addr) { |
| Ok(addr) => { |
| match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await { |
| Ok((_, modified)) => { |
| if modified != Modifier::None { |
| modify_cnt += 1 |
| } |
| } |
| Err(err) => { |
| warn!( |
| context, |
| "Failed to add address {} from address book: {}", addr, err |
| ); |
| } |
| } |
| } |
| Err(err) => { |
| warn!(context, "{:#}.", err); |
| } |
| } |
| } |
| if modify_cnt > 0 { |
| context.emit_event(EventType::ContactsChanged(None)); |
| } |
|
|
| Ok(modify_cnt) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| pub async fn get_all( |
| context: &Context, |
| listflags: u32, |
| query: Option<&str>, |
| ) -> Result<Vec<ContactId>> { |
| let self_addrs = context |
| .get_all_self_addrs() |
| .await? |
| .into_iter() |
| .collect::<HashSet<_>>(); |
| let mut add_self = false; |
| let mut ret = Vec::new(); |
| let flag_add_self = (listflags & constants::DC_GCL_ADD_SELF) != 0; |
| let flag_address = (listflags & constants::DC_GCL_ADDRESS) != 0; |
| let minimal_origin = if context.get_config_bool(Config::Bot).await? { |
| Origin::Unknown |
| } else { |
| Origin::IncomingReplyTo |
| }; |
| if query.is_some() { |
| let s3str_like_cmd = format!("%{}%", query.unwrap_or("").to_lowercase()); |
| context |
| .sql |
| .query_map( |
| " |
| SELECT c.id, c.addr FROM contacts c |
| WHERE c.id>? |
| AND (c.fingerprint='')=? |
| AND c.origin>=? |
| AND c.blocked=0 |
| AND (IFNULL(c.name_normalized,IIF(c.name='',c.authname,c.name)) LIKE ? OR c.addr LIKE ?) |
| ORDER BY c.origin>=? DESC, c.last_seen DESC, c.id DESC |
| ", |
| ( |
| ContactId::LAST_SPECIAL, |
| flag_address, |
| minimal_origin, |
| &s3str_like_cmd, |
| &s3str_like_cmd, |
| Origin::CreateChat, |
| ), |
| |row| { |
| let id: ContactId = row.get(0)?; |
| let addr: String = row.get(1)?; |
| Ok((id, addr)) |
| }, |
| |rows| { |
| for row in rows { |
| let (id, addr) = row?; |
| if !self_addrs.contains(&addr) { |
| ret.push(id); |
| } |
| } |
| Ok(()) |
| }, |
| ) |
| .await?; |
|
|
| if let Some(query) = query { |
| let self_addr = context |
| .get_config(Config::ConfiguredAddr) |
| .await? |
| .unwrap_or_default(); |
| let self_name = context |
| .get_config(Config::Displayname) |
| .await? |
| .unwrap_or_default(); |
| let self_name2 = stock_str::self_msg(context); |
|
|
| if self_addr.contains(query) |
| || self_name.contains(query) |
| || self_name2.await.contains(query) |
| { |
| add_self = true; |
| } |
| } else { |
| add_self = true; |
| } |
| } else { |
| add_self = true; |
|
|
| context |
| .sql |
| .query_map( |
| "SELECT id, addr FROM contacts |
| WHERE id>? |
| AND (fingerprint='')=? |
| AND origin>=? |
| AND blocked=0 |
| ORDER BY origin>=? DESC, last_seen DESC, id DESC", |
| ( |
| ContactId::LAST_SPECIAL, |
| flag_address, |
| minimal_origin, |
| Origin::CreateChat, |
| ), |
| |row| { |
| let id: ContactId = row.get(0)?; |
| let addr: String = row.get(1)?; |
| Ok((id, addr)) |
| }, |
| |rows| { |
| for row in rows { |
| let (id, addr) = row?; |
| if !self_addrs.contains(&addr) { |
| ret.push(id); |
| } |
| } |
| Ok(()) |
| }, |
| ) |
| .await?; |
| } |
|
|
| if flag_add_self && add_self { |
| ret.push(ContactId::SELF); |
| } |
|
|
| Ok(ret) |
| } |
|
|
| |
| |
| |
| |
| |
| async fn update_blocked_mailinglist_contacts(context: &Context) -> Result<()> { |
| context |
| .sql |
| .transaction(move |transaction| { |
| let mut stmt = transaction.prepare( |
| "SELECT name, grpid, type FROM chats WHERE (type=? OR type=?) AND blocked=?", |
| )?; |
| let rows = stmt.query_map( |
| (Chattype::Mailinglist, Chattype::InBroadcast, Blocked::Yes), |
| |row| { |
| let name: String = row.get(0)?; |
| let grpid: String = row.get(1)?; |
| let typ: Chattype = row.get(2)?; |
| Ok((name, grpid, typ)) |
| }, |
| )?; |
| let blocked_mailinglists = rows.collect::<std::result::Result<Vec<_>, _>>()?; |
| for (name, grpid, typ) in blocked_mailinglists { |
| let count = transaction.query_row( |
| "SELECT COUNT(id) FROM contacts WHERE addr=?", |
| [&grpid], |
| |row| { |
| let count: isize = row.get(0)?; |
| Ok(count) |
| }, |
| )?; |
| if count == 0 { |
| transaction.execute("INSERT INTO contacts (addr) VALUES (?)", [&grpid])?; |
| } |
|
|
| let fingerprint = if typ == Chattype::InBroadcast { |
| |
| |
| "Blocked_broadcast" |
| } else { |
| "" |
| }; |
| |
| transaction.execute( |
| " |
| UPDATE contacts |
| SET name=?, name_normalized=IIF(?1='',name_normalized,?), origin=?, blocked=1, fingerprint=? |
| WHERE addr=? |
| ", |
| ( |
| &name, |
| normalize_text(&name), |
| Origin::MailinglistAddress, |
| fingerprint, |
| &grpid, |
| ), |
| )?; |
| } |
| Ok(()) |
| }) |
| .await?; |
| Ok(()) |
| } |
|
|
| |
| pub async fn get_blocked_cnt(context: &Context) -> Result<usize> { |
| let count = context |
| .sql |
| .count( |
| "SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0", |
| (ContactId::LAST_SPECIAL,), |
| ) |
| .await?; |
| Ok(count) |
| } |
|
|
| |
| pub async fn get_all_blocked(context: &Context) -> Result<Vec<ContactId>> { |
| Contact::update_blocked_mailinglist_contacts(context) |
| .await |
| .context("cannot update blocked mailinglist contacts")?; |
|
|
| let list = context |
| .sql |
| .query_map_vec( |
| "SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY last_seen DESC, id DESC;", |
| (ContactId::LAST_SPECIAL,), |
| |row| { |
| let contact_id: ContactId = row.get(0)?; |
| Ok(contact_id) |
| } |
| ) |
| .await?; |
| Ok(list) |
| } |
|
|
| |
| |
| |
| |
| |
| pub async fn get_encrinfo(context: &Context, contact_id: ContactId) -> Result<String> { |
| ensure!( |
| !contact_id.is_special(), |
| "Can not provide encryption info for special contact" |
| ); |
|
|
| let contact = Contact::get_by_id(context, contact_id).await?; |
| let addr = context |
| .get_config(Config::ConfiguredAddr) |
| .await? |
| .unwrap_or_default(); |
|
|
| let Some(fingerprint_other) = contact.fingerprint() else { |
| return Ok(stock_str::encr_none(context).await); |
| }; |
| let fingerprint_other = fingerprint_other.to_string(); |
|
|
| let stock_message = if contact.public_key(context).await?.is_some() { |
| stock_str::e2e_available(context).await |
| } else { |
| stock_str::encr_none(context).await |
| }; |
|
|
| let finger_prints = stock_str::finger_prints(context).await; |
| let mut ret = format!("{stock_message}.\n{finger_prints}:"); |
|
|
| let fingerprint_self = load_self_public_key(context) |
| .await? |
| .dc_fingerprint() |
| .to_string(); |
| if addr < contact.addr { |
| cat_fingerprint( |
| &mut ret, |
| &stock_str::self_msg(context).await, |
| &addr, |
| &fingerprint_self, |
| ); |
| cat_fingerprint( |
| &mut ret, |
| contact.get_display_name(), |
| &contact.addr, |
| &fingerprint_other, |
| ); |
| } else { |
| cat_fingerprint( |
| &mut ret, |
| contact.get_display_name(), |
| &contact.addr, |
| &fingerprint_other, |
| ); |
| cat_fingerprint( |
| &mut ret, |
| &stock_str::self_msg(context).await, |
| &addr, |
| &fingerprint_self, |
| ); |
| } |
|
|
| Ok(ret) |
| } |
|
|
| |
| |
| |
| |
| |
| pub async fn delete(context: &Context, contact_id: ContactId) -> Result<()> { |
| ensure!(!contact_id.is_special(), "Can not delete special contact"); |
|
|
| context |
| .sql |
| .transaction(move |transaction| { |
| |
| |
| let deleted_contacts = transaction.execute( |
| "DELETE FROM contacts WHERE id=? |
| AND (SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?)=0;", |
| (contact_id, contact_id), |
| )?; |
| if deleted_contacts == 0 { |
| transaction.execute( |
| "UPDATE contacts SET origin=? WHERE id=?;", |
| (Origin::Hidden, contact_id), |
| )?; |
| } |
| Ok(()) |
| }) |
| .await?; |
|
|
| context.emit_event(EventType::ContactsChanged(None)); |
| Ok(()) |
| } |
|
|
| |
| pub async fn update_param(&self, context: &Context) -> Result<()> { |
| context |
| .sql |
| .execute( |
| "UPDATE contacts SET param=? WHERE id=?", |
| (self.param.to_string(), self.id), |
| ) |
| .await?; |
| Ok(()) |
| } |
|
|
| |
| pub async fn update_status(&self, context: &Context) -> Result<()> { |
| context |
| .sql |
| .execute( |
| "UPDATE contacts SET status=? WHERE id=?", |
| (&self.status, self.id), |
| ) |
| .await?; |
| Ok(()) |
| } |
|
|
| |
| pub fn get_id(&self) -> ContactId { |
| self.id |
| } |
|
|
| |
| pub fn get_addr(&self) -> &str { |
| &self.addr |
| } |
|
|
| |
| |
| pub fn is_key_contact(&self) -> bool { |
| self.fingerprint.is_some() |
| } |
|
|
| |
| |
| |
| pub fn fingerprint(&self) -> Option<Fingerprint> { |
| if let Some(fingerprint) = &self.fingerprint { |
| fingerprint.parse().ok() |
| } else { |
| None |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| pub async fn public_key(&self, context: &Context) -> Result<Option<SignedPublicKey>> { |
| if self.id == ContactId::SELF { |
| return Ok(Some(load_self_public_key(context).await?)); |
| } |
|
|
| if let Some(fingerprint) = &self.fingerprint { |
| if let Some(public_key_bytes) = context |
| .sql |
| .query_row_optional( |
| "SELECT public_key |
| FROM public_keys |
| WHERE fingerprint=?", |
| (fingerprint,), |
| |row| { |
| let bytes: Vec<u8> = row.get(0)?; |
| Ok(bytes) |
| }, |
| ) |
| .await? |
| { |
| let public_key = SignedPublicKey::from_slice(&public_key_bytes)?; |
| Ok(Some(public_key)) |
| } else { |
| Ok(None) |
| } |
| } else { |
| Ok(None) |
| } |
| } |
|
|
| |
| pub fn get_authname(&self) -> &str { |
| &self.authname |
| } |
|
|
| |
| |
| |
| |
| |
| pub fn get_name(&self) -> &str { |
| &self.name |
| } |
|
|
| |
| |
| |
| |
| |
| pub fn get_display_name(&self) -> &str { |
| if !self.name.is_empty() { |
| return &self.name; |
| } |
| if !self.authname.is_empty() { |
| return &self.authname; |
| } |
| &self.addr |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| pub fn get_name_n_addr(&self) -> String { |
| if !self.name.is_empty() { |
| format!("{} ({})", self.name, self.addr) |
| } else if !self.authname.is_empty() { |
| format!("{} ({})", self.authname, self.addr) |
| } else { |
| (&self.addr).into() |
| } |
| } |
|
|
| |
| |
| |
| pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> { |
| self.get_profile_image_ex(context, true).await |
| } |
|
|
| |
| |
| |
| async fn get_profile_image_ex( |
| &self, |
| context: &Context, |
| show_fallback_icon: bool, |
| ) -> Result<Option<PathBuf>> { |
| if self.id == ContactId::SELF { |
| if let Some(p) = context.get_config(Config::Selfavatar).await? { |
| return Ok(Some(PathBuf::from(p))); |
| } |
| } else if self.id == ContactId::DEVICE { |
| return Ok(Some(chat::get_device_icon(context).await?)); |
| } |
| if show_fallback_icon && !self.id.is_special() && !self.is_key_contact() { |
| return Ok(Some(chat::get_unencrypted_icon(context).await?)); |
| } |
| if let Some(image_rel) = self.param.get(Param::ProfileImage) |
| && !image_rel.is_empty() |
| { |
| return Ok(Some(get_abs_path(context, Path::new(image_rel)))); |
| } |
| Ok(None) |
| } |
|
|
| |
| |
| |
| pub fn get_color(&self) -> u32 { |
| get_color(self.id == ContactId::SELF, &self.addr, &self.fingerprint()) |
| } |
|
|
| |
| |
| |
| |
| pub async fn get_or_gen_color(&self, context: &Context) -> Result<u32> { |
| let mut fpr = self.fingerprint(); |
| if fpr.is_none() && self.id == ContactId::SELF { |
| fpr = Some(load_self_public_key(context).await?.dc_fingerprint()); |
| } |
| Ok(get_color(self.id == ContactId::SELF, &self.addr, &fpr)) |
| } |
|
|
| |
| |
| |
| pub fn get_status(&self) -> &str { |
| self.status.as_str() |
| } |
|
|
| |
| pub async fn e2ee_avail(&self, context: &Context) -> Result<bool> { |
| if self.id == ContactId::SELF { |
| |
| return Ok(true); |
| } |
| Ok(self.public_key(context).await?.is_some()) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| pub async fn is_verified(&self, context: &Context) -> Result<bool> { |
| |
| |
| if self.id == ContactId::SELF { |
| return Ok(true); |
| } |
|
|
| Ok(self.get_verifier_id(context).await?.is_some()) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<Option<ContactId>>> { |
| let verifier_id: u32 = context |
| .sql |
| .query_get_value("SELECT verifier FROM contacts WHERE id=?", (self.id,)) |
| .await? |
| .with_context(|| format!("Contact {} does not exist", self.id))?; |
|
|
| if verifier_id == 0 { |
| Ok(None) |
| } else if verifier_id == self.id.to_u32() { |
| Ok(Some(None)) |
| } else { |
| Ok(Some(Some(ContactId::new(verifier_id)))) |
| } |
| } |
|
|
| |
| pub async fn get_real_cnt(context: &Context) -> Result<usize> { |
| if !context.sql.is_open().await { |
| return Ok(0); |
| } |
|
|
| let count = context |
| .sql |
| .count( |
| "SELECT COUNT(*) FROM contacts WHERE id>?;", |
| (ContactId::LAST_SPECIAL,), |
| ) |
| .await?; |
| Ok(count) |
| } |
|
|
| |
| pub async fn real_exists_by_id(context: &Context, contact_id: ContactId) -> Result<bool> { |
| if contact_id.is_special() { |
| return Ok(false); |
| } |
|
|
| let exists = context |
| .sql |
| .exists("SELECT COUNT(*) FROM contacts WHERE id=?;", (contact_id,)) |
| .await?; |
| Ok(exists) |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| pub fn get_color(is_self: bool, addr: &str, fingerprint: &Option<Fingerprint>) -> u32 { |
| if let Some(fingerprint) = fingerprint { |
| str_to_color(&fingerprint.hex()) |
| } else if is_self { |
| 0x808080 |
| } else { |
| str_to_color(&to_lowercase(addr)) |
| } |
| } |
|
|
| |
| |
| |
| fn update_chat_names( |
| context: &Context, |
| transaction: &rusqlite::Connection, |
| contact_id: ContactId, |
| ) -> Result<()> { |
| let chat_id: Option<ChatId> = transaction.query_row( |
| "SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)", |
| (Chattype::Single, contact_id), |
| |row| { |
| let chat_id: ChatId = row.get(0)?; |
| Ok(chat_id) |
| } |
| ).optional()?; |
|
|
| if let Some(chat_id) = chat_id { |
| let (addr, name, authname) = transaction.query_row( |
| "SELECT addr, name, authname |
| FROM contacts |
| WHERE id=?", |
| (contact_id,), |
| |row| { |
| let addr: String = row.get(0)?; |
| let name: String = row.get(1)?; |
| let authname: String = row.get(2)?; |
| Ok((addr, name, authname)) |
| }, |
| )?; |
|
|
| let chat_name = if !name.is_empty() { |
| name |
| } else if !authname.is_empty() { |
| authname |
| } else { |
| addr |
| }; |
|
|
| let count = transaction.execute( |
| "UPDATE chats SET name=?1, name_normalized=?2 WHERE id=?3 AND name!=?1", |
| (&chat_name, normalize_text(&chat_name), chat_id), |
| )?; |
|
|
| if count > 0 { |
| |
| context.emit_event(EventType::ChatModified(chat_id)); |
| chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id); |
| } |
| } |
|
|
| Ok(()) |
| } |
|
|
| pub(crate) async fn set_blocked( |
| context: &Context, |
| sync: sync::Sync, |
| contact_id: ContactId, |
| new_blocking: bool, |
| ) -> Result<()> { |
| ensure!( |
| !contact_id.is_special(), |
| "Can't block special contact {contact_id}" |
| ); |
| let contact = Contact::get_by_id(context, contact_id).await?; |
|
|
| if contact.blocked != new_blocking { |
| context |
| .sql |
| .execute( |
| "UPDATE contacts SET blocked=? WHERE id=?;", |
| (i32::from(new_blocking), contact_id), |
| ) |
| .await?; |
|
|
| |
| |
| |
| |
| |
| if context |
| .sql |
| .execute( |
| r#" |
| UPDATE chats |
| SET blocked=? |
| WHERE type=? AND id IN ( |
| SELECT chat_id FROM chats_contacts WHERE contact_id=? |
| ); |
| "#, |
| (new_blocking, Chattype::Single, contact_id), |
| ) |
| .await |
| .is_ok() |
| { |
| Contact::mark_noticed(context, contact_id).await?; |
| context.emit_event(EventType::ContactsChanged(Some(contact_id))); |
| } |
|
|
| |
| |
| if !new_blocking |
| && contact.origin == Origin::MailinglistAddress |
| && let Some((chat_id, ..)) = chat::get_chat_id_by_grpid(context, &contact.addr).await? |
| { |
| chat_id.unblock_ex(context, Nosync).await?; |
| } |
|
|
| if sync.into() { |
| let action = match new_blocking { |
| true => chat::SyncAction::Block, |
| false => chat::SyncAction::Unblock, |
| }; |
| let sync_id = if let Some(fingerprint) = contact.fingerprint() { |
| chat::SyncId::ContactFingerprint(fingerprint.hex()) |
| } else { |
| chat::SyncId::ContactAddr(contact.addr.clone()) |
| }; |
|
|
| chat::sync(context, sync_id, action) |
| .await |
| .log_err(context) |
| .ok(); |
| } |
| } |
|
|
| chatlist_events::emit_chatlist_changed(context); |
| Ok(()) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| pub(crate) async fn set_profile_image( |
| context: &Context, |
| contact_id: ContactId, |
| profile_image: &AvatarAction, |
| ) -> Result<()> { |
| let mut contact = Contact::get_by_id(context, contact_id).await?; |
| let changed = match profile_image { |
| AvatarAction::Change(profile_image) => { |
| if contact_id == ContactId::SELF { |
| context |
| .set_config_ex(Nosync, Config::Selfavatar, Some(profile_image)) |
| .await?; |
| } else { |
| contact.param.set(Param::ProfileImage, profile_image); |
| } |
| true |
| } |
| AvatarAction::Delete => { |
| if contact_id == ContactId::SELF { |
| context |
| .set_config_ex(Nosync, Config::Selfavatar, None) |
| .await?; |
| } else { |
| contact.param.remove(Param::ProfileImage); |
| } |
| true |
| } |
| }; |
| if changed { |
| contact.update_param(context).await?; |
| context.emit_event(EventType::ContactsChanged(Some(contact_id))); |
| chatlist_events::emit_chatlist_item_changed_for_contact_chat(context, contact_id).await; |
| } |
| Ok(()) |
| } |
|
|
| |
| |
| |
| pub(crate) async fn set_status( |
| context: &Context, |
| contact_id: ContactId, |
| status: String, |
| ) -> Result<()> { |
| if contact_id == ContactId::SELF { |
| context |
| .set_config_ex(Nosync, Config::Selfstatus, Some(&status)) |
| .await?; |
| } else { |
| let mut contact = Contact::get_by_id(context, contact_id).await?; |
|
|
| if contact.status != status { |
| contact.status = status; |
| contact.update_status(context).await?; |
| context.emit_event(EventType::ContactsChanged(Some(contact_id))); |
| } |
| } |
| Ok(()) |
| } |
|
|
| |
| pub(crate) async fn update_last_seen( |
| context: &Context, |
| contact_id: ContactId, |
| timestamp: i64, |
| ) -> Result<()> { |
| ensure!( |
| !contact_id.is_special(), |
| "Can not update special contact last seen timestamp" |
| ); |
|
|
| if context |
| .sql |
| .execute( |
| "UPDATE contacts SET last_seen = ?1 WHERE last_seen < ?1 AND id = ?2", |
| (timestamp, contact_id), |
| ) |
| .await? |
| > 0 |
| && timestamp > time() - SEEN_RECENTLY_SECONDS |
| { |
| context.emit_event(EventType::ContactsChanged(Some(contact_id))); |
| context |
| .scheduler |
| .interrupt_recently_seen(contact_id, timestamp) |
| .await; |
| } |
| Ok(()) |
| } |
|
|
| |
| |
| |
| pub(crate) async fn mark_contact_id_as_verified( |
| context: &Context, |
| contact_id: ContactId, |
| verifier_id: Option<ContactId>, |
| ) -> Result<()> { |
| ensure_and_debug_assert_ne!(contact_id, ContactId::SELF,); |
| ensure_and_debug_assert_ne!( |
| Some(contact_id), |
| verifier_id, |
| "Contact cannot be verified by self", |
| ); |
| let by_self = verifier_id == Some(ContactId::SELF); |
| let mut verifier_id = verifier_id.unwrap_or(contact_id); |
| context |
| .sql |
| .transaction(|transaction| { |
| let contact_fingerprint: String = transaction.query_row( |
| "SELECT fingerprint FROM contacts WHERE id=?", |
| (contact_id,), |
| |row| row.get(0), |
| )?; |
| if contact_fingerprint.is_empty() { |
| bail!("Non-key-contact {contact_id} cannot be verified"); |
| } |
| if verifier_id != ContactId::SELF { |
| let (verifier_fingerprint, verifier_verifier_id): (String, ContactId) = transaction |
| .query_row( |
| "SELECT fingerprint, verifier FROM contacts WHERE id=?", |
| (verifier_id,), |
| |row| Ok((row.get(0)?, row.get(1)?)), |
| )?; |
| if verifier_fingerprint.is_empty() { |
| bail!( |
| "Contact {contact_id} cannot be verified by non-key-contact {verifier_id}" |
| ); |
| } |
| ensure!( |
| verifier_id == contact_id || verifier_verifier_id != ContactId::UNDEFINED, |
| "Contact {contact_id} cannot be verified by unverified contact {verifier_id}", |
| ); |
| if verifier_verifier_id == verifier_id { |
| |
| |
| |
| |
| verifier_id = contact_id; |
| } |
| } |
| transaction.execute( |
| "UPDATE contacts SET verifier=?1 |
| WHERE id=?2 AND (verifier=0 OR verifier=id OR ?3)", |
| (verifier_id, contact_id, by_self), |
| )?; |
| Ok(()) |
| }) |
| .await?; |
| Ok(()) |
| } |
|
|
| fn cat_fingerprint(ret: &mut String, name: &str, addr: &str, fingerprint: &str) { |
| *ret += &format!("\n\n{name} ({addr}):\n{fingerprint}"); |
| } |
|
|
| fn split_address_book(book: &str) -> Vec<(&str, &str)> { |
| book.lines() |
| .collect::<Vec<&str>>() |
| .chunks(2) |
| .filter_map(|chunk| { |
| let name = chunk.first()?; |
| let addr = chunk.get(1)?; |
| Some((*name, *addr)) |
| }) |
| .collect() |
| } |
|
|
| #[derive(Debug)] |
| pub(crate) struct RecentlySeenInterrupt { |
| contact_id: ContactId, |
| timestamp: i64, |
| } |
|
|
| #[derive(Debug)] |
| pub(crate) struct RecentlySeenLoop { |
| |
| handle: task::JoinHandle<()>, |
|
|
| interrupt_send: Sender<RecentlySeenInterrupt>, |
| } |
|
|
| impl RecentlySeenLoop { |
| pub(crate) fn new(context: Context) -> Self { |
| let (interrupt_send, interrupt_recv) = channel::bounded(1); |
|
|
| let handle = task::spawn(Self::run(context, interrupt_recv)); |
| Self { |
| handle, |
| interrupt_send, |
| } |
| } |
|
|
| async fn run(context: Context, interrupt: Receiver<RecentlySeenInterrupt>) { |
| type MyHeapElem = (Reverse<i64>, ContactId); |
|
|
| let now = SystemTime::now(); |
| let now_ts = now |
| .duration_since(SystemTime::UNIX_EPOCH) |
| .unwrap_or_default() |
| .as_secs() as i64; |
|
|
| |
| |
| |
| |
| |
| let mut unseen_queue: BinaryHeap<MyHeapElem> = context |
| .sql |
| .query_map_collect( |
| "SELECT id, last_seen FROM contacts |
| WHERE last_seen > ?", |
| (now_ts - SEEN_RECENTLY_SECONDS,), |
| |row| { |
| let contact_id: ContactId = row.get("id")?; |
| let last_seen: i64 = row.get("last_seen")?; |
| Ok((Reverse(last_seen + SEEN_RECENTLY_SECONDS), contact_id)) |
| }, |
| ) |
| .await |
| .unwrap_or_default(); |
|
|
| loop { |
| let now = SystemTime::now(); |
| let (until, contact_id) = |
| if let Some((Reverse(timestamp), contact_id)) = unseen_queue.peek() { |
| ( |
| UNIX_EPOCH |
| + Duration::from_secs((*timestamp).try_into().unwrap_or(u64::MAX)) |
| + Duration::from_secs(1), |
| Some(contact_id), |
| ) |
| } else { |
| |
| (now + Duration::from_secs(86400), None) |
| }; |
|
|
| if let Ok(duration) = until.duration_since(now) { |
| info!( |
| context, |
| "Recently seen loop waiting for {} or interrupt", |
| duration_to_str(duration) |
| ); |
|
|
| match timeout(duration, interrupt.recv()).await { |
| Err(_) => { |
| |
| if let Some(contact_id) = contact_id { |
| context.emit_event(EventType::ContactsChanged(Some(*contact_id))); |
| chatlist_events::emit_chatlist_item_changed_for_contact_chat( |
| &context, |
| *contact_id, |
| ) |
| .await; |
| unseen_queue.pop(); |
| } |
| } |
| Ok(Err(err)) => { |
| warn!( |
| context, |
| "Error receiving an interruption in recently seen loop: {}", err |
| ); |
| |
| |
| return; |
| } |
| Ok(Ok(RecentlySeenInterrupt { |
| contact_id, |
| timestamp, |
| })) => { |
| |
| if contact_id != ContactId::UNDEFINED { |
| unseen_queue |
| .push((Reverse(timestamp + SEEN_RECENTLY_SECONDS), contact_id)); |
| } |
| } |
| } |
| } else { |
| info!( |
| context, |
| "Recently seen loop is not waiting, event is already due." |
| ); |
|
|
| |
| if let Some(contact_id) = contact_id { |
| context.emit_event(EventType::ContactsChanged(Some(*contact_id))); |
| chatlist_events::emit_chatlist_item_changed_for_contact_chat( |
| &context, |
| *contact_id, |
| ) |
| .await; |
| } |
| unseen_queue.pop(); |
| } |
| } |
| } |
|
|
| pub(crate) fn try_interrupt(&self, contact_id: ContactId, timestamp: i64) { |
| self.interrupt_send |
| .try_send(RecentlySeenInterrupt { |
| contact_id, |
| timestamp, |
| }) |
| .ok(); |
| } |
|
|
| #[cfg(test)] |
| pub(crate) async fn interrupt(&self, contact_id: ContactId, timestamp: i64) { |
| self.interrupt_send |
| .send(RecentlySeenInterrupt { |
| contact_id, |
| timestamp, |
| }) |
| .await |
| .unwrap(); |
| } |
|
|
| pub(crate) async fn abort(self) { |
| self.handle.abort(); |
|
|
| |
| |
| |
| self.handle.await.ok(); |
| } |
| } |
|
|
| #[cfg(test)] |
| mod contact_tests; |
|
|