feat: client version information

This commit is contained in:
B. Petersen
2026-08-11 14:45:42 +02:00
parent 8df4fe1c17
commit ee7bfb7937
6 changed files with 200 additions and 0 deletions
+8
View File
@@ -64,6 +64,7 @@ use self::types::{
JsonrpcMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype, JsonrpcMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
}, },
}; };
use crate::api::types::appversions::JsonrpcAppVersionInfo;
use crate::api::types::chat_list::{ChatListItemFetchResult, get_chat_list_item_by_id}; use crate::api::types::chat_list::{ChatListItemFetchResult, get_chat_list_item_by_id};
use crate::api::types::login_param::TransportListEntry; use crate::api::types::login_param::TransportListEntry;
use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath}; use crate::api::types::qr::{QrObject, SecurejoinSource, SecurejoinUiPath};
@@ -2789,6 +2790,13 @@ impl CommandApi {
Err(anyhow!("chat with id {chat_id} doesn't have draft message")) Err(anyhow!("chat with id {chat_id} doesn't have draft message"))
} }
} }
/// Get version information of clients.
async fn get_app_versions(&self, account_id: u32) -> Result<JsonrpcAppVersionInfo> {
let ctx = self.get_context(account_id).await?;
let info = deltachat::appversions::get_app_versions(&ctx).await?;
JsonrpcAppVersionInfo::from_core_type(info)
}
} }
// Helper functions (to prevent code duplication) // Helper functions (to prevent code duplication)
@@ -0,0 +1,47 @@
use anyhow::Result;
use deltachat::appversions::AppVersionInfo;
use serde::{Deserialize, Serialize};
use typescript_type_def::TypeDef;
/// Version information of clients.
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "AppVersionInfo", rename_all = "camelCase")]
pub struct JsonrpcAppVersionInfo {
/// Array of clients with version information.
pub clients: Vec<JsonrpcAppClient>,
}
/// Version information of a single client, eg. "deltachat" or "ubuntutouch".
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "AppClient", rename_all = "camelCase")]
pub struct JsonrpcAppClient {
/// ID how the client identifies itself, eg. "deltachat" or "ubuntutouch".
pub client_id: String,
/// Array of sources for that client.
pub sources: Vec<JsonrpcAppSource>,
}
/// 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 {
/// ID how the client identifies a source, eg. "gplay" or "fdroid".
pub app_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,
}
impl JsonrpcAppVersionInfo {
pub fn from_core_type(info: AppVersionInfo) -> Result<Self> {
let value = serde_json::to_value(info)?;
Ok(serde_json::from_value(value)?)
}
}
+1
View File
@@ -1,4 +1,5 @@
pub mod account; pub mod account;
pub mod appversions;
pub mod calls; pub mod calls;
pub mod chat; pub mod chat;
pub mod chat_list; pub mod chat_list;
+134
View File
@@ -0,0 +1,134 @@
//! 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::context::Context;
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)]
pub struct AppVersionInfo {
/// Array clients with version information.
clients: Vec<AppClient>,
}
/// Version infomation of a single client, eg. "deltachat" or "ubuntutouch".
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub 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"
app_id: String,
/// Always increasing version number.
version_integer: u32,
/// Any version string.
version_string: String,
/// Where to download that version.
download_url: String,
}
/// Get version information of clients.
///
/// If no version information are available, `clients` is set to an empty array.
///
/// The information is coming from the relay via IMAP METADATA and is not cached.
/// A call to `get_app_versions()` is cheap and does not involve network or database calls.
pub async fn get_app_versions(context: &Context) -> Result<AppVersionInfo> {
if let Some(metadata) = context.metadata.read().await.values().next()
&& let Some(json) = &metadata.app_versions
{
let app_versions: AppVersionInfo = serde_json::from_str(json)?;
return Ok(app_versions);
}
Ok(AppVersionInfo::default())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::TestContextManager;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_app_version_info_deserialize() -> Result<()> {
let json = r##"{
"clients": [
{
"clientId": "deltachat",
"sources": [
{
"appId": "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].app_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(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_app_versions() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let versions = get_app_versions(alice).await?;
assert!(versions.clients.is_empty());
Ok(())
}
}
+9
View File
@@ -140,6 +140,10 @@ pub(crate) struct ServerMetadata {
/// should be fetched from the server /// should be fetched from the server
/// to be ready for WebRTC calls. /// to be ready for WebRTC calls.
pub ice_servers_expiration_timestamp: i64, 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)>> { struct UidGrouper<T: Iterator<Item = (i64, u32, String)>> {
@@ -1342,6 +1346,7 @@ impl Session {
let mut max_smtp_rcpt_to = None; let mut max_smtp_rcpt_to = None;
let mut ice_servers = None; let mut ice_servers = None;
let mut ice_servers_expiration_timestamp = 0; let mut ice_servers_expiration_timestamp = 0;
let mut app_versions = None;
let mailbox = ""; let mailbox = "";
let options = ""; let options = "";
@@ -1397,6 +1402,9 @@ impl Session {
} }
} }
} }
"/shared/vendor/deltachat/appversions" => {
app_versions = m.value;
}
_ => {} _ => {}
} }
} }
@@ -1418,6 +1426,7 @@ impl Session {
supports_push: max_smtp_rcpt_to.is_some() || self.capabilities.has_xdeltapush, supports_push: max_smtp_rcpt_to.is_some() || self.capabilities.has_xdeltapush,
ice_servers, ice_servers,
ice_servers_expiration_timestamp, ice_servers_expiration_timestamp,
app_versions,
}, },
); );
Ok(()) Ok(())
+1
View File
@@ -54,6 +54,7 @@ pub(crate) mod events;
pub use events::*; pub use events::*;
mod aheader; mod aheader;
pub mod appversions;
mod automatic_relay_management; mod automatic_relay_management;
pub mod blob; pub mod blob;
pub mod calls; pub mod calls;