mod rows;
use rows::{DnsRecordRow, DomainRequestRow, DomainRow, NameServerRow, SubdomainRow, TldRow};
use rust_i18n::t;
use timeweb_rs::{apis::domains_api, models as dm};
use crate::{error::TwcError, output::OutputFormat};
fn fmt_id<T: std::fmt::Display>(v: T) -> String {
v.to_string()
}
fn build_dns_record(record_type: &str, value: String) -> Result<dm::CreateDnsV2, TwcError> {
match record_type {
"A" => Ok(dm::CreateDnsV2::A(Box::new(dm::A::new(
dm::a_______::Type::A,
value
)))),
"AAAA" => Ok(dm::CreateDnsV2::Aaaa(Box::new(dm::Aaaa::new(
dm::aaaa_______::Type::Aaaa,
value
)))),
"CNAME" => Ok(dm::CreateDnsV2::Cname(Box::new(dm::Cname::new(
dm::cname_______::Type::Cname,
value
)))),
"MX" => Ok(dm::CreateDnsV2::Mx(Box::new(dm::Mx::new(
dm::mx_______::Type::Mx,
value,
10.0
)))),
"TXT" => Ok(dm::CreateDnsV2::Txt(Box::new(dm::Txt::new(
dm::txt_______::Type::Txt,
value
)))),
"SRV" => Ok(dm::CreateDnsV2::Srv(Box::new(dm::Srv::new(
dm::srv_______::Type::Srv,
value,
"_tcp".to_string(),
0.0,
80.0,
"localhost".to_string()
)))),
other => Err(TwcError::Api(format!(
"unsupported DNS record type: {other} (use A, AAAA, CNAME, MX, TXT, SRV)"
)))
}
}
pub async fn list(
config: &timeweb_rs::apis::configuration::Configuration,
limit: Option<i32>,
offset: Option<i32>,
format: OutputFormat
) -> Result<(), TwcError> {
let resp = domains_api::get_domains(config, limit, offset, None, None, None, None).await?;
let rows: Vec<DomainRow> = resp
.domains
.iter()
.map(|d| DomainRow {
id: fmt_id(d.id),
fqdn: d.fqdn.clone(),
status: format!("{:?}", d.domain_status),
expires: d.expiration.clone(),
auto_prolong: d
.is_autoprolong_enabled
.map_or_else(|| "n/a".to_string(), |v| v.to_string()),
days_left: format!("{:.0}", d.days_left)
})
.collect();
match format {
OutputFormat::Table => {
if rows.is_empty() {
println!("{}", t!("cli.no_domains"));
} else {
let table = crate::output::render_table(&rows);
println!("{table}");
}
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.domains)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
for d in &resp.domains {
println!("{}\t{}", fmt_id(d.id), d.fqdn);
}
}
}
Ok(())
}
pub async fn info(
config: &timeweb_rs::apis::configuration::Configuration,
fqdn: String,
format: OutputFormat
) -> Result<(), TwcError> {
let resp = domains_api::get_domain(config, &fqdn).await?;
let d = &resp.domain;
match format {
OutputFormat::Table => {
println!("ID: {}", fmt_id(d.id));
println!("FQDN: {}", d.fqdn);
println!("Status: {:?}", d.domain_status);
println!("Expiration: {}", d.expiration);
println!("Days Left: {:.0}", d.days_left);
println!(
"Auto Prolong: {}",
d.is_autoprolong_enabled
.map_or_else(|| "n/a".to_string(), |v| v.to_string())
);
println!("Premium: {}", d.is_premium);
println!("Prolong Allowed: {}", d.is_prolong_allowed);
println!("Technical: {}", d.is_technical);
println!(
"Whois Privacy: {}",
d.is_whois_privacy_enabled
.map_or_else(|| "n/a".to_string(), |v| v.to_string())
);
println!(
"Linked IP: {}",
d.linked_ip.clone().unwrap_or_default()
);
println!(
"Paid Till: {}",
d.paid_till.clone().unwrap_or_else(|| "n/a".to_string())
);
println!(
"Provider: {}",
d.provider.clone().unwrap_or_else(|| "n/a".to_string())
);
println!(
"TLD ID: {}",
d.tld_id
.map_or_else(|| "n/a".to_string(), |v| format!("{v:.0}"))
);
println!(
"Person ID: {}",
d.person_id
.map_or_else(|| "n/a".to_string(), |v| format!("{v:.0}"))
);
println!("Subdomains: {} subdomains", d.subdomains.len());
for sub in &d.subdomains {
println!(
" - {} (ip: {})",
sub.fqdn,
sub.linked_ip.clone().unwrap_or_default()
);
}
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.domain)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
println!("{}\t{}\t{:?}", fmt_id(d.id), d.fqdn, d.domain_status);
}
}
Ok(())
}
pub async fn check(
config: &timeweb_rs::apis::configuration::Configuration,
fqdn: String
) -> Result<(), TwcError> {
let resp = domains_api::check_domain(config, &fqdn).await?;
if resp.is_domain_available {
println!("{}", t!("cli.domain_available", fqdn => fqdn));
} else {
println!("{}", t!("cli.domain_not_available", fqdn => fqdn));
}
Ok(())
}
pub async fn add(
config: &timeweb_rs::apis::configuration::Configuration,
fqdn: String,
format: OutputFormat
) -> Result<(), TwcError> {
domains_api::add_domain(config, &fqdn).await?;
match format {
OutputFormat::Table => {
println!("{}", t!("cli.domain_added", fqdn => fqdn));
}
OutputFormat::Json | OutputFormat::Yaml => {
println!("{{\"fqdn\": \"{fqdn}\", \"status\": \"added\"}}");
}
OutputFormat::Quiet => {
println!("{fqdn}\tadded");
}
}
Ok(())
}
pub async fn delete(
config: &timeweb_rs::apis::configuration::Configuration,
fqdn: String
) -> Result<(), TwcError> {
domains_api::delete_domain(config, &fqdn).await?;
println!("{}", t!("cli.domain_deleted", fqdn => fqdn));
Ok(())
}
pub async fn dns_list(
config: &timeweb_rs::apis::configuration::Configuration,
fqdn: String,
format: OutputFormat
) -> Result<(), TwcError> {
let resp = domains_api::get_domain_dns_records(config, &fqdn, None, None).await?;
let rows: Vec<DnsRecordRow> = resp
.dns_records
.iter()
.map(|r| {
let priority_str = match &*r.data {
dm::DnsRecordData {
priority: Some(p), ..
} => format!("{p:.0}"),
_ => String::new()
};
DnsRecordRow {
id: r
.id
.as_ref()
.and_then(|v| v.map(|vv| format!("{vv:.0}")))
.unwrap_or_default(),
r#type: format!("{:?}", r.r#type),
value: r.data.value.clone(),
ttl: r
.ttl
.map_or_else(|| "n/a".to_string(), |v| format!("{v:.0}")),
priority: priority_str
}
})
.collect();
match format {
OutputFormat::Table => {
if rows.is_empty() {
println!("{}", t!("cli.no_dns_records"));
} else {
let table = crate::output::render_table(&rows);
println!("{table}");
}
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.dns_records)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
for r in &resp.dns_records {
let id_str =
r.id.as_ref()
.and_then(|v| v.map(|vv| format!("{vv:.0}")))
.unwrap_or_default();
let type_str = format!("{:?}", r.r#type);
println!("{id_str}\t{type_str}\t{}", r.data.value);
}
}
}
Ok(())
}
pub async fn dns_add(
config: &timeweb_rs::apis::configuration::Configuration,
fqdn: String,
record_type: String,
value: String,
format: OutputFormat
) -> Result<(), TwcError> {
let dns = build_dns_record(&record_type.to_uppercase(), value.clone())?;
let _resp = domains_api::create_domain_dns_record_v2(config, &fqdn, dns).await?;
match format {
OutputFormat::Table => {
println!(
"{}",
t!("cli.dns_record_added", rtype => record_type, fqdn => fqdn)
);
}
OutputFormat::Json | OutputFormat::Yaml => {
println!(
"{{\"fqdn\": \"{fqdn}\", \"type\": \"{record_type}\", \"status\": \"added\"}}"
);
}
OutputFormat::Quiet => {
println!("{fqdn}\t{record_type}\t{value}\tadded");
}
}
Ok(())
}
pub async fn dns_delete(
config: &timeweb_rs::apis::configuration::Configuration,
fqdn: String,
record_id: i32
) -> Result<(), TwcError> {
domains_api::delete_domain_dns_record_v2(config, &fqdn, record_id).await?;
println!(
"{}",
t!("cli.dns_record_deleted", id => record_id, fqdn => fqdn)
);
Ok(())
}
pub async fn dns_update(
config: &timeweb_rs::apis::configuration::Configuration,
fqdn: String,
record_id: i32,
record_type: String,
value: String,
format: OutputFormat
) -> Result<(), TwcError> {
let dns = build_dns_record(&record_type.to_uppercase(), value)?;
let _resp = domains_api::update_domain_dns_record_v2(config, &fqdn, record_id, dns).await?;
match format {
OutputFormat::Table => {
println!(
"{}",
t!("cli.dns_record_updated", id => record_id, fqdn => fqdn)
);
}
OutputFormat::Json | OutputFormat::Yaml => {
println!(
"{{\"fqdn\": \"{fqdn}\", \"record_id\": {record_id}, \"status\": \"updated\"}}"
);
}
OutputFormat::Quiet => {
println!("{fqdn}\t{record_id}\t{record_type}\tupdated");
}
}
Ok(())
}
pub async fn ns_list(
config: &timeweb_rs::apis::configuration::Configuration,
fqdn: String,
format: OutputFormat
) -> Result<(), TwcError> {
let resp = domains_api::get_domain_name_servers(config, &fqdn).await?;
let rows: Vec<NameServerRow> = resp
.name_servers
.iter()
.flat_map(|ns| {
ns.items
.iter()
.map(|item| NameServerRow {
host: item.host.clone(),
ips: item.ips.join(", ")
})
.collect::<Vec<_>>()
})
.collect();
match format {
OutputFormat::Table => {
if rows.is_empty() {
println!("{}", t!("cli.no_name_servers"));
} else {
let table = crate::output::render_table(&rows);
println!("{table}");
}
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.name_servers)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
for ns in &resp.name_servers {
for item in &ns.items {
println!("{}\t{}", item.host, item.ips.join(","));
}
}
}
}
Ok(())
}
pub async fn ns_update(
config: &timeweb_rs::apis::configuration::Configuration,
fqdn: String,
ns1: String,
ns2: String,
format: OutputFormat
) -> Result<(), TwcError> {
let update = dm::UpdateDomainNameServers::new(vec![
dm::UpdateDomainNameServersNameServersInner::new(ns1.clone()),
dm::UpdateDomainNameServersNameServersInner::new(ns2.clone()),
]);
let resp = domains_api::update_domain_name_servers(config, &fqdn, update).await?;
match format {
OutputFormat::Table => {
println!("{}", t!("cli.name_servers_updated", fqdn => fqdn));
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.name_servers)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
println!("{fqdn}\t{ns1}\t{ns2}\tupdated");
}
}
Ok(())
}
pub async fn subdomain_list(
config: &timeweb_rs::apis::configuration::Configuration,
fqdn: String,
format: OutputFormat
) -> Result<(), TwcError> {
let resp = domains_api::get_domain(config, &fqdn).await?;
let d = &resp.domain;
let rows: Vec<SubdomainRow> = d
.subdomains
.iter()
.map(|s| SubdomainRow {
id: fmt_id(s.id),
fqdn: s.fqdn.clone(),
ip: s.linked_ip.clone().unwrap_or_default()
})
.collect();
match format {
OutputFormat::Table => {
if rows.is_empty() {
println!("{}", t!("cli.no_subdomains"));
} else {
let table = crate::output::render_table(&rows);
println!("{table}");
}
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &d.subdomains)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
for s in &d.subdomains {
println!("{}\t{}", fmt_id(s.id), s.fqdn);
}
}
}
Ok(())
}
pub async fn subdomain_add(
config: &timeweb_rs::apis::configuration::Configuration,
fqdn: String,
subdomain: String,
format: OutputFormat
) -> Result<(), TwcError> {
let _resp = domains_api::add_subdomain(config, &fqdn, &subdomain).await?;
match format {
OutputFormat::Table => {
println!(
"{}",
t!("cli.domain_subdomain_added", subdomain => subdomain, fqdn => fqdn)
);
}
OutputFormat::Json | OutputFormat::Yaml => {
println!(
"{{\"fqdn\": \"{fqdn}\", \"subdomain\": \"{subdomain}\", \"status\": \"added\"}}"
);
}
OutputFormat::Quiet => {
println!("{fqdn}\t{subdomain}\tadded");
}
}
Ok(())
}
pub async fn subdomain_delete(
config: &timeweb_rs::apis::configuration::Configuration,
fqdn: String,
subdomain: String
) -> Result<(), TwcError> {
domains_api::delete_subdomain(config, &fqdn, &subdomain).await?;
println!(
"{}",
t!("cli.domain_subdomain_deleted", subdomain => subdomain, fqdn => fqdn)
);
Ok(())
}
pub async fn request_list(
config: &timeweb_rs::apis::configuration::Configuration,
format: OutputFormat
) -> Result<(), TwcError> {
let resp = domains_api::get_domain_requests(config, None).await?;
let rows: Vec<DomainRequestRow> = resp
.requests
.iter()
.map(|r| DomainRequestRow {
id: fmt_id(r.id),
fqdn: r.fqdn.clone(),
r#type: format!("{:?}", r.r#type),
date: r.date.to_rfc3339(),
message: r.message.clone().unwrap_or_default()
})
.collect();
match format {
OutputFormat::Table => {
if rows.is_empty() {
println!("{}", t!("cli.no_domain_requests"));
} else {
let table = crate::output::render_table(&rows);
println!("{table}");
}
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.requests)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
for r in &resp.requests {
let type_str = format!("{:?}", r.r#type);
println!("{}\t{}\t{type_str}", fmt_id(r.id), r.fqdn);
}
}
}
Ok(())
}
pub async fn tld_list(
config: &timeweb_rs::apis::configuration::Configuration,
format: OutputFormat
) -> Result<(), TwcError> {
let resp = domains_api::get_tlds(config, None, None).await?;
let rows: Vec<TldRow> = resp
.top_level_domains
.iter()
.map(|t| TldRow {
id: fmt_id(t.id),
name: t.name.clone(),
price: format!("{:.2}", t.price),
registrar: format!("{:?}", t.registrar),
is_published: t.is_published.to_string(),
is_registered: t.is_registered.to_string()
})
.collect();
match format {
OutputFormat::Table => {
if rows.is_empty() {
println!("{}", t!("cli.no_tlds"));
} else {
let table = crate::output::render_table(&rows);
println!("{table}");
}
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.top_level_domains)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
for t in &resp.top_level_domains {
let price_str = format!("{:.2}", t.price);
println!("{}\t{}\t{price_str}", t.name, fmt_id(t.id));
}
}
}
Ok(())
}
pub async fn auto_prolong(
config: &timeweb_rs::apis::configuration::Configuration,
fqdn: String,
enabled: bool,
format: OutputFormat
) -> Result<(), TwcError> {
let update = dm::UpdateDomain {
is_autoprolong_enabled: Some(enabled),
linked_ip: None
};
let resp = domains_api::update_domain_auto_prolongation(config, &fqdn, update).await?;
let d = &resp.domain;
match format {
OutputFormat::Table => {
let msg = if enabled {
t!("cli.domain_autoprolong_enabled", fqdn => d.fqdn, id => fmt_id(d.id))
} else {
t!("cli.domain_autoprolong_disabled", fqdn => d.fqdn, id => fmt_id(d.id))
};
println!("{msg}");
}
OutputFormat::Json | OutputFormat::Yaml => {
let out = crate::output::serialized(format, &resp.domain)
.transpose()?
.unwrap_or_default();
println!("{out}");
}
OutputFormat::Quiet => {
let state = if enabled { "on" } else { "off" };
println!("{fqdn}\t{state}");
}
}
Ok(())
}