use serde::{Deserialize, Serialize};
use std::time::Instant;
use tracing::debug;
#[allow(unused_imports)]
use tracing::info;
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")
);
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(start_time: Instant) -> anyhow::Result<UpdateInfo> {
let version = env!("CARGO_PKG_VERSION");
let edition = if cfg!(feature = "commerce") {
"commerce"
} else {
"community"
};
let uptime_secs = start_time.elapsed().as_secs();
let build_time = env!("BUILD_TIME_FMT");
let client = reqwest::Client::new();
let resp = client
.get("https://miuda.ai/api/check_update")
.query(&[
("version", version),
("edition", edition),
("uptime", &uptime_secs.to_string()),
("build_time", build_time),
])
.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,
) {
#[cfg(debug_assertions)]
{
debug!("Skipping update check in debug mode");
let _ = db;
let _ = token;
}
#[cfg(not(debug_assertions))]
tokio::spawn(async move {
let start_time = Instant::now();
loop {
match check_update(start_time).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)) => {}
}
}
});
}