Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,36 @@ pub struct AccountDeleteArgs {
}

#[derive(Debug, Args)]
#[command(after_help = "Authentication Modes:
1. Saved Account Mode:
--account <index>

2. Explicit Credentials Mode:
--bot-token <token> --user-id <user_id> [--route-tag <tag>]

Usage Rules:
- Omit all auth params to use the first saved account.
- Or use exactly one of the modes above.
- --account cannot be combined with --bot-token or --user-id.
- In Explicit mode, both --bot-token and --user-id are required.")]
pub struct GetContextTokenArgs {
/// Saved account user ID. If omitted, the first saved account is used.
#[arg(long)]
#[arg(
long,
help = "Saved account index from `wechat-cli account list`. Required for Saved Account Mode."
)]
pub account: Option<usize>,
#[arg(long, help = "Target user ID. Required in Explicit Credentials Mode.")]
pub user_id: Option<String>,
#[arg(
long,
help = "Explicit bot token. Required in Explicit Credentials Mode."
)]
pub bot_token: Option<String>,
#[arg(
long,
help = "Optional route tag used only in Explicit Credentials Mode."
)]
pub route_tag: Option<String>,
}

#[derive(Debug, Args)]
Expand Down
85 changes: 78 additions & 7 deletions src/commands/get_context_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,35 @@ use anyhow::{Context, Result, bail};

use crate::{
commands::account::{build_client, resolve_user_id},
storage,
commands::send::{SendTarget, resolve_send_target},
storage::{self},
wechat::api::is_session_expired,
wechat::models::InboundMessage,
};

pub async fn run(user_id: Option<&str>) -> Result<()> {
let resolved_id = resolve_user_id(user_id)?;
let session = storage::get_account_data(&resolved_id)
.with_context(|| format!("failed to load account data for `{resolved_id}`"))?;
let client = build_client(&session);
let user_id = session.user_id;
pub async fn run(
account: Option<usize>,
user_id: Option<&str>,
bot_token: Option<&str>,
route_tag: Option<&str>,
) -> Result<()> {
let target = if account.is_none() && user_id.is_none() && bot_token.is_none() {
let resolved_id = resolve_user_id(None)?;
let session = storage::get_account_data(&resolved_id)
.with_context(|| format!("failed to load account data for `{resolved_id}`"))?;
SendTarget::Saved {
user_id: resolved_id,
client: build_client(&session),
}
} else {
resolve_send_target(account, user_id, bot_token, route_tag)?
};

let (user_id, client) = match target {
SendTarget::Saved { user_id, client } => (user_id, client),
SendTarget::Explicit { user_id, client } => (user_id, client),
};

let mut consecutive_errors = 0u32;

eprintln!("waiting for the bound user to send a message for `{user_id}`; press Ctrl+C to stop");
Expand Down Expand Up @@ -72,3 +90,56 @@ fn extract_context_token(user_id: &str, message: &InboundMessage) -> Option<Stri
None
}
}

#[cfg(test)]
mod tests {
use crate::commands::send::resolve_send_target;

#[test]
fn test_shared_resolve_rejects_mixed_modes() {
let result = resolve_send_target(Some(0), None, Some("token"), None);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("cannot be used with"),
"Expected error about mixing modes, got: {}",
err_msg
);
}

#[test]
fn test_shared_resolve_rejects_missing_bot_token() {
let result = resolve_send_target(None, Some("user@im.wechat"), None, None);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("bot-token"),
"Expected error about missing bot-token, got: {}",
err_msg
);
}

#[test]
fn test_shared_resolve_rejects_missing_user_id() {
let result = resolve_send_target(None, None, Some("token"), None);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("user-id"),
"Expected error about missing user-id, got: {}",
err_msg
);
}

#[test]
fn test_shared_resolve_rejects_account_with_user_id() {
let result = resolve_send_target(Some(0), Some("user@im.wechat"), None, None);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("cannot be used with"),
"Expected error about cannot use together, got: {}",
err_msg
);
}
}
4 changes: 2 additions & 2 deletions src/commands/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub async fn run(
}

#[derive(Debug)]
enum SendTarget {
pub enum SendTarget {
Saved {
user_id: String,
client: WeixinApiClient,
Expand Down Expand Up @@ -127,7 +127,7 @@ async fn send_media(
Ok(())
}

fn resolve_send_target(
pub fn resolve_send_target(
account: Option<usize>,
user_id: Option<&str>,
bot_token: Option<&str>,
Expand Down
8 changes: 7 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@ async fn main() -> AnyResult<()> {
AccountCommand::Delete(args) => commands::account::delete_account(args.account)?,
},
Command::GetContextToken(args) => {
commands::get_context_token::run(args.user_id.as_deref()).await?;
commands::get_context_token::run(
args.account,
args.user_id.as_deref(),
args.bot_token.as_deref(),
args.route_tag.as_deref(),
)
.await?;
}
Command::Send(args) => {
commands::send::run(
Expand Down
Loading