diff --git a/Cargo.lock b/Cargo.lock index bf35e16..85f1dd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,7 +230,7 @@ dependencies = [ [[package]] name = "cnblogs_lib" -version = "0.0.0-dev" +version = "0.3.0" dependencies = [ "anstream", "anstyle", diff --git a/Cargo.toml b/Cargo.toml index fe5c0cc..20e2b60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cnblogs_lib" # WRN: Version will be updated by CI while create a tag, NERVER change this. -version = "0.0.0-dev" +version = "0.3.0" edition = "2024" description = "Cnblogs' command line tool" license = "MIT" diff --git a/src/api/urls.rs b/src/api/urls.rs index da7b75f..f8713c3 100644 --- a/src/api/urls.rs +++ b/src/api/urls.rs @@ -5,6 +5,8 @@ pub const OPENAPI: &str = "https://api.cnblogs.com/api"; pub const OAUTH: &str = "https://oauth.cnblogs.com"; pub const USER: &str = formatcp!("{}/users", OPENAPI); +pub const USER_FOLLOWERS: &str = formatcp!("{}/followers", USER); +pub const USER_FOLLOWING: &str = formatcp!("{}/following", USER); pub const STATUS: &str = formatcp!("{}/statuses/", OPENAPI); pub const COMMENTS_PATH: &str = "comments"; diff --git a/src/api/user.rs b/src/api/user.rs index a0732ba..129d247 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -1,7 +1,11 @@ use anyhow::Result; use reqwest::{Client, Response}; -use crate::{api::urls, models::user::UserInfo, tools::IntoAnyhowResult}; +use crate::{ + api::urls, + models::user::{UserFollow, UserInfo}, + tools::IntoAnyhowResult, +}; pub async fn raw_user_info(c: &Client) -> Result { c.get(urls::USER).send().await.into_anyhow_result() @@ -10,3 +14,57 @@ pub async fn raw_user_info(c: &Client) -> Result { pub async fn user_info(c: &Client) -> Result { raw_user_info(c).await?.json().await.into_anyhow_result() } + +/// 获取用户粉丝列表的原始响应 +/// page - 分页参数,包含 pageIndex 和 pageSize +pub async fn raw_user_followers( + c: &Client, + page: impl serde::Serialize + Send + Sync, +) -> Result { + c.get(urls::USER_FOLLOWERS) + .query(&page) + .send() + .await + .into_anyhow_result() +} + +/// 获取用户粉丝列表 +/// +/// page - 分页参数,包含 pageIndex 和 pageSize +pub async fn user_followers( + c: &Client, + page: impl serde::Serialize + Send + Sync, +) -> Result { + raw_user_followers(c, page) + .await? + .json() + .await + .into_anyhow_result() +} + +/// 获取用户粉丝列表的原始响应 +/// page - 分页参数,包含 pageIndex 和 pageSize +pub async fn raw_user_following( + c: &Client, + page: impl serde::Serialize + Send + Sync, +) -> Result { + c.get(urls::USER_FOLLOWING) + .query(&page) + .send() + .await + .into_anyhow_result() +} + +/// 获取用户粉丝列表 +/// +/// page - 分页参数,包含 pageIndex 和 pageSize +pub async fn user_following( + c: &Client, + page: impl serde::Serialize + Send + Sync, +) -> Result { + raw_user_following(c, page) + .await? + .json() + .await + .into_anyhow_result() +} diff --git a/src/commands/user.rs b/src/commands/user.rs index 8b0f81f..1e47b57 100644 --- a/src/commands/user.rs +++ b/src/commands/user.rs @@ -13,6 +13,10 @@ pub struct UserCommand { /// 提供通过access token登录,状态查询,退出,显示当前token功能 #[derive(Debug, Subcommand)] pub enum UserAction { + /// 查看用户粉丝列表 + Follower(FollowArg), + /// 查看用户关注列表 + Following(FollowArg), /// 用户登录,需提供access token。 Login { #[clap(value_parser = NonEmptyStringValueParser::new())] @@ -25,3 +29,16 @@ pub enum UserAction { /// 显示当前登录token Token, } + +#[derive(serde::Serialize, Debug, Args)] +pub struct FollowArg { + /// 分页页码(从1开始) + #[arg(long = "page-index", default_value_t = 1)] + #[serde(rename = "pageIndex")] + pub page_index: u64, + + /// 每页显示的条数,默认20 + #[arg(long = "page-size", default_value_t = 20)] + #[serde(rename = "pageSize")] + pub page_size: u64, +} diff --git a/src/logic/user.rs b/src/logic/user.rs index f01bdab..c614b88 100644 --- a/src/logic/user.rs +++ b/src/logic/user.rs @@ -7,7 +7,7 @@ use owo_colors::OwoColorize; use reqwest::header::{AUTHORIZATION, HeaderMap}; use reqwest::{ClientBuilder, StatusCode}; -use crate::commands::user::{UserAction, UserCommand}; +use crate::commands::user::{FollowArg, UserAction, UserCommand}; use crate::context::Context; use crate::context::config::Cache; use crate::tools::http::IntoNoParseResult; @@ -15,6 +15,8 @@ use crate::{api, models}; pub async fn endpoint(cmd: UserCommand, ctx: &mut Context) -> anyhow::Result<()> { match cmd.commands { + UserAction::Follower(arg) => handle_followers(ctx, arg).await, + UserAction::Following(arg) => handle_following(ctx, arg).await, UserAction::Login { token } => handle_login(token, ctx).await, UserAction::Logout => handle_logout(ctx), UserAction::Status => user_info(ctx).await, @@ -68,3 +70,37 @@ async fn user_info(ctx: &mut Context) -> Result<()> { fn handle_logout(ctx: &Context) -> Result<()> { ctx.clean() } + +async fn handle_followers(ctx: &mut Context, arg: FollowArg) -> Result<()> { + let followers = api::user::user_followers(&ctx.client, &arg).await?; + + ctx.terminal.writeln( + format!( + "{} 人关注了您, 当前{}页,每页数量{}", + followers.total_count, arg.page_index, arg.page_size + ) + .bright_green(), + )?; + + followers + .items + .iter() + .for_each(|f| ctx.terminal.writeln(f.as_format()).unwrap()); + Ok(()) +} + +async fn handle_following(ctx: &mut Context, arg: FollowArg) -> Result<()> { + let following = api::user::user_following(&ctx.client, &arg).await?; + ctx.terminal.writeln( + format!( + "您关注了 {} 人, 当前{}页,每页数量{}", + following.total_count, arg.page_index, arg.page_size + ) + .bright_green(), + )?; + following + .items + .iter() + .for_each(|f| ctx.terminal.writeln(f.as_format()).unwrap()); + Ok(()) +} diff --git a/src/models/user.rs b/src/models/user.rs index bb8681d..8f00d0f 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -43,3 +43,38 @@ impl UserInfo { info.join("\n") } } + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct FollowInfo { + pub alias: String, + pub space_user_id: u64, + pub display_name: String, + pub blog_app: Option, +} + +impl FollowInfo { + pub fn as_format(&self) -> String { + format!( + "{name} [#{id}] [{blog}]", + name = self.display_name.bright_blue(), + id = self.space_user_id.bright_green(), + blog = self + .blog_app + .as_ref() + .map_or("无博客".red().to_string(), |app| format!( + "https://www.cnblogs.com/{}", + app + ) + .blue() + .to_string()) + ) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct UserFollow { + pub items: Vec, + pub total_count: u64, +}