Skip to main content

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}