mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 21:46:35 +03:00
Use footer as a contact status
This commit is contained in:
@@ -4099,6 +4099,19 @@ char* dc_contact_get_profile_image (const dc_contact_t* contact);
|
||||
uint32_t dc_contact_get_color (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Get the contact's status.
|
||||
*
|
||||
* Status is the last signature received in a message from this contact.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return Contact status, if any.
|
||||
* Empty string otherwise.
|
||||
* Must be released by using dc_str_unref() after usage.
|
||||
*/
|
||||
char* dc_contact_get_status (const dc_contact_t* contact);
|
||||
|
||||
/**
|
||||
* Check if a contact is blocked.
|
||||
*
|
||||
|
||||
@@ -3275,6 +3275,16 @@ pub unsafe extern "C" fn dc_contact_get_color(contact: *mut dc_contact_t) -> u32
|
||||
ffi_contact.contact.get_color()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_get_status(contact: *mut dc_contact_t) -> *mut libc::c_char {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_status()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.get_status().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_is_blocked(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
|
||||
@@ -77,6 +77,14 @@ class Contact(object):
|
||||
return None
|
||||
return from_dc_charpointer(dc_res)
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Get contact status.
|
||||
|
||||
:returns: contact status, empty string if it doesn't exist.
|
||||
"""
|
||||
return from_dc_charpointer(lib.dc_contact_get_status(self._dc_contact))
|
||||
|
||||
def create_chat(self):
|
||||
""" create or get an existing 1:1 chat object for the specified contact or contact id.
|
||||
|
||||
|
||||
@@ -2167,6 +2167,23 @@ class TestOnlineAccount:
|
||||
updated_name = update_name()
|
||||
assert updated_name == "Renamed"
|
||||
|
||||
def test_status(self, acfactory):
|
||||
"""Test that status is transferred over the network."""
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac1.set_config("selfstatus", "New status")
|
||||
chat12.send_text("hi")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hi"
|
||||
assert msg.get_sender_contact().status == "New status"
|
||||
|
||||
ac1.set_config("selfstatus", "")
|
||||
chat12.send_text("hello")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
assert msg.get_sender_contact().status == ""
|
||||
|
||||
def test_group_quote(self, acfactory, lp):
|
||||
"""Test quoting in a group with a new member who have not seen the quoted message."""
|
||||
ac1, ac2, ac3 = accounts = acfactory.get_many_online_accounts(3)
|
||||
|
||||
@@ -71,6 +71,9 @@ pub struct Contact {
|
||||
|
||||
/// Parameters as Param::ProfileImage
|
||||
pub param: Params,
|
||||
|
||||
/// Last seen message signature for this contact, to be displayed in the profile.
|
||||
status: String,
|
||||
}
|
||||
|
||||
/// Possible origins of a contact.
|
||||
@@ -172,7 +175,7 @@ impl Contact {
|
||||
let mut res = context
|
||||
.sql
|
||||
.query_row(
|
||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param
|
||||
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status
|
||||
FROM contacts c
|
||||
WHERE c.id=?;",
|
||||
paramsv![contact_id as i32],
|
||||
@@ -185,6 +188,7 @@ impl Contact {
|
||||
blocked: row.get::<_, Option<i32>>(3)?.unwrap_or_default() != 0,
|
||||
origin: row.get(2)?,
|
||||
param: row.get::<_, String>(5)?.parse().unwrap_or_default(),
|
||||
status: row.get(6).unwrap_or_default(),
|
||||
};
|
||||
Ok(contact)
|
||||
},
|
||||
@@ -196,6 +200,10 @@ impl Contact {
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
res.status = context
|
||||
.get_config(Config::Selfstatus)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
} else if contact_id == DC_CONTACT_ID_DEVICE {
|
||||
res.name = context
|
||||
.stock_str(StockMessage::DeviceMessages)
|
||||
@@ -841,7 +849,8 @@ impl Contact {
|
||||
Ok(contact)
|
||||
}
|
||||
|
||||
pub async fn update_param(&mut self, context: &Context) -> Result<()> {
|
||||
/// Updates `param` column in the database.
|
||||
pub async fn update_param(&self, context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -852,6 +861,18 @@ impl Contact {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates `status` column in the database.
|
||||
pub async fn update_status(&self, context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET status=? WHERE id=?",
|
||||
paramsv![self.status, self.id as i32],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the ID of the contact.
|
||||
pub fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
@@ -932,6 +953,13 @@ impl Contact {
|
||||
dc_str_to_color(&self.addr)
|
||||
}
|
||||
|
||||
/// Gets the contact's status.
|
||||
///
|
||||
/// Status is the last signature received in a message from this contact.
|
||||
pub fn get_status(&self) -> &str {
|
||||
self.status.as_str()
|
||||
}
|
||||
|
||||
/// Check if a contact was verified. E.g. by a secure-join QR code scan
|
||||
/// and if the key has not changed since this verification.
|
||||
///
|
||||
@@ -1163,6 +1191,18 @@ pub(crate) async fn set_profile_image(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets contact status.
|
||||
pub(crate) async fn set_status(context: &Context, contact_id: u32, status: String) -> Result<()> {
|
||||
let mut contact = Contact::load_from_db(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(())
|
||||
}
|
||||
|
||||
/// Normalize a name.
|
||||
///
|
||||
/// - Remove quotes (come from some bad MUA implementations)
|
||||
|
||||
@@ -237,6 +237,17 @@ pub(crate) async fn dc_receive_imf_inner(
|
||||
};
|
||||
}
|
||||
|
||||
// Always update the status, even if there is no footer, to allow removing the status.
|
||||
if let Err(err) = contact::set_status(
|
||||
&context,
|
||||
from_id,
|
||||
mime_parser.footer.clone().unwrap_or_default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(context, "cannot update contact status: {}", err);
|
||||
}
|
||||
|
||||
// Get user-configured server deletion
|
||||
let delete_server_after = context.get_config_delete_server_after().await;
|
||||
|
||||
|
||||
@@ -390,10 +390,11 @@ mod tests {
|
||||
let input = include_str!("../test-data/message/gmx-quote-body.eml");
|
||||
let dehtml = dehtml(input).unwrap();
|
||||
println!("{}", dehtml);
|
||||
let (msg, forwarded, cut, top_quote) = simplify(dehtml, false);
|
||||
let (msg, forwarded, cut, top_quote, footer) = simplify(dehtml, false);
|
||||
assert_eq!(msg, "Test");
|
||||
assert_eq!(forwarded, false);
|
||||
assert_eq!(cut, false);
|
||||
assert_eq!(top_quote.as_deref(), Some("test"));
|
||||
assert_eq!(footer, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,9 @@ pub struct MimeMessage {
|
||||
pub(crate) mdn_reports: Vec<Report>,
|
||||
pub(crate) failure_report: Option<FailureReport>,
|
||||
|
||||
/// Standard USENET signature, if any.
|
||||
pub(crate) footer: Option<String>,
|
||||
|
||||
// if this flag is set, the parts/text/etc. are just close to the original mime-message;
|
||||
// clients should offer a way to view the original message in this case
|
||||
pub is_mime_modified: bool,
|
||||
@@ -233,6 +236,7 @@ impl MimeMessage {
|
||||
user_avatar: None,
|
||||
group_avatar: None,
|
||||
failure_report: None,
|
||||
footer: None,
|
||||
is_mime_modified: false,
|
||||
decoded_data: Vec::new(),
|
||||
};
|
||||
@@ -752,9 +756,9 @@ impl MimeMessage {
|
||||
|
||||
let mut dehtml_failed = false;
|
||||
|
||||
let (simplified_txt, is_forwarded, is_cut, top_quote) =
|
||||
let (simplified_txt, is_forwarded, is_cut, top_quote, footer) =
|
||||
if decoded_data.is_empty() {
|
||||
("".to_string(), false, false, None)
|
||||
("".to_string(), false, false, None, None)
|
||||
} else {
|
||||
let is_html = mime_type == mime::TEXT_HTML;
|
||||
let out = if is_html {
|
||||
@@ -814,6 +818,8 @@ impl MimeMessage {
|
||||
if is_forwarded {
|
||||
self.is_forwarded = true;
|
||||
}
|
||||
|
||||
self.footer = footer;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -17,16 +17,16 @@ pub fn escape_message_footer_marks(text: &str) -> String {
|
||||
}
|
||||
|
||||
/// Remove standard (RFC 3676, §4.3) footer if it is found.
|
||||
/// Returns `(lines, is_footer_removed)` tuple;
|
||||
/// `is_footer_removed` is set to `true` if the footer was actually removed from `lines`
|
||||
/// Returns `(lines, footer_lines)` tuple;
|
||||
/// `footer_lines` is set to `Some` if the footer was actually removed from `lines`
|
||||
/// (which is equal to the input array otherwise).
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_message_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
fn remove_message_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<&'a [&'a str]>) {
|
||||
let mut nearly_standard_footer = None;
|
||||
for (ix, &line) in lines.iter().enumerate() {
|
||||
match line {
|
||||
// some providers encode `-- ` to `-- =20` which results in `-- `
|
||||
"-- " | "-- " => return (&lines[..ix], true),
|
||||
"-- " | "-- " => return (&lines[..ix], lines.get(ix + 1..)),
|
||||
// some providers encode `-- ` to `=2D-` which results in only `--`;
|
||||
// use that only when no other footer is found
|
||||
// and if the line before is empty and the line after is not empty
|
||||
@@ -42,9 +42,9 @@ fn remove_message_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
}
|
||||
}
|
||||
if let Some(ix) = nearly_standard_footer {
|
||||
return (&lines[..ix], true);
|
||||
return (&lines[..ix], lines.get(ix + 1..));
|
||||
}
|
||||
(lines, false)
|
||||
(lines, None)
|
||||
}
|
||||
|
||||
/// Remove nonstandard footer and a boolean indicating whether such footer was removed.
|
||||
@@ -73,9 +73,12 @@ pub(crate) fn split_lines(buf: &str) -> Vec<&str> {
|
||||
|
||||
/// Simplify message text for chat display.
|
||||
/// Remove quotes, signatures, trailing empty lines etc.
|
||||
/// Returns `(text, is_forwarded, is_cut, quote)` tuple,
|
||||
/// Returns `(text, is_forwarded, is_cut, quote, footer)` tuple,
|
||||
/// returning the simplified text and some additional information gained from the input.
|
||||
pub fn simplify(mut input: String, is_chat_message: bool) -> (String, bool, bool, Option<String>) {
|
||||
pub fn simplify(
|
||||
mut input: String,
|
||||
is_chat_message: bool,
|
||||
) -> (String, bool, bool, Option<String>, Option<String>) {
|
||||
let mut is_cut = false;
|
||||
|
||||
input.retain(|c| c != '\r');
|
||||
@@ -84,8 +87,9 @@ pub fn simplify(mut input: String, is_chat_message: bool) -> (String, bool, bool
|
||||
|
||||
let (lines, mut top_quote) = remove_top_quote(lines);
|
||||
let original_lines = &lines;
|
||||
let (lines, footer_removed) = remove_message_footer(lines);
|
||||
is_cut = is_cut || footer_removed;
|
||||
let (lines, footer_lines) = remove_message_footer(lines);
|
||||
let footer = footer_lines.map(|footer_lines| render_message(footer_lines, false));
|
||||
is_cut = is_cut || footer.is_some();
|
||||
|
||||
let text = if is_chat_message {
|
||||
render_message(lines, false)
|
||||
@@ -108,13 +112,13 @@ pub fn simplify(mut input: String, is_chat_message: bool) -> (String, bool, bool
|
||||
if !is_chat_message {
|
||||
top_quote = top_quote.map(|quote| {
|
||||
let quote_lines = split_lines("e);
|
||||
let (quote_lines, footer_removed) = remove_message_footer("e_lines);
|
||||
is_cut = is_cut || footer_removed;
|
||||
let (quote_lines, quote_footer_lines) = remove_message_footer("e_lines);
|
||||
is_cut = is_cut || quote_footer_lines.is_some();
|
||||
|
||||
render_message(quote_lines, false)
|
||||
});
|
||||
}
|
||||
(text, is_forwarded, is_cut, top_quote)
|
||||
(text, is_forwarded, is_cut, top_quote, footer)
|
||||
}
|
||||
|
||||
/// Skips "forwarded message" header.
|
||||
@@ -269,7 +273,7 @@ mod tests {
|
||||
#[test]
|
||||
// proptest does not support [[:graphical:][:space:]] regex.
|
||||
fn test_simplify_plain_text_fuzzy(input in "[!-~\t \n]+") {
|
||||
let (output, _is_forwarded, _, _) = simplify(input, true);
|
||||
let (output, _is_forwarded, _, _, _) = simplify(input, true);
|
||||
assert!(output.split('\n').all(|s| s != "-- "));
|
||||
}
|
||||
}
|
||||
@@ -277,7 +281,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_dont_remove_whole_message() {
|
||||
let input = "\n------\nFailed\n------\n\nUh-oh, this workflow did not succeed!\n\nlots of other text".to_string();
|
||||
let (plain, is_forwarded, is_cut, _) = simplify(input, false);
|
||||
let (plain, is_forwarded, is_cut, _, _) = simplify(input, false);
|
||||
assert_eq!(
|
||||
plain,
|
||||
"------\nFailed\n------\n\nUh-oh, this workflow did not succeed!\n\nlots of other text"
|
||||
@@ -289,16 +293,20 @@ mod tests {
|
||||
#[test]
|
||||
fn test_chat_message() {
|
||||
let input = "Hi! How are you?\n\n---\n\nI am good.\n-- \nSent with my Delta Chat Messenger: https://delta.chat".to_string();
|
||||
let (plain, is_forwarded, is_cut, _) = simplify(input, true);
|
||||
let (plain, is_forwarded, is_cut, _, footer) = simplify(input, true);
|
||||
assert_eq!(plain, "Hi! How are you?\n\n---\n\nI am good.");
|
||||
assert!(!is_forwarded);
|
||||
assert!(is_cut);
|
||||
assert_eq!(
|
||||
footer.unwrap(),
|
||||
"Sent with my Delta Chat Messenger: https://delta.chat"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simplify_trim() {
|
||||
let input = "line1\n\r\r\rline2".to_string();
|
||||
let (plain, is_forwarded, is_cut, _) = simplify(input, false);
|
||||
let (plain, is_forwarded, is_cut, _, _) = simplify(input, false);
|
||||
|
||||
assert_eq!(plain, "line1\nline2");
|
||||
assert!(!is_forwarded);
|
||||
@@ -308,11 +316,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_simplify_forwarded_message() {
|
||||
let input = "---------- Forwarded message ----------\r\nFrom: test@example.com\r\n\r\nForwarded message\r\n-- \r\nSignature goes here".to_string();
|
||||
let (plain, is_forwarded, is_cut, _) = simplify(input, false);
|
||||
let (plain, is_forwarded, is_cut, _, footer) = simplify(input, false);
|
||||
|
||||
assert_eq!(plain, "Forwarded message");
|
||||
assert!(is_forwarded);
|
||||
assert!(is_cut);
|
||||
assert_eq!(footer.unwrap(), "Signature goes here");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -354,50 +363,60 @@ mod tests {
|
||||
#[test]
|
||||
fn test_remove_message_footer() {
|
||||
let input = "text\n--\nno footer".to_string();
|
||||
let (plain, _, is_cut, _) = simplify(input, true);
|
||||
let (plain, _, is_cut, _, footer) = simplify(input, true);
|
||||
assert_eq!(plain, "text\n--\nno footer");
|
||||
assert_eq!(footer, None);
|
||||
assert!(!is_cut);
|
||||
|
||||
let input = "text\n\n--\n\nno footer".to_string();
|
||||
let (plain, _, is_cut, _) = simplify(input, true);
|
||||
let (plain, _, is_cut, _, footer) = simplify(input, true);
|
||||
assert_eq!(plain, "text\n\n--\n\nno footer");
|
||||
assert_eq!(footer, None);
|
||||
assert!(!is_cut);
|
||||
|
||||
let input = "text\n\n-- no footer\n\n".to_string();
|
||||
let (plain, _, _, _) = simplify(input, true);
|
||||
let (plain, _, _, _, footer) = simplify(input, true);
|
||||
assert_eq!(plain, "text\n\n-- no footer");
|
||||
assert_eq!(footer, None);
|
||||
|
||||
let input = "text\n\n--\nno footer\n-- \nfooter".to_string();
|
||||
let (plain, _, is_cut, _) = simplify(input, true);
|
||||
let (plain, _, is_cut, _, footer) = simplify(input, true);
|
||||
assert_eq!(plain, "text\n\n--\nno footer");
|
||||
assert!(is_cut);
|
||||
assert_eq!(footer.unwrap(), "footer");
|
||||
|
||||
let input = "text\n\n--\ntreated as footer when unescaped".to_string();
|
||||
let (plain, _, is_cut, _) = simplify(input.clone(), true);
|
||||
let (plain, _, is_cut, _, footer) = simplify(input.clone(), true);
|
||||
assert_eq!(plain, "text"); // see remove_message_footer() for some explanations
|
||||
assert!(is_cut);
|
||||
assert_eq!(footer.unwrap(), "treated as footer when unescaped");
|
||||
let escaped = escape_message_footer_marks(&input);
|
||||
let (plain, _, is_cut, _) = simplify(escaped, true);
|
||||
let (plain, _, is_cut, _, footer) = simplify(escaped, true);
|
||||
assert_eq!(plain, "text\n\n--\ntreated as footer when unescaped");
|
||||
assert!(!is_cut);
|
||||
assert_eq!(footer, None);
|
||||
|
||||
// Nonstandard footer sent by https://siju.es/
|
||||
let input = "Message text here\n---Desde mi teléfono con SIJÚ\n\nQuote here".to_string();
|
||||
let (plain, _, is_cut, _) = simplify(input.clone(), false);
|
||||
let (plain, _, is_cut, _, footer) = simplify(input.clone(), false);
|
||||
assert_eq!(plain, "Message text here [...]");
|
||||
assert!(is_cut);
|
||||
let (plain, _, is_cut, _) = simplify(input.clone(), true);
|
||||
assert_eq!(footer, None);
|
||||
let (plain, _, is_cut, _, footer) = simplify(input.clone(), true);
|
||||
assert_eq!(plain, input);
|
||||
assert!(!is_cut);
|
||||
assert_eq!(footer, None);
|
||||
|
||||
let input = "--\ntreated as footer when unescaped".to_string();
|
||||
let (plain, _, is_cut, _) = simplify(input.clone(), true);
|
||||
let (plain, _, is_cut, _, footer) = simplify(input.clone(), true);
|
||||
assert_eq!(plain, ""); // see remove_message_footer() for some explanations
|
||||
assert!(is_cut);
|
||||
assert_eq!(footer.unwrap(), "treated as footer when unescaped");
|
||||
|
||||
let escaped = escape_message_footer_marks(&input);
|
||||
let (plain, _, is_cut, _) = simplify(escaped, true);
|
||||
let (plain, _, is_cut, _, footer) = simplify(escaped, true);
|
||||
assert_eq!(plain, "--\ntreated as footer when unescaped");
|
||||
assert!(!is_cut);
|
||||
assert_eq!(footer, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1500,6 +1500,15 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label);
|
||||
.await?;
|
||||
sql.set_raw_config_int(context, "dbversion", 74).await?;
|
||||
}
|
||||
if dbversion < 75 {
|
||||
info!(context, "[migration] v75");
|
||||
sql.execute(
|
||||
"ALTER TABLE contacts ADD COLUMN status TEXT DEFAULT '';",
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
sql.set_raw_config_int(context, "dbversion", 75).await?;
|
||||
}
|
||||
|
||||
// (2) updates that require high-level objects
|
||||
// (the structure is complete now and all objects are usable)
|
||||
|
||||
Reference in New Issue
Block a user