use std::io::{self, BufRead, Write}; use std::net::SocketAddr; use std::path::Path; use std::sync::Arc; use itsgoin_core::node::Node; use itsgoin_core::types::DeviceProfile; #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "info,iroh=warn,swarm_discovery=warn".parse().unwrap()), ) .init(); let args: Vec = std::env::args().collect(); // Parse flags before anything else let mut data_dir = String::from("./itsgoin-data"); let mut import_key: Option = None; let mut bind_addr: Option = None; let mut daemon = false; let mut mobile = false; let mut web_port: Option = None; let mut i = 1; while i < args.len() { match args[i].as_str() { "--import-key" => { if i + 1 >= args.len() { eprintln!("Error: --import-key requires a 64-char hex key argument"); std::process::exit(1); } import_key = Some(args[i + 1].clone()); i += 2; } "--bind" => { if i + 1 >= args.len() { eprintln!("Error: --bind requires an address argument (e.g. 0.0.0.0:4433)"); std::process::exit(1); } bind_addr = Some(args[i + 1].parse().unwrap_or_else(|e| { eprintln!("Error: invalid bind address '{}': {}", args[i + 1], e); std::process::exit(1); })); i += 2; } "--daemon" => { daemon = true; i += 1; } "--mobile" => { mobile = true; i += 1; } "--web" => { if i + 1 >= args.len() { eprintln!("Error: --web requires a port argument (e.g. --web 8080)"); std::process::exit(1); } web_port = Some(args[i + 1].parse().unwrap_or_else(|e| { eprintln!("Error: invalid web port '{}': {}", args[i + 1], e); std::process::exit(1); })); i += 2; } _ => { if data_dir == "./itsgoin-data" { data_dir = args[i].clone(); } i += 1; } } } // Handle --import-key before opening the node if let Some(hex_key) = import_key { println!("Importing identity key into {}...", data_dir); match Node::import_identity(Path::new(&data_dir), &hex_key) { Ok(()) => println!("Identity key imported successfully."), Err(e) => { eprintln!("Error importing key: {}", e); std::process::exit(1); } } } let profile = if mobile { DeviceProfile::Mobile } else { DeviceProfile::Desktop }; println!("Starting ItsGoin node (data: {}, profile: {:?})...", data_dir, profile); let node = Arc::new(Node::open_with_bind(&data_dir, bind_addr, profile).await?); // Wait briefly for address resolution tokio::time::sleep(std::time::Duration::from_secs(1)).await; let node_id_hex = hex::encode(node.node_id); let addr = node.endpoint_addr(); let sockets: Vec<_> = addr.ip_addrs().collect(); // Show our display name if set let my_name = node.get_display_name(&node.node_id).await.unwrap_or(None); let name_display = my_name.as_deref().unwrap_or("(not set)"); println!(); println!("========================================"); println!(" ItsGoin node running"); println!(" Name: {}", name_display); println!(" Node ID: {}", node_id_hex); for sock in &sockets { println!(" Listen: {}", sock); } println!("========================================"); // Print a connect string others can paste if let Some(sock) = sockets.first() { println!(" Share this to connect:"); println!(" connect {}@{}", node_id_hex, sock); } println!(); println!("Commands:"); println!(" post Create a public post"); println!(" post-to Encrypted post (circle name, node_id, or 'friends')"); println!(" feed Show your feed"); println!(" posts Show all known posts"); println!(" follow Follow a node"); println!(" unfollow Unfollow a node"); println!(" follows List followed nodes"); println!(" connect @ Connect to a peer and sync"); println!(" connect Connect via address resolution"); println!(" sync Sync with all known peers"); println!(" peers List known peers"); println!(" circles List circles"); println!(" create-circle Create a circle"); println!(" delete-circle Delete a circle"); println!(" add-to-circle Add member to circle"); println!(" remove-from-circle Remove member from circle"); println!(" delete Delete one of your posts"); println!(" revoke [mode] Revoke access (mode: sync|reencrypt)"); println!(" revoke-circle [m] Revoke circle access for a node"); println!(" redundancy Show replica counts for your posts"); println!(" worm Worm lookup (find peer beyond 3-hop map)"); println!(" connections Show mesh connections"); println!(" social-routes Show social routing cache"); println!(" name Set your display name"); println!(" stats Show node stats"); println!(" export-key Export identity key (KEEP SECRET)"); println!(" id Show this node's ID"); println!(" quit Shut down"); println!(); // Start background tasks (v2: mesh connections) let _accept_handle = node.start_accept_loop(); let _pull_handle = node.start_pull_cycle(300); // 5 min pull cycle let _diff_handle = node.start_diff_cycle(120); // 2 min routing diff let _rebalance_handle = node.start_rebalance_cycle(600); // 10 min rebalance let _growth_handle = node.start_growth_loop(); // reactive mesh growth let _recovery_handle = node.start_recovery_loop(); // reactive anchor reconnect on mesh loss let _checkin_handle = node.start_social_checkin_cycle(3600); // 1 hour social checkin let _anchor_handle = node.start_anchor_register_cycle(600); // 10 min anchor register let _upnp_handle = node.start_upnp_renewal_cycle(); // UPnP lease renewal (if mapped) let _upnp_tcp_handle = node.start_upnp_tcp_renewal_cycle(); // UPnP TCP lease renewal let _http_handle = node.start_http_server(); // HTTP post delivery (if publicly reachable) let _bootstrap_check = node.start_bootstrap_connectivity_check(); // 24h isolation check let _replication_handle = node.start_replication_cycle(600); // 10 min active replication // Start blob eviction cycle (every 5 min, default 1 GB cache limit) let cache_max_bytes: u64 = { let storage = node.storage.get().await; storage.get_setting("cache_size_bytes") .ok() .flatten() .and_then(|s| s.parse().ok()) .unwrap_or(1_073_741_824u64) }; let _eviction_handle = Node::start_eviction_cycle(Arc::clone(&node), 300, cache_max_bytes); let _web_handle = if let Some(wp) = web_port { Some(node.start_web_handler(wp)) } else { None }; if daemon { println!("Running as daemon. Press Ctrl+C to stop."); tokio::signal::ctrl_c().await?; println!("Shutting down..."); return Ok(()); } // Interactive command loop let stdin = io::stdin(); let reader = stdin.lock(); print!("> "); io::stdout().flush()?; for line in reader.lines() { let line = line?; let line = line.trim().to_string(); if line.is_empty() { print!("> "); io::stdout().flush()?; continue; } let parts: Vec<&str> = line.splitn(2, ' ').collect(); let cmd = parts[0]; let arg = parts.get(1).map(|s| s.trim()); match cmd { "post" => { if let Some(text) = arg { match node.create_post(text.to_string()).await { Ok((id, _post)) => { println!("Posted! ID: {}", hex::encode(id)); } Err(e) => println!("Error: {}", e), } } else { println!("Usage: post "); } } "feed" => match node.get_feed().await { Ok(posts) => { if posts.is_empty() { println!("(feed is empty - follow some nodes first)"); } for (id, post, vis, decrypted) in posts { print_post(&id, &post, &vis, decrypted.as_deref(), &node).await; } } Err(e) => println!("Error: {}", e), }, "posts" => match node.get_all_posts().await { Ok(posts) => { if posts.is_empty() { println!("(no posts yet)"); } for (id, post, vis, decrypted) in posts { print_post(&id, &post, &vis, decrypted.as_deref(), &node).await; } } Err(e) => println!("Error: {}", e), }, "follow" => { if let Some(id_hex) = arg { match itsgoin_core::parse_node_id_hex(id_hex) { Ok(nid) => { node.follow(&nid).await?; println!("Following {}", &id_hex[..16.min(id_hex.len())]); } Err(e) => println!("Invalid node ID: {}", e), } } else { println!("Usage: follow "); } } "unfollow" => { if let Some(id_hex) = arg { match itsgoin_core::parse_node_id_hex(id_hex) { Ok(nid) => { node.unfollow(&nid).await?; println!("Unfollowed {}", &id_hex[..16.min(id_hex.len())]); } Err(e) => println!("Invalid node ID: {}", e), } } else { println!("Usage: unfollow "); } } "follows" => match node.list_follows().await { Ok(follows) => { if follows.is_empty() { println!("(not following anyone)"); } for nid in follows { let name = node.get_display_name(&nid).await.unwrap_or(None); let label = if nid == node.node_id { " (you)" } else { "" }; if let Some(name) = name { println!(" {} ({}){}", name, &hex::encode(nid)[..12], label); } else { println!(" {}{}", hex::encode(nid), label); } } } Err(e) => println!("Error: {}", e), }, "connect" => { if let Some(addr_str) = arg { if addr_str.contains('@') { // Full connect string: node_id@ip:port match itsgoin_core::parse_connect_string(addr_str) { Ok((nid, endpoint_addr)) => { println!("Connecting and syncing..."); node.add_peer(nid).await?; node.follow(&nid).await?; match node.sync_with_addr(endpoint_addr).await { Ok(()) => { let name = node.get_display_name(&nid).await.unwrap_or(None); if let Some(name) = name { println!("Sync complete with {}!", name); } else { println!("Sync complete!"); } } Err(e) => println!("Sync failed: {}", e), } } Err(e) => println!("Invalid address: {}", e), } } else { // Node ID only: use address resolution (N2/N3 + worm) match itsgoin_core::parse_node_id_hex(addr_str) { Ok(nid) => { println!("Resolving address..."); node.add_peer(nid).await?; node.follow(&nid).await?; match node.sync_with(nid).await { Ok(()) => { let name = node.get_display_name(&nid).await.unwrap_or(None); if let Some(name) = name { println!("Connected to {}!", name); } else { println!("Connected!"); } } Err(e) => println!("Address resolution failed: {}", e), } } Err(e) => println!("Invalid node ID: {}", e), } } } else { println!("Usage: connect or connect "); } } "sync" => { println!("Syncing with all known peers..."); match node.sync_all().await { Ok(()) => println!("Sync complete!"), Err(e) => println!("Sync error: {}", e), } } "post-to" => { if let Some(rest) = arg { let parts: Vec<&str> = rest.splitn(2, ' ').collect(); if parts.len() < 2 { println!("Usage: post-to "); println!(" target: 'friends', circle name, or node_id hex"); } else { let target = parts[0]; let text = parts[1]; let intent = if target == "friends" { itsgoin_core::types::VisibilityIntent::Friends } else if let Ok(nid) = itsgoin_core::parse_node_id_hex(target) { itsgoin_core::types::VisibilityIntent::Direct(vec![nid]) } else { itsgoin_core::types::VisibilityIntent::Circle(target.to_string()) }; match node .create_post_with_visibility(text.to_string(), intent, vec![]) .await { Ok((id, _post, _vis)) => { println!("Encrypted post! ID: {}", hex::encode(id)); } Err(e) => println!("Error: {}", e), } } } else { println!("Usage: post-to "); } } "circles" => match node.list_circles().await { Ok(circles) => { if circles.is_empty() { println!("(no circles)"); } for c in circles { println!( " {} ({} members, created {})", c.name, c.members.len(), chrono_lite(c.created_at / 1000) ); for m in &c.members { let name = node.get_display_name(m).await.unwrap_or(None); let label = name.unwrap_or_else(|| hex::encode(m)[..12].to_string()); println!(" - {}", label); } } } Err(e) => println!("Error: {}", e), }, "create-circle" => { if let Some(name) = arg { match node.create_circle(name.to_string()).await { Ok(()) => println!("Circle '{}' created", name), Err(e) => println!("Error: {}", e), } } else { println!("Usage: create-circle "); } } "delete-circle" => { if let Some(name) = arg { match node.delete_circle(name.to_string()).await { Ok(()) => println!("Circle '{}' deleted", name), Err(e) => println!("Error: {}", e), } } else { println!("Usage: delete-circle "); } } "add-to-circle" => { if let Some(rest) = arg { let parts: Vec<&str> = rest.splitn(2, ' ').collect(); if parts.len() < 2 { println!("Usage: add-to-circle "); } else { match itsgoin_core::parse_node_id_hex(parts[1]) { Ok(nid) => { match node.add_to_circle(parts[0].to_string(), nid).await { Ok(()) => println!("Added to circle '{}'", parts[0]), Err(e) => println!("Error: {}", e), } } Err(e) => println!("Invalid node ID: {}", e), } } } else { println!("Usage: add-to-circle "); } } "remove-from-circle" => { if let Some(rest) = arg { let parts: Vec<&str> = rest.splitn(2, ' ').collect(); if parts.len() < 2 { println!("Usage: remove-from-circle "); } else { match itsgoin_core::parse_node_id_hex(parts[1]) { Ok(nid) => { match node.remove_from_circle(parts[0].to_string(), nid).await { Ok(()) => println!("Removed from circle '{}'", parts[0]), Err(e) => println!("Error: {}", e), } } Err(e) => println!("Invalid node ID: {}", e), } } } else { println!("Usage: remove-from-circle "); } } "delete" => { if let Some(id_hex) = arg { match itsgoin_core::parse_node_id_hex(id_hex) { Ok(post_id) => match node.delete_post(&post_id).await { Ok(()) => println!("Post deleted: {}", &id_hex[..16.min(id_hex.len())]), Err(e) => println!("Error: {}", e), }, Err(e) => println!("Invalid post ID: {}", e), } } else { println!("Usage: delete "); } } "revoke" => { if let Some(rest) = arg { let parts: Vec<&str> = rest.splitn(3, ' ').collect(); if parts.len() < 2 { println!("Usage: revoke [sync|reencrypt]"); } else { let mode = match parts.get(2).unwrap_or(&"sync") { &"reencrypt" => itsgoin_core::types::RevocationMode::ReEncrypt, _ => itsgoin_core::types::RevocationMode::SyncAccessList, }; match ( itsgoin_core::parse_node_id_hex(parts[0]), itsgoin_core::parse_node_id_hex(parts[1]), ) { (Ok(post_id), Ok(node_id)) => { match node.revoke_post_access(&post_id, &node_id, mode).await { Ok(Some(new_id)) => { println!("Re-encrypted. New post ID: {}", hex::encode(new_id)); } Ok(None) => println!("Access revoked (sync mode)"), Err(e) => println!("Error: {}", e), } } (Err(e), _) => println!("Invalid post ID: {}", e), (_, Err(e)) => println!("Invalid node ID: {}", e), } } } else { println!("Usage: revoke [sync|reencrypt]"); } } "revoke-circle" => { if let Some(rest) = arg { let parts: Vec<&str> = rest.splitn(3, ' ').collect(); if parts.len() < 2 { println!("Usage: revoke-circle [sync|reencrypt]"); } else { let mode = match parts.get(2).unwrap_or(&"sync") { &"reencrypt" => itsgoin_core::types::RevocationMode::ReEncrypt, _ => itsgoin_core::types::RevocationMode::SyncAccessList, }; match itsgoin_core::parse_node_id_hex(parts[1]) { Ok(node_id) => { match node.revoke_circle_access(parts[0], &node_id, mode).await { Ok(count) => println!("Revoked access on {} posts", count), Err(e) => println!("Error: {}", e), } } Err(e) => println!("Invalid node ID: {}", e), } } } else { println!("Usage: revoke-circle [sync|reencrypt]"); } } "redundancy" => { match node.get_redundancy_summary().await { Ok((total, zero, one, two_plus)) => { println!("Redundancy for your {} authored posts:", total); println!(" No replicas: {} posts", zero); println!(" 1 replica: {} posts", one); println!(" 2+ replicas: {} posts", two_plus); if zero > 0 { println!(" WARNING: {} posts have no peer replicas!", zero); } } Err(e) => println!("Error: {}", e), } } "peers" => match node.list_peer_records().await { Ok(records) => { if records.is_empty() { println!("(no known peers)"); } for rec in records { let name = node.get_display_name(&rec.node_id).await.unwrap_or(None); let id_short = &hex::encode(rec.node_id)[..12]; let label = if let Some(name) = name { format!("{} ({})", name, id_short) } else { hex::encode(rec.node_id) }; let anchor = if rec.is_anchor { " [anchor]" } else { "" }; let addrs = if rec.addresses.is_empty() { String::from("(mDNS only)") } else { rec.addresses.iter().map(|a| a.to_string()).collect::>().join(", ") }; let intro = if let Some(ib) = rec.introduced_by { let ib_name = node.get_display_name(&ib).await.unwrap_or(None); format!( " via {}", ib_name.unwrap_or_else(|| hex::encode(ib)[..12].to_string()) ) } else { String::new() }; println!(" {}{} [{}]{}", label, anchor, addrs, intro); } } Err(e) => println!("Error: {}", e), }, "name" => { if let Some(display_name) = arg { match node.set_profile(display_name.to_string(), String::new()).await { Ok(profile) => { println!("Display name set to: {}", profile.display_name); } Err(e) => println!("Error: {}", e), } } else { // Show current name match node.my_profile().await { Ok(Some(profile)) => println!("Your name: {}", profile.display_name), Ok(None) => println!("No display name set. Usage: name "), Err(e) => println!("Error: {}", e), } } } "stats" => match node.stats().await { Ok(stats) => { println!("Posts: {}", stats.post_count); println!("Peers: {}", stats.peer_count); println!("Following: {}", stats.follow_count); } Err(e) => println!("Error: {}", e), }, "personas" => match node.list_posting_identities().await { Ok(identities) => { let default = { let s = node.storage.get().await; s.get_default_posting_id().ok().flatten() }; if identities.is_empty() { println!("(no posting identities)"); } for id in identities { let mark = if Some(id.node_id) == default { " *" } else { "" }; let name = if id.display_name.is_empty() { "(unnamed)" } else { &id.display_name }; println!(" {} {} {}{}", &hex::encode(id.node_id)[..12], name, id.created_at, mark); } } Err(e) => println!("Error: {}", e), }, "create-persona" => { let name = arg.unwrap_or("").to_string(); match node.create_posting_identity(name).await { Ok(id) => { println!("Created posting identity: {}", hex::encode(id.node_id)); } Err(e) => println!("Error: {}", e), } } "delete-persona" => { if let Some(id_hex) = arg { match itsgoin_core::parse_node_id_hex(id_hex) { Ok(nid) => match node.delete_posting_identity(&nid).await { Ok(()) => println!("Deleted posting identity"), Err(e) => println!("Error: {}", e), }, Err(e) => println!("Invalid node id: {}", e), } } else { println!("Usage: delete-persona "); } } "post-as" => { if let Some(rest) = arg { let parts: Vec<&str> = rest.splitn(2, ' ').collect(); if parts.len() < 2 { println!("Usage: post-as "); } else { match itsgoin_core::parse_node_id_hex(parts[0]) { Ok(nid) => { match node .create_post_as(&nid, parts[1].to_string(), itsgoin_core::types::VisibilityIntent::Public, vec![]) .await { Ok((id, _, _)) => println!("Posted as persona! ID: {}", hex::encode(id)), Err(e) => println!("Error: {}", e), } } Err(e) => println!("Invalid node id: {}", e), } } } else { println!("Usage: post-as "); } } "export-key" => { match node.export_identity_hex() { Ok(hex_key) => { println!("WARNING: This is your SECRET identity key. Anyone with"); println!("this key can impersonate you. Keep it safe!"); println!(); println!("{}", hex_key); println!(); println!("To import on another device, use:"); println!(" itsgoin --import-key {}", hex_key); } Err(e) => println!("Error: {}", e), } } "id" => { let addr = node.endpoint_addr(); let sockets: Vec<_> = addr.ip_addrs().collect(); println!("Node ID: {}", node_id_hex); for sock in &sockets { if let Some(first_sock) = sockets.first() { println!("Connect: {}@{}", node_id_hex, first_sock); break; } println!("Listen: {}", sock); } } "worm" => { if let Some(id_hex) = arg { match itsgoin_core::parse_node_id_hex(id_hex) { Ok(nid) => { println!("Worm lookup for {}...", &id_hex[..16.min(id_hex.len())]); let start = std::time::Instant::now(); match node.worm_lookup(&nid).await { Ok(Some(wr)) => { let elapsed = start.elapsed(); if wr.node_id == nid { println!("Found! ({:.1}ms)", elapsed.as_secs_f64() * 1000.0); } else { println!("Found via recent peer {}! ({:.1}ms)", &hex::encode(wr.node_id)[..12], elapsed.as_secs_f64() * 1000.0); } if wr.addresses.is_empty() { println!(" (no address resolved)"); } else { for addr in &wr.addresses { println!(" Address: {}", addr); } } println!(" Reporter: {}", &hex::encode(wr.reporter)[..12]); } Ok(None) => { let elapsed = start.elapsed(); println!("Not found ({:.1}ms)", elapsed.as_secs_f64() * 1000.0); } Err(e) => println!("Error: {}", e), } } Err(e) => println!("Invalid node ID: {}", e), } } else { println!("Usage: worm "); } } "connections" => { let conns = node.list_connections().await; if conns.is_empty() { println!("(no mesh connections)"); } else { println!("Mesh connections ({}):", conns.len()); for (nid, slot_kind, connected_at) in conns { let name = node.get_display_name(&nid).await.unwrap_or(None); let id_short = &hex::encode(nid)[..12]; let label = name.map(|n| format!("{} ({})", n, id_short)) .unwrap_or_else(|| hex::encode(nid)); let duration_secs = { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_millis() as u64; (now.saturating_sub(connected_at)) / 1000 }; println!(" {} [{:?}] connected {}s ago", label, slot_kind, duration_secs); } } } "social-routes" => { match node.list_social_routes().await { Ok(routes) => { if routes.is_empty() { println!("(no social routes)"); } else { println!("Social routes ({}):", routes.len()); for r in routes { let name = node.get_display_name(&r.node_id).await.unwrap_or(None); let id_short = &hex::encode(r.node_id)[..12]; let label = name.map(|n| format!("{} ({})", n, id_short)) .unwrap_or_else(|| hex::encode(r.node_id)); let addrs = if r.addresses.is_empty() { String::from("(no addr)") } else { r.addresses.iter().map(|a| a.to_string()).collect::>().join(", ") }; println!(" {} [{:?}] {} [{}] via {:?} peers:{}", label, r.relation, r.status, addrs, r.reach_method, r.peer_addresses.len()); } } } Err(e) => println!("Error: {}", e), } } "quit" | "exit" | "q" => { println!("Shutting down..."); break; } _ => { println!("Unknown command: {}", cmd); } } print!("> "); io::stdout().flush()?; } Ok(()) } async fn print_post( id: &[u8; 32], post: &itsgoin_core::types::Post, vis: &itsgoin_core::types::PostVisibility, decrypted: Option<&str>, node: &Node, ) { let author_hex = hex::encode(post.author); let author_short = &author_hex[..12]; let is_me = &post.author == &node.node_id; let author_name = node.get_display_name(&post.author).await.unwrap_or(None); let author_label = match (author_name, is_me) { (Some(name), true) => format!("{} (you)", name), (Some(name), false) => name, (None, true) => format!("{} (you)", author_short), (None, false) => author_short.to_string(), }; let vis_label = match vis { itsgoin_core::types::PostVisibility::Public => String::new(), itsgoin_core::types::PostVisibility::Encrypted { recipients } => { format!(" [encrypted, {} recipients]", recipients.len()) } itsgoin_core::types::PostVisibility::GroupEncrypted { epoch, .. } => { format!(" [group-encrypted, epoch {}]", epoch) } }; let ts = post.timestamp_ms / 1000; let datetime = chrono_lite(ts); let display_content = match vis { itsgoin_core::types::PostVisibility::Public => post.content.as_str().to_string(), itsgoin_core::types::PostVisibility::Encrypted { .. } | itsgoin_core::types::PostVisibility::GroupEncrypted { .. } => match decrypted { Some(text) => text.to_string(), None => "(encrypted)".to_string(), }, }; println!("---"); println!(" {} | {}{}", author_label, datetime, vis_label); println!(" {}", display_content); println!(" id: {}", &hex::encode(id)[..16]); } /// Minimal timestamp formatting without pulling in chrono fn chrono_lite(unix_secs: u64) -> String { let secs_per_min = 60u64; let secs_per_hour = 3600u64; let secs_per_day = 86400u64; let days_since_epoch = unix_secs / secs_per_day; let time_of_day = unix_secs % secs_per_day; let hours = time_of_day / secs_per_hour; let minutes = (time_of_day % secs_per_hour) / secs_per_min; let mut year = 1970u64; let mut remaining_days = days_since_epoch; loop { let days_in_year = if is_leap(year) { 366 } else { 365 }; if remaining_days < days_in_year { break; } remaining_days -= days_in_year; year += 1; } let month_days: Vec = if is_leap(year) { vec![31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] } else { vec![31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] }; let mut month = 1u64; for &d in &month_days { if remaining_days < d { break; } remaining_days -= d; month += 1; } let day = remaining_days + 1; format!( "{:04}-{:02}-{:02} {:02}:{:02} UTC", year, month, day, hours, minutes ) } fn is_leap(year: u64) -> bool { (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) }