mirror of
https://github.com/chatmail/core.git
synced 2026-09-20 12:08:50 +03:00
Compare commits
15 Commits
v2.60.0
...
hpk/collap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d25fa95ffb | ||
|
|
793ee26723 | ||
|
|
3cd051a91e | ||
|
|
4f93d237ec | ||
|
|
340cb9c34c | ||
|
|
3b8aca042d | ||
|
|
1a69e34cc8 | ||
|
|
c955d8b34a | ||
|
|
8ffab367ed | ||
|
|
a075f17252 | ||
|
|
f9b58732b6 | ||
|
|
6a4d08d2e3 | ||
|
|
4c37ac4565 | ||
|
|
a636f3a36e | ||
|
|
ee7bfb7937 |
@@ -64,6 +64,7 @@ use self::types::{
|
||||
JsonrpcMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
|
||||
},
|
||||
};
|
||||
use crate::api::types::appversions::JsonrpcAppSource;
|
||||
use crate::api::types::chat_list::{ChatListItemFetchResult, get_chat_list_item_by_id};
|
||||
use crate::api::types::login_param::TransportListEntry;
|
||||
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
|
||||
@@ -2789,6 +2790,39 @@ impl CommandApi {
|
||||
Err(anyhow!("chat with id {chat_id} doesn't have draft message"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get version information of a specific client and source
|
||||
/// across all configured accounts and transports.
|
||||
///
|
||||
/// Returns the source with the highest `version_integer`.
|
||||
/// If no matching version information is available at all, `None` is returned.
|
||||
///
|
||||
/// UIs shall call the function after a reasonable time after app start,
|
||||
/// when most relays have reported the information they have, say 30 seconds.
|
||||
/// After that, once a day.
|
||||
/// (it is accepted if by the simple approach an update message is delayed.
|
||||
/// an event was considered, but that seemed more complex for few benefit:
|
||||
/// as we do not know if "late" relays will report "better" versions,
|
||||
/// also there we would work with timeouts etc.)
|
||||
///
|
||||
/// If the reported `version_integer` is larger than the running app version,
|
||||
/// the UI shall report to the user, that an update is available,
|
||||
/// and, if possible, offer a direct update by the given URL.
|
||||
///
|
||||
/// Security note: consumers need to verify themselves
|
||||
/// that downloaded app files are valid before installing them.
|
||||
async fn get_app_version(
|
||||
&self,
|
||||
client_id: String,
|
||||
source_id: String,
|
||||
) -> Result<Option<JsonrpcAppSource>> {
|
||||
let accounts = self.accounts.read().await;
|
||||
Ok(
|
||||
deltachat::appversions::get_app_version(&accounts, &client_id, &source_id)
|
||||
.await?
|
||||
.map(JsonrpcAppSource::from_core_type),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions (to prevent code duplication)
|
||||
|
||||
29
deltachat-jsonrpc/src/api/types/appversions.rs
Normal file
29
deltachat-jsonrpc/src/api/types/appversions.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use deltachat::appversions::AppSource;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
/// Version information of a single source of a client, eg. "gplay" or "fdroid".
|
||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "AppSource", rename_all = "camelCase")]
|
||||
pub struct JsonrpcAppSource {
|
||||
/// Always increasing version number.
|
||||
pub version_integer: u32,
|
||||
|
||||
/// Any version string.
|
||||
pub version_string: String,
|
||||
|
||||
/// Where to download that version.
|
||||
/// Security note: consumers need to verify themselves
|
||||
/// that downloaded app files are valid before installing them.
|
||||
pub download_url: String,
|
||||
}
|
||||
|
||||
impl JsonrpcAppSource {
|
||||
pub fn from_core_type(source: AppSource) -> Self {
|
||||
JsonrpcAppSource {
|
||||
version_integer: source.version_integer,
|
||||
version_string: source.version_string,
|
||||
download_url: source.download_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod account;
|
||||
pub mod appversions;
|
||||
pub mod calls;
|
||||
pub mod chat;
|
||||
pub mod chat_list;
|
||||
|
||||
327
src/appversions.rs
Normal file
327
src/appversions.rs
Normal file
@@ -0,0 +1,327 @@
|
||||
//! Get version information of clients.
|
||||
//!
|
||||
//! Used by clients to inform about updates.
|
||||
//! The version information comes in via IMAP METADATA,
|
||||
//! (as JSON) and is parsed to `AppVersionInfo`.
|
||||
use crate::accounts::Accounts;
|
||||
use crate::context::Context;
|
||||
use crate::log::warn;
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Version information of clients as used on the wire.
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
struct AppVersionInfo {
|
||||
/// Array of clients with version information.
|
||||
clients: Vec<AppClient>,
|
||||
}
|
||||
|
||||
/// Version information of a single client, eg. "deltachat" or "ubuntutouch".
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
struct AppClient {
|
||||
/// ID how the client identifies itself, eg. "deltachat" or "ubuntutouch"
|
||||
client_id: String,
|
||||
|
||||
/// Array of sources for that client.
|
||||
sources: Vec<AppSource>,
|
||||
}
|
||||
|
||||
/// Version information of a single source of a client, eg. "gplay" or "fdroid".
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct AppSource {
|
||||
/// ID how the client identifies a source, eg. "gplay" or "fdroid"
|
||||
source_id: String,
|
||||
|
||||
/// Always increasing version number.
|
||||
pub version_integer: u32,
|
||||
|
||||
/// Any version string.
|
||||
pub version_string: String,
|
||||
|
||||
/// Where to download that version.
|
||||
pub download_url: String,
|
||||
}
|
||||
|
||||
/// Returns version information from every relay of every profile.
|
||||
///
|
||||
/// Another transport of the same profile and another profile
|
||||
/// are the same thing for version information,
|
||||
/// so they are flattened into a single list.
|
||||
async fn all_app_versions(accounts: &Accounts) -> Vec<(Context, String)> {
|
||||
let mut result = Vec::new();
|
||||
for account_id in accounts.get_all() {
|
||||
let Some(context) = accounts.get_account(account_id) else {
|
||||
continue;
|
||||
};
|
||||
for metadata in context.metadata.read().await.values() {
|
||||
if let Some(json) = &metadata.app_versions {
|
||||
result.push((context.clone(), json.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Get version information of a specific client and source.
|
||||
///
|
||||
/// Iterates over all accounts and all transports,
|
||||
/// checking version information set by IMAP METADATA,
|
||||
/// and returns the source with the highest `version_integer`.
|
||||
///
|
||||
/// If no matching version information is available at all, `None` is returned.
|
||||
pub async fn get_app_version(
|
||||
accounts: &Accounts,
|
||||
client_id: &str,
|
||||
source_id: &str,
|
||||
) -> Result<Option<AppSource>> {
|
||||
let mut best: Option<AppSource> = None;
|
||||
|
||||
for (context, json) in all_app_versions(accounts).await {
|
||||
let app_versions: AppVersionInfo = match serde_json::from_str(&json) {
|
||||
Ok(app_versions) => app_versions,
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to parse appversions: {err:#}.");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let candidate = app_versions
|
||||
.clients
|
||||
.into_iter()
|
||||
.find(|c| c.client_id == client_id)
|
||||
.and_then(|c| c.sources.into_iter().find(|s| s.source_id == source_id));
|
||||
|
||||
if let Some(candidate) = candidate
|
||||
&& best
|
||||
.as_ref()
|
||||
.is_none_or(|b| candidate.version_integer > b.version_integer)
|
||||
{
|
||||
best = Some(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(best)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_app_version_info_deserialize() -> Result<()> {
|
||||
let json = r##"{
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "deltachat",
|
||||
"sources": [
|
||||
{
|
||||
"sourceId": "gplay",
|
||||
"versionInteger": 754,
|
||||
"versionString": "2.57.0",
|
||||
"downloadUrl": "https://github.com/deltachat/deltachat-android/releases/download/v2.57.0/deltachat-gplay-release-2.57.0.apk"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##;
|
||||
let versions: AppVersionInfo = serde_json::from_str(json)?;
|
||||
assert_eq!(versions.clients.len(), 1);
|
||||
assert_eq!(versions.clients[0].client_id, "deltachat");
|
||||
assert_eq!(versions.clients[0].sources.len(), 1);
|
||||
assert_eq!(versions.clients[0].sources[0].source_id, "gplay");
|
||||
assert_eq!(versions.clients[0].sources[0].version_integer, 754);
|
||||
assert_eq!(versions.clients[0].sources[0].version_string, "2.57.0");
|
||||
assert_eq!(
|
||||
versions.clients[0].sources[0].download_url,
|
||||
"https://github.com/deltachat/deltachat-android/releases/download/v2.57.0/deltachat-gplay-release-2.57.0.apk"
|
||||
);
|
||||
|
||||
// missing fields are set to defaults, additional fields are ignored, errors are errors
|
||||
let json = r##"{
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "deltachat",
|
||||
"xsource": "bang"
|
||||
}
|
||||
],
|
||||
"foo": "bar"
|
||||
}"##;
|
||||
let versions: AppVersionInfo = serde_json::from_str(json)?;
|
||||
assert_eq!(versions.clients.len(), 1);
|
||||
assert_eq!(versions.clients[0].client_id, "deltachat");
|
||||
assert_eq!(versions.clients[0].sources.len(), 0);
|
||||
|
||||
let json = "{}";
|
||||
let versions: AppVersionInfo = serde_json::from_str(json)?;
|
||||
assert_eq!(versions.clients.len(), 0);
|
||||
|
||||
let json = "";
|
||||
assert!(serde_json::from_str::<AppVersionInfo>(json).is_err());
|
||||
|
||||
let json = "bad json";
|
||||
assert!(serde_json::from_str::<AppVersionInfo>(json).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mockup_app_versions(accounts: &Accounts, account_id: u32, json: &str) {
|
||||
let context = accounts.get_account(account_id).expect("account exists");
|
||||
let mut metadata = context.metadata.write().await;
|
||||
let transport_id = 1;
|
||||
metadata.entry(transport_id).or_default().app_versions = Some(json.to_string());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_app_versions() -> Result<()> {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||
|
||||
// no accounts configured, means no versions are reported
|
||||
let version = get_app_version(&accounts, "non-", "existant").await?;
|
||||
assert!(version.is_none());
|
||||
|
||||
// first account reports two clients, with one and two sources
|
||||
let account_id1 = accounts.add_account().await?;
|
||||
let json = r##"{
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "basta",
|
||||
"sources": [
|
||||
{
|
||||
"sourceId": "web",
|
||||
"versionInteger": 754,
|
||||
"versionString": "2.57.0",
|
||||
"downloadUrl": "https://example.org/basta-2.57.0.apk"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientId": "foo",
|
||||
"sources": [
|
||||
{
|
||||
"sourceId": "bar",
|
||||
"versionInteger": 42,
|
||||
"versionString": "42.0",
|
||||
"downloadUrl": "https://foo.bar/42.0.prg"
|
||||
},
|
||||
{
|
||||
"sourceId": "baz",
|
||||
"versionInteger": 1337,
|
||||
"versionString": "13.37",
|
||||
"downloadUrl": "https://dl.org/1337.acc"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##;
|
||||
mockup_app_versions(&accounts, account_id1, json).await;
|
||||
|
||||
let version = get_app_version(&accounts, "basta", "web").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 754);
|
||||
assert_eq!(version.version_string, "2.57.0");
|
||||
assert_eq!(version.download_url, "https://example.org/basta-2.57.0.apk");
|
||||
|
||||
let version = get_app_version(&accounts, "foo", "bar").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 42);
|
||||
assert_eq!(version.version_string, "42.0");
|
||||
assert_eq!(version.download_url, "https://foo.bar/42.0.prg");
|
||||
|
||||
let version = get_app_version(&accounts, "foo", "baz").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 1337);
|
||||
assert_eq!(version.version_string, "13.37");
|
||||
assert_eq!(version.download_url, "https://dl.org/1337.acc");
|
||||
|
||||
let version = get_app_version(&accounts, "non-", "existant").await?;
|
||||
assert!(version.is_none());
|
||||
|
||||
// a second account reports a newer version for "bar"
|
||||
let account_id2 = accounts.add_account().await?;
|
||||
let json = r##"{
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "foo",
|
||||
"sources": [
|
||||
{
|
||||
"sourceId": "bar",
|
||||
"versionInteger": 43,
|
||||
"versionString": "43.0",
|
||||
"downloadUrl": "https://foo.bar/43.0-is-newer.prg"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##;
|
||||
mockup_app_versions(&accounts, account_id2, json).await;
|
||||
|
||||
let version = get_app_version(&accounts, "basta", "web").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 754);
|
||||
assert_eq!(version.version_string, "2.57.0");
|
||||
assert_eq!(version.download_url, "https://example.org/basta-2.57.0.apk");
|
||||
|
||||
let version = get_app_version(&accounts, "foo", "bar").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 43);
|
||||
assert_eq!(version.version_string, "43.0");
|
||||
assert_eq!(version.download_url, "https://foo.bar/43.0-is-newer.prg");
|
||||
|
||||
let version = get_app_version(&accounts, "foo", "baz").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 1337);
|
||||
assert_eq!(version.version_string, "13.37");
|
||||
assert_eq!(version.download_url, "https://dl.org/1337.acc");
|
||||
|
||||
let version = get_app_version(&accounts, "non-", "existant").await?;
|
||||
assert!(version.is_none());
|
||||
|
||||
// a third account reports a older version for "bar", that is ignored
|
||||
let account_id3 = accounts.add_account().await?;
|
||||
let json = r##"{
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "foo",
|
||||
"sources": [
|
||||
{
|
||||
"sourceId": "bar",
|
||||
"versionInteger": 39,
|
||||
"versionString": "39.0",
|
||||
"downloadUrl": "https://foo.bar/39.0-is-too-old.prg"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##;
|
||||
mockup_app_versions(&accounts, account_id3, json).await;
|
||||
|
||||
let version = get_app_version(&accounts, "basta", "web").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 754);
|
||||
assert_eq!(version.version_string, "2.57.0");
|
||||
assert_eq!(version.download_url, "https://example.org/basta-2.57.0.apk");
|
||||
|
||||
let version = get_app_version(&accounts, "foo", "bar").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 43);
|
||||
assert_eq!(version.version_string, "43.0");
|
||||
assert_eq!(version.download_url, "https://foo.bar/43.0-is-newer.prg");
|
||||
|
||||
let version = get_app_version(&accounts, "foo", "baz").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 1337);
|
||||
assert_eq!(version.version_string, "13.37");
|
||||
assert_eq!(version.download_url, "https://dl.org/1337.acc");
|
||||
|
||||
let version = get_app_version(&accounts, "non-", "existant").await?;
|
||||
assert!(version.is_none());
|
||||
|
||||
// second account returns an invalid json, that account is skipped, but app versionss are still gathered from the other
|
||||
let json = r"bad json!";
|
||||
mockup_app_versions(&accounts, account_id2, json).await;
|
||||
|
||||
let version = get_app_version(&accounts, "foo", "bar").await?.unwrap();
|
||||
assert_eq!(version.version_integer, 42);
|
||||
assert_eq!(version.version_string, "42.0");
|
||||
assert_eq!(version.download_url, "https://foo.bar/42.0.prg");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
23
src/imap.rs
23
src/imap.rs
@@ -140,6 +140,10 @@ pub(crate) struct ServerMetadata {
|
||||
/// should be fetched from the server
|
||||
/// to be ready for WebRTC calls.
|
||||
pub ice_servers_expiration_timestamp: i64,
|
||||
|
||||
/// App versions, as raw JSON string.
|
||||
/// Consumed by get_app_versions().
|
||||
pub app_versions: Option<String>,
|
||||
}
|
||||
|
||||
struct UidGrouper<T: Iterator<Item = (i64, u32, String)>> {
|
||||
@@ -1293,6 +1297,10 @@ impl Session {
|
||||
let now = time();
|
||||
|
||||
// Refresh TURN server credentials if they expire in 12 hours.
|
||||
//
|
||||
// Moreover, Take the chance to update `app_versions` as well.
|
||||
// As best effort, even checking every some days is good enough -
|
||||
// and saves one additional time get get_metadata() call.
|
||||
if now + 3600 * 12 < old_metadata.ice_servers_expiration_timestamp {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1303,7 +1311,11 @@ impl Session {
|
||||
let mailbox = "";
|
||||
let options = "";
|
||||
let metadata = self
|
||||
.get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
|
||||
.get_metadata(
|
||||
mailbox,
|
||||
options,
|
||||
"(/shared/vendor/deltachat/turn /shared/vendor/deltachat/appversions)",
|
||||
)
|
||||
.await?;
|
||||
for m in metadata {
|
||||
if m.entry == "/shared/vendor/deltachat/turn"
|
||||
@@ -1319,6 +1331,8 @@ impl Session {
|
||||
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
|
||||
}
|
||||
}
|
||||
} else if m.entry == "/shared/vendor/deltachat/appversions" {
|
||||
old_metadata.app_versions = m.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1342,6 +1356,7 @@ impl Session {
|
||||
let mut max_smtp_rcpt_to = None;
|
||||
let mut ice_servers = None;
|
||||
let mut ice_servers_expiration_timestamp = 0;
|
||||
let mut app_versions = None;
|
||||
|
||||
let mailbox = "";
|
||||
let options = "";
|
||||
@@ -1349,7 +1364,7 @@ impl Session {
|
||||
.get_metadata(
|
||||
mailbox,
|
||||
options,
|
||||
"(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn /shared/vendor/deltachat/maxsmtprecipients)",
|
||||
"(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn /shared/vendor/deltachat/maxsmtprecipients /shared/vendor/deltachat/appversions)",
|
||||
)
|
||||
.await?;
|
||||
for m in metadata {
|
||||
@@ -1397,6 +1412,9 @@ impl Session {
|
||||
}
|
||||
}
|
||||
}
|
||||
"/shared/vendor/deltachat/appversions" => {
|
||||
app_versions = m.value;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1418,6 +1436,7 @@ impl Session {
|
||||
supports_push: max_smtp_rcpt_to.is_some() || self.capabilities.has_xdeltapush,
|
||||
ice_servers,
|
||||
ice_servers_expiration_timestamp,
|
||||
app_versions,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
|
||||
@@ -54,6 +54,7 @@ pub(crate) mod events;
|
||||
pub use events::*;
|
||||
|
||||
mod aheader;
|
||||
pub mod appversions;
|
||||
mod automatic_relay_management;
|
||||
pub mod blob;
|
||||
pub mod calls;
|
||||
|
||||
Reference in New Issue
Block a user