use serde::{Deserialize, Serialize};
use tracing::{debug, info};
pub const VERSION_INFO: &str = concat!(
"rustpbx ",
env!("CARGO_PKG_VERSION"),
"\nBuild Time: ",
env!("BUILD_TIME_FMT"),
"\nGit Commit: ",
env!("GIT_COMMIT_HASH"),
"\nGit Branch: ",
env!("GIT_BRANCH"),
"\nGit Status: ",
env!("GIT_DIRTY")
);
pub const SHORT_VERSION: &str = env!("SHORT_VERSION");
pub fn get_version_info() -> &'static str {
VERSION_INFO
}
pub fn get_short_version() -> &'static str {
SHORT_VERSION
}
pub fn get_useragent() -> String {
format!("rustpbx/{} (built {})", env!("CARGO_PKG_VERSION"), env!("BUILD_DATE"))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateInfo {
pub has_update: bool,
pub latest_version: String,
pub release_notes: Option<String>,
pub download_url: Option<String>,
}
pub async fn check_update() -> anyhow::Result<UpdateInfo> {
let version = env!("CARGO_PKG_VERSION");
let edition = if cfg!(feature = "commerce") {
"commerce"
} else {
"community"
};
let client = reqwest::Client::new();
let resp = client
.get("https://miuda.ai/api/check_update")
.query(&[("version", version), ("edition", edition)])
.header("User-Agent", get_useragent())
.timeout(std::time::Duration::from_secs(5))
.send()
.await;
let resp = match resp {
Ok(r) => r,
Err(e) if e.is_timeout() || e.is_connect() => {
anyhow::bail!("version check unreachable (network/timeout): {e}");
}
Err(e) => anyhow::bail!("version check request error: {e}"),
};
let status = resp.status();
let body = resp.text().await?;
debug!("version check response: status={} body={}", status, body);
let info: UpdateInfo = serde_json::from_str(&body)
.map_err(|e| anyhow::anyhow!("version check parse error: {e}, status={status}, body={body}"))?;
Ok(info)
}
pub fn spawn_update_checker(
db: sea_orm::DatabaseConnection,
token: tokio_util::sync::CancellationToken,
) {
tokio::spawn(async move {
loop {
match check_update().await {
Ok(info) if info.has_update => {
use crate::models::system_notification::{ActiveModel, Column, Entity};
use sea_orm::{ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter};
let title = format!("New version available: {}", info.latest_version);
let exists = Entity::find()
.filter(Column::Title.eq(&title))
.one(&db)
.await
.ok()
.flatten()
.is_some();
if !exists {
let body = info.release_notes.clone().unwrap_or_default();
let am = ActiveModel {
id: sea_orm::ActiveValue::NotSet,
kind: Set("update".to_string()),
title: Set(title.clone()),
body: Set(body),
read: Set(false),
created_at: Set(chrono::Utc::now()),
};
match am.insert(&db).await {
Ok(_) => info!(latest = %info.latest_version, "update notification created"),
Err(e) => debug!("failed to insert update notification: {e}"),
}
}
}
Ok(_) => debug!("version check: already up-to-date"),
Err(e) => debug!("version check failed: {e}"),
}
tokio::select! {
_ = token.cancelled() => break,
_ = tokio::time::sleep(std::time::Duration::from_secs(24 * 3600)) => {}
}
}
});
}