1use crate::commands::{Command, OutputFormat};
6use crate::constants::ui::emoji;
7use crate::errors::prelude::{CliError, Result as CliResult};
8use crate::file_utils;
9use crate::output::{CliResponse, OutputFormatter};
10use crate::utils::output::print_success_result;
11use crate::utils::{
12 validate_chat_id, validate_file_path, validate_message_id, validate_message_text,
13 validate_voice_file_path,
14};
15
16use async_trait::async_trait;
17use clap::{Subcommand, ValueHint};
18use colored::Colorize;
19use serde_json::json;
20use tracing::{debug, info};
21use vkteams_bot::prelude::*;
22
23#[derive(Subcommand, Debug, Clone)]
25pub enum MessagingCommands {
26 SendText {
28 #[arg(short = 'u', long, required = true, value_name = "CHAT_ID", value_hint = ValueHint::Username)]
29 chat_id: String,
30 #[arg(short = 'm', long, required = true, value_name = "MESSAGE")]
31 message: String,
32 },
33 SendFile {
35 #[arg(short = 'u', long, required = true, value_name = "CHAT_ID", value_hint = ValueHint::Username)]
36 chat_id: String,
37 #[arg(short = 'p', long, required = true, value_name = "FILE_PATH", value_hint = ValueHint::FilePath)]
38 file_path: String,
39 },
40 SendVoice {
42 #[arg(short = 'u', long, required = true, value_name = "CHAT_ID", value_hint = ValueHint::Username)]
43 chat_id: String,
44 #[arg(short = 'p', long, required = true, value_name = "FILE_PATH", value_hint = ValueHint::FilePath)]
45 file_path: String,
46 },
47 EditMessage {
49 #[arg(short = 'c', long, required = true, value_name = "CHAT_ID", value_hint = ValueHint::Username)]
50 chat_id: String,
51 #[arg(short = 'm', long, required = true, value_name = "MESSAGE_ID")]
52 message_id: String,
53 #[arg(short = 't', long, required = true, value_name = "NEW_TEXT")]
54 new_text: String,
55 },
56 DeleteMessage {
58 #[arg(short = 'c', long, required = true, value_name = "CHAT_ID", value_hint = ValueHint::Username)]
59 chat_id: String,
60 #[arg(short = 'm', long, required = true, value_name = "MESSAGE_ID")]
61 message_id: String,
62 },
63 PinMessage {
65 #[arg(short = 'c', long, required = true, value_name = "CHAT_ID", value_hint = ValueHint::Username)]
66 chat_id: String,
67 #[arg(short = 'm', long, required = true, value_name = "MESSAGE_ID")]
68 message_id: String,
69 },
70 UnpinMessage {
72 #[arg(short = 'c', long, required = true, value_name = "CHAT_ID", value_hint = ValueHint::Username)]
73 chat_id: String,
74 #[arg(short = 'm', long, required = true, value_name = "MESSAGE_ID")]
75 message_id: String,
76 },
77}
78
79#[async_trait]
80impl Command for MessagingCommands {
81 async fn execute(&self, bot: &Bot) -> CliResult<()> {
82 match self {
83 MessagingCommands::SendText { chat_id, message } => {
84 execute_send_text(bot, chat_id, message).await
85 }
86 MessagingCommands::SendFile { chat_id, file_path } => {
87 execute_send_file(bot, chat_id, file_path).await
88 }
89 MessagingCommands::SendVoice { chat_id, file_path } => {
90 execute_send_voice(bot, chat_id, file_path).await
91 }
92 MessagingCommands::EditMessage {
93 chat_id,
94 message_id,
95 new_text,
96 } => execute_edit_message(bot, chat_id, message_id, new_text).await,
97 MessagingCommands::DeleteMessage {
98 chat_id,
99 message_id,
100 } => execute_delete_message(bot, chat_id, message_id).await,
101 MessagingCommands::PinMessage {
102 chat_id,
103 message_id,
104 } => execute_pin_message(bot, chat_id, message_id).await,
105 MessagingCommands::UnpinMessage {
106 chat_id,
107 message_id,
108 } => execute_unpin_message(bot, chat_id, message_id).await,
109 }
110 }
111
112 async fn execute_with_output(&self, bot: &Bot, output_format: &OutputFormat) -> CliResult<()> {
114 let response = match self {
115 MessagingCommands::SendText { chat_id, message } => {
116 execute_send_text_structured(bot, chat_id, message).await
117 }
118 MessagingCommands::SendFile { chat_id, file_path } => {
119 execute_send_file_structured(bot, chat_id, file_path).await
120 }
121 MessagingCommands::SendVoice { chat_id, file_path } => {
122 execute_send_voice_structured(bot, chat_id, file_path).await
123 }
124 MessagingCommands::EditMessage {
125 chat_id,
126 message_id,
127 new_text,
128 } => execute_edit_message_structured(bot, chat_id, message_id, new_text).await,
129 MessagingCommands::DeleteMessage {
130 chat_id,
131 message_id,
132 } => execute_delete_message_structured(bot, chat_id, message_id).await,
133 MessagingCommands::PinMessage {
134 chat_id,
135 message_id,
136 } => execute_pin_message_structured(bot, chat_id, message_id).await,
137 MessagingCommands::UnpinMessage {
138 chat_id,
139 message_id,
140 } => execute_unpin_message_structured(bot, chat_id, message_id).await,
141 };
142
143 OutputFormatter::print(&response, output_format)?;
144
145 if !response.success {
146 return Err(CliError::UnexpectedError("Command failed".to_string()));
147 }
148
149 Ok(())
150 }
151
152 fn name(&self) -> &'static str {
153 match self {
154 MessagingCommands::SendText { .. } => "send-text",
155 MessagingCommands::SendFile { .. } => "send-file",
156 MessagingCommands::SendVoice { .. } => "send-voice",
157 MessagingCommands::EditMessage { .. } => "edit-message",
158 MessagingCommands::DeleteMessage { .. } => "delete-message",
159 MessagingCommands::PinMessage { .. } => "pin-message",
160 MessagingCommands::UnpinMessage { .. } => "unpin-message",
161 }
162 }
163
164 fn validate(&self) -> CliResult<()> {
165 match self {
166 MessagingCommands::SendText { chat_id, message } => {
167 validate_chat_id(chat_id)?;
168 validate_message_text(message)?;
169 }
170 MessagingCommands::SendFile { chat_id, file_path } => {
171 validate_chat_id(chat_id)?;
172 validate_file_path(file_path)?;
173 }
174 MessagingCommands::SendVoice { chat_id, file_path } => {
175 validate_chat_id(chat_id)?;
176 validate_voice_file_path(file_path)?;
177 }
178 MessagingCommands::EditMessage {
179 chat_id,
180 message_id,
181 new_text,
182 } => {
183 validate_chat_id(chat_id)?;
184 validate_message_id(message_id)?;
185 validate_message_text(new_text)?;
186 }
187 MessagingCommands::DeleteMessage {
188 chat_id,
189 message_id,
190 }
191 | MessagingCommands::PinMessage {
192 chat_id,
193 message_id,
194 }
195 | MessagingCommands::UnpinMessage {
196 chat_id,
197 message_id,
198 } => {
199 validate_chat_id(chat_id)?;
200 validate_message_id(message_id)?;
201 }
202 }
203 Ok(())
204 }
205}
206
207async fn execute_send_text_structured(
211 bot: &Bot,
212 chat_id: &str,
213 message: &str,
214) -> CliResponse<serde_json::Value> {
215 debug!("Sending text message to {}", chat_id);
216
217 let parser = MessageTextParser::new().add(MessageTextFormat::Plain(message.to_string()));
218 let request =
219 match RequestMessagesSendText::new(ChatId::from_borrowed_str(chat_id)).set_text(parser) {
220 Ok(req) => req,
221 Err(e) => {
222 return CliResponse::error("send-text", format!("Failed to create message: {e}"));
223 }
224 };
225
226 match bot.send_api_request(request).await {
227 Ok(result) => {
228 info!("Successfully sent text message to {}", chat_id);
229 let data = json!({
230 "chat_id": chat_id,
231 "message": message,
232 "message_id": result.msg_id
233 });
234 CliResponse::success("send-text", data)
235 }
236 Err(e) => CliResponse::error("send-text", format!("Failed to send message: {e}")),
237 }
238}
239
240async fn execute_send_file_structured(
241 bot: &Bot,
242 chat_id: &str,
243 file_path: &str,
244) -> CliResponse<serde_json::Value> {
245 debug!("Sending file {} to {}", file_path, chat_id);
246
247 match file_utils::upload_file(bot, chat_id, file_path).await {
248 Ok(file_id) => {
249 info!("Successfully sent file to {}", chat_id);
250 let data = json!({
251 "chat_id": chat_id,
252 "file_path": file_path,
253 "file_id": file_id
254 });
255 CliResponse::success("send-file", data)
256 }
257 Err(e) => CliResponse::error("send-file", format!("Failed to send file: {e}")),
258 }
259}
260
261async fn execute_send_voice_structured(
262 bot: &Bot,
263 chat_id: &str,
264 file_path: &str,
265) -> CliResponse<serde_json::Value> {
266 debug!("Sending voice message {} to {}", file_path, chat_id);
267
268 match file_utils::upload_voice(bot, chat_id, file_path).await {
269 Ok(file_id) => {
270 info!("Successfully sent voice message to {}", chat_id);
271 let data = json!({
272 "chat_id": chat_id,
273 "file_path": file_path,
274 "file_id": file_id
275 });
276 CliResponse::success("send-voice", data)
277 }
278 Err(e) => CliResponse::error("send-voice", format!("Failed to send voice: {e}")),
279 }
280}
281
282async fn execute_edit_message_structured(
283 bot: &Bot,
284 chat_id: &str,
285 message_id: &str,
286 new_text: &str,
287) -> CliResponse<serde_json::Value> {
288 debug!("Editing message {} in {}", message_id, chat_id);
289
290 let parser = MessageTextParser::new().add(MessageTextFormat::Plain(new_text.to_string()));
291 let request = match RequestMessagesEditText::new((
292 ChatId::from_borrowed_str(chat_id),
293 MsgId(message_id.to_string()),
294 ))
295 .set_text(parser)
296 {
297 Ok(req) => req,
298 Err(e) => {
299 return CliResponse::error("edit-message", format!("Failed to set message text: {e}"));
300 }
301 };
302
303 match bot.send_api_request(request).await {
304 Ok(_result) => {
305 info!("Successfully edited message {} in {}", message_id, chat_id);
306 let data = json!({
307 "chat_id": chat_id,
308 "message_id": message_id,
309 "new_text": new_text
310 });
311 CliResponse::success("edit-message", data)
312 }
313 Err(e) => CliResponse::error("edit-message", format!("Failed to edit message: {e}")),
314 }
315}
316
317async fn execute_delete_message_structured(
318 bot: &Bot,
319 chat_id: &str,
320 message_id: &str,
321) -> CliResponse<serde_json::Value> {
322 debug!("Deleting message {} from {}", message_id, chat_id);
323
324 let request = RequestMessagesDeleteMessages::new((
325 ChatId::from_borrowed_str(chat_id),
326 MsgId(message_id.to_string()),
327 ));
328
329 match bot.send_api_request(request).await {
330 Ok(_result) => {
331 info!(
332 "Successfully deleted message {} from {}",
333 message_id, chat_id
334 );
335 let data = json!({
336 "chat_id": chat_id,
337 "message_id": message_id,
338 "action": "deleted"
339 });
340 CliResponse::success("delete-message", data)
341 }
342 Err(e) => CliResponse::error("delete-message", format!("Failed to delete message: {e}")),
343 }
344}
345
346async fn execute_pin_message_structured(
347 bot: &Bot,
348 chat_id: &str,
349 message_id: &str,
350) -> CliResponse<serde_json::Value> {
351 debug!("Pinning message {} in {}", message_id, chat_id);
352
353 let request = RequestChatsPinMessage::new((
354 ChatId::from_borrowed_str(chat_id),
355 MsgId(message_id.to_string()),
356 ));
357
358 match bot.send_api_request(request).await {
359 Ok(_result) => {
360 info!("Successfully pinned message {} in {}", message_id, chat_id);
361 let data = json!({
362 "chat_id": chat_id,
363 "message_id": message_id,
364 "action": "pinned"
365 });
366 CliResponse::success("pin-message", data)
367 }
368 Err(e) => CliResponse::error("pin-message", format!("Failed to pin message: {e}")),
369 }
370}
371
372async fn execute_unpin_message_structured(
373 bot: &Bot,
374 chat_id: &str,
375 message_id: &str,
376) -> CliResponse<serde_json::Value> {
377 debug!("Unpinning message {} from {}", message_id, chat_id);
378
379 let request = RequestChatsUnpinMessage::new((
380 ChatId::from_borrowed_str(chat_id),
381 MsgId(message_id.to_string()),
382 ));
383
384 match bot.send_api_request(request).await {
385 Ok(_result) => {
386 info!(
387 "Successfully unpinned message {} from {}",
388 message_id, chat_id
389 );
390 let data = json!({
391 "chat_id": chat_id,
392 "message_id": message_id,
393 "action": "unpinned"
394 });
395 CliResponse::success("unpin-message", data)
396 }
397 Err(e) => CliResponse::error("unpin-message", format!("Failed to unpin message: {e}")),
398 }
399}
400
401async fn execute_send_text(bot: &Bot, chat_id: &str, message: &str) -> CliResult<()> {
403 debug!("Sending text message to {}", chat_id);
404
405 let parser = MessageTextParser::new().add(MessageTextFormat::Plain(message.to_string()));
406 let request = RequestMessagesSendText::new(ChatId::from_borrowed_str(chat_id))
407 .set_text(parser)
408 .map_err(|e| CliError::InputError(format!("Failed to create message: {e}")))?;
409
410 let result = bot
411 .send_api_request(request)
412 .await
413 .map_err(CliError::ApiError)?;
414
415 info!("Successfully sent text message to {}", chat_id);
416 print_success_result(&result, &OutputFormat::Pretty)?;
417 Ok(())
418}
419
420async fn execute_send_file(bot: &Bot, chat_id: &str, file_path: &str) -> CliResult<()> {
421 debug!("Sending file {} to {}", file_path, chat_id);
422
423 file_utils::upload_file(bot, chat_id, file_path).await?;
424
425 info!("Successfully sent file to {}", chat_id);
426 println!(
427 "{} File sent successfully to {}",
428 emoji::CHECK,
429 chat_id.green()
430 );
431 Ok(())
432}
433
434async fn execute_send_voice(bot: &Bot, chat_id: &str, file_path: &str) -> CliResult<()> {
435 debug!("Sending voice message {} to {}", file_path, chat_id);
436
437 file_utils::upload_voice(bot, chat_id, file_path).await?;
438
439 info!("Successfully sent voice message to {}", chat_id);
440 println!(
441 "{} Voice message sent successfully to {}",
442 emoji::CHECK,
443 chat_id.green()
444 );
445 Ok(())
446}
447
448async fn execute_edit_message(
449 bot: &Bot,
450 chat_id: &str,
451 message_id: &str,
452 new_text: &str,
453) -> CliResult<()> {
454 debug!("Editing message {} in {}", message_id, chat_id);
455
456 let parser = MessageTextParser::new().add(MessageTextFormat::Plain(new_text.to_string()));
457 let request = RequestMessagesEditText::new((
458 ChatId::from_borrowed_str(chat_id),
459 MsgId(message_id.to_string()),
460 ))
461 .set_text(parser)
462 .map_err(|e| CliError::InputError(format!("Failed to set message text: {e}")))?;
463
464 let result = bot
465 .send_api_request(request)
466 .await
467 .map_err(CliError::ApiError)?;
468
469 info!("Successfully edited message {} in {}", message_id, chat_id);
470 print_success_result(&result, &OutputFormat::Pretty)?;
471 Ok(())
472}
473
474async fn execute_delete_message(bot: &Bot, chat_id: &str, message_id: &str) -> CliResult<()> {
475 debug!("Deleting message {} from {}", message_id, chat_id);
476
477 let request = RequestMessagesDeleteMessages::new((
478 ChatId::from_borrowed_str(chat_id),
479 MsgId(message_id.to_string()),
480 ));
481
482 let result = bot
483 .send_api_request(request)
484 .await
485 .map_err(CliError::ApiError)?;
486
487 info!(
488 "Successfully deleted message {} from {}",
489 message_id, chat_id
490 );
491 print_success_result(&result, &OutputFormat::Pretty)?;
492 Ok(())
493}
494
495async fn execute_pin_message(bot: &Bot, chat_id: &str, message_id: &str) -> CliResult<()> {
496 debug!("Pinning message {} in {}", message_id, chat_id);
497
498 let request = RequestChatsPinMessage::new((
499 ChatId::from_borrowed_str(chat_id),
500 MsgId(message_id.to_string()),
501 ));
502
503 let result = bot
504 .send_api_request(request)
505 .await
506 .map_err(CliError::ApiError)?;
507
508 info!("Successfully pinned message {} in {}", message_id, chat_id);
509 print_success_result(&result, &OutputFormat::Pretty)?;
510 Ok(())
511}
512
513async fn execute_unpin_message(bot: &Bot, chat_id: &str, message_id: &str) -> CliResult<()> {
514 debug!("Unpinning message {} from {}", message_id, chat_id);
515
516 let request = RequestChatsUnpinMessage::new((
517 ChatId::from_borrowed_str(chat_id),
518 MsgId(message_id.to_string()),
519 ));
520
521 let result = bot
522 .send_api_request(request)
523 .await
524 .map_err(CliError::ApiError)?;
525
526 info!(
527 "Successfully unpinned message {} from {}",
528 message_id, chat_id
529 );
530 print_success_result(&result, &OutputFormat::Pretty)?;
531 Ok(())
532}
533
534#[cfg(test)]
537mod tests {
538 use super::*;
539 use tokio::runtime::Runtime;
540
541 #[test]
542 fn test_send_text_valid() {
543 let cmd = MessagingCommands::SendText {
544 chat_id: "user123".to_string(),
545 message: "Hello".to_string(),
546 };
547 assert!(cmd.validate().is_ok());
548 }
549
550 #[test]
551 fn test_send_text_invalid_chat_id() {
552 let cmd = MessagingCommands::SendText {
553 chat_id: "user with spaces".to_string(),
554 message: "Hello".to_string(),
555 };
556 assert!(cmd.validate().is_err());
557 }
558
559 #[test]
560 fn test_send_text_empty_message() {
561 let cmd = MessagingCommands::SendText {
562 chat_id: "user123".to_string(),
563 message: "".to_string(),
564 };
565 assert!(cmd.validate().is_err());
566 }
567
568 #[test]
569 fn test_send_file_invalid_path() {
570 let cmd = MessagingCommands::SendFile {
571 chat_id: "user123".to_string(),
572 file_path: "nonexistent.file".to_string(),
573 };
574 assert!(cmd.validate().is_err());
576 }
577
578 #[test]
579 fn test_send_voice_invalid_path() {
580 let cmd = MessagingCommands::SendVoice {
581 chat_id: "user123".to_string(),
582 file_path: "nonexistent.ogg".to_string(),
583 };
584 assert!(cmd.validate().is_err());
585 }
586
587 #[test]
588 fn test_edit_message_invalid_message_id() {
589 let cmd = MessagingCommands::EditMessage {
590 chat_id: "user123".to_string(),
591 message_id: "id with space".to_string(),
592 new_text: "new text".to_string(),
593 };
594 assert!(cmd.validate().is_err());
595 }
596
597 #[test]
598 fn test_delete_message_empty_message_id() {
599 let cmd = MessagingCommands::DeleteMessage {
600 chat_id: "user123".to_string(),
601 message_id: "".to_string(),
602 };
603 assert!(cmd.validate().is_err());
604 }
605
606 #[test]
607 fn test_pin_message_valid() {
608 let cmd = MessagingCommands::PinMessage {
609 chat_id: "user123".to_string(),
610 message_id: "msg123".to_string(),
611 };
612 assert!(cmd.validate().is_ok());
613 }
614
615 #[test]
616 fn test_unpin_message_invalid_chat_id() {
617 let cmd = MessagingCommands::UnpinMessage {
618 chat_id: "invalid id".to_string(),
619 message_id: "msg123".to_string(),
620 };
621 assert!(cmd.validate().is_err());
622 }
623
624 fn dummy_bot() -> Bot {
625 Bot::with_params(&APIVersionUrl::V1, "dummy_token", "https://dummy.api.com").unwrap()
626 }
627
628 #[test]
629 fn test_execute_send_text_api_error() {
630 let cmd = MessagingCommands::SendText {
631 chat_id: "12345@chat".to_string(),
632 message: "hello".to_string(),
633 };
634 let bot = dummy_bot();
635 let rt = Runtime::new().unwrap();
636 let res = rt.block_on(cmd.execute(&bot));
637 assert!(res.is_err());
638 }
639
640 #[test]
641 fn test_execute_send_file_api_error() {
642 let cmd = MessagingCommands::SendFile {
643 chat_id: "12345@chat".to_string(),
644 file_path: "/tmp/file.txt".to_string(),
645 };
646 let bot = dummy_bot();
647 let rt = Runtime::new().unwrap();
648 let res = rt.block_on(cmd.execute(&bot));
649 assert!(res.is_err());
650 }
651
652 #[test]
653 fn test_execute_send_voice_api_error() {
654 let cmd = MessagingCommands::SendVoice {
655 chat_id: "12345@chat".to_string(),
656 file_path: "/tmp/voice.ogg".to_string(),
657 };
658 let bot = dummy_bot();
659 let rt = Runtime::new().unwrap();
660 let res = rt.block_on(cmd.execute(&bot));
661 assert!(res.is_err());
662 }
663
664 #[test]
665 fn test_execute_edit_message_api_error() {
666 let cmd = MessagingCommands::EditMessage {
667 chat_id: "12345@chat".to_string(),
668 message_id: "msgid".to_string(),
669 new_text: "new text".to_string(),
670 };
671 let bot = dummy_bot();
672 let rt = Runtime::new().unwrap();
673 let res = rt.block_on(cmd.execute(&bot));
674 assert!(res.is_err());
675 }
676
677 #[test]
678 fn test_execute_delete_message_api_error() {
679 let cmd = MessagingCommands::DeleteMessage {
680 chat_id: "12345@chat".to_string(),
681 message_id: "msgid".to_string(),
682 };
683 let bot = dummy_bot();
684 let rt = Runtime::new().unwrap();
685 let res = rt.block_on(cmd.execute(&bot));
686 assert!(res.is_err());
687 }
688
689 #[test]
690 fn test_execute_pin_message_api_error() {
691 let cmd = MessagingCommands::PinMessage {
692 chat_id: "12345@chat".to_string(),
693 message_id: "msgid".to_string(),
694 };
695 let bot = dummy_bot();
696 let rt = Runtime::new().unwrap();
697 let res = rt.block_on(cmd.execute(&bot));
698 assert!(res.is_err());
699 }
700
701 #[test]
702 fn test_execute_unpin_message_api_error() {
703 let cmd = MessagingCommands::UnpinMessage {
704 chat_id: "12345@chat".to_string(),
705 message_id: "msgid".to_string(),
706 };
707 let bot = dummy_bot();
708 let rt = Runtime::new().unwrap();
709 let res = rt.block_on(cmd.execute(&bot));
710 assert!(res.is_err());
711 }
712}