1use crate::commands::{Command, OutputFormat};
4use crate::errors::prelude::{CliError, Result as CliResult};
5use crate::output::{CliResponse, OutputFormatter};
6use async_trait::async_trait;
7use base64::Engine;
8use clap::{Args, Subcommand};
9use serde_json::json;
10use vkteams_bot::prelude::*;
11
12#[derive(Debug, Clone, Subcommand)]
13pub enum FileCommands {
14 Upload(UploadFileArgs),
16 UploadText(UploadTextArgs),
18 UploadJson(UploadJsonArgs),
20 Info(FileInfoArgs),
22}
23
24#[derive(Debug, Clone, Args)]
25pub struct UploadFileArgs {
26 #[arg(long)]
28 pub name: String,
29
30 #[arg(long)]
32 pub content_base64: String,
33
34 #[arg(long)]
36 pub caption: Option<String>,
37
38 #[arg(long)]
40 pub reply_msg_id: Option<String>,
41
42 #[arg(long)]
44 pub chat_id: Option<String>,
45}
46
47#[derive(Debug, Clone, Args)]
48pub struct UploadTextArgs {
49 #[arg(long)]
51 pub name: String,
52
53 #[arg(long)]
55 pub content: String,
56
57 #[arg(long)]
59 pub caption: Option<String>,
60
61 #[arg(long)]
63 pub reply_msg_id: Option<String>,
64
65 #[arg(long)]
67 pub chat_id: Option<String>,
68}
69
70#[derive(Debug, Clone, Args)]
71pub struct UploadJsonArgs {
72 #[arg(long)]
74 pub name: String,
75
76 #[arg(long)]
78 pub json_data: String,
79
80 #[arg(long, default_value = "true")]
82 pub pretty: bool,
83
84 #[arg(long)]
86 pub caption: Option<String>,
87
88 #[arg(long)]
90 pub reply_msg_id: Option<String>,
91
92 #[arg(long)]
94 pub chat_id: Option<String>,
95}
96
97#[derive(Debug, Clone, Args)]
98pub struct FileInfoArgs {
99 #[arg(long)]
101 pub file_id: String,
102}
103
104impl FileCommands {
105 pub async fn execute_with_output(
106 &self,
107 bot: &Bot,
108 output_format: &OutputFormat,
109 ) -> CliResult<()> {
110 let response = match self {
111 FileCommands::Upload(args) => self.handle_upload(bot, args).await,
112 FileCommands::UploadText(args) => self.handle_upload_text(bot, args).await,
113 FileCommands::UploadJson(args) => self.handle_upload_json(bot, args).await,
114 FileCommands::Info(args) => self.handle_file_info(bot, args).await,
115 };
116
117 OutputFormatter::print(&response, output_format)?;
118
119 if !response.success {
120 std::process::exit(1);
121 }
122
123 Ok(())
124 }
125
126 async fn handle_upload(
127 &self,
128 bot: &Bot,
129 args: &UploadFileArgs,
130 ) -> CliResponse<serde_json::Value> {
131 let file_content = match base64::engine::general_purpose::STANDARD
133 .decode(&args.content_base64)
134 {
135 Ok(content) => content,
136 Err(e) => {
137 return CliResponse::error("upload-file", format!("Invalid base64 content: {e}"));
138 }
139 };
140
141 if file_content.len() > 100 * 1024 * 1024 {
143 return CliResponse::error("upload-file", "File too large (max 100MB)");
144 }
145
146 let chat_id = match &args.chat_id {
147 Some(id) => ChatId::from_borrowed_str(id),
148 None => {
149 match std::env::var("VKTEAMS_BOT_CHAT_ID") {
151 Ok(id) => ChatId::from_borrowed_str(&id),
152 Err(_) => {
153 return CliResponse::error(
154 "upload-file",
155 "No chat ID provided and VKTEAMS_BOT_CHAT_ID not set",
156 );
157 }
158 }
159 }
160 };
161
162 let mut req = RequestMessagesSendFile::new((
163 chat_id,
164 MultipartName::FileContent {
165 filename: args.name.clone(),
166 content: file_content.clone(),
167 },
168 ));
169
170 if let Some(caption) = &args.caption {
171 req = req.with_text(caption.clone());
172 }
173
174 if let Some(reply_msg_id) = &args.reply_msg_id {
175 req = req.with_reply_msg_id(MsgId(reply_msg_id.clone()));
176 }
177
178 match bot.send_api_request(req).await {
179 Ok(response) => {
180 let data = json!({
181 "message_id": response.msg_id,
182 "file_name": args.name,
183 "file_size": file_content.len(),
184 "file_size_formatted": format_file_size(file_content.len()),
185 "caption": args.caption
186 });
187 CliResponse::success("upload-file", data)
188 }
189 Err(e) => CliResponse::error("upload-file", format!("Failed to upload file: {e}")),
190 }
191 }
192
193 async fn handle_upload_text(
194 &self,
195 bot: &Bot,
196 args: &UploadTextArgs,
197 ) -> CliResponse<serde_json::Value> {
198 let file_content = args.content.as_bytes().to_vec();
199
200 let chat_id = match &args.chat_id {
201 Some(id) => ChatId::from_borrowed_str(id),
202 None => match std::env::var("VKTEAMS_BOT_CHAT_ID") {
203 Ok(id) => ChatId::from_borrowed_str(&id),
204 Err(_) => {
205 return CliResponse::error(
206 "upload-text",
207 "No chat ID provided and VKTEAMS_BOT_CHAT_ID not set",
208 );
209 }
210 },
211 };
212
213 let mut req = RequestMessagesSendFile::new((
214 chat_id,
215 MultipartName::FileContent {
216 filename: args.name.clone(),
217 content: file_content.clone(),
218 },
219 ));
220
221 if let Some(caption) = &args.caption {
222 req = req.with_text(caption.clone());
223 }
224
225 if let Some(reply_msg_id) = &args.reply_msg_id {
226 req = req.with_reply_msg_id(MsgId(reply_msg_id.clone()));
227 }
228
229 match bot.send_api_request(req).await {
230 Ok(response) => {
231 let data = json!({
232 "message_id": response.msg_id,
233 "file_name": args.name,
234 "file_size": file_content.len(),
235 "content_preview": if args.content.len() > 100 {
236 format!("{}...", &args.content[..100])
237 } else {
238 args.content.clone()
239 },
240 "caption": args.caption
241 });
242 CliResponse::success("upload-text", data)
243 }
244 Err(e) => CliResponse::error("upload-text", format!("Failed to upload text file: {e}")),
245 }
246 }
247
248 async fn handle_upload_json(
249 &self,
250 bot: &Bot,
251 args: &UploadJsonArgs,
252 ) -> CliResponse<serde_json::Value> {
253 let json_value: serde_json::Value = match serde_json::from_str(&args.json_data) {
255 Ok(value) => value,
256 Err(e) => {
257 return CliResponse::error("upload-json", format!("Invalid JSON data: {e}"));
258 }
259 };
260
261 let formatted_json = if args.pretty {
262 match serde_json::to_string_pretty(&json_value) {
263 Ok(s) => s,
264 Err(e) => {
265 return CliResponse::error(
266 "upload-json",
267 format!("Failed to format JSON: {e}"),
268 );
269 }
270 }
271 } else {
272 match serde_json::to_string(&json_value) {
273 Ok(s) => s,
274 Err(e) => {
275 return CliResponse::error(
276 "upload-json",
277 format!("Failed to serialize JSON: {e}"),
278 );
279 }
280 }
281 };
282
283 let final_filename = if args.name.ends_with(".json") {
284 args.name.clone()
285 } else {
286 format!("{}.json", args.name)
287 };
288
289 let file_content = formatted_json.as_bytes().to_vec();
290
291 let chat_id = match &args.chat_id {
292 Some(id) => ChatId::from_borrowed_str(id),
293 None => match std::env::var("VKTEAMS_BOT_CHAT_ID") {
294 Ok(id) => ChatId::from_borrowed_str(&id),
295 Err(_) => {
296 return CliResponse::error(
297 "upload-json",
298 "No chat ID provided and VKTEAMS_BOT_CHAT_ID not set",
299 );
300 }
301 },
302 };
303
304 let mut req = RequestMessagesSendFile::new((
305 chat_id,
306 MultipartName::FileContent {
307 filename: final_filename.clone(),
308 content: file_content.clone(),
309 },
310 ));
311
312 if let Some(caption) = &args.caption {
313 req = req.with_text(caption.clone());
314 }
315
316 if let Some(reply_msg_id) = &args.reply_msg_id {
317 req = req.with_reply_msg_id(MsgId(reply_msg_id.clone()));
318 }
319
320 match bot.send_api_request(req).await {
321 Ok(response) => {
322 let data = json!({
323 "message_id": response.msg_id,
324 "file_name": final_filename,
325 "file_size": file_content.len(),
326 "pretty_formatted": args.pretty,
327 "json_valid": true,
328 "caption": args.caption
329 });
330 CliResponse::success("upload-json", data)
331 }
332 Err(e) => CliResponse::error("upload-json", format!("Failed to upload JSON file: {e}")),
333 }
334 }
335
336 async fn handle_file_info(
337 &self,
338 bot: &Bot,
339 args: &FileInfoArgs,
340 ) -> CliResponse<serde_json::Value> {
341 let req = RequestFilesGetInfo::new(FileId(args.file_id.clone()));
342
343 match bot.send_api_request(req).await {
344 Ok(response) => {
345 let data = json!({
346 "file_type": response.file_type,
347 "file_size": response.file_size,
348 "file_name": response.file_name,
349 "url": response.url
350 });
351 CliResponse::success("file-info", data)
352 }
353 Err(e) => CliResponse::error("file-info", format!("Failed to get file info: {e}")),
354 }
355 }
356}
357
358#[async_trait]
359impl Command for FileCommands {
360 async fn execute(&self, bot: &Bot) -> CliResult<()> {
361 self.execute_with_output(bot, &OutputFormat::Pretty).await
363 }
364
365 fn name(&self) -> &'static str {
366 match self {
367 FileCommands::Upload(_) => "upload-file",
368 FileCommands::UploadText(_) => "upload-text",
369 FileCommands::UploadJson(_) => "upload-json",
370 FileCommands::Info(_) => "file-info",
371 }
372 }
373
374 fn validate(&self) -> CliResult<()> {
375 match self {
376 FileCommands::Upload(args) => {
377 if args.name.is_empty() {
378 return Err(CliError::InputError(
379 "File name cannot be empty".to_string(),
380 ));
381 }
382 if args.content_base64.is_empty() {
383 return Err(CliError::InputError(
384 "File content cannot be empty".to_string(),
385 ));
386 }
387 }
388 FileCommands::UploadText(args) => {
389 if args.name.is_empty() {
390 return Err(CliError::InputError(
391 "File name cannot be empty".to_string(),
392 ));
393 }
394 }
395 FileCommands::UploadJson(args) => {
396 if args.name.is_empty() {
397 return Err(CliError::InputError(
398 "File name cannot be empty".to_string(),
399 ));
400 }
401 if args.json_data.is_empty() {
402 return Err(CliError::InputError(
403 "JSON data cannot be empty".to_string(),
404 ));
405 }
406 }
407 FileCommands::Info(args) => {
408 if args.file_id.is_empty() {
409 return Err(CliError::InputError("File ID cannot be empty".to_string()));
410 }
411 }
412 }
413 Ok(())
414 }
415}
416
417fn format_file_size(size: usize) -> String {
418 const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
419 let mut size = size as f64;
420 let mut unit_index = 0;
421
422 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
423 size /= 1024.0;
424 unit_index += 1;
425 }
426
427 if unit_index == 0 {
428 format!("{} {}", size as usize, UNITS[unit_index])
429 } else {
430 format!("{:.1} {}", size, UNITS[unit_index])
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 #[test]
439 fn test_format_file_size() {
440 assert_eq!(format_file_size(0), "0 B");
441 assert_eq!(format_file_size(512), "512 B");
442 assert_eq!(format_file_size(1024), "1.0 KB");
443 assert_eq!(format_file_size(1536), "1.5 KB");
444 assert_eq!(format_file_size(1024 * 1024), "1.0 MB");
445 assert_eq!(format_file_size(1024 * 1024 * 1024), "1.0 GB");
446 }
447
448 #[test]
449 fn test_upload_args_validation() {
450 let args = UploadFileArgs {
451 name: "".to_string(),
452 content_base64: "content".to_string(),
453 caption: None,
454 reply_msg_id: None,
455 chat_id: None,
456 };
457
458 let cmd = FileCommands::Upload(args);
459 assert!(cmd.validate().is_err());
460 }
461
462 #[test]
463 fn test_json_filename_extension() {
464 let args = UploadJsonArgs {
465 name: "test".to_string(),
466 json_data: "{}".to_string(),
467 pretty: true,
468 caption: None,
469 reply_msg_id: None,
470 chat_id: None,
471 };
472
473 let expected_filename = if args.name.ends_with(".json") {
475 args.name.clone()
476 } else {
477 format!("{}.json", args.name)
478 };
479
480 assert_eq!(expected_filename, "test.json");
481 }
482
483 #[test]
484 fn test_file_commands_name() {
485 let upload_cmd = FileCommands::Upload(UploadFileArgs {
486 name: "test.txt".to_string(),
487 content_base64: "dGVzdA==".to_string(),
488 caption: None,
489 reply_msg_id: None,
490 chat_id: None,
491 });
492 assert_eq!(upload_cmd.name(), "upload-file");
493
494 let upload_text_cmd = FileCommands::UploadText(UploadTextArgs {
495 name: "test.txt".to_string(),
496 content: "test content".to_string(),
497 caption: None,
498 reply_msg_id: None,
499 chat_id: None,
500 });
501 assert_eq!(upload_text_cmd.name(), "upload-text");
502
503 let upload_json_cmd = FileCommands::UploadJson(UploadJsonArgs {
504 name: "test.json".to_string(),
505 json_data: "{}".to_string(),
506 pretty: true,
507 caption: None,
508 reply_msg_id: None,
509 chat_id: None,
510 });
511 assert_eq!(upload_json_cmd.name(), "upload-json");
512
513 let info_cmd = FileCommands::Info(FileInfoArgs {
514 file_id: "test_file_id".to_string(),
515 });
516 assert_eq!(info_cmd.name(), "file-info");
517 }
518
519 #[test]
520 fn test_upload_file_validation() {
521 let valid_args = UploadFileArgs {
523 name: "test.txt".to_string(),
524 content_base64: "dGVzdA==".to_string(),
525 caption: None,
526 reply_msg_id: None,
527 chat_id: None,
528 };
529 let cmd = FileCommands::Upload(valid_args);
530 assert!(cmd.validate().is_ok());
531
532 let invalid_name_args = UploadFileArgs {
534 name: "".to_string(),
535 content_base64: "dGVzdA==".to_string(),
536 caption: None,
537 reply_msg_id: None,
538 chat_id: None,
539 };
540 let cmd = FileCommands::Upload(invalid_name_args);
541 assert!(cmd.validate().is_err());
542
543 let invalid_content_args = UploadFileArgs {
545 name: "test.txt".to_string(),
546 content_base64: "".to_string(),
547 caption: None,
548 reply_msg_id: None,
549 chat_id: None,
550 };
551 let cmd = FileCommands::Upload(invalid_content_args);
552 assert!(cmd.validate().is_err());
553 }
554
555 #[test]
556 fn test_upload_text_validation() {
557 let valid_args = UploadTextArgs {
559 name: "test.txt".to_string(),
560 content: "test content".to_string(),
561 caption: None,
562 reply_msg_id: None,
563 chat_id: None,
564 };
565 let cmd = FileCommands::UploadText(valid_args);
566 assert!(cmd.validate().is_ok());
567
568 let invalid_args = UploadTextArgs {
570 name: "".to_string(),
571 content: "test content".to_string(),
572 caption: None,
573 reply_msg_id: None,
574 chat_id: None,
575 };
576 let cmd = FileCommands::UploadText(invalid_args);
577 assert!(cmd.validate().is_err());
578 }
579
580 #[test]
581 fn test_upload_json_validation() {
582 let valid_args = UploadJsonArgs {
584 name: "test.json".to_string(),
585 json_data: "{}".to_string(),
586 pretty: true,
587 caption: None,
588 reply_msg_id: None,
589 chat_id: None,
590 };
591 let cmd = FileCommands::UploadJson(valid_args);
592 assert!(cmd.validate().is_ok());
593
594 let invalid_name_args = UploadJsonArgs {
596 name: "".to_string(),
597 json_data: "{}".to_string(),
598 pretty: true,
599 caption: None,
600 reply_msg_id: None,
601 chat_id: None,
602 };
603 let cmd = FileCommands::UploadJson(invalid_name_args);
604 assert!(cmd.validate().is_err());
605
606 let invalid_json_args = UploadJsonArgs {
608 name: "test.json".to_string(),
609 json_data: "".to_string(),
610 pretty: true,
611 caption: None,
612 reply_msg_id: None,
613 chat_id: None,
614 };
615 let cmd = FileCommands::UploadJson(invalid_json_args);
616 assert!(cmd.validate().is_err());
617 }
618
619 #[test]
620 fn test_file_info_validation() {
621 let valid_args = FileInfoArgs {
623 file_id: "test_file_id".to_string(),
624 };
625 let cmd = FileCommands::Info(valid_args);
626 assert!(cmd.validate().is_ok());
627
628 let invalid_args = FileInfoArgs {
630 file_id: "".to_string(),
631 };
632 let cmd = FileCommands::Info(invalid_args);
633 assert!(cmd.validate().is_err());
634 }
635
636 #[test]
637 fn test_json_filename_with_extension() {
638 let args = UploadJsonArgs {
640 name: "test.json".to_string(),
641 json_data: "{}".to_string(),
642 pretty: true,
643 caption: None,
644 reply_msg_id: None,
645 chat_id: None,
646 };
647
648 let final_filename = if args.name.ends_with(".json") {
649 args.name.clone()
650 } else {
651 format!("{}.json", args.name)
652 };
653
654 assert_eq!(final_filename, "test.json");
655 }
656
657 #[test]
658 fn test_json_filename_without_extension() {
659 let args = UploadJsonArgs {
661 name: "test".to_string(),
662 json_data: "{}".to_string(),
663 pretty: true,
664 caption: None,
665 reply_msg_id: None,
666 chat_id: None,
667 };
668
669 let final_filename = if args.name.ends_with(".json") {
670 args.name.clone()
671 } else {
672 format!("{}.json", args.name)
673 };
674
675 assert_eq!(final_filename, "test.json");
676 }
677
678 #[test]
679 fn test_format_file_size_edge_cases() {
680 assert_eq!(
682 format_file_size(1024 * 1024 * 1024 + 512 * 1024 * 1024),
683 "1.5 GB"
684 );
685 assert_eq!(format_file_size(2048), "2.0 KB");
686 assert_eq!(format_file_size(1023), "1023 B");
687 assert_eq!(format_file_size(1025), "1.0 KB");
688 }
689
690 #[test]
691 fn test_execute_with_default_format() {
692 let cmd = FileCommands::Info(FileInfoArgs {
694 file_id: "test_file_id".to_string(),
695 });
696
697 assert_eq!(cmd.name(), "file-info");
700
701 assert!(cmd.validate().is_ok());
703 }
704
705 #[test]
706 fn test_upload_file_args_structure() {
707 let args = UploadFileArgs {
708 name: "test.txt".to_string(),
709 content_base64: "dGVzdA==".to_string(),
710 caption: Some("Test caption".to_string()),
711 reply_msg_id: Some("msg_123".to_string()),
712 chat_id: Some("chat_456".to_string()),
713 };
714
715 assert_eq!(args.name, "test.txt");
716 assert_eq!(args.content_base64, "dGVzdA==");
717 assert_eq!(args.caption, Some("Test caption".to_string()));
718 assert_eq!(args.reply_msg_id, Some("msg_123".to_string()));
719 assert_eq!(args.chat_id, Some("chat_456".to_string()));
720 }
721
722 #[test]
723 fn test_upload_text_args_structure() {
724 let args = UploadTextArgs {
725 name: "test.txt".to_string(),
726 content: "test content".to_string(),
727 caption: Some("Test caption".to_string()),
728 reply_msg_id: Some("msg_123".to_string()),
729 chat_id: Some("chat_456".to_string()),
730 };
731
732 assert_eq!(args.name, "test.txt");
733 assert_eq!(args.content, "test content");
734 assert_eq!(args.caption, Some("Test caption".to_string()));
735 assert_eq!(args.reply_msg_id, Some("msg_123".to_string()));
736 assert_eq!(args.chat_id, Some("chat_456".to_string()));
737 }
738
739 #[test]
740 fn test_upload_json_args_structure() {
741 let args = UploadJsonArgs {
742 name: "test.json".to_string(),
743 json_data: r#"{"key": "value"}"#.to_string(),
744 pretty: false,
745 caption: Some("Test caption".to_string()),
746 reply_msg_id: Some("msg_123".to_string()),
747 chat_id: Some("chat_456".to_string()),
748 };
749
750 assert_eq!(args.name, "test.json");
751 assert_eq!(args.json_data, r#"{"key": "value"}"#);
752 assert!(!args.pretty);
753 assert_eq!(args.caption, Some("Test caption".to_string()));
754 assert_eq!(args.reply_msg_id, Some("msg_123".to_string()));
755 assert_eq!(args.chat_id, Some("chat_456".to_string()));
756 }
757
758 #[test]
759 fn test_file_info_args_structure() {
760 let args = FileInfoArgs {
761 file_id: "test_file_id_123".to_string(),
762 };
763
764 assert_eq!(args.file_id, "test_file_id_123");
765 }
766
767 #[test]
768 fn test_file_commands_debug_and_clone() {
769 let cmd = FileCommands::Upload(UploadFileArgs {
770 name: "test.txt".to_string(),
771 content_base64: "dGVzdA==".to_string(),
772 caption: None,
773 reply_msg_id: None,
774 chat_id: None,
775 });
776
777 let debug_str = format!("{cmd:?}");
779 assert!(debug_str.contains("Upload"));
780 assert!(debug_str.contains("test.txt"));
781
782 let cloned_cmd = cmd.clone();
784 assert_eq!(cloned_cmd.name(), cmd.name());
785 }
786
787 #[test]
788 fn test_args_debug_and_clone() {
789 let upload_args = UploadFileArgs {
790 name: "test.txt".to_string(),
791 content_base64: "dGVzdA==".to_string(),
792 caption: None,
793 reply_msg_id: None,
794 chat_id: None,
795 };
796
797 let debug_str = format!("{upload_args:?}");
799 assert!(debug_str.contains("test.txt"));
800
801 let cloned_args = upload_args.clone();
802 assert_eq!(cloned_args.name, upload_args.name);
803 }
804}