From 5c571520a0cbc832ccdeb8e6285f8ee6303bcdad Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 21 Nov 2021 12:14:11 +0000 Subject: [PATCH] contact: use `last_seen` column It was there since the C core, labeled with "/* last_seen is for future use */" but never actually used. The comment was lost during the translation from C to Rust. --- deltachat-ffi/deltachat.h | 10 +++++ deltachat-ffi/src/lib.rs | 10 +++++ python/src/deltachat/contact.py | 18 ++++++--- src/contact.rs | 71 +++++++++++++++++++++++++++++++-- src/dc_receive_imf.rs | 4 ++ 5 files changed, 104 insertions(+), 9 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index dc84e8642..65a6a88f2 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -4382,6 +4382,16 @@ uint32_t dc_contact_get_color (const dc_contact_t* contact); */ char* dc_contact_get_status (const dc_contact_t* contact); +/** + * Get the contact's last seen timestamp. + * + * @memberof dc_contact_t + * @param contact The contact object. + * @return Last seen timestamp. + * 0 on error or if the contact was never seen. + */ +int64_t dc_contact_get_last_seen (const dc_contact_t* contact); + /** * Check if a contact is blocked. * diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 3037882b7..b8962af9a 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -3544,6 +3544,16 @@ pub unsafe extern "C" fn dc_contact_get_status(contact: *mut dc_contact_t) -> *m ffi_contact.contact.get_status().strdup() } +#[no_mangle] +pub unsafe extern "C" fn dc_contact_get_last_seen(contact: *mut dc_contact_t) -> i64 { + if contact.is_null() { + eprintln!("ignoring careless call to dc_contact_get_last_seen()"); + return 0; + } + let ffi_contact = &*contact; + ffi_contact.contact.last_seen() +} + #[no_mangle] pub unsafe extern "C" fn dc_contact_is_blocked(contact: *mut dc_contact_t) -> libc::c_int { if contact.is_null() { diff --git a/python/src/deltachat/contact.py b/python/src/deltachat/contact.py index f29f1a096..83e415081 100644 --- a/python/src/deltachat/contact.py +++ b/python/src/deltachat/contact.py @@ -1,12 +1,13 @@ """ Contact object. """ -from . import props -from .cutil import from_dc_charpointer, from_optional_dc_charpointer -from .capi import lib, ffi -from .chat import Chat -from . import const +from datetime import date, datetime, timezone from typing import Optional +from . import const, props +from .capi import ffi, lib +from .chat import Chat +from .cutil import from_dc_charpointer, from_optional_dc_charpointer + class Contact(object): """ Delta-Chat Contact. @@ -48,6 +49,13 @@ class Contact(object): # deprecated alias display_name = name + @props.with_doc + def last_seen(self) -> date: + """Last seen timestamp.""" + return datetime.fromtimestamp( + lib.dc_contact_get_last_seen(self._dc_contact), timezone.utc + ) + def is_blocked(self): """ Return True if the contact is blocked. """ return lib.dc_contact_is_blocked(self._dc_contact) diff --git a/src/contact.rs b/src/contact.rs index 10b88e2e5..8d53c4214 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -66,6 +66,9 @@ pub struct Contact { /// Blocked state. Use dc_contact_is_blocked to access this field. pub blocked: bool, + /// Time when the contact was seen last time, Unix time in seconds. + last_seen: i64, + /// The origin/source of the contact. pub origin: Origin, @@ -184,7 +187,8 @@ impl Contact { let mut contact = context .sql .query_row( - "SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status + "SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen, + c.authname, c.param, c.status FROM contacts c WHERE c.id=?;", paramsv![contact_id as i32], @@ -193,15 +197,17 @@ impl Contact { let addr: String = row.get(1)?; let origin: Origin = row.get(2)?; let blocked: Option = row.get(3)?; - let authname: String = row.get(4)?; - let param: String = row.get(5)?; - let status: Option = row.get(6)?; + let last_seen: i64 = row.get(4)?; + let authname: String = row.get(5)?; + let param: String = row.get(6)?; + let status: Option = row.get(7)?; let contact = Self { id: contact_id, name, authname, addr, blocked: blocked.unwrap_or_default(), + last_seen, origin, param: param.parse().unwrap_or_default(), status: status.unwrap_or_default(), @@ -233,6 +239,11 @@ impl Contact { self.blocked } + /// Returns last seen timestamp. + pub fn last_seen(&self) -> i64 { + self.last_seen + } + /// Check if a contact is blocked. pub async fn is_blocked_load(context: &Context, id: u32) -> Result { let blocked = Self::load_from_db(context, id).await?.blocked; @@ -1288,6 +1299,27 @@ pub(crate) async fn set_status( Ok(()) } +/// 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, + timestamp: i64, +) -> Result<()> { + ensure!( + contact_id > DC_CONTACT_ID_LAST_SPECIAL, + "Can not update special contact last seen timestamp" + ); + + context + .sql + .execute( + "UPDATE contacts SET last_seen = ?1 WHERE last_seen < ?1 AND id = ?2", + paramsv![timestamp, contact_id], + ) + .await?; + Ok(()) +} + /// Normalize a name. /// /// - Remove quotes (come from some bad MUA implementations) @@ -1374,6 +1406,7 @@ mod tests { use super::*; use crate::chat::send_text_msg; + use crate::dc_receive_imf::dc_receive_imf; use crate::message::Message; use crate::test_utils::{self, TestContext}; @@ -2031,4 +2064,34 @@ CCCB 5AA9 F6E1 141C 9431 Ok(()) } + + #[async_std::test] + async fn test_last_seen() -> Result<()> { + let alice = TestContext::new_alice().await; + + let (contact_id, _) = + Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated) + .await?; + let contact = Contact::load_from_db(&alice, contact_id).await?; + assert_eq!(contact.last_seen(), 0); + + let mime = br#"Subject: Hello +Message-ID: message@example.net +To: Alice +From: Bob +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no +Chat-Version: 1.0 +Date: Sun, 22 Mar 2020 22:37:55 +0000 + +Hi."#; + dc_receive_imf(&alice, mime, "Inbox", 1, false).await?; + let msg = alice.get_last_msg().await; + + let timestamp = msg.get_timestamp(); + assert!(timestamp > 0); + let contact = Contact::load_from_db(&alice, contact_id).await?; + assert_eq!(contact.last_seen(), timestamp); + + Ok(()) + } } diff --git a/src/dc_receive_imf.rs b/src/dc_receive_imf.rs index be4cebec9..d3692a742 100644 --- a/src/dc_receive_imf.rs +++ b/src/dc_receive_imf.rs @@ -216,6 +216,10 @@ pub(crate) async fn dc_receive_imf_inner( .await .map_err(|err| err.context("add_parts error"))?; + if from_id > DC_CONTACT_ID_LAST_SPECIAL { + contact::update_last_seen(context, from_id, sent_timestamp).await?; + } + // Update gossiped timestamp for the chat if someone else or our other device sent // Autocrypt-Gossip for all recipients in the chat to avoid sending Autocrypt-Gossip ourselves // and waste traffic.