diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 08a7b37..91c3279 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -18,4 +18,4 @@ runs: - name: Build release if: ${{ inputs.release == 'true' }} shell: bash - run: cargo build --target ${{ inputs.target }} -r + run: cargo build --release --target ${{ inputs.target }} -r diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml index a517808..0390cdf 100644 --- a/.github/workflows/build-dev.yml +++ b/.github/workflows/build-dev.yml @@ -39,12 +39,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Setup Rust toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - override: true - toolchain: nightly-2023-09-06 - components: rustfmt, clippy + uses: dsherret/rust-toolchain-file@v1 - name: Show Rust toolchain version shell: bash diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 976d5ef..be887ef 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -30,15 +30,10 @@ jobs: - name: Checkout uses: actions/checkout@v3 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.ref }} # github.ref 变量将自动填充为触发事件的分支或标签名 - name: Setup Rust toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - override: true - toolchain: nightly-2023-09-06 - components: rustfmt, clippy + uses: dsherret/rust-toolchain-file@v1 - name: Setup musl-tools if: matrix.targets.target == 'x86_64-unknown-linux-musl' @@ -99,4 +94,3 @@ jobs: uses: softprops/action-gh-release@v1 with: files: ${{ steps.gen-name.outputs.NAME }}.zip - diff --git a/Cargo.lock b/Cargo.lock index 7fc3726..096293d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -227,7 +227,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] -name = "cnb" +name = "cnblogs_lib" version = "0.0.0-dev" dependencies = [ "anyhow", @@ -483,9 +483,9 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" [[package]] name = "h2" -version = "0.3.20" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes", "fnv", @@ -493,7 +493,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap 2.0.0", "slab", "tokio", "tokio-util", diff --git a/Cargo.toml b/Cargo.toml index edffc32..02684e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "cnb" +name = "cnblogs_lib" # WRN: Version will be updated by CI while create a tag, NERVER change this. version = "0.0.0-dev" edition = "2021" @@ -8,6 +8,7 @@ license = "MIT" repository = "https://github.com/cnblogs/cli" keywords = ["cli", "cnblogs", "blog"] categories = ["command-line-utilities"] +default-run = "cnb" [profile.dev] lto = true @@ -47,3 +48,7 @@ colored = "2.0.4" terminal_size = "0.2.6" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[[bin]] +name = "cnb" +path = "src/bin/cnb.rs" \ No newline at end of file diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..a58b5d5 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,70 @@ +# 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] +``` + +## 闪存cli + +闪存cli设计如下: + +```sh +cnb ing query # 默认10条s +cnb ing query --id 123456 --id 123 +cnb ing query -n 1 -s 10 +cnb ing query --type f -n 2 -s 10 +# 根据tag查找,-g为tag名称 -n 2 -s 10 分页 +cnb ing query -t t -g Linux +cnb ing create hello --private --lucky +cnb ing create hello --private --lucky --tag hello +cnb ing delete --id 123456 +``` + +TODO: “提到我”存在解析问题。待完善。 diff --git a/rust-fmt.toml b/rust-fmt.toml new file mode 100644 index 0000000..9829022 --- /dev/null +++ b/rust-fmt.toml @@ -0,0 +1,5 @@ +max_width = 79 # 设置最大行宽为 100 个字符 +tab_spaces = 4 # 设置缩进宽度为 4 个空格 +edition = "2021" # 设置 Rust 版本(根据实际项目版本进行调整) +use_small_heuristics = "Max" # 设置换行策略 +newline_style = "Auto" # 设置换行符风格,根据平台自动选择 diff --git a/rust-toolchain.toml b/rust-toolchain.toml index fac5529..5b6bd4a 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,4 @@ [toolchain] -profile = "default" -channel = "nightly-2023-12-27" +profile = "minimal" +channel = "nightly-2024-01-01" +components = [ "rustfmt", "clippy" ] diff --git a/src/apis/ing/comment.rs b/src/apis/ing/comment.rs new file mode 100644 index 0000000..c6ae97a --- /dev/null +++ b/src/apis/ing/comment.rs @@ -0,0 +1,61 @@ +//! 闪存评论相关 +//! + +use anyhow::{Ok, Result}; +use reqwest::{Client, Response}; +use serde::{Deserialize, Serialize}; + +use crate::{api::ing::get_comment_list::IngCommentEntry, infra::http::RequestBuilderExt, openapi}; + +/// 闪存评论及评论回复 +/// +/// replay_to: 在web端有一个ReplyToUserId,这里盲猜是这个 +/// parent_comment_id: 0 是对某条闪存评论,如果对闪存评论要回应,这里则是闪存评论的id +/// content: 评论内容。 如果是对闪存评论回应,则应加上`@用户名称` +/// +#[derive(Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "PascalCase")] +#[serde(default)] +pub struct StatusComment { + #[serde(skip)] + pub status_id: String, + pub replay_to: u64, + pub parent_comment_id: u64, + pub content: String, +} + +/// 根据闪存ID发表一个评论 +pub async fn post(token: String, sc: StatusComment) -> Result { + let r = Client::new() + .post(openapi!("/statuses/{}/comments", sc.parent_comment_id)) + .pat_auth(token.as_str()) + .form(&sc) + .send() + .await? + .error_for_status()?; + Ok(r) +} + +/// 根据闪存ID获取评论 +pub async fn get(token: &str, status_id: &str) -> Result> { + let r = Client::new() + .get(openapi!("/statuses/{}/comments", status_id)) + .pat_auth(token) + .send() + .await? + .error_for_status()? + .json() + .await?; + Ok(r) +} + +/// 根据闪存ID和commentid删除评论 +pub async fn delete(token: &str, status_id: &str, comment_id: &str) -> Result<()> { + Client::new() + .delete(openapi!("/statuses/{}/comments/{}", status_id, comment_id)) + .pat_auth(token) + .send() + .await? + .error_for_status()?; + Ok(()) +} diff --git a/src/apis/ing/mod.rs b/src/apis/ing/mod.rs new file mode 100644 index 0000000..ee9243f --- /dev/null +++ b/src/apis/ing/mod.rs @@ -0,0 +1,195 @@ +//! 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} +//! + +pub mod comment; + +use anyhow::{Ok, Result}; +use clap::{Parser, ValueEnum}; +use reqwest::{Client, Response}; +use serde::{Deserialize, Serialize}; + +use crate::{ + api::ing::{get_list::IngEntry, IngSendFrom}, + infra::http::RequestBuilderExt, + openapi, +}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +#[serde(default)] +pub struct IngContent { + pub content: String, + pub is_private: bool, + pub lucky: bool, + pub client_type: IngSendFrom, +} + +impl Default for IngContent { + fn default() -> Self { + Self { + content: "".to_string(), + is_private: true, + lucky: false, + client_type: IngSendFrom::default(), + } + } +} + +impl Default for IngSendFrom { + fn default() -> Self { + Self::Cli + } +} + +/// 查询条件,用于根据类别查询 +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(default)] +pub struct QeurySet { + #[serde(skip)] + pub r#type: QueryIngType, + pub page_index: u64, + pub page_size: u64, + #[serde(skip_serializing_if = "String::is_empty")] + pub tag: String, +} + +impl Default for QeurySet { + fn default() -> Self { + Self { + r#type: QueryIngType::default(), + page_index: 1, + page_size: 10, + tag: "".to_string(), + } + } +} + +/// +/// Follow = 1, 关注 +/// Myself = 4, 我的 +/// Public = 5, 全站 +/// RecentComment = 6, 新回应 +/// MyComment = 7, 我回应 +/// Tag = 10, tag 必填 +/// Comment = 13 回复我 +/// Mention = 14, @我 +#[derive(Debug, Clone, ValueEnum, Parser)] +pub enum QueryIngType { + Following = 1, + My = 4, + All = 5, + RecentComment = 6, + MyComment = 7, + Tag = 10, + Comment = 13, + Mention = 14, +} + +impl Default for QueryIngType { + fn default() -> Self { + Self::All + } +} + +impl From for QueryIngType { + fn from(value: u8) -> Self { + match value { + 1 => Self::Following, + 4 => Self::My, + 6 => Self::RecentComment, + 7 => Self::MyComment, + 10 => Self::Tag, + 13 => Self::Comment, + 14 => Self::Mention, + _ => Self::All, + } + } +} + +impl QueryIngType { + const fn as_u8(&self) -> u8 { + match self { + Self::Following => 1, + Self::My => 4, + Self::All => 5, + Self::RecentComment => 6, + Self::MyComment => 7, + Self::Tag => 10, + Self::Mention => 14, + Self::Comment => 13, + } + } +} + +pub async fn lastest(token: &str) -> Result { + let c = Client::new() + .get(openapi!("/statuses/recent")) + .pat_auth(token) + .send() + .await? + .error_for_status()?; + Ok(c) +} + +/// 根据条件查询 +/// +/// 如果是tag是,一定要传入Tag,tag是自己想查询的比如Linux,Debian,Python等等。 +/// 页数是从1开始的 +pub async fn query(token: &str, q: &QeurySet) -> Result> { + let r = Client::new() + .get(openapi!("/statuses/@{}", q.r#type.as_u8())) + .pat_auth(token) + .query(&q) + .send() + .await? + .error_for_status()? + .json::>() + .await?; + Ok(r) +} + +/// 根据ID查询 +pub async fn query_by_id(token: &str, id: &u64) -> Result { + let r = Client::new() + .get(openapi!("/statuses/{}", id)) + .pat_auth(token) + .send() + .await? + .error_for_status()? + .json::() + .await?; + Ok(r) +} + +/// 发布一条闪存 +pub async fn post(token: &str, c: &IngContent) -> Result { + let r = Client::new() + .post(openapi!("/statuses")) + .pat_auth(token) + .json(c) + .send() + .await?; + Ok(r) +} + +/// 删除一条闪存 +pub async fn delete(token: &str, id: u64) -> Result { + let r = Client::new() + .post(openapi!("/statuses/{}", id)) + .pat_auth(token) + .send() + .await?; + Ok(r) +} diff --git a/src/apis/mod.rs b/src/apis/mod.rs new file mode 100644 index 0000000..67572a9 --- /dev/null +++ b/src/apis/mod.rs @@ -0,0 +1,19 @@ +//! cnblogs 闪存接口模块 +//! +//! 封装[cnblogs Api](https://api.cnblogs.com/Help#0aee001a01835c83a3277a500ffc9040)至以下模块中: +//! +//! - statuses: 闪存相关api。 +//! - blogs: 博客相关 +//! - news: 新闻相关 +//! - questions: 问题相关 +//! - edu: edu 相关 +//! - user: 用户相关 +//! - token: 认证相关 +//! - marks: 收藏相关 + +pub mod ing; +pub mod token; + +pub const OAUTH_CLIENT: &str = "https://api.cnblogs.com/token"; +pub const OAUTH_TOKEN: &str = "https://oauth.cnblogs.com/connect/token"; +pub const OAUTHORIZE: &str = "https://oauth.cnblogs.com/connect/authorize"; diff --git a/src/apis/token/mod.rs b/src/apis/token/mod.rs new file mode 100644 index 0000000..be6b877 --- /dev/null +++ b/src/apis/token/mod.rs @@ -0,0 +1,134 @@ +//! Token +//! +//! TokenApi的封装 +//! +//! OAuth认证,提供两种方式接口。 +//! +//! 1. Client_Credentials +//! 2. Authorization_Code +//! + +use super::{OAUTH_CLIENT, OAUTH_TOKEN}; +use anyhow::Result; +use reqwest::Client; +use serde::{Deserialize, Serialize}; + +/// 认证授权后的授权 +/// +/// # Filed +/// +/// - access_token: Token String +/// - expires_in: 过期时间 +/// - token_type: Token认证方式 +/// - refresh_token: 过期后刷新Token,如果是ClientCredentials,此字段无用。 +/// - id_token: id,如果是ClientCredentials,此字段无用 +/// - scope: 客户端权限 +/// +#[derive(Debug, Serialize, Deserialize, Default)] +#[serde(default)] +pub struct OAuthToken { + pub id_token: String, + pub access_token: String, + pub expires_in: u64, + pub token_type: String, + pub refresh_token: String, + pub scope: String, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(default)] +pub struct ClientCredentialsReq { + pub client_id: String, + pub client_secret: String, + pub grant_type: String, +} + +pub async fn client_credentials(req: ClientCredentialsReq) -> Result { + let c = Client::new().post(OAUTH_CLIENT); + let r = c + .form(&req) + .send() + .await? + .error_for_status()? + .json::() + .await?; + Ok(r) +} + +/// OAuth 获取Token结构体 +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct OauthTokenReq { + // pub client_id: String, // string 是 授权ID client_id + // pub client_secret: String, // string 是 密钥 client_secret + // pub grant_type: String, // string 是 授权模式 authorization_code + #[serde(flatten)] + pub cc: ClientCredentialsReq, + pub code: String, // string 是 授权码 code + pub redirect_uri: String, // string 是 回调地址(默认) https://oauth.cnblogs.com/auth/callback +} + +impl OauthTokenReq { + pub fn new(client_id: String, client_secret: String, code: String) -> Self { + Self { + cc: ClientCredentialsReq { + client_id, + client_secret, + grant_type: "authorization_code".to_string(), + }, + code, + redirect_uri: "https://oauth.cnblogs.com/auth/callback".to_string(), + } + } +} + +/// 获取令牌 +pub async fn authorization_code(req: OauthTokenReq) -> Result { + let c = Client::new().post(OAUTH_TOKEN); + let r = c + .form(&req) + .send() + .await? + .error_for_status()? + .json::() + .await?; + Ok(r) +} + +/// OAuth 获取Token结构体 +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct RefreshTokenReq { + // pub client_id: String, + // pub client_secret: String, + // pub grant_type: String, + #[serde(flatten)] + pub cc: ClientCredentialsReq, + pub refresh_token: String, +} + +impl RefreshTokenReq { + pub fn new(client_id: String, client_secret: String, refresh_token: String) -> Self { + Self { + cc: ClientCredentialsReq { + client_id, + client_secret, + grant_type: "refresh_token".to_string(), + }, + refresh_token, + } + } +} + +/// 刷新令牌 +/// +/// 令牌过期后重新获取。 +pub async fn refresh_token(req: RefreshTokenReq) -> Result { + let c = Client::new().post(OAUTH_TOKEN); + let r = c + .form(&req) + .send() + .await? + .error_for_status()? + .json::() + .await?; + Ok(r) +} diff --git a/src/args/cmd/ing.rs b/src/args/cmd/ing.rs index 974c6bc..5305c23 100644 --- a/src/args/cmd/ing.rs +++ b/src/args/cmd/ing.rs @@ -1,5 +1,11 @@ -use crate::api::ing::IngType; -use clap::{Parser, Subcommand}; +use crate::{ + api::ing::{IngSendFrom, IngType}, + apis::{ + self, + ing::{IngContent, QeurySet}, + }, +}; +use clap::{Args, Parser, Subcommand, ValueEnum}; #[derive(Parser, Debug)] #[non_exhaustive] @@ -54,4 +60,123 @@ pub enum Cmd { #[arg(default_value_t = true)] align: bool, }, + + /// 根据条件查询闪存。 + /// + Query(QueryIng), + + /// 创建闪存 + Create(CreateIng), + + /// 根据ID删除闪存 + Delete { id: Vec }, +} + +#[derive(Debug, Args)] +pub struct CreateIng { + /// 闪存内容 + pub content: String, + + /// 是否私有,默认是全站 + #[arg(short, long, default_value_t = false)] + pub private: bool, + + /// 是否发布为幸运 + #[arg(short, long, default_value_t = false)] + pub lucky: bool, + + /// 是否发布在某个标签下,默认不发布标签。 + #[arg(short, long, default_value = "")] + pub tag: String, +} + +impl From for IngContent { + fn from(value: CreateIng) -> Self { + let mut cont = String::new(); + if !value.tag.is_empty() { + cont.push_str(format!("[{}]", value.tag).as_str()) + } + cont.push_str(value.content.as_str()); + Self { + content: cont, + is_private: value.private, + lucky: value.lucky, + client_type: IngSendFrom::Cli, + } + } +} + +/// 查询参数 +/// 当使用type为 +#[derive(Debug, Args, Clone)] +pub struct QueryIng { + /// 查询类型 + #[arg( + short, + long, + value_name = "TYPE", + default_value_t = QueryType::F, + default_missing_value = "f", + value_enum + )] + pub r#type: QueryType, + /// 分页查询,起始索引是1 + #[arg(short('n'), long, default_value_t = 1)] + pub page_index: u64, + /// 分页查询数量, 默认是10 + #[arg(short('s'), long, default_value_t = 10)] + pub page_size: u64, + /// 按照标签查询 + #[arg(short('g'), long)] + pub tag: Option, + /// 根据ID查询 + #[arg(short, long)] + pub id: Option>, +} + +impl From<&QueryIng> for QeurySet { + fn from(value: &QueryIng) -> Self { + Self { + r#type: value.r#type.clone().into(), + page_index: value.page_index, + page_size: value.page_size, + tag: value.tag.clone().unwrap_or_default(), + } + } +} + +/// 过滤的类型 +#[derive(Debug, Clone, ValueEnum, Parser)] +pub enum QueryType { + /// 关注 + F = 1, + /// 我的 + My = 4, + /// 全站 + P = 5, + /// 新回应 + Rc = 6, + /// 我回应 + Mc = 7, + /// 按照Tag过滤,使用T时,如果没有Query::tag或者站点不存在,则不会有结果。 + T = 10, + /// 回复我 + C = 13, + /// 提到我 + M = 14, +} + +impl From for apis::ing::QueryIngType { + fn from(value: QueryType) -> Self { + match value { + QueryType::F => Self::Following, + QueryType::My => Self::My, + QueryType::P => Self::All, + QueryType::Rc => Self::RecentComment, + QueryType::Mc => Self::MyComment, + QueryType::T => Self::Tag, + QueryType::C => Self::Comment, + QueryType::M => Self::Mention, + } + } } diff --git a/src/args/parser/ing.rs b/src/args/parser/ing.rs index 62ed32b..dc5991c 100644 --- a/src/args/parser/ing.rs +++ b/src/args/parser/ing.rs @@ -1,4 +1,5 @@ use crate::api::ing::IngType; +use crate::args::cmd::ing::{CreateIng, QueryIng}; use crate::args::parser::{get_skip, get_take}; use crate::args::{cmd, Args, Cmd}; use crate::infra::option::WrapOption; @@ -67,3 +68,90 @@ pub fn comment_ing(args: &Args) -> Option<(&String, usize)> { } .wrap_some() } + +#[allow(unused)] +pub fn query(args: &Args) -> Option { + match args { + Args { + cmd: + Some(Cmd::Ing(cmd::ing::Opt { + cmd: + Some(cmd::ing::Cmd::Query(QueryIng { + r#type, + page_index, + page_size, + tag, + id, + })), + publish: None, + comment: None, + })), + id: None, + rev: _, + skip, + take, + global_opt: _, + } => QueryIng { + r#type: r#type.clone(), + page_index: *page_index, + page_size: *page_size, + tag: tag.clone(), + id: id.clone(), + }, + _ => return None, + } + .wrap_some() +} + +#[allow(unused)] +pub fn create_ing(args: &Args) -> Option { + match args { + Args { + cmd: + Some(Cmd::Ing(cmd::ing::Opt { + cmd: + Some(cmd::ing::Cmd::Create(CreateIng { + content, + private, + lucky, + tag, + })), + publish: None, + comment: None, + })), + id: None, + rev: _, + skip, + take, + global_opt: _, + } => CreateIng { + content: content.clone(), + private: *private, + lucky: *lucky, + tag: tag.clone(), + }, + _ => return None, + } + .wrap_some() +} + +#[allow(unused)] +pub fn delete(args: &Args) -> Option> { + match args { + Args { + cmd: + Some(Cmd::Ing(cmd::ing::Opt { + cmd: Some(cmd::ing::Cmd::Delete { id }), + publish: None, + comment: None, + })), + id: None, + rev: _, + skip, + take, + global_opt: _, + } => id.clone(), + _ => return None, + } + .wrap_some() +} diff --git a/src/main.rs b/src/bin/cnb.rs similarity index 70% rename from src/main.rs rename to src/bin/cnb.rs index 7b361dc..298b60a 100644 --- a/src/main.rs +++ b/src/bin/cnb.rs @@ -1,36 +1,33 @@ #![feature(try_blocks)] #![feature(if_let_guard)] #![feature(let_chains)] -#![feature(type_name_of_val)] #![feature(iterator_try_collect)] #![feature(iterator_try_reduce)] #![warn(clippy::all, clippy::nursery, clippy::cargo_common_metadata)] -use crate::api::auth::session; -use crate::api::fav::Fav; -use crate::api::ing::Ing; -use crate::api::news::News; -use crate::api::post::Post; -use crate::api::user::User; -use crate::args::cmd::post::{CreateCmd, UpdateCmd}; -use crate::args::parser::no_operation; -use crate::args::{parser, Args}; -use crate::infra::fp::currying::eq; -use crate::infra::infer::infer; -use crate::infra::iter::{ExactSizeIteratorExt, IntoIteratorExt}; -use crate::infra::option::OptionExt; -use crate::infra::result::WrapResult; +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::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 cnblogs_lib::{display, logic}; use colored::Colorize; use std::env; -pub mod api; -pub mod args; -pub mod display; -pub mod infra; - fn show_non_printable_chars(text: String) -> String { #[inline] fn make_red(str: &str) -> String { @@ -45,6 +42,7 @@ fn show_non_printable_chars(text: String) -> String { .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) @@ -86,20 +84,47 @@ async fn main() -> Result<()> { 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() + + _ if let Some(q) = parser::ing::query(&args) => { + let ing_with_comment_iter = + logic::ing::get_ings_and_comments(pat.unwrap().as_str(), &q) .await - .into_iter() - .collect::>>()? - }).map(|vec| vec.into_iter().dyn_rev(rev)); + .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, true)? + } + + _ if let Some(ids) = parser::ing::delete(&args) => { + let a = pat.as_ref().unwrap().as_str(); + logic::ing::delete_by_ing_id(a, ids).await; + "".to_string() + } + + _ if let Some(ci) = parser::ing::create_ing(&args) => { + let a = pat.as_ref().unwrap().as_str(); + logic::ing::create_ing_with_arg(a, ci).await; + "".to_string() + } + + _ 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)? } @@ -113,7 +138,9 @@ async fn main() -> Result<()> { } _ if let Some((content, id)) = parser::ing::comment_ing(&args) => { let content = try { - Ing::new(pat?).comment(id, content.clone(), None, None).await?; + Ing::new(pat?) + .comment(id, content.clone(), None, None) + .await?; content }; foe.then(|| panic_if_err(&content)); @@ -131,7 +158,8 @@ async fn main() -> Result<()> { } _ if let Some(id) = parser::post::show_post_comment(&args) => { let comment_iter = Post::new(pat?) - .get_comment_list(id).await + .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)? @@ -164,18 +192,28 @@ async fn main() -> Result<()> { let result = Post::new(pat?) .search_site(skip, take, kw) .await - .map(|vec | vec.into_iter().dyn_rev(rev)); + .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 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 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) @@ -197,9 +235,8 @@ async fn main() -> Result<()> { 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 no_operation(&args) => infer::(Args::command()).render_help().to_string(), + _ => "Invalid usage, follow '--help' for more information".to_owned(), }; if global_opt.quiet { diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4c7b191 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,13 @@ +#![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; +pub mod logic; diff --git a/src/logic/ing.rs b/src/logic/ing.rs new file mode 100644 index 0000000..53862ed --- /dev/null +++ b/src/logic/ing.rs @@ -0,0 +1,91 @@ +//! 闪存相关逻辑 +//! + +use anyhow::{Ok, Result}; + +use crate::{ + api::{ + self, + ing::{get_comment_list::IngCommentEntry, get_list::IngEntry}, + }, + apis::ing::{comment, delete, post, query as iq, query_by_id}, + args::cmd::ing::{CreateIng, QueryIng}, + infra::iter::IntoIteratorExt, +}; + +/// 根据queryset查询 +/// TODO: 提到我和我评论的解析存在问题。 +pub async fn get_ings_and_comments( + t: &str, + q: &QueryIng, +) -> Result)>> { + if let Some(ids) = &q.id { + let a = ids + .iter() + .map(|id| async move { query_by_id(t, id).await }) + .join_all() + .await + .into_iter() + .filter(|x| { + if x.is_err() { + eprintln!("{}", x.as_ref().err().unwrap()) + } + x.is_ok() + }) + .map(|x| x.unwrap()) + .collect::>(); + + get_ing_comments(t, a).await + } else { + let a = iq(t, &q.into()) + .await? + .into_iter() + .collect::>(); + get_ing_comments(t, a).await + } +} + +// TODO: 分类细化 +/// 初步提取公共部分 +pub async fn get_ing_comments( + t: &str, + i: Vec, +) -> Result)>> { + let a = i + .into_iter() + .map(|ing| async { + let result = comment::get(t, ing.id.to_string().as_str()).await; + result.map(|comment_vec| (ing, comment_vec)) + }) + .join_all() + .await + .into_iter() + .collect::>>()?; + Ok(a) +} + +/// 通过ID删除 +pub async fn delete_by_ing_id(t: &str, ids: Vec) { + ids.into_iter() + .map(|id| async move { delete(t, id).await }) + .join_all() + .await + .iter() + .for_each(|x| { + if x.is_err() { + eprintln!("{:?}", x.as_ref().err().unwrap().to_string()) + } + }); +} + +/// 创建闪存 +pub async fn create_ing_with_arg(t: &str, c: CreateIng) { + let cc = c.into(); + let e = post(t, &cc).await; + + if e.is_err() { + eprintln!("{:?}", e.as_ref().err().unwrap().to_string()); + } else { + println!("🙈 ! {:?}", cc.content); + } +} diff --git a/src/logic/mod.rs b/src/logic/mod.rs new file mode 100644 index 0000000..14d8057 --- /dev/null +++ b/src/logic/mod.rs @@ -0,0 +1,6 @@ +//! cli操作逻辑 +//! +//! 此模块暂定封装操作逻辑,比如是闪存的curd,闪存评论的curd。 +//! + +pub mod ing;