use crate::commands::{Command, OutputFormat};
use crate::constants::api::actions;
use crate::errors::prelude::{CliError, Result as CliResult};
use crate::output::{CliResponse, OutputFormatter};
use crate::utils::output::print_success_result;
use crate::utils::{
validate_chat_about, validate_chat_action, validate_chat_id, validate_chat_title,
validate_cursor,
};
use async_trait::async_trait;
use clap::{Subcommand, ValueHint};
use serde_json::json;
use tracing::{debug, info};
use vkteams_bot::prelude::*;
#[derive(Subcommand, Debug, Clone)]
pub enum ChatCommands {
GetChatInfo {
#[arg(short = 'c', long, required = true, value_name = "CHAT_ID", value_hint = ValueHint::Username)]
chat_id: String,
},
GetProfile {
#[arg(short = 'u', long, required = true, value_name = "USER_ID", value_hint = ValueHint::Username)]
user_id: String,
},
GetChatMembers {
#[arg(short = 'c', long, required = true, value_name = "CHAT_ID", value_hint = ValueHint::Username)]
chat_id: String,
#[arg(long, value_name = "CURSOR")]
cursor: Option<String>,
},
SetChatTitle {
#[arg(short = 'c', long, required = true, value_name = "CHAT_ID", value_hint = ValueHint::Username)]
chat_id: String,
#[arg(short = 't', long, required = true, value_name = "TITLE")]
title: String,
},
SetChatAbout {
#[arg(short = 'c', long, required = true, value_name = "CHAT_ID", value_hint = ValueHint::Username)]
chat_id: String,
#[arg(short = 'a', long, required = true, value_name = "ABOUT")]
about: String,
},
SendAction {
#[arg(short = 'c', long, required = true, value_name = "CHAT_ID", value_hint = ValueHint::Username)]
chat_id: String,
#[arg(short = 'a', long, required = true, value_name = "ACTION")]
action: String,
},
GetChatAdmins {
#[arg(short = 'c', long, required = true, value_name = "CHAT_ID", value_hint = ValueHint::Username)]
chat_id: String,
},
}
#[async_trait]
impl Command for ChatCommands {
async fn execute(&self, bot: &Bot) -> CliResult<()> {
match self {
ChatCommands::GetChatInfo { chat_id } => execute_get_chat_info(bot, chat_id).await,
ChatCommands::GetProfile { user_id } => execute_get_profile(bot, user_id).await,
ChatCommands::GetChatMembers { chat_id, cursor } => {
execute_get_chat_members(bot, chat_id, cursor.as_deref()).await
}
ChatCommands::SetChatTitle { chat_id, title } => {
execute_set_chat_title(bot, chat_id, title).await
}
ChatCommands::SetChatAbout { chat_id, about } => {
execute_set_chat_about(bot, chat_id, about).await
}
ChatCommands::SendAction { chat_id, action } => {
execute_send_action(bot, chat_id, action).await
}
ChatCommands::GetChatAdmins { chat_id } => execute_get_chat_admins(bot, chat_id).await,
}
}
async fn execute_with_output(&self, bot: &Bot, output_format: &OutputFormat) -> CliResult<()> {
let response = match self {
ChatCommands::GetChatInfo { chat_id } => {
execute_get_chat_info_structured(bot, chat_id).await
}
ChatCommands::GetProfile { user_id } => {
execute_get_profile_structured(bot, user_id).await
}
ChatCommands::GetChatMembers { chat_id, cursor } => {
execute_get_chat_members_structured(bot, chat_id, cursor.as_deref()).await
}
ChatCommands::SetChatTitle { chat_id, title } => {
execute_set_chat_title_structured(bot, chat_id, title).await
}
ChatCommands::SetChatAbout { chat_id, about } => {
execute_set_chat_about_structured(bot, chat_id, about).await
}
ChatCommands::SendAction { chat_id, action } => {
execute_send_action_structured(bot, chat_id, action).await
}
ChatCommands::GetChatAdmins { chat_id } => {
execute_get_chat_admins_structured(bot, chat_id).await
}
};
OutputFormatter::print(&response, output_format)?;
if !response.success {
return Err(CliError::UnexpectedError("Command failed".to_string()));
}
Ok(())
}
fn name(&self) -> &'static str {
match self {
ChatCommands::GetChatInfo { .. } => "get-chat-info",
ChatCommands::GetProfile { .. } => "get-profile",
ChatCommands::GetChatMembers { .. } => "get-chat-members",
ChatCommands::SetChatTitle { .. } => "set-chat-title",
ChatCommands::SetChatAbout { .. } => "set-chat-about",
ChatCommands::SendAction { .. } => "send-action",
ChatCommands::GetChatAdmins { .. } => "get-chat-admins",
}
}
fn validate(&self) -> CliResult<()> {
match self {
ChatCommands::GetChatInfo { chat_id }
| ChatCommands::GetProfile { user_id: chat_id } => {
validate_chat_id(chat_id)?;
}
ChatCommands::GetChatMembers { chat_id, cursor } => {
validate_chat_id(chat_id)?;
if let Some(cursor_val) = cursor {
validate_cursor(cursor_val)?;
}
}
ChatCommands::SetChatTitle { chat_id, title } => {
validate_chat_id(chat_id)?;
validate_chat_title(title)?;
}
ChatCommands::SetChatAbout { chat_id, about } => {
validate_chat_id(chat_id)?;
validate_chat_about(about)?;
}
ChatCommands::SendAction { chat_id, action } => {
validate_chat_id(chat_id)?;
validate_chat_action(action)?;
}
ChatCommands::GetChatAdmins { chat_id } => {
validate_chat_id(chat_id)?;
}
}
Ok(())
}
}
async fn execute_get_chat_info_structured(
bot: &Bot,
chat_id: &str,
) -> CliResponse<serde_json::Value> {
debug!("Getting chat info for {}", chat_id);
let request = RequestChatsGetInfo::new(ChatId::from_borrowed_str(chat_id));
match bot.send_api_request(request).await {
Ok(result) => {
info!("Successfully retrieved chat info for {}", chat_id);
let data = json!({
"chat_id": chat_id,
"chat_info": result
});
CliResponse::success("get-chat-info", data)
}
Err(e) => CliResponse::error("get-chat-info", format!("Failed to get chat info: {e}")),
}
}
async fn execute_get_profile_structured(
bot: &Bot,
user_id: &str,
) -> CliResponse<serde_json::Value> {
debug!("Getting profile for user {}", user_id);
let request = RequestChatsGetInfo::new(ChatId::from_borrowed_str(user_id));
match bot.send_api_request(request).await {
Ok(result) => {
info!("Successfully retrieved profile for user {}", user_id);
let data = json!({
"user_id": user_id,
"profile": result
});
CliResponse::success("get-profile", data)
}
Err(e) => CliResponse::error("get-profile", format!("Failed to get profile: {e}")),
}
}
async fn execute_get_chat_members_structured(
bot: &Bot,
chat_id: &str,
cursor: Option<&str>,
) -> CliResponse<serde_json::Value> {
debug!("Getting chat members for {}", chat_id);
let mut request = RequestChatsGetMembers::new(ChatId::from_borrowed_str(chat_id));
if let Some(cursor_val) = cursor {
match cursor_val.parse::<u32>() {
Ok(cursor_num) => {
request = request.with_cursor(cursor_num);
}
Err(e) => {
return CliResponse::error(
"get-chat-members",
format!("Invalid cursor value, must be a number: {e}"),
);
}
}
}
match bot.send_api_request(request).await {
Ok(result) => {
info!("Successfully retrieved members for chat {}", chat_id);
let data = json!({
"chat_id": chat_id,
"cursor": cursor,
"members": result
});
CliResponse::success("get-chat-members", data)
}
Err(e) => CliResponse::error(
"get-chat-members",
format!("Failed to get chat members: {e}"),
),
}
}
async fn execute_set_chat_title_structured(
bot: &Bot,
chat_id: &str,
title: &str,
) -> CliResponse<serde_json::Value> {
debug!("Setting chat title for {} to {}", chat_id, title);
let request =
RequestChatsSetTitle::new((ChatId::from_borrowed_str(chat_id), title.to_string()));
match bot.send_api_request(request).await {
Ok(_result) => {
info!("Successfully set title for chat {}: {}", chat_id, title);
let data = json!({
"chat_id": chat_id,
"title": title,
"action": "title_updated"
});
CliResponse::success("set-chat-title", data)
}
Err(e) => CliResponse::error("set-chat-title", format!("Failed to set chat title: {e}")),
}
}
async fn execute_set_chat_about_structured(
bot: &Bot,
chat_id: &str,
about: &str,
) -> CliResponse<serde_json::Value> {
debug!("Setting chat description for {} to {}", chat_id, about);
let request =
RequestChatsSetAbout::new((ChatId::from_borrowed_str(chat_id), about.to_string()));
match bot.send_api_request(request).await {
Ok(_result) => {
info!(
"Successfully set description for chat {}: {}",
chat_id, about
);
let data = json!({
"chat_id": chat_id,
"about": about,
"action": "about_updated"
});
CliResponse::success("set-chat-about", data)
}
Err(e) => CliResponse::error("set-chat-about", format!("Failed to set chat about: {e}")),
}
}
async fn execute_send_action_structured(
bot: &Bot,
chat_id: &str,
action: &str,
) -> CliResponse<serde_json::Value> {
debug!("Sending {} action to chat {}", action, chat_id);
let chat_action = match action {
actions::TYPING => ChatActions::Typing,
actions::LOOKING => ChatActions::Looking,
_ => {
return CliResponse::error(
"send-action",
format!(
"Unknown action: {}. Available actions: {}, {}",
action,
actions::TYPING,
actions::LOOKING
),
);
}
};
let request = RequestChatsSendAction::new((ChatId::from_borrowed_str(chat_id), chat_action));
match bot.send_api_request(request).await {
Ok(_result) => {
info!("Successfully sent {} action to chat {}", action, chat_id);
let data = json!({
"chat_id": chat_id,
"action": action,
"status": "sent"
});
CliResponse::success("send-action", data)
}
Err(e) => CliResponse::error("send-action", format!("Failed to send action: {e}")),
}
}
async fn execute_get_chat_admins_structured(
bot: &Bot,
chat_id: &str,
) -> CliResponse<serde_json::Value> {
debug!("Getting chat administrators for {}", chat_id);
let request = RequestChatsGetAdmins::new(ChatId::from_borrowed_str(chat_id));
match bot.send_api_request(request).await {
Ok(result) => {
info!("Successfully retrieved administrators for chat {}", chat_id);
let data = json!({
"chat_id": chat_id,
"admins": result
});
CliResponse::success("get-chat-admins", data)
}
Err(e) => CliResponse::error("get-chat-admins", format!("Failed to get chat admins: {e}")),
}
}
async fn execute_get_chat_info(bot: &Bot, chat_id: &str) -> CliResult<()> {
debug!("Getting chat info for {}", chat_id);
let request = RequestChatsGetInfo::new(ChatId::from_borrowed_str(chat_id));
let result = bot
.send_api_request(request)
.await
.map_err(CliError::ApiError)?;
info!("Successfully retrieved chat info for {}", chat_id);
print_success_result(&result, &OutputFormat::Pretty)?;
Ok(())
}
async fn execute_get_profile(bot: &Bot, user_id: &str) -> CliResult<()> {
debug!("Getting profile for user {}", user_id);
let request = RequestChatsGetInfo::new(ChatId::from_borrowed_str(user_id));
let result = bot
.send_api_request(request)
.await
.map_err(CliError::ApiError)?;
info!("Successfully retrieved profile for user {}", user_id);
print_success_result(&result, &OutputFormat::Pretty)?;
Ok(())
}
async fn execute_get_chat_members(bot: &Bot, chat_id: &str, cursor: Option<&str>) -> CliResult<()> {
debug!("Getting chat members for {}", chat_id);
let mut request = RequestChatsGetMembers::new(ChatId::from_borrowed_str(chat_id));
if let Some(cursor_val) = cursor {
match cursor_val.parse::<u32>() {
Ok(cursor_num) => {
request = request.with_cursor(cursor_num);
}
Err(e) => {
return Err(CliError::InputError(format!(
"Invalid cursor value, must be a number: {e}"
)));
}
}
}
let result = bot
.send_api_request(request)
.await
.map_err(CliError::ApiError)?;
info!("Successfully retrieved members for chat {}", chat_id);
print_success_result(&result, &OutputFormat::Pretty)?;
Ok(())
}
async fn execute_set_chat_title(bot: &Bot, chat_id: &str, title: &str) -> CliResult<()> {
debug!("Setting chat title for {} to {}", chat_id, title);
let request =
RequestChatsSetTitle::new((ChatId::from_borrowed_str(chat_id), title.to_string()));
let result = bot
.send_api_request(request)
.await
.map_err(CliError::ApiError)?;
info!("Successfully set title for chat {}: {}", chat_id, title);
print_success_result(&result, &OutputFormat::Pretty)?;
Ok(())
}
async fn execute_set_chat_about(bot: &Bot, chat_id: &str, about: &str) -> CliResult<()> {
debug!("Setting chat description for {} to {}", chat_id, about);
let request =
RequestChatsSetAbout::new((ChatId::from_borrowed_str(chat_id), about.to_string()));
let result = bot
.send_api_request(request)
.await
.map_err(CliError::ApiError)?;
info!(
"Successfully set description for chat {}: {}",
chat_id, about
);
print_success_result(&result, &OutputFormat::Pretty)?;
Ok(())
}
async fn execute_send_action(bot: &Bot, chat_id: &str, action: &str) -> CliResult<()> {
debug!("Sending {} action to chat {}", action, chat_id);
let chat_action = match action {
actions::TYPING => ChatActions::Typing,
actions::LOOKING => ChatActions::Looking,
_ => {
return Err(CliError::InputError(format!(
"Unknown action: {}. Available actions: {}, {}",
action,
actions::TYPING,
actions::LOOKING
)));
}
};
let request = RequestChatsSendAction::new((ChatId::from_borrowed_str(chat_id), chat_action));
let result = bot
.send_api_request(request)
.await
.map_err(CliError::ApiError)?;
info!("Successfully sent {} action to chat {}", action, chat_id);
print_success_result(&result, &OutputFormat::Pretty)?;
Ok(())
}
async fn execute_get_chat_admins(bot: &Bot, chat_id: &str) -> CliResult<()> {
debug!("Getting chat administrators for {}", chat_id);
let request = RequestChatsGetAdmins::new(ChatId::from_borrowed_str(chat_id));
let result = bot
.send_api_request(request)
.await
.map_err(CliError::ApiError)?;
info!("Successfully retrieved administrators for chat {}", chat_id);
print_success_result(&result, &OutputFormat::Pretty)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::runtime::Runtime;
#[test]
fn test_parse_chat_id_valid() {
let input = "12345@chat";
let res = validate_chat_id(input);
assert!(res.is_ok());
}
#[test]
fn test_parse_chat_id_invalid() {
let input = "user with spaces";
let res = validate_chat_id(input);
assert!(res.is_err());
}
#[test]
fn test_handle_empty_input() {
let input = "";
let res = validate_chat_id(input);
assert!(res.is_err());
}
#[test]
fn test_parse_cursor_valid() {
let input = "123";
let res = validate_cursor(input);
assert!(res.is_ok());
}
#[test]
fn test_parse_cursor_empty() {
let input = "";
let res = validate_cursor(input);
assert!(res.is_err());
}
#[test]
fn test_validate_set_chat_title_invalid() {
let cmd = ChatCommands::SetChatTitle {
chat_id: "12345@chat".to_string(),
title: "".to_string(),
};
let res = cmd.validate();
assert!(res.is_err());
}
#[test]
fn test_validate_set_chat_about_invalid() {
let cmd = ChatCommands::SetChatAbout {
chat_id: "12345@chat".to_string(),
about: "".to_string(),
};
let res = cmd.validate();
assert!(res.is_err());
}
#[test]
fn test_validate_send_action_invalid() {
let cmd = ChatCommands::SendAction {
chat_id: "12345@chat".to_string(),
action: "invalid_action".to_string(),
};
let res = cmd.validate();
assert!(res.is_err());
}
#[test]
fn test_validate_get_profile_invalid() {
let cmd = ChatCommands::GetProfile {
user_id: "".to_string(),
};
let res = cmd.validate();
assert!(res.is_err());
}
#[test]
fn test_validate_get_chat_members_invalid_cursor() {
let cmd = ChatCommands::GetChatMembers {
chat_id: "12345@chat".to_string(),
cursor: Some("not_a_number".to_string()),
};
let res = cmd.validate();
assert!(res.is_err()); }
#[test]
fn test_validate_get_chat_members_empty_cursor() {
let cmd = ChatCommands::GetChatMembers {
chat_id: "12345@chat".to_string(),
cursor: Some("".to_string()),
};
let res = cmd.validate();
assert!(res.is_err());
}
#[test]
fn test_validate_set_chat_title_long() {
let cmd = ChatCommands::SetChatTitle {
chat_id: "12345@chat".to_string(),
title: "a".repeat(300),
};
let res = cmd.validate();
assert!(res.is_err());
}
#[test]
fn test_validate_set_chat_about_long() {
let cmd = ChatCommands::SetChatAbout {
chat_id: "12345@chat".to_string(),
about: "a".repeat(300),
};
let res = cmd.validate();
assert!(res.is_ok()); }
fn dummy_bot() -> Bot {
Bot::with_params(&APIVersionUrl::V1, "dummy_token", "https://dummy.api.com").unwrap()
}
#[test]
fn test_execute_get_chat_info_api_error() {
let cmd = ChatCommands::GetChatInfo {
chat_id: "12345@chat".to_string(),
};
let bot = dummy_bot();
let rt = Runtime::new().unwrap();
let res = rt.block_on(cmd.execute(&bot));
assert!(res.is_err());
}
#[test]
fn test_execute_get_profile_api_error() {
let cmd = ChatCommands::GetProfile {
user_id: "user123".to_string(),
};
let bot = dummy_bot();
let rt = Runtime::new().unwrap();
let res = rt.block_on(cmd.execute(&bot));
assert!(res.is_err());
}
#[test]
fn test_execute_get_chat_members_api_error() {
let cmd = ChatCommands::GetChatMembers {
chat_id: "12345@chat".to_string(),
cursor: None,
};
let bot = dummy_bot();
let rt = Runtime::new().unwrap();
let res = rt.block_on(cmd.execute(&bot));
assert!(res.is_err());
}
#[test]
fn test_execute_set_chat_title_api_error() {
let cmd = ChatCommands::SetChatTitle {
chat_id: "12345@chat".to_string(),
title: "New Title".to_string(),
};
let bot = dummy_bot();
let rt = Runtime::new().unwrap();
let res = rt.block_on(cmd.execute(&bot));
assert!(res.is_err());
}
#[test]
fn test_execute_set_chat_about_api_error() {
let cmd = ChatCommands::SetChatAbout {
chat_id: "12345@chat".to_string(),
about: "About text".to_string(),
};
let bot = dummy_bot();
let rt = Runtime::new().unwrap();
let res = rt.block_on(cmd.execute(&bot));
assert!(res.is_err());
}
#[test]
fn test_execute_send_action_invalid_action() {
let cmd = ChatCommands::SendAction {
chat_id: "12345@chat".to_string(),
action: "invalid".to_string(),
};
let bot = dummy_bot();
let rt = Runtime::new().unwrap();
let res = rt.block_on(cmd.execute(&bot));
assert!(res.is_err());
let err = res.unwrap_err();
match err {
CliError::InputError(msg) => assert!(msg.contains("Unknown action")),
_ => panic!("Expected InputError for unknown action"),
}
}
}
#[cfg(test)]
mod happy_path_tests {
use super::*;
use crate::utils::bot::create_dummy_bot;
use tokio_test::block_on;
#[test]
fn test_execute_get_chat_info_success() {
let bot = create_dummy_bot();
let res = block_on(execute_get_chat_info(&bot, "chat123"));
let _ = res;
}
#[test]
fn test_execute_get_profile_success() {
let bot = create_dummy_bot();
let res = block_on(execute_get_profile(&bot, "user123"));
let _ = res;
}
#[test]
fn test_execute_get_chat_members_success() {
let bot = create_dummy_bot();
let res = block_on(execute_get_chat_members(&bot, "chat123", Some("1")));
let _ = res;
}
#[test]
fn test_execute_set_chat_title_success() {
let bot = create_dummy_bot();
let res = block_on(execute_set_chat_title(&bot, "chat123", "New Title"));
let _ = res;
}
#[test]
fn test_execute_set_chat_about_success() {
let bot = create_dummy_bot();
let res = block_on(execute_set_chat_about(&bot, "chat123", "About"));
let _ = res;
}
#[test]
fn test_execute_send_action_success() {
let bot = create_dummy_bot();
let res = block_on(execute_send_action(&bot, "chat123", "typing"));
let _ = res;
}
}