use std::collections::BTreeMap; use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc}; use clap::Parser; use exver::{Version, VersionRange}; use imbl_value::InternedString; use itertools::Itertools; use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async}; use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::context::CliContext; use crate::prelude::*; use crate::registry::context::RegistryContext; use crate::registry::device_info::DeviceInfo; use crate::registry::os::index::OsVersionInfo; use crate::sign::AnyVerifyingKey; use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable}; pub mod signer; pub fn version_api() -> ParentHandler { ParentHandler::new() .subcommand( "add", from_fn_async(add_version) .with_metadata("admin", Value::Bool(true)) .with_metadata("get_signer", Value::Bool(true)) .no_display() .with_about("about.add-os-version") .with_call_remote::(), ) .subcommand( "remove", from_fn_async(remove_version) .with_metadata("admin", Value::Bool(true)) .no_display() .with_about("about.remove-os-version") .with_call_remote::(), ) .subcommand( "signer", signer::signer_api::().with_about("about.add-remove-list-version-signers"), ) .subcommand( "get", from_fn_async(get_version) .with_metadata("authenticated", Value::Bool(false)) .with_metadata("get_device_info", Value::Bool(true)) .with_display_serializable() .with_custom_display_fn(|handle, result| { display_version_info(handle.params, result) }) .with_about("about.get-os-versions-info") .with_call_remote::(), ) } #[derive(Debug, Deserialize, Serialize, Parser, TS)] #[command(rename_all = "kebab-case")] #[serde(rename_all = "camelCase")] #[ts(export)] pub struct AddVersionParams { #[ts(type = "string")] #[arg(help = "help.arg.os-version")] pub version: Version, #[arg(help = "help.arg.version-headline")] pub headline: String, #[arg(help = "help.arg.release-notes")] pub release_notes: String, #[ts(type = "string")] #[arg(help = "help.arg.source-version-range")] pub source_version: VersionRange, #[arg(skip)] #[ts(skip)] #[serde(rename = "__Auth_signer")] pub signer: Option, } pub async fn add_version( ctx: RegistryContext, AddVersionParams { version, headline, release_notes, source_version, signer, }: AddVersionParams, ) -> Result<(), Error> { ctx.db .mutate(|db| { let signer = signer .map(|s| db.as_index().as_signers().get_signer(&s)) .transpose()?; db.as_index_mut() .as_os_mut() .as_versions_mut() .upsert(&version, || Ok(OsVersionInfo::default()))? .mutate(|i| { i.headline = headline; i.release_notes = release_notes; i.source_version = source_version; i.authorized.extend(signer); Ok(()) }) }) .await .result } #[derive(Debug, Deserialize, Serialize, Parser, TS)] #[command(rename_all = "kebab-case")] #[serde(rename_all = "camelCase")] #[ts(export)] pub struct RemoveVersionParams { #[ts(type = "string")] #[arg(help = "help.arg.os-version")] pub version: Version, } pub async fn remove_version( ctx: RegistryContext, RemoveVersionParams { version }: RemoveVersionParams, ) -> Result<(), Error> { ctx.db .mutate(|db| { db.as_index_mut() .as_os_mut() .as_versions_mut() .remove(&version)?; Ok(()) }) .await .result } #[derive(Debug, Deserialize, Serialize, Parser, TS)] #[command(rename_all = "kebab-case")] #[serde(rename_all = "camelCase")] #[ts(export)] pub struct GetOsVersionParams { #[ts(type = "string | null")] #[arg(long = "src", help = "help.arg.source-version")] pub source_version: Option, #[ts(type = "string | null")] #[arg(long, help = "help.arg.target-version-range")] pub target_version: Option, #[arg(long = "id", help = "help.arg.server-id")] server_id: Option, #[ts(type = "string | null")] #[arg(long, help = "help.arg.platform")] platform: Option, #[ts(skip)] #[arg(skip)] #[serde(rename = "__DeviceInfo_device_info")] pub device_info: Option, } struct PgDateTime(DateTime); impl sqlx::Type for PgDateTime { fn type_info() -> ::TypeInfo { sqlx::postgres::PgTypeInfo::with_oid(sqlx::postgres::types::Oid(1184)) } } impl sqlx::Encode<'_, sqlx::Postgres> for PgDateTime { fn encode_by_ref( &self, buf: &mut ::ArgumentBuffer<'_>, ) -> Result { fn postgres_epoch_datetime() -> NaiveDateTime { NaiveDate::from_ymd_opt(2000, 1, 1) .expect("expected 2000-01-01 to be a valid NaiveDate") .and_hms_opt(0, 0, 0) .expect("expected 2000-01-01T00:00:00 to be a valid NaiveDateTime") } let micros = (self.0.naive_utc() - postgres_epoch_datetime()) .num_microseconds() .ok_or_else(|| format!("NaiveDateTime out of range for Postgres: {:?}", self.0))?; micros.encode(buf) } fn size_hint(&self) -> usize { std::mem::size_of::() } } pub async fn get_version( ctx: RegistryContext, GetOsVersionParams { source_version: source, target_version: target, server_id, platform, device_info, }: GetOsVersionParams, ) -> Result // BTreeMap { let source = source.or_else(|| device_info.as_ref().map(|d| d.os.version.clone())); let platform = platform.or_else(|| device_info.as_ref().map(|d| d.os.platform.clone())); if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, &platform) { let created_at = Utc::now(); sqlx::query("INSERT INTO user_activity (created_at, server_id, arch) VALUES ($1, $2, $3)") .bind(PgDateTime(created_at)) .bind(server_id) .bind(&**arch) .execute(pool) .await .with_kind(ErrorKind::Database)?; } let target = target.unwrap_or(VersionRange::Any); let mut res = to_value::>( &ctx.db .peek() .await .into_index() .into_os() .into_versions() .into_entries()? .into_iter() .map(|(v, i)| i.de().map(|i| (v, i))) .filter_ok(|(version, info)| { platform .as_ref() .map_or(true, |p| info.squashfs.contains_key(p)) && version.satisfies(&target) && source .as_ref() .map_or(true, |s| s.satisfies(&info.source_version)) }) .collect::>()?, )?; // TODO: remove if device_info.map_or(false, |d| { "0.4.0-alpha.17" .parse::() .map_or(false, |v| d.os.version <= v) }) { for (_, v) in res .as_object_mut() .into_iter() .map(|v| v.iter_mut()) .flatten() { for asset_ty in ["iso", "squashfs", "img"] { for (_, v) in v[asset_ty] .as_object_mut() .into_iter() .map(|v| v.iter_mut()) .flatten() { v["url"] = v["urls"][0].clone(); } } } } Ok(res) } pub fn display_version_info( params: WithIoFormat, info: Value, // BTreeMap, ) -> Result<(), Error> { use prettytable::*; let info = from_value::>(info)?; if let Some(format) = params.format { return display_serializable(format, info); } let mut table = Table::new(); table.add_row(row![bc => "VERSION", "HEADLINE", "RELEASE NOTES", "ISO PLATFORMS", "IMG PLATFORMS", "SQUASHFS PLATFORMS", ]); for (version, info) in &info { table.add_row(row![ &version.to_string(), &info.headline, &info.release_notes, &info.iso.keys().into_iter().join(", "), &info.img.keys().into_iter().join(", "), &info.squashfs.keys().into_iter().join(", "), ]); } table.print_tty(false)?; Ok(()) }