vkteams_bot_cli/commands/messaging/
mod.rs

1//! Messaging commands module
2//!
3//! This module contains all commands related to sending and managing messages.
4
5use 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/// All messaging-related commands
24#[derive(Subcommand, Debug, Clone)]
25pub enum MessagingCommands {
26    /// Send text message to user or chat
27    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    /// Send file to user or chat
34    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    /// Send voice message to user or chat
41    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    /// Edit existing message
48    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    /// Delete message from chat
57    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    /// Pin message in chat
64    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    /// Unpin message from chat
71    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    /// New method for structured output support
113    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
207// Command execution functions
208
209// Structured output versions
210async 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
401// Legacy output versions (for backward compatibility)
402async 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// Validation functions are now imported from utils/validation module
535
536#[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        // Путь не существует, validate_file_path вернет ошибку
575        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}