1mod api;
2mod client;
3pub mod commands;
4mod config;
5mod display;
6pub mod login;
7
8pub fn client_build(
10 cookie_store: std::sync::Arc<reqwest_cookie_store::CookieStoreMutex>,
11) -> anyhow::Result<reqwest::Client> {
12 client::build(cookie_store)
13}
14
15use anyhow::Result;
16use clap::{Parser, Subcommand};
17
18#[derive(Parser)]
19#[command(name = "elective", about = "北大选课网 CLI 客户端", version)]
20pub struct Cli {
21 #[command(subcommand)]
22 pub command: Commands,
23}
24
25#[derive(Subcommand)]
26pub enum Commands {
27 Login {
29 #[arg(short, long)]
31 password: bool,
32 #[arg(short, long)]
34 username: Option<String>,
35 #[arg(long)]
37 open: bool,
38 #[arg(short, long, value_enum)]
40 dual: Option<login::DualDegree>,
41 },
42 Status,
44 Logout,
46
47 Show,
49
50 #[command(alias = "ls")]
52 List {
53 #[arg(short, long)]
55 page: Option<usize>,
56 },
57
58 Set,
60
61 Unset,
63
64 #[command(alias = "captcha")]
66 ConfigCaptcha {
67 backend: String,
69 },
70
71 Launch {
73 #[arg(short = 't', long, default_value = "15")]
75 interval: u64,
76 },
77
78 Otp {
80 #[command(subcommand)]
81 action: OtpAction,
82 },
83}
84
85#[derive(Subcommand)]
86pub enum OtpAction {
87 Bind {
89 #[arg(short, long)]
90 username: Option<String>,
91 #[arg(long, conflicts_with = "verify")]
93 send: bool,
94 #[arg(long, value_name = "CODE", conflicts_with = "send")]
96 verify: Option<String>,
97 },
98 Set {
100 secret: String,
101 #[arg(short, long)]
102 username: Option<String>,
103 },
104 Show,
106 Clear,
108}
109
110fn init_tracing() {
111 tracing_subscriber::fmt()
112 .with_env_filter(
113 tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "warn".into()),
114 )
115 .init();
116}
117
118pub async fn run() -> Result<()> {
119 init_tracing();
120 let cli = Cli::parse();
121 dispatch(cli.command).await
122}
123
124pub async fn run_from<I, T>(args: I) -> Result<()>
125where
126 I: IntoIterator<Item = T>,
127 T: Into<std::ffi::OsString> + Clone,
128{
129 let cli = Cli::try_parse_from(args)?;
130 dispatch(cli.command).await
131}
132
133pub async fn dispatch(command: Commands) -> Result<()> {
134 match command {
135 Commands::Login {
137 password,
138 username,
139 open,
140 dual,
141 } => {
142 if password {
143 login::login_with_password(username.as_deref(), dual.as_ref()).await?;
144 } else {
145 let qr_mode = if open {
146 pkuinfo_common::qr::QrDisplayMode::Open
147 } else {
148 pkuinfo_common::qr::QrDisplayMode::Terminal
149 };
150 login::login_with_qrcode(qr_mode, dual.as_ref()).await?;
151 }
152 }
153 Commands::Status => login::status()?,
154 Commands::Logout => login::logout()?,
155
156 Commands::Show => commands::cmd_show().await?,
158 Commands::List { page } => {
159 let page = page.map(|p| p.saturating_sub(1));
161 commands::cmd_list(page).await?;
162 }
163
164 Commands::Set => commands::cmd_set().await?,
166 Commands::Unset => commands::cmd_unset()?,
167 Commands::ConfigCaptcha { backend } => commands::cmd_config_captcha(&backend)?,
168
169 Commands::Launch { interval } => commands::cmd_launch(interval).await?,
171
172 Commands::Otp { action } => {
174 let store = pkuinfo_common::session::Store::new("elective")?;
175 handle_otp(action, store.config_dir()).await?;
176 }
177 }
178 Ok(())
179}
180
181async fn handle_otp(action: OtpAction, config_dir: &std::path::Path) -> anyhow::Result<()> {
182 use colored::Colorize;
183 match action {
184 OtpAction::Bind {
185 username,
186 send,
187 verify,
188 } => {
189 if send {
190 pkuinfo_common::otp::bind_otp_send_sms(config_dir, username.as_deref()).await?;
191 } else if let Some(code) = verify {
192 pkuinfo_common::otp::bind_otp_verify(config_dir, &code).await?;
193 } else {
194 pkuinfo_common::otp::bind_otp_interactive(config_dir, username.as_deref()).await?;
195 }
196 }
197 OtpAction::Set { secret, username } => {
198 let uid = username.unwrap_or_default();
199 pkuinfo_common::otp::set_otp_secret(config_dir, &secret, &uid)?;
200 }
201 OtpAction::Show => match pkuinfo_common::otp::get_current_otp(config_dir)? {
202 Some(code) => {
203 let config = pkuinfo_common::otp::load_otp_config(config_dir)?
204 .ok_or_else(|| anyhow::anyhow!("OTP 配置文件缺失,请先运行 `otp bind`"))?;
205 println!(
206 "{} {} ({})",
207 "OTP:".green().bold(),
208 code.bold(),
209 config.user_id
210 );
211 }
212 None => {
213 println!(
214 "{} 未配置 OTP。使用 `otp bind` 绑定或 `otp set <SECRET>` 手动设置",
215 "○".red()
216 );
217 }
218 },
219 OtpAction::Clear => {
220 pkuinfo_common::otp::clear_otp_config(config_dir)?;
221 println!("{} OTP 配置已清除", "✓".green());
222 }
223 }
224 Ok(())
225}
226
227pub const VERSION: &str = env!("CARGO_PKG_VERSION");