1mod api;
2mod client;
3pub mod commands;
4mod display;
5pub mod login;
6
7use anyhow::Result;
8use clap::{Parser, Subcommand};
9
10#[derive(Parser)]
11#[command(name = "campuscard", about = "北大校园卡 CLI 客户端", version)]
12pub struct Cli {
13 #[command(subcommand)]
14 pub command: Commands,
15}
16
17#[derive(Subcommand)]
18pub enum Commands {
19 Login {
21 #[arg(short, long)]
23 password: bool,
24 #[arg(short, long)]
26 username: Option<String>,
27 #[arg(long)]
29 open: bool,
30 },
31 Status,
33 Logout,
35
36 Info,
38
39 Pay {
41 #[arg(short, long)]
43 output: Option<String>,
44 },
45
46 Recharge {
48 #[arg(short, long)]
50 amount: Option<f64>,
51 },
52
53 #[command(alias = "ls")]
55 Bills {
56 #[arg(short, long, default_value = "1")]
58 page: usize,
59 #[arg(short = 'n', long, default_value = "10")]
61 size: usize,
62 #[arg(short, long)]
64 month: Option<String>,
65 },
66
67 Stats {
69 #[arg(short, long)]
71 month: Option<String>,
72 },
73
74 Otp {
76 #[command(subcommand)]
77 action: OtpAction,
78 },
79}
80
81#[derive(Subcommand)]
82pub enum OtpAction {
83 Bind {
85 #[arg(short, long)]
87 username: Option<String>,
88 #[arg(long, conflicts_with = "verify")]
90 send: bool,
91 #[arg(long, value_name = "CODE", conflicts_with = "send")]
93 verify: Option<String>,
94 },
95 Set {
97 secret: String,
99 #[arg(short, long)]
101 username: Option<String>,
102 },
103 Show,
105 Clear,
107}
108
109fn init_tracing() {
110 tracing_subscriber::fmt()
111 .with_env_filter(
112 tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "warn".into()),
113 )
114 .init();
115}
116
117pub async fn run() -> Result<()> {
118 init_tracing();
119 let cli = Cli::parse();
120 dispatch(cli.command).await
121}
122
123pub async fn run_from<I, T>(args: I) -> Result<()>
124where
125 I: IntoIterator<Item = T>,
126 T: Into<std::ffi::OsString> + Clone,
127{
128 let cli = Cli::try_parse_from(args)?;
129 dispatch(cli.command).await
130}
131
132pub async fn dispatch(command: Commands) -> Result<()> {
133 match command {
134 Commands::Login {
136 password,
137 username,
138 open,
139 } => {
140 if password {
141 login::login_with_password(username.as_deref()).await?;
142 } else {
143 let qr_mode = if open {
144 pkuinfo_common::qr::QrDisplayMode::Open
145 } else {
146 pkuinfo_common::qr::QrDisplayMode::Terminal
147 };
148 login::login_with_qrcode(qr_mode).await?;
149 }
150 }
151 Commands::Status => login::status()?,
152 Commands::Logout => login::logout()?,
153
154 Commands::Info => commands::cmd_info().await?,
156 Commands::Pay { output } => commands::cmd_pay(output.as_deref()).await?,
157 Commands::Recharge { amount } => commands::cmd_recharge(amount).await?,
158 Commands::Bills { page, size, month } => {
159 commands::cmd_bills(Some(page), Some(size), month.as_deref()).await?;
160 }
161 Commands::Stats { month } => commands::cmd_stats(month.as_deref()).await?,
162
163 Commands::Otp { action } => {
165 let store = pkuinfo_common::session::Store::new("campuscard")?;
166 handle_otp(action, store.config_dir()).await?;
167 }
168 }
169 Ok(())
170}
171
172async fn handle_otp(action: OtpAction, config_dir: &std::path::Path) -> anyhow::Result<()> {
173 use colored::Colorize;
174 match action {
175 OtpAction::Bind {
176 username,
177 send,
178 verify,
179 } => {
180 if send {
181 pkuinfo_common::otp::bind_otp_send_sms(config_dir, username.as_deref()).await?;
182 } else if let Some(code) = verify {
183 pkuinfo_common::otp::bind_otp_verify(config_dir, &code).await?;
184 } else {
185 pkuinfo_common::otp::bind_otp_interactive(config_dir, username.as_deref()).await?;
186 }
187 }
188 OtpAction::Set { secret, username } => {
189 let uid = username.unwrap_or_default();
190 pkuinfo_common::otp::set_otp_secret(config_dir, &secret, &uid)?;
191 }
192 OtpAction::Show => match pkuinfo_common::otp::get_current_otp(config_dir)? {
193 Some(code) => {
194 let config = pkuinfo_common::otp::load_otp_config(config_dir)?
195 .ok_or_else(|| anyhow::anyhow!("OTP 配置文件缺失,请先运行 `otp bind`"))?;
196 println!(
197 "{} {} ({})",
198 "OTP:".green().bold(),
199 code.bold(),
200 config.user_id
201 );
202 }
203 None => {
204 println!(
205 "{} 未配置 OTP。使用 `otp bind` 绑定或 `otp set <SECRET>` 手动设置",
206 "○".red()
207 );
208 }
209 },
210 OtpAction::Clear => {
211 pkuinfo_common::otp::clear_otp_config(config_dir)?;
212 println!("{} OTP 配置已清除", "✓".green());
213 }
214 }
215 Ok(())
216}
217
218pub const VERSION: &str = env!("CARGO_PKG_VERSION");