diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..197dea1 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,52 @@ +# Cnblogs 命令行工具 + +[![Build / Release](https://github.com/cnblogs/cli/actions/workflows/build-release.yml/badge.svg)](https://github.com/cnblogs/cli/actions/workflows/build-release.yml) +[![Build / Development](https://github.com/cnblogs/cli/actions/workflows/build-dev.yml/badge.svg)](https://github.com/cnblogs/cli/actions/workflows/build-dev.yml) + +从 CLI 访问 cnblogs。 + +## Cnbogs Cli 设计 + +从Cnblogs的[OpenAPI](https://api.cnblogs.com/help)来说,API主要有以下几类: + +1. Token: 认证 +2. Users: 仅提供当前登录用户信息 +3. Blogs: 博客的CURD及其评论的查看和增加, +4. Marks: 收藏的CURD +5. News: 新闻的查询,新闻评论的CURD +6. Statuses: 闪存CURD。 +7. Questions: 问题相关操作 +8. Edu: 班级相关 +9. Articles: 知识库的查找。 +10. Zzk: 找找看 + +### cli的使用 + +目前cli的使用如下: + +```shell +# Check your post list +cnb post --list +# Check your post +cnb --id 114514 post --show +# Create and publish post +cnb post create --title 'Hello' --body 'world!' --publish +# Change your post body +cnb --id 114514 post update --body 'niconiconiconi' + +# Show ing list +cnb ing list +# Publish ing +cnb ing --publish 'Hello world!' +# Comment to ing +cnb --id 114514 ing --comment 'Awesome!' + +# Check your user infomation +cnb user --info +``` + +大体上使用如上的设计,支持子命令,相关操作的设计按照RESTFUL的思路设计实现,博客的相关操作设计如下: + +```shell +cnb posts [comment] [list,create,query,delete,update] --[id/file/quertset] --[pagesize,pagecount] +``` diff --git a/src/apis/mod.rs b/src/apis/mod.rs new file mode 100644 index 0000000..b2fa28a --- /dev/null +++ b/src/apis/mod.rs @@ -0,0 +1,14 @@ +//! cnblogs 闪存接口模块 +//! +//! 封装[cnblogs Api](https://api.cnblogs.com/Help#0aee001a01835c83a3277a500ffc9040)至以下模块中: +//! +//! - statuses: 闪存相关api。 +//! - blogs: 博客相关 +//! - news: 新闻相关 +//! - questions: 问题相关 +//! - edu: edu 相关 +//! - user: 用户相关 +//! - token: 认证相关 +//! - marks: 收藏相关 + +pub mod statuses; diff --git a/src/apis/statuses/mod.rs b/src/apis/statuses/mod.rs new file mode 100644 index 0000000..2347f22 --- /dev/null +++ b/src/apis/statuses/mod.rs @@ -0,0 +1,13 @@ +//! cnblogs 闪存接口模块 +//! +//! 实现封装[cnblogs Api](https://api.cnblogs.com/Help#0aee001a01835c83a3277a500ffc9040)中的`Statuses`。 +//! +//! - 获取最新一条闪存内容 https://api.cnblogs.com/api/statuses/recent +//! - 发布闪存评论 https://api.cnblogs.com/api/statuses/{statusId}/comments +//! - 获取闪存评论 https://api.cnblogs.com/api/statuses/{statusId}/comments +//! - 删除闪存评论 https://api.cnblogs.com/api/statuses/{statusId}/comments/{id} +//! - 发布闪存 https://api.cnblogs.com/api/statuses +//! - 删除闪存 https://api.cnblogs.com/api/statuses/{id} +//! - 根据类型获取闪存列表 https://api.cnblogs.com/api/statuses/@{type}?pageIndex={pageIndex}&pageSize={pageSize}&tag={tag} +//! - 根据Id获取闪存 https://api.cnblogs.com/api/statuses/{id} +//! diff --git a/src/bin/cnb.rs b/src/bin/cnb.rs new file mode 100644 index 0000000..64fe301 --- /dev/null +++ b/src/bin/cnb.rs @@ -0,0 +1,241 @@ +#![feature(try_blocks)] +#![feature(if_let_guard)] +#![feature(let_chains)] +#![feature(iterator_try_collect)] +#![feature(iterator_try_reduce)] +#![warn(clippy::all, clippy::nursery, clippy::cargo_common_metadata)] + +extern crate cnblogs_lib; + +use anyhow::Result; +use clap::Parser; +use clap::{Command, CommandFactory}; +use cnblogs_lib::api::auth::session; +use cnblogs_lib::api::fav::Fav; +use cnblogs_lib::api::ing::Ing; +use cnblogs_lib::api::news::News; +use cnblogs_lib::api::post::Post; +use cnblogs_lib::api::user::User; +use cnblogs_lib::args::cmd::post::{CreateCmd, UpdateCmd}; +use cnblogs_lib::args::parser::no_operation; +use cnblogs_lib::args::{parser, Args}; +use cnblogs_lib::display; +use cnblogs_lib::infra::fp::currying::eq; +use cnblogs_lib::infra::infer::infer; +use cnblogs_lib::infra::iter::{ExactSizeIteratorExt, IntoIteratorExt}; +use cnblogs_lib::infra::option::OptionExt; +use cnblogs_lib::infra::result::WrapResult; +use colored::Colorize; +use std::env; + +fn show_non_printable_chars(text: String) -> String { + #[inline] + fn make_red(str: &str) -> String { + format!("{}", str.red()) + } + + text.replace(' ', &make_red("·")) + .replace('\0', &make_red("␀\0")) + .replace('\t', &make_red("␉\t")) + .replace('\n', &make_red("␊\n")) + .replace('\r', &make_red("␍\r")) + .replace("\r\n", &make_red("␍␊\r\n")) +} + +#[allow(clippy::missing_const_for_fn)] +fn panic_if_err(result: &Result) { + if let Err(e) = result { + panic!("{}", e) + } +} + +#[tokio::main(flavor = "multi_thread")] +async fn main() -> Result<()> { + let args_vec = env::args().collect::>(); + if args_vec.iter().any(eq(&"--debug".to_owned())) { + dbg!(args_vec); + } + + let args: Args = Args::parse(); + let global_opt = &args.global_opt; + if global_opt.debug { + dbg!(&args); + } + + let pat = global_opt.with_pat.clone().or_eval_result(session::get_pat); + let style = &global_opt.style; + let time_style = &global_opt.time_style; + let rev = args.rev; + let foe = global_opt.fail_on_error; + + let output = match args { + _ if let Some(pat) = parser::user::login(&args) => { + let cfg_path = session::login(pat); + foe.then(|| panic_if_err(&cfg_path)); + display::login(style, &cfg_path) + } + _ if parser::user::logout(&args) => { + let cfg_path = session::logout(); + foe.then(|| panic_if_err(&cfg_path)); + display::logout(style, &cfg_path) + } + _ if parser::user::user_info(&args) => { + let user_info = User::new(pat?).get_info().await; + foe.then(|| panic_if_err(&user_info)); + display::user_info(style, &user_info)? + } + _ if let Some((skip, take, r#type, align)) = parser::ing::list_ing(&args) => { + let ing_with_comment_iter = infer::>( + try { + let ing_api = Ing::new(pat?); + let ing_vec = ing_api.get_list(skip, take, &r#type).await?; + ing_vec + .into_iter() + .map(|ing| async { + let result = ing_api.get_comment_list(ing.id).await; + result.map(|comment_vec| (ing, comment_vec)) + }) + .join_all() + .await + .into_iter() + .collect::>>()? + }, + ) + .map(|vec| vec.into_iter().dyn_rev(rev)); + foe.then(|| panic_if_err(&ing_with_comment_iter)); + display::list_ing(style, time_style, ing_with_comment_iter, align)? + } + _ if let Some(content) = parser::ing::publish_ing(&args) => { + let content = try { + Ing::new(pat?).publish(content).await?; + content + }; + foe.then(|| panic_if_err(&content)); + display::publish_ing(style, &content) + } + _ if let Some((content, id)) = parser::ing::comment_ing(&args) => { + let content = try { + Ing::new(pat?) + .comment(id, content.clone(), None, None) + .await?; + content + }; + foe.then(|| panic_if_err(&content)); + display::comment_ing(style, &content) + } + _ if let Some(id) = parser::post::show_post(&args) => { + let entry = Post::new(pat?).get_one(id).await; + foe.then(|| panic_if_err(&entry)); + display::show_post(style, &entry)? + } + _ if let Some(id) = parser::post::show_post_meta(&args) => { + let entry = Post::new(pat?).get_one(id).await; + foe.then(|| panic_if_err(&entry)); + display::show_post_meta(style, time_style, &entry)? + } + _ if let Some(id) = parser::post::show_post_comment(&args) => { + let comment_iter = Post::new(pat?) + .get_comment_list(id) + .await + .map(|vec| vec.into_iter().dyn_rev(rev)); + foe.then(|| panic_if_err(&comment_iter)); + display::show_post_comment(style, time_style, comment_iter)? + } + _ if let Some((skip, take)) = parser::post::list_post(&args) => { + let meta_iter = Post::new(pat?) + .get_meta_list(skip, take) + .await + .map(|(vec, count)| (vec.into_iter().dyn_rev(rev), count)); + foe.then(|| panic_if_err(&meta_iter)); + display::list_post(style, meta_iter)? + } + _ if let Some(id) = parser::post::delete_post(&args) => { + let id = try { + Post::new(pat?).del_one(id).await?; + id + }; + foe.then(|| panic_if_err(&id)); + display::delete_post(style, &id) + } + _ if let Some((kw, skip, take)) = parser::post::search_self_post(&args) => { + let result = Post::new(pat?) + .search_self(skip, take, kw) + .await + .map(|(vec, count)| (vec.into_iter().dyn_rev(rev), count)); + foe.then(|| panic_if_err(&result)); + display::search_self_post(style, result)? + } + _ if let Some((kw, skip, take)) = parser::post::search_site_post(&args) => { + let result = Post::new(pat?) + .search_site(skip, take, kw) + .await + .map(|vec| vec.into_iter().dyn_rev(rev)); + foe.then(|| panic_if_err(&result)); + display::search_site_post(style, time_style, result)? + } + _ if let Some(create_cmd) = parser::post::create_post(&args) => { + let CreateCmd { + title, + body, + publish, + .. + } = create_cmd; + let id = Post::new(pat?).create(title, body, *publish).await; + foe.then(|| panic_if_err(&id)); + display::create_post(style, &id) + } + _ if let Some((id, update_cmd)) = parser::post::update_post(&args) => { + let UpdateCmd { + title, + body, + publish, + .. + } = update_cmd; + let id = Post::new(pat?).update(id, title, body, publish).await; + foe.then(|| panic_if_err(&id)); + display::update_post(style, &id) + } + _ if let Some((skip, take)) = parser::news::list_news(&args) => { + let news_iter = News::new(pat?) + .get_list(skip, take) + .await + .map(|vec| vec.into_iter().dyn_rev(rev)); + foe.then(|| panic_if_err(&news_iter)); + display::list_news(style, time_style, news_iter)? + } + _ if let Some((skip, take)) = parser::fav::list_fav(&args) => { + let fav_iter = Fav::new(pat?) + .get_list(skip, take) + .await + .map(|vec| vec.into_iter().dyn_rev(rev)); + foe.then(|| panic_if_err(&fav_iter)); + display::list_fav(style, time_style, fav_iter)? + } + + _ if no_operation(&args) => infer::(Args::command()).render_help().to_string(), + _ => "Invalid usage, follow '--help' for more information".to_owned(), + }; + + if global_opt.quiet { + return ().wrap_ok(); + } + + let output = { + let output = if output.ends_with("\n\n") { + output[..output.len() - 1].to_owned() + } else if output.ends_with('\n') { + output + } else { + format!("{}\n", output) + }; + if global_opt.debug { + show_non_printable_chars(output) + } else { + output + } + }; + + print!("{}", output); + + ().wrap_ok() +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..984f7bc --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,12 @@ +#![feature(try_blocks)] +#![feature(if_let_guard)] +#![feature(let_chains)] +#![feature(iterator_try_collect)] +#![feature(iterator_try_reduce)] +#![warn(clippy::all, clippy::nursery, clippy::cargo_common_metadata)] + +pub mod api; +pub mod apis; +pub mod args; +pub mod display; +pub mod infra;