wx_uploader/lib.rs
1//! WeChat Public Account Markdown Uploader Library
2//!
3//! A library for uploading markdown files to WeChat public accounts with automatic
4//! cover image generation and frontmatter management.
5//!
6//! ## Features
7//!
8//! - Upload individual markdown files or process directories recursively
9//! - Parse and manage YAML frontmatter to track publication status
10//! - Automatically generate cover images using Gemini (default) or OpenAI
11//! - Skip already published files in directory processing mode
12//! - Support for custom themes and code highlighters via frontmatter
13//!
14//! ## Usage
15//!
16//! ```rust,no_run
17//! use wx_uploader::{WxUploader, Config, Result};
18//!
19//! #[tokio::main]
20//! async fn main() -> Result<()> {
21//! let config = Config::from_env()?;
22//! let uploader = WxUploader::new(config).await?;
23//!
24//! // Upload a single file
25//! uploader.upload_file("article.md", true).await?;
26//!
27//! // Process a directory
28//! uploader.process_directory("./articles").await?;
29//!
30//! Ok(())
31//! }
32//! ```
33
34pub mod cli;
35pub mod error;
36pub mod gemini;
37pub mod markdown;
38pub mod models;
39pub mod openai;
40pub mod output;
41pub mod wechat;
42
43pub use error::{Error, Result};
44pub use models::{Config, Frontmatter};
45// Core uploader functionality is implemented directly in this module
46
47use std::path::Path;
48
49/// Core uploader functionality combining WeChat and image generation clients
50pub struct WxUploader {
51 wechat_client: wechat::WeChatClient,
52 config: Config,
53}
54
55impl WxUploader {
56 /// Creates a new uploader instance with the provided configuration
57 ///
58 /// # Arguments
59 ///
60 /// * `config` - Configuration containing API keys and settings
61 ///
62 /// # Errors
63 ///
64 /// Returns an error if WeChat client initialization fails
65 pub async fn new(config: Config) -> Result<Self> {
66 let wechat_client = wechat::WeChatClient::new(
67 config.wechat_app_id.clone(),
68 config.wechat_app_secret.clone(),
69 )
70 .await
71 .map_err(|e| Error::wechat(e.to_string()))?;
72
73 Ok(Self {
74 wechat_client,
75 config,
76 })
77 }
78
79 /// Forces a refresh of the WeChat access token
80 ///
81 /// This clears the in-memory token cache and fetches a new token from the WeChat API.
82 ///
83 /// # Errors
84 ///
85 /// Returns an error if the token refresh fails
86 pub async fn refresh_token(&self) -> Result<String> {
87 self.wechat_client
88 .refresh_token()
89 .await
90 .map_err(|e| Error::wechat(e.to_string()))
91 }
92
93 /// Uploads a single markdown file to WeChat
94 ///
95 /// # Arguments
96 ///
97 /// * `path` - Path to the markdown file
98 /// * `force` - If true, uploads regardless of published status
99 ///
100 /// # Errors
101 ///
102 /// Returns an error if the upload process fails
103 pub async fn upload_file<P: AsRef<Path>>(&self, path: P, force: bool) -> Result<()> {
104 wechat::upload_file(
105 &self.wechat_client,
106 &self.config,
107 path.as_ref(),
108 force,
109 self.config.verbose,
110 )
111 .await
112 }
113
114 /// Processes all markdown files in a directory recursively
115 ///
116 /// Files marked as published will be skipped unless forced.
117 ///
118 /// # Arguments
119 ///
120 /// * `dir` - Directory path to process
121 ///
122 /// # Errors
123 ///
124 /// Returns an error if directory processing fails
125 pub async fn process_directory<P: AsRef<Path>>(&self, dir: P) -> Result<()> {
126 wechat::process_directory(
127 &self.wechat_client,
128 &self.config,
129 dir.as_ref(),
130 self.config.verbose,
131 )
132 .await
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[tokio::test]
141 async fn test_uploader_creation_without_keys() {
142 let config = Config {
143 wechat_app_id: "test_app_id".to_string(),
144 wechat_app_secret: "test_secret".to_string(),
145 openai_api_key: None,
146 gemini_api_key: None,
147 verbose: false,
148 };
149
150 // This would fail in real scenario without valid WeChat credentials,
151 // but tests the structure
152 let result = WxUploader::new(config).await;
153 // We expect this to fail with network/auth error, not a compilation error
154 assert!(result.is_err());
155 }
156
157 #[test]
158 fn test_config_from_env_missing_required() {
159 // Clear environment variables
160 unsafe {
161 std::env::remove_var("WECHAT_APP_ID");
162 std::env::remove_var("WECHAT_APP_SECRET");
163 }
164
165 let result = Config::from_env();
166 assert!(result.is_err());
167 }
168}