use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde_json::{Map, Value};
use crate::client::{MicropubAction, MicropubClient, MicropubRequest};
use crate::config::{load_token, Config};
use crate::draft::Draft;
use crate::media::{find_media_references, replace_paths, resolve_path, upload_file};
pub async fn cmd_publish(draft_path: &str, backdate: Option<DateTime<Utc>>) -> Result<()> {
let draft_id = std::path::Path::new(draft_path)
.file_stem()
.and_then(|s| s.to_str())
.context("Invalid draft path")?;
let mut draft = Draft::load(draft_id)?;
let config = Config::load()?;
let profile_name = draft
.metadata
.profile
.as_deref()
.or(Some(config.default_profile.as_str()))
.context("No profile specified and no default profile set")?;
let profile = config
.get_profile(profile_name)
.context(format!("Profile not found: {}", profile_name))?;
let token = load_token(profile_name)?;
let mut media_refs = find_media_references(&draft.content);
for photo_path in &draft.metadata.photo {
if !photo_path.starts_with("http://") && !photo_path.starts_with("https://") {
media_refs.push(photo_path.clone());
}
}
let mut replacements = Vec::new();
let mut uploaded_photo_urls = Vec::new();
if !media_refs.is_empty() {
let media_endpoint = profile.media_endpoint.as_ref()
.context(format!(
"No media endpoint found for profile '{}'. Re-authenticate to discover media endpoint:\n micropub auth {}",
profile_name, profile.domain
))?;
println!("Uploading {} media file(s)...", media_refs.len());
for local_path in media_refs {
let resolved = resolve_path(&local_path, None)?;
println!(" Uploading: {}", resolved.display());
let url = upload_file(media_endpoint, &token, &resolved).await?;
println!(" -> {}", url);
replacements.push((local_path.clone(), url.clone()));
if draft.metadata.photo.contains(&local_path) {
uploaded_photo_urls.push(url);
}
}
}
let final_content = replace_paths(&draft.content, &replacements);
let mut properties = Map::new();
properties.insert(
"content".to_string(),
Value::Array(vec![Value::String(final_content)]),
);
if let Some(name) = &draft.metadata.name {
properties.insert(
"name".to_string(),
Value::Array(vec![Value::String(name.clone())]),
);
}
if !draft.metadata.category.is_empty() {
properties.insert(
"category".to_string(),
Value::Array(
draft
.metadata
.category
.iter()
.map(|c| Value::String(c.clone()))
.collect(),
),
);
}
if !draft.metadata.photo.is_empty() {
let photo_values: Vec<Value> = if !uploaded_photo_urls.is_empty() {
uploaded_photo_urls
.iter()
.map(|url| Value::String(url.clone()))
.collect()
} else {
draft
.metadata
.photo
.iter()
.map(|p| Value::String(p.clone()))
.collect()
};
properties.insert("photo".to_string(), Value::Array(photo_values));
}
if !draft.metadata.syndicate_to.is_empty() {
properties.insert(
"mp-syndicate-to".to_string(),
Value::Array(
draft
.metadata
.syndicate_to
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
),
);
}
let published_date = backdate.or(draft.metadata.published);
if let Some(date) = published_date {
properties.insert(
"published".to_string(),
Value::Array(vec![Value::String(date.to_rfc3339())]),
);
}
let request = MicropubRequest {
action: MicropubAction::Create,
properties,
url: None,
};
let micropub_endpoint = profile
.micropub_endpoint
.as_ref()
.context("No micropub endpoint configured for this profile")?;
let client = MicropubClient::new(micropub_endpoint.clone(), token);
println!("Publishing to {}...", profile.domain);
let response = client.send(&request).await?;
draft.metadata.status = Some("published".to_string());
draft.metadata.url = response.url.clone();
draft.metadata.published_at = Some(Utc::now());
let archive_path = draft.archive()?;
println!("✓ Published successfully!");
if let Some(url) = response.url {
println!(" URL: {}", url);
}
println!(" Draft archived to: {}", archive_path.display());
Ok(())
}