diff --git a/src/api/fav/get_list.rs b/src/api/fav/get_list.rs new file mode 100644 index 0000000..26f8d8c --- /dev/null +++ b/src/api/fav/get_list.rs @@ -0,0 +1,63 @@ +use crate::api::fav::Fav; +use crate::infra::http::{body_or_err, RequestBuilderExt, VecExt as HttpVecExt}; +use crate::infra::iter::IntoIteratorExt; +use crate::infra::json; +use crate::infra::result::IntoResult; +use crate::infra::vec::VecExt; +use crate::openapi; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::ops::ControlFlow; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FavEntry { + #[serde(rename = "Title")] + pub title: String, + #[serde(rename = "LinkUrl")] + pub url: String, + #[serde(rename = "Summary")] + pub summary: String, + #[serde(rename = "Tags")] + pub tags: Vec, + #[serde(rename = "DateAdded")] + pub create_time: String, +} + +impl Fav { + pub async fn get_list(&self, skip: usize, take: usize) -> Result> { + let client = &reqwest::Client::new(); + + let range = (skip + 1)..=(skip + take); + let cf = range + .map(|i| async move { + let req = { + let query = vec![("pageIndex", i), ("pageSize", 1)].into_query_string(); + let url = openapi!("/Bookmarks?{}", query); + client.get(url).pat_auth(&self.pat) + }; + + let resp = req.send().await?; + + let body = body_or_err(resp).await?; + + json::deserialize::>(&body)? + .pop() + .into_ok::() + }) + .join_all() + .await + .into_iter() + .try_fold(vec![], |acc, it| match it { + Ok(maybe) => match maybe { + Some(entry) => ControlFlow::Continue(acc.chain_push(entry)), + None => ControlFlow::Break(Ok(acc)), + }, + Err(e) => ControlFlow::Break(Err(e)), + }); + + match cf { + ControlFlow::Continue(vec) => Ok(vec), + ControlFlow::Break(result) => result, + } + } +} diff --git a/src/api/fav/mod.rs b/src/api/fav/mod.rs new file mode 100644 index 0000000..a1e5984 --- /dev/null +++ b/src/api/fav/mod.rs @@ -0,0 +1,12 @@ +pub mod get_list; + +// Aka cnblogs wz +pub struct Fav { + pat: String, +} + +impl Fav { + pub const fn new(pat: String) -> Self { + Self { pat } + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 4ada5b2..94018dd 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,4 +1,5 @@ pub mod auth; +pub mod fav; pub mod ing; pub mod news; pub mod post; diff --git a/src/args/cmd/fav.rs b/src/args/cmd/fav.rs new file mode 100644 index 0000000..b74079a --- /dev/null +++ b/src/args/cmd/fav.rs @@ -0,0 +1,11 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +pub struct Opt { + #[arg(verbatim_doc_comment)] + /// Show favorite list, order by time in DESC + /// Example: cnb fav --list + #[arg(long)] + #[arg(short = 'l')] + pub list: bool, +} diff --git a/src/args/cmd/mod.rs b/src/args/cmd/mod.rs index 4d1226d..afc88dc 100644 --- a/src/args/cmd/mod.rs +++ b/src/args/cmd/mod.rs @@ -1,3 +1,4 @@ +pub mod fav; pub mod ing; pub mod news; pub mod post; @@ -19,4 +20,7 @@ pub enum Cmd { /// News operations #[clap(visible_alias = "n")] News(news::Opt), + /// Favorite operations + #[clap(visible_alias = "f")] + Fav(fav::Opt), } diff --git a/src/args/mod.rs b/src/args/mod.rs index c344055..435d319 100644 --- a/src/args/mod.rs +++ b/src/args/mod.rs @@ -17,6 +17,7 @@ pub enum TimeStyle { Normal, } +// TODO: flatten options in struct? #[derive(Debug, Parser)] #[command(author, about, long_about = None, version)] pub struct Args { diff --git a/src/args/parser.rs b/src/args/parser.rs index 5049d6d..f0e10fe 100644 --- a/src/args/parser.rs +++ b/src/args/parser.rs @@ -457,3 +457,27 @@ pub fn list_news(args: &Args) -> Option<(usize, usize)> { } .into_some() } + +pub fn list_fav(args: &Args) -> Option<(usize, usize)> { + match args { + Args { + cmd: Some(Cmd::Fav(cmd::fav::Opt { list: true })), + id: None, + with_pat: _, + rev: _, + skip, + take, + debug: _, + style: _, + time_style: _, + fail_on_error: _, + quiet: _, + } => { + let skip = get_skip(skip); + let take = get_take(take); + (skip, take) + } + _ => return None, + } + .into_some() +} diff --git a/src/display/colorful.rs b/src/display/colorful.rs index 47a6910..bfae13c 100644 --- a/src/display/colorful.rs +++ b/src/display/colorful.rs @@ -1,3 +1,4 @@ +use crate::api::fav::get_list::FavEntry; use crate::api::ing::get_comment_list::IngCommentEntry; use crate::api::ing::get_list::IngEntry; use crate::api::ing::{ @@ -348,3 +349,48 @@ pub fn list_news( acc }) } + +// TODO: lift out rev option +pub fn list_fav( + time_style: &TimeStyle, + fav_list: &Result>, + rev: bool, +) -> Result { + let fav_list = match fav_list { + Ok(o) => o, + Err(e) => return fmt_err(e).into_ok(), + }; + + fav_list + .iter() + .dyn_rev(rev) + .map(|fav| try { + let mut buf = String::new(); + { + let buf = &mut buf; + let create_time = display_cnb_time(&fav.create_time, time_style); + writeln!(buf, "{} {}", create_time.dimmed(), fav.url.dimmed())?; + writeln!(buf, " {}", fav.title)?; + + let summary = { + fav.summary.width_split(get_term_width() - 4).map_or_else( + || fav.summary.clone(), + |vec| { + vec.into_iter() + .map(|line| format!(" {}", line)) + .collect::>() + .join("\n") + }, + ) + }; + if summary.is_empty().not() { + writeln!(buf, "{}", summary.dimmed())?; + } + } + buf + }) + .try_fold(String::new(), |mut acc, buf: Result| try { + write!(&mut acc, "\n{}", buf?)?; + acc + }) +} diff --git a/src/display/json.rs b/src/display/json.rs index 3bab319..5f3a1b1 100644 --- a/src/display/json.rs +++ b/src/display/json.rs @@ -1,3 +1,4 @@ +use crate::api::fav::get_list::FavEntry; use crate::api::ing::get_comment_list::IngCommentEntry; use crate::api::ing::get_list::IngEntry; use crate::api::news::get_list::NewsEntry; @@ -139,3 +140,14 @@ pub fn list_news(news_list: &Result>, rev: bool) -> Result>, rev: bool) -> Result { + let news_list = match news_list { + Ok(o) => o, + Err(e) => return fmt_err(e).into_ok(), + }; + + let vec = news_list.iter().dyn_rev(rev).collect::>(); + + json::serialize(vec.clone()) +} diff --git a/src/display/mod.rs b/src/display/mod.rs index e1f94df..16ed815 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -1,3 +1,4 @@ +use crate::api::fav::get_list::FavEntry; use crate::api::ing::get_comment_list::IngCommentEntry; use crate::api::ing::get_list::IngEntry; use crate::api::news::get_list::NewsEntry; @@ -160,3 +161,16 @@ pub fn list_news( Style::Json => json::list_news(news_list, rev), } } + +pub fn list_fav( + style: &Style, + time_style: &TimeStyle, + fav_list: &Result>, + rev: bool, +) -> Result { + match style { + Style::Colorful => colorful::list_fav(time_style, fav_list, rev), + Style::Normal => normal::list_fav(time_style, fav_list, rev), + Style::Json => json::list_fav(fav_list, rev), + } +} diff --git a/src/display/normal.rs b/src/display/normal.rs index 3406fca..8d8e03c 100644 --- a/src/display/normal.rs +++ b/src/display/normal.rs @@ -1,3 +1,4 @@ +use crate::api::fav::get_list::FavEntry; use crate::api::ing::get_comment_list::IngCommentEntry; use crate::api::ing::get_list::IngEntry; use crate::api::ing::{ @@ -340,3 +341,47 @@ pub fn list_news( acc }) } + +pub fn list_fav( + time_style: &TimeStyle, + fav_list: &Result>, + rev: bool, +) -> Result { + let fav_list = match fav_list { + Ok(o) => o, + Err(e) => return fmt_err(e).into_ok(), + }; + + fav_list + .iter() + .dyn_rev(rev) + .map(|fav| try { + let mut buf = String::new(); + { + let buf = &mut buf; + let create_time = display_cnb_time(&fav.create_time, time_style); + writeln!(buf, "{} {}", create_time, fav.url)?; + writeln!(buf, " {}", fav.title)?; + + let summary = { + fav.summary.width_split(get_term_width() - 4).map_or_else( + || fav.summary.clone(), + |vec| { + vec.into_iter() + .map(|line| format!(" {}", line)) + .collect::>() + .join("\n") + }, + ) + }; + if summary.is_empty().not() { + writeln!(buf, "{}", summary)?; + } + } + buf + }) + .try_fold(String::new(), |mut acc, buf: Result| try { + write!(&mut acc, "\n{}", buf?)?; + acc + }) +} diff --git a/src/main.rs b/src/main.rs index d013962..d992371 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ #![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; @@ -164,6 +165,11 @@ async fn main() -> Result<()> { foe.then(|| panic_if_err(&news_vec)); display::list_news(style, time_style, &news_vec, rev)? } + _ if let Some((skip, take)) = parser::list_fav(&args) => { + let fav_vec = Fav::new(pat?).get_list(skip, take).await; + foe.then(|| panic_if_err(&fav_vec)); + display::list_fav(style, time_style, &fav_vec, rev)? + } _ if no_operation(&args) => infer::(Args::command()).render_help().to_string(),