mirror of
https://github.com/chatmail/core.git
synced 2026-05-15 12:56:30 +03:00
feat: Sync self-avatar across devices (#4893)
Use sync messages for that as it is done for e.g. Config::Displayname. Maybe we need to remove avatar synchronisation via usual messages then, but let's think of it a bit.
This commit is contained in:
25
src/blob.rs
25
src/blob.rs
@@ -8,6 +8,7 @@ use std::iter::FusedIterator;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{format_err, Context as _, Result};
|
use anyhow::{format_err, Context as _, Result};
|
||||||
|
use base64::Engine as _;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use image::{DynamicImage, GenericImageView, ImageFormat, ImageOutputFormat};
|
use image::{DynamicImage, GenericImageView, ImageFormat, ImageOutputFormat};
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
@@ -312,6 +313,30 @@ impl<'a> BlobObject<'a> {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns path to the stored Base64-decoded blob.
|
||||||
|
///
|
||||||
|
/// If `data` represents an image of known format, this adds the corresponding extension to
|
||||||
|
/// `suggested_file_stem`.
|
||||||
|
pub(crate) async fn store_from_base64(
|
||||||
|
context: &Context,
|
||||||
|
data: &str,
|
||||||
|
suggested_file_stem: &str,
|
||||||
|
) -> Result<String> {
|
||||||
|
let buf = base64::engine::general_purpose::STANDARD.decode(data)?;
|
||||||
|
let ext = if let Ok(format) = image::guess_format(&buf) {
|
||||||
|
if let Some(ext) = format.extensions_str().first() {
|
||||||
|
format!(".{ext}")
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
let blob =
|
||||||
|
BlobObject::create(context, &format!("{suggested_file_stem}{ext}"), &buf).await?;
|
||||||
|
Ok(blob.as_name().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<()> {
|
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<()> {
|
||||||
let blob_abs = self.to_abs_path();
|
let blob_abs = self.to_abs_path();
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ use std::path::Path;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::{ensure, Context as _, Result};
|
use anyhow::{ensure, Context as _, Result};
|
||||||
|
use base64::Engine as _;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use strum::{EnumProperty, IntoEnumIterator};
|
use strum::{EnumProperty, IntoEnumIterator};
|
||||||
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
|
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
use crate::blob::BlobObject;
|
use crate::blob::BlobObject;
|
||||||
use crate::constants::{self, DC_VERSION_STR};
|
use crate::constants::{self, DC_VERSION_STR};
|
||||||
@@ -361,8 +363,8 @@ impl Config {
|
|||||||
///
|
///
|
||||||
/// This must be checked on both sides so that if there are different client versions, the
|
/// This must be checked on both sides so that if there are different client versions, the
|
||||||
/// synchronisation of a particular option is either done or not done in both directions.
|
/// synchronisation of a particular option is either done or not done in both directions.
|
||||||
/// Moreover, receivers of a config value need to check if a key can be synced because some
|
/// Moreover, receivers of a config value need to check if a key can be synced because if it is
|
||||||
/// settings (e.g. Avatar path) could otherwise lead to exfiltration of files from a receiver's
|
/// a file path, it could otherwise lead to exfiltration of files from a receiver's
|
||||||
/// device if we assume an attacker to have control of a device in a multi-device setting or if
|
/// device if we assume an attacker to have control of a device in a multi-device setting or if
|
||||||
/// multiple users are sharing an account. Another example is `Self::SyncMsgs` itself which
|
/// multiple users are sharing an account. Another example is `Self::SyncMsgs` itself which
|
||||||
/// mustn't be controlled by other devices.
|
/// mustn't be controlled by other devices.
|
||||||
@@ -373,7 +375,7 @@ impl Config {
|
|||||||
}
|
}
|
||||||
matches!(
|
matches!(
|
||||||
self,
|
self,
|
||||||
Self::Displayname | Self::MdnsEnabled | Self::ShowEmails
|
Self::Displayname | Self::MdnsEnabled | Self::ShowEmails | Self::Selfavatar,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,6 +505,23 @@ impl Context {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes [`SyncData::Config`] item sent by other device.
|
||||||
|
pub(crate) async fn sync_config(&self, key: &Config, value: &str) -> Result<()> {
|
||||||
|
let config_value;
|
||||||
|
let value = match key {
|
||||||
|
Config::Selfavatar if value.is_empty() => None,
|
||||||
|
Config::Selfavatar => {
|
||||||
|
config_value = BlobObject::store_from_base64(self, value, "avatar").await?;
|
||||||
|
Some(config_value.as_str())
|
||||||
|
}
|
||||||
|
_ => Some(value),
|
||||||
|
};
|
||||||
|
match key.is_synced() {
|
||||||
|
true => self.set_config_ex(Nosync, *key, value).await,
|
||||||
|
false => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn check_config(key: Config, value: Option<&str>) -> Result<()> {
|
fn check_config(key: Config, value: Option<&str>) -> Result<()> {
|
||||||
match key {
|
match key {
|
||||||
Config::Socks5Enabled
|
Config::Socks5Enabled
|
||||||
@@ -565,15 +584,24 @@ impl Context {
|
|||||||
.execute("UPDATE contacts SET selfavatar_sent=0;", ())
|
.execute("UPDATE contacts SET selfavatar_sent=0;", ())
|
||||||
.await?;
|
.await?;
|
||||||
match value {
|
match value {
|
||||||
Some(value) => {
|
Some(path) => {
|
||||||
let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?;
|
let mut blob = BlobObject::new_from_path(self, path.as_ref()).await?;
|
||||||
blob.recode_to_avatar_size(self).await?;
|
blob.recode_to_avatar_size(self).await?;
|
||||||
self.sql
|
self.sql
|
||||||
.set_raw_config(key.as_ref(), Some(blob.as_name()))
|
.set_raw_config(key.as_ref(), Some(blob.as_name()))
|
||||||
.await?;
|
.await?;
|
||||||
|
if sync {
|
||||||
|
let buf = fs::read(blob.to_abs_path()).await?;
|
||||||
|
better_value = base64::engine::general_purpose::STANDARD.encode(buf);
|
||||||
|
value = Some(&better_value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.sql.set_raw_config(key.as_ref(), None).await?;
|
self.sql.set_raw_config(key.as_ref(), None).await?;
|
||||||
|
if sync {
|
||||||
|
better_value = String::new();
|
||||||
|
value = Some(&better_value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.emit_event(EventType::SelfavatarChanged);
|
self.emit_event(EventType::SelfavatarChanged);
|
||||||
@@ -950,6 +978,23 @@ mod tests {
|
|||||||
Some("Alice Sync".to_string())
|
Some("Alice Sync".to_string())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert!(alice0.get_config(Config::Selfavatar).await?.is_none());
|
||||||
|
let file = alice0.dir.path().join("avatar.png");
|
||||||
|
let bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
||||||
|
tokio::fs::write(&file, bytes).await?;
|
||||||
|
alice0
|
||||||
|
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
|
||||||
|
.await?;
|
||||||
|
sync(&alice0, &alice1).await;
|
||||||
|
assert!(alice1
|
||||||
|
.get_config(Config::Selfavatar)
|
||||||
|
.await?
|
||||||
|
.filter(|path| path.ends_with(".png"))
|
||||||
|
.is_some());
|
||||||
|
alice0.set_config(Config::Selfavatar, None).await?;
|
||||||
|
sync(&alice0, &alice1).await;
|
||||||
|
assert!(alice1.get_config(Config::Selfavatar).await?.is_none());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use std::pin::Pin;
|
|||||||
use std::str;
|
use std::str;
|
||||||
|
|
||||||
use anyhow::{bail, Context as _, Result};
|
use anyhow::{bail, Context as _, Result};
|
||||||
use base64::Engine as _;
|
|
||||||
use deltachat_derive::{FromSql, ToSql};
|
use deltachat_derive::{FromSql, ToSql};
|
||||||
use format_flowed::unformat_flowed;
|
use format_flowed::unformat_flowed;
|
||||||
use lettre_email::mime::{self, Mime};
|
use lettre_email::mime::{self, Mime};
|
||||||
@@ -722,37 +721,20 @@ impl MimeMessage {
|
|||||||
) -> Option<AvatarAction> {
|
) -> Option<AvatarAction> {
|
||||||
if header_value == "0" {
|
if header_value == "0" {
|
||||||
Some(AvatarAction::Delete)
|
Some(AvatarAction::Delete)
|
||||||
} else if let Some(avatar) = header_value
|
} else if let Some(base64) = header_value
|
||||||
.split_ascii_whitespace()
|
.split_ascii_whitespace()
|
||||||
.collect::<String>()
|
.collect::<String>()
|
||||||
.strip_prefix("base64:")
|
.strip_prefix("base64:")
|
||||||
.map(|x| base64::engine::general_purpose::STANDARD.decode(x))
|
|
||||||
{
|
{
|
||||||
// Avatar sent directly in the header as base64.
|
match BlobObject::store_from_base64(context, base64, "avatar").await {
|
||||||
if let Ok(decoded_data) = avatar {
|
Ok(path) => Some(AvatarAction::Change(path)),
|
||||||
let extension = if let Ok(format) = image::guess_format(&decoded_data) {
|
Err(err) => {
|
||||||
if let Some(ext) = format.extensions_str().first() {
|
warn!(
|
||||||
format!(".{ext}")
|
context,
|
||||||
} else {
|
"Could not decode and save avatar to blob file: {:#}", err,
|
||||||
String::new()
|
);
|
||||||
}
|
None
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
match BlobObject::create(context, &format!("avatar{extension}"), &decoded_data)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(blob) => Some(AvatarAction::Change(blob.as_name().to_string())),
|
|
||||||
Err(err) => {
|
|
||||||
warn!(
|
|
||||||
context,
|
|
||||||
"Could not save decoded avatar to blob file: {:#}", err
|
|
||||||
);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Avatar sent in attachment, as previous versions of Delta Chat did.
|
// Avatar sent in attachment, as previous versions of Delta Chat did.
|
||||||
|
|||||||
@@ -276,10 +276,7 @@ impl Context {
|
|||||||
AddQrToken(token) => self.add_qr_token(token).await,
|
AddQrToken(token) => self.add_qr_token(token).await,
|
||||||
DeleteQrToken(token) => self.delete_qr_token(token).await,
|
DeleteQrToken(token) => self.delete_qr_token(token).await,
|
||||||
AlterChat { id, action } => self.sync_alter_chat(id, action).await,
|
AlterChat { id, action } => self.sync_alter_chat(id, action).await,
|
||||||
SyncData::Config { key, val } => match key.is_synced() {
|
SyncData::Config { key, val } => self.sync_config(key, val).await,
|
||||||
true => self.set_config_ex(Sync::Nosync, *key, Some(val)).await,
|
|
||||||
false => Ok(()),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
SyncDataOrUnknown::Unknown(data) => {
|
SyncDataOrUnknown::Unknown(data) => {
|
||||||
warn!(self, "Ignored unknown sync item: {data}.");
|
warn!(self, "Ignored unknown sync item: {data}.");
|
||||||
|
|||||||
Reference in New Issue
Block a user