Skip to main content

vk_bot_api/
api.rs

1use crate::error::{VkError, VkResponseExt, VkResult};
2use crate::keyboard::Keyboard;
3use serde::Deserialize;
4use serde_json::Value;
5use std::collections::HashMap;
6use std::time::Duration;
7use tokio::time::sleep;
8
9#[cfg(feature = "reqwest")]
10use reqwest::Client;
11
12#[cfg(feature = "uuid")]
13use uuid::Uuid;
14
15#[derive(Debug, Clone)]
16pub struct VkApiConfig {
17    pub version: String,
18    pub timeout: u64,
19    pub max_retries: u32,
20    pub retry_delay: u64,
21    pub enable_logging: bool,
22    pub endpoint: String,
23}
24
25impl Default for VkApiConfig {
26    fn default() -> Self {
27        Self {
28            version: "5.199".to_string(),
29            timeout: 30,
30            max_retries: 3,
31            retry_delay: 1000,
32            enable_logging: false,
33            endpoint: "https://api.vk.com/method/".to_string(),
34        }
35    }
36}
37
38#[derive(Debug, Default)]
39pub struct VkApiBuilder {
40    token: Option<String>,
41    config: VkApiConfig,
42}
43
44impl VkApiBuilder {
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    pub fn token<T: Into<String>>(mut self, token: T) -> Self {
50        self.token = Some(token.into());
51        self
52    }
53
54    pub fn version<T: Into<String>>(mut self, version: T) -> Self {
55        self.config.version = version.into();
56        self
57    }
58
59    pub fn timeout(mut self, timeout: u64) -> Self {
60        self.config.timeout = timeout;
61        self
62    }
63
64    pub fn max_retries(mut self, max_retries: u32) -> Self {
65        self.config.max_retries = max_retries;
66        self
67    }
68
69    pub fn retry_delay(mut self, retry_delay: u64) -> Self {
70        self.config.retry_delay = retry_delay;
71        self
72    }
73
74    pub fn enable_logging(mut self, enable: bool) -> Self {
75        self.config.enable_logging = enable;
76        self
77    }
78
79    pub fn endpoint<T: Into<String>>(mut self, endpoint: T) -> Self {
80        self.config.endpoint = endpoint.into();
81        self
82    }
83
84    pub fn build(self) -> VkResult<VkApi> {
85        let token = self
86            .token
87            .ok_or_else(|| VkError::ConfigError("Token is required".to_string()))?;
88
89        #[cfg(feature = "reqwest")]
90        let client = Client::builder()
91            .timeout(Duration::from_secs(self.config.timeout))
92            .pool_max_idle_per_host(10)
93            .tcp_keepalive(Duration::from_secs(60))
94            .build()
95            .map_err(|e| VkError::NetworkError(e.to_string()))?;
96
97        Ok(VkApi {
98            #[cfg(feature = "reqwest")]
99            client,
100            token,
101            config: self.config,
102        })
103    }
104}
105
106#[derive(Debug, Clone)]
107pub struct VkApi {
108    #[cfg(feature = "reqwest")]
109    client: Client,
110    token: String,
111    config: VkApiConfig,
112}
113
114impl VkApi {
115    pub fn new<T: Into<String>>(token: T) -> VkResult<Self> {
116        VkApiBuilder::new().token(token).build()
117    }
118
119    pub fn builder() -> VkApiBuilder {
120        VkApiBuilder::new()
121    }
122
123    pub fn token(&self) -> &str {
124        &self.token
125    }
126
127    pub fn version(&self) -> &str {
128        &self.config.version
129    }
130
131    #[cfg(feature = "reqwest")]
132    async fn call_method_with_retry(
133        &self,
134        method: &str,
135        mut params: HashMap<String, String>,
136    ) -> VkResult<Value> {
137        let mut retries = 0;
138
139        params.insert("access_token".to_string(), self.token.clone());
140        params.insert("v".to_string(), self.config.version.clone());
141
142        loop {
143            match self.call_method_once(method, &params).await {
144                Ok(response) => return Ok(response),
145                Err(e) => {
146                    retries += 1;
147
148                    if retries >= self.config.max_retries || !self.should_retry(&e) {
149                        return Err(e);
150                    }
151
152                    let delay_ms = self.config.retry_delay * 2u64.pow(retries - 1);
153                    sleep(Duration::from_millis(delay_ms)).await;
154                }
155            }
156        }
157    }
158
159    #[cfg(feature = "reqwest")]
160    fn should_retry(&self, error: &VkError) -> bool {
161        match error {
162            VkError::RateLimit => true,
163            VkError::NetworkError(_) => true,
164            VkError::Timeout(_) => true,
165            VkError::HttpError(e) => e
166                .status()
167                .map(|status| status.is_server_error() || status.as_u16() == 429)
168                .unwrap_or(false),
169            VkError::ApiError { code, .. } => {
170                matches!(*code, 6 | 9 | 10 | 14)
171            }
172            _ => false,
173        }
174    }
175
176    #[cfg(feature = "reqwest")]
177    async fn call_method_once(
178        &self,
179        method: &str,
180        params: &HashMap<String, String>,
181    ) -> VkResult<Value> {
182        let url = format!("{}{}", self.config.endpoint, method);
183
184        if self.config.enable_logging {
185            crate::vk_log!("API call: {} with {} params", method, params.len());
186        }
187
188        let response = self
189            .client
190            .post(&url)
191            .form(params)
192            .send()
193            .await
194            .map_err(|e| {
195                if e.is_timeout() {
196                    VkError::Timeout(e.to_string())
197                } else {
198                    VkError::HttpError(e)
199                }
200            })?;
201
202        self.handle_response(response).await
203    }
204
205    #[cfg(feature = "reqwest")]
206    async fn handle_response(&self, response: reqwest::Response) -> VkResult<Value> {
207        let status = response.status();
208
209        if status == 429 {
210            return Err(VkError::RateLimit);
211        }
212
213        if !status.is_success() {
214            return Err(VkError::HttpError(
215                response.error_for_status().err().unwrap(),
216            ));
217        }
218
219        let json_response: Value = response.json().await?;
220
221        if VkResponseExt::has_error(&json_response) {
222            return VkResponseExt::extract_error(json_response);
223        }
224
225        Ok(json_response)
226    }
227
228    #[cfg(feature = "reqwest")]
229    pub async fn call_method(
230        &self,
231        method: &str,
232        params: HashMap<String, String>,
233    ) -> VkResult<Value> {
234        self.call_method_with_retry(method, params).await
235    }
236
237    #[cfg(not(feature = "reqwest"))]
238    pub async fn call_method(
239        &self,
240        _method: &str,
241        _params: HashMap<String, String>,
242    ) -> VkResult<Value> {
243        Err(VkError::ConfigError(
244            "reqwest feature is required for API calls".to_string(),
245        ))
246    }
247
248    #[allow(clippy::too_many_arguments)]
249    pub async fn messages_send(
250        &self,
251        peer_id: i64,
252        message: &str,
253        keyboard: Option<&Keyboard>,
254        attachment: Option<&str>,
255        sticker_id: Option<i64>,
256        reply_to: Option<i64>,
257        forward_messages: Option<&[i64]>,
258        disable_mentions: bool,
259        dont_parse_links: bool,
260        random_id: Option<i64>,
261    ) -> VkResult<i64> {
262        let mut params = HashMap::new();
263        params.insert("peer_id".to_string(), peer_id.to_string());
264        params.insert("message".to_string(), message.to_string());
265
266        #[cfg(feature = "uuid")]
267        let random_id = random_id.unwrap_or_else(|| {
268            let uuid = Uuid::new_v4();
269            (uuid.as_u128() & 0x7FFFFFFFFFFFFFFF) as i64
270        });
271
272        #[cfg(not(feature = "uuid"))]
273        let random_id = random_id.unwrap_or(0);
274
275        params.insert("random_id".to_string(), random_id.to_string());
276
277        if let Some(keyboard) = keyboard {
278            params.insert("keyboard".to_string(), keyboard.to_json_string());
279        }
280
281        if let Some(attachment) = attachment {
282            params.insert("attachment".to_string(), attachment.to_string());
283        }
284
285        if let Some(sticker_id) = sticker_id {
286            params.insert("sticker_id".to_string(), sticker_id.to_string());
287        }
288
289        if let Some(reply_to) = reply_to {
290            params.insert("reply_to".to_string(), reply_to.to_string());
291        }
292
293        if let Some(forward_messages) = forward_messages {
294            let ids: Vec<String> = forward_messages.iter().map(|id| id.to_string()).collect();
295            params.insert("forward_messages".to_string(), ids.join(","));
296        }
297
298        if disable_mentions {
299            params.insert("disable_mentions".to_string(), "1".to_string());
300        }
301
302        if dont_parse_links {
303            params.insert("dont_parse_links".to_string(), "1".to_string());
304        }
305
306        let response = self.call_method("messages.send", params).await?;
307        let message_id = response["response"].as_i64().ok_or_else(|| {
308            VkError::InvalidResponse("Expected message_id in response".to_string())
309        })?;
310
311        Ok(message_id)
312    }
313
314    #[allow(clippy::too_many_arguments)]
315    pub async fn messages_edit(
316        &self,
317        peer_id: i64,
318        message_id: i64,
319        message: &str,
320        keyboard: Option<&Keyboard>,
321        attachment: Option<&str>,
322        keep_forward_messages: bool,
323        keep_snippets: bool,
324    ) -> VkResult<bool> {
325        let mut params = HashMap::new();
326        params.insert("peer_id".to_string(), peer_id.to_string());
327        params.insert("message_id".to_string(), message_id.to_string());
328        params.insert("message".to_string(), message.to_string());
329
330        if let Some(keyboard) = keyboard {
331            params.insert("keyboard".to_string(), keyboard.to_json_string());
332        }
333
334        if let Some(attachment) = attachment {
335            params.insert("attachment".to_string(), attachment.to_string());
336        }
337
338        if keep_forward_messages {
339            params.insert("keep_forward_messages".to_string(), "1".to_string());
340        }
341
342        if keep_snippets {
343            params.insert("keep_snippets".to_string(), "1".to_string());
344        }
345
346        let response = self.call_method("messages.edit", params).await?;
347        Ok(response["response"].as_i64().unwrap_or(0) == 1)
348    }
349
350    pub async fn messages_delete(
351        &self,
352        message_ids: &[i64],
353        delete_for_all: bool,
354        spam: bool,
355    ) -> VkResult<serde_json::Map<String, Value>> {
356        let mut params = HashMap::new();
357        let ids: Vec<String> = message_ids.iter().map(|id| id.to_string()).collect();
358        params.insert("message_ids".to_string(), ids.join(","));
359
360        if delete_for_all {
361            params.insert("delete_for_all".to_string(), "1".to_string());
362        }
363
364        if spam {
365            params.insert("spam".to_string(), "1".to_string());
366        }
367
368        let response = self.call_method("messages.delete", params).await?;
369        Ok(response["response"]
370            .as_object()
371            .ok_or_else(|| VkError::InvalidResponse("Expected object in response".to_string()))?
372            .clone())
373    }
374
375    pub async fn messages_restore(&self, message_id: i64) -> VkResult<bool> {
376        let mut params = HashMap::new();
377        params.insert("message_id".to_string(), message_id.to_string());
378
379        let response = self.call_method("messages.restore", params).await?;
380        Ok(response["response"].as_i64().unwrap_or(0) == 1)
381    }
382
383    pub async fn messages_mark_as_read(
384        &self,
385        peer_id: i64,
386        start_message_id: Option<i64>,
387    ) -> VkResult<bool> {
388        let mut params = HashMap::new();
389        params.insert("peer_id".to_string(), peer_id.to_string());
390
391        if let Some(start_id) = start_message_id {
392            params.insert("start_message_id".to_string(), start_id.to_string());
393        }
394
395        let response = self.call_method("messages.markAsRead", params).await?;
396        Ok(response["response"].as_i64().unwrap_or(0) == 1)
397    }
398
399    pub async fn messages_mark_as_important(
400        &self,
401        message_ids: &[i64],
402        important: Option<i32>,
403    ) -> VkResult<Vec<i64>> {
404        let mut params = HashMap::new();
405        let ids: Vec<String> = message_ids.iter().map(|id| id.to_string()).collect();
406        params.insert("message_ids".to_string(), ids.join(","));
407
408        if let Some(important) = important {
409            params.insert("important".to_string(), important.to_string());
410        }
411
412        let response = self.call_method("messages.markAsImportant", params).await?;
413        let marked_ids: Vec<i64> = response["response"]
414            .as_array()
415            .ok_or_else(|| VkError::InvalidResponse("Expected array in response".to_string()))?
416            .iter()
417            .filter_map(|v| v.as_i64())
418            .collect();
419
420        Ok(marked_ids)
421    }
422
423    pub async fn messages_get_conversations(
424        &self,
425        offset: i32,
426        count: i32,
427        filter: Option<&str>,
428    ) -> VkResult<Value> {
429        let mut params = HashMap::new();
430        params.insert("offset".to_string(), offset.to_string());
431        params.insert("count".to_string(), count.to_string());
432
433        if let Some(filter) = filter {
434            params.insert("filter".to_string(), filter.to_string());
435        }
436
437        self.call_method("messages.getConversations", params).await
438    }
439
440    pub async fn messages_get_conversation_members(
441        &self,
442        peer_id: i64,
443        fields: Option<&str>,
444    ) -> VkResult<Value> {
445        let mut params = HashMap::new();
446        params.insert("peer_id".to_string(), peer_id.to_string());
447
448        if let Some(fields) = fields {
449            params.insert("fields".to_string(), fields.to_string());
450        }
451
452        self.call_method("messages.getConversationMembers", params)
453            .await
454    }
455
456    pub async fn messages_get_history(
457        &self,
458        peer_id: i64,
459        offset: i32,
460        count: i32,
461        start_message_id: Option<i64>,
462        rev: bool,
463    ) -> VkResult<Value> {
464        let mut params = HashMap::new();
465        params.insert("peer_id".to_string(), peer_id.to_string());
466        params.insert("offset".to_string(), offset.to_string());
467        params.insert("count".to_string(), count.to_string());
468
469        if let Some(start_id) = start_message_id {
470            params.insert("start_message_id".to_string(), start_id.to_string());
471        }
472
473        if rev {
474            params.insert("rev".to_string(), "1".to_string());
475        }
476
477        self.call_method("messages.getHistory", params).await
478    }
479
480    pub async fn messages_get_by_id(
481        &self,
482        message_ids: &[i64],
483        preview_length: i32,
484        extended: bool,
485    ) -> VkResult<Value> {
486        let mut params = HashMap::new();
487        let ids: Vec<String> = message_ids.iter().map(|id| id.to_string()).collect();
488        params.insert("message_ids".to_string(), ids.join(","));
489        params.insert("preview_length".to_string(), preview_length.to_string());
490
491        if extended {
492            params.insert("extended".to_string(), "1".to_string());
493        }
494
495        self.call_method("messages.getById", params).await
496    }
497
498    pub async fn messages_search(
499        &self,
500        query: &str,
501        peer_id: Option<i64>,
502        date: Option<i64>,
503        count: i32,
504    ) -> VkResult<Value> {
505        let mut params = HashMap::new();
506        params.insert("q".to_string(), query.to_string());
507        params.insert("count".to_string(), count.to_string());
508
509        if let Some(peer_id) = peer_id {
510            params.insert("peer_id".to_string(), peer_id.to_string());
511        }
512
513        if let Some(date) = date {
514            params.insert("date".to_string(), date.to_string());
515        }
516
517        self.call_method("messages.search", params).await
518    }
519
520    pub async fn messages_search_conversations(
521        &self,
522        query: &str,
523        count: i32,
524        extended: bool,
525        fields: Option<&str>,
526    ) -> VkResult<Value> {
527        let mut params = HashMap::new();
528        params.insert("q".to_string(), query.to_string());
529        params.insert("count".to_string(), count.to_string());
530
531        if extended {
532            params.insert("extended".to_string(), "1".to_string());
533        }
534
535        if let Some(fields) = fields {
536            params.insert("fields".to_string(), fields.to_string());
537        }
538
539        self.call_method("messages.searchConversations", params)
540            .await
541    }
542
543    pub async fn messages_get_attachments(
544        &self,
545        peer_id: i64,
546        media_type: &str,
547        start_from: Option<&str>,
548        count: i32,
549    ) -> VkResult<Value> {
550        let mut params = HashMap::new();
551        params.insert("peer_id".to_string(), peer_id.to_string());
552        params.insert("media_type".to_string(), media_type.to_string());
553        params.insert("count".to_string(), count.to_string());
554
555        if let Some(start_from) = start_from {
556            params.insert("start_from".to_string(), start_from.to_string());
557        }
558
559        self.call_method("messages.getAttachments", params).await
560    }
561
562    pub async fn messages_get_invite_link(&self, peer_id: i64, reset: bool) -> VkResult<String> {
563        let mut params = HashMap::new();
564        params.insert("peer_id".to_string(), peer_id.to_string());
565
566        if reset {
567            params.insert("reset".to_string(), "1".to_string());
568        }
569
570        let response = self.call_method("messages.getInviteLink", params).await?;
571        let link = response["response"]["link"]
572            .as_str()
573            .ok_or_else(|| VkError::MissingField("link".to_string()))?
574            .to_string();
575
576        Ok(link)
577    }
578
579    pub async fn messages_remove_chat_user(
580        &self,
581        chat_id: i64,
582        user_id: i64,
583        member_id: Option<i64>,
584    ) -> VkResult<bool> {
585        let mut params = HashMap::new();
586        params.insert("chat_id".to_string(), chat_id.to_string());
587        params.insert("user_id".to_string(), user_id.to_string());
588
589        if let Some(member_id) = member_id {
590            params.insert("member_id".to_string(), member_id.to_string());
591        }
592
593        let response = self.call_method("messages.removeChatUser", params).await?;
594        Ok(response["response"].as_i64().unwrap_or(0) == 1)
595    }
596
597    pub async fn messages_add_chat_user(&self, chat_id: i64, user_id: i64) -> VkResult<bool> {
598        let mut params = HashMap::new();
599        params.insert("chat_id".to_string(), chat_id.to_string());
600        params.insert("user_id".to_string(), user_id.to_string());
601
602        let response = self.call_method("messages.addChatUser", params).await?;
603        Ok(response["response"].as_i64().unwrap_or(0) == 1)
604    }
605
606    pub async fn messages_create_chat(
607        &self,
608        user_ids: &[i64],
609        title: Option<&str>,
610    ) -> VkResult<i64> {
611        let mut params = HashMap::new();
612        let ids: Vec<String> = user_ids.iter().map(|id| id.to_string()).collect();
613        params.insert("user_ids".to_string(), ids.join(","));
614
615        if let Some(title) = title {
616            params.insert("title".to_string(), title.to_string());
617        }
618
619        let response = self.call_method("messages.createChat", params).await?;
620        let chat_id = response["response"]
621            .as_i64()
622            .ok_or_else(|| VkError::InvalidResponse("Expected chat_id in response".to_string()))?;
623
624        Ok(chat_id)
625    }
626
627    pub async fn messages_set_activity(
628        &self,
629        peer_id: i64,
630        user_id: Option<i64>,
631        activity_type: &str,
632    ) -> VkResult<bool> {
633        let mut params = HashMap::new();
634        params.insert("peer_id".to_string(), peer_id.to_string());
635        params.insert("type".to_string(), activity_type.to_string());
636
637        if let Some(user_id) = user_id {
638            params.insert("user_id".to_string(), user_id.to_string());
639        }
640
641        let response = self.call_method("messages.setActivity", params).await?;
642        Ok(response["response"].as_i64().unwrap_or(0) == 1)
643    }
644
645    pub async fn messages_send_message_event_answer(
646        &self,
647        event_id: &str,
648        user_id: i64,
649        peer_id: i64,
650        event_data: Option<&str>,
651    ) -> VkResult<bool> {
652        let mut params = HashMap::new();
653        params.insert("event_id".to_string(), event_id.to_string());
654        params.insert("user_id".to_string(), user_id.to_string());
655        params.insert("peer_id".to_string(), peer_id.to_string());
656
657        if let Some(event_data) = event_data {
658            params.insert("event_data".to_string(), event_data.to_string());
659        }
660
661        let response = self
662            .call_method("messages.sendMessageEventAnswer", params)
663            .await?;
664        Ok(response["response"].as_i64().unwrap_or(0) == 1)
665    }
666
667    pub async fn users_get(
668        &self,
669        user_ids: &[i64],
670        fields: Option<&str>,
671        name_case: Option<&str>,
672    ) -> VkResult<Value> {
673        let mut params = HashMap::new();
674        let ids: Vec<String> = user_ids.iter().map(|id| id.to_string()).collect();
675        params.insert("user_ids".to_string(), ids.join(","));
676
677        if let Some(fields) = fields {
678            params.insert("fields".to_string(), fields.to_string());
679        }
680
681        if let Some(name_case) = name_case {
682            params.insert("name_case".to_string(), name_case.to_string());
683        }
684
685        self.call_method("users.get", params).await
686    }
687
688    pub async fn groups_get_long_poll_server(&self, group_id: i64) -> VkResult<LongPollServer> {
689        let mut params = HashMap::new();
690        params.insert("group_id".to_string(), group_id.to_string());
691
692        let response = self.call_method("groups.getLongPollServer", params).await?;
693
694        let server = response["response"]["server"]
695            .as_str()
696            .ok_or_else(|| VkError::MissingField("server".to_string()))?
697            .to_string();
698
699        let key = response["response"]["key"]
700            .as_str()
701            .ok_or_else(|| VkError::MissingField("key".to_string()))?
702            .to_string();
703
704        let ts = response["response"]["ts"]
705            .as_str()
706            .ok_or_else(|| VkError::MissingField("ts".to_string()))?
707            .to_string();
708
709        Ok(LongPollServer { server, key, ts })
710    }
711
712    pub async fn groups_get_by_id(
713        &self,
714        group_ids: &[i64],
715        fields: Option<&str>,
716    ) -> VkResult<Value> {
717        let mut params = HashMap::new();
718        let ids: Vec<String> = group_ids.iter().map(|id| id.to_string()).collect();
719        params.insert("group_ids".to_string(), ids.join(","));
720
721        if let Some(fields) = fields {
722            params.insert("fields".to_string(), fields.to_string());
723        }
724
725        self.call_method("groups.getById", params).await
726    }
727
728    pub async fn send_message(&self, peer_id: i64, message: &str) -> VkResult<i64> {
729        self.messages_send(
730            peer_id, message, None, None, None, None, None, false, false, None,
731        )
732        .await
733    }
734
735    pub async fn send_message_with_keyboard(
736        &self,
737        peer_id: i64,
738        message: &str,
739        keyboard: &Keyboard,
740    ) -> VkResult<i64> {
741        self.messages_send(
742            peer_id,
743            message,
744            Some(keyboard),
745            None,
746            None,
747            None,
748            None,
749            false,
750            false,
751            None,
752        )
753        .await
754    }
755
756    pub async fn send_reply(&self, peer_id: i64, message: &str, reply_to: i64) -> VkResult<i64> {
757        self.messages_send(
758            peer_id,
759            message,
760            None,
761            None,
762            None,
763            Some(reply_to),
764            None,
765            false,
766            false,
767            None,
768        )
769        .await
770    }
771
772    // ==================== MEDIA UPLOAD METHODS ====================
773
774    /// Get upload server for photos
775    pub async fn photos_get_messages_upload_server(&self, peer_id: i64) -> VkResult<UploadServer> {
776        let mut params = HashMap::new();
777        params.insert("peer_id".to_string(), peer_id.to_string());
778
779        let response = self
780            .call_method("photos.getMessagesUploadServer", params)
781            .await?;
782
783        let upload_url = response["response"]["upload_url"]
784            .as_str()
785            .ok_or_else(|| VkError::MissingField("upload_url".to_string()))?
786            .to_string();
787
788        Ok(UploadServer { upload_url })
789    }
790
791    /// Save uploaded photo
792    pub async fn photos_save_messages_photo(
793        &self,
794        photo: &str,
795        server: i64,
796        hash: &str,
797    ) -> VkResult<Vec<SavedPhoto>> {
798        let mut params = HashMap::new();
799        params.insert("photo".to_string(), photo.to_string());
800        params.insert("server".to_string(), server.to_string());
801        params.insert("hash".to_string(), hash.to_string());
802
803        let response = self.call_method("photos.saveMessagesPhoto", params).await?;
804
805        let photos: Vec<SavedPhoto> =
806            serde_json::from_value(response["response"].clone()).map_err(VkError::JsonError)?;
807
808        Ok(photos)
809    }
810
811    /// Get upload server for documents
812    pub async fn docs_get_upload_server(
813        &self,
814        peer_id: Option<i64>,
815        doc_type: Option<&str>,
816    ) -> VkResult<UploadServer> {
817        let mut params = HashMap::new();
818
819        if let Some(peer_id) = peer_id {
820            params.insert("peer_id".to_string(), peer_id.to_string());
821        }
822
823        if let Some(doc_type) = doc_type {
824            params.insert("type".to_string(), doc_type.to_string());
825        }
826
827        let response = self.call_method("docs.getUploadServer", params).await?;
828
829        let upload_url = response["response"]["upload_url"]
830            .as_str()
831            .ok_or_else(|| VkError::MissingField("upload_url".to_string()))?
832            .to_string();
833
834        Ok(UploadServer { upload_url })
835    }
836
837    /// Get upload server for documents (for community messages)
838    pub async fn docs_get_messages_upload_server(
839        &self,
840        peer_id: i64,
841        doc_type: Option<&str>,
842    ) -> VkResult<UploadServer> {
843        let mut params = HashMap::new();
844        params.insert("peer_id".to_string(), peer_id.to_string());
845
846        if let Some(doc_type) = doc_type {
847            params.insert("type".to_string(), doc_type.to_string());
848        }
849
850        let response = self
851            .call_method("docs.getMessagesUploadServer", params)
852            .await?;
853
854        let upload_url = response["response"]["upload_url"]
855            .as_str()
856            .ok_or_else(|| VkError::MissingField("upload_url".to_string()))?
857            .to_string();
858
859        Ok(UploadServer { upload_url })
860    }
861
862    /// Save uploaded document
863    pub async fn docs_save(
864        &self,
865        file: &str,
866        title: Option<&str>,
867        tags: Option<&str>,
868    ) -> VkResult<SavedDocument> {
869        let mut params = HashMap::new();
870        params.insert("file".to_string(), file.to_string());
871
872        if let Some(title) = title {
873            params.insert("title".to_string(), title.to_string());
874        }
875
876        if let Some(tags) = tags {
877            params.insert("tags".to_string(), tags.to_string());
878        }
879
880        let response = self.call_method("docs.save", params).await?;
881
882        crate::vk_log!("docs.save response: {}", response);
883
884        let doc_response: DocSaveResponse =
885            serde_json::from_value(response["response"].clone()).map_err(VkError::JsonError)?;
886
887        crate::vk_log!(
888            "Parsed doc response: type={:?}, has_doc={}, has_audio_msg={}",
889            doc_response.doc_type,
890            doc_response.doc.is_some(),
891            doc_response.audio_message.is_some()
892        );
893
894        Ok(SavedDocument {
895            doc: doc_response.doc,
896            audio_message: doc_response.audio_message,
897        })
898    }
899
900    // ==================== UPLOAD HELPERS ====================
901
902    /// Upload file to VK server
903    #[cfg(feature = "reqwest")]
904    pub async fn upload_file(
905        &self,
906        upload_url: &str,
907        file_data: Vec<u8>,
908        filename: &str,
909    ) -> VkResult<UploadResponse> {
910        // Detect content type from filename extension
911        let content_type = Self::detect_content_type(filename);
912
913        let mut part = reqwest::multipart::Part::bytes(file_data).file_name(filename.to_string());
914
915        if let Some(ct) = content_type {
916            part = part
917                .mime_str(&ct)
918                .map_err(|e| VkError::InternalError(format!("Invalid MIME type: {}", e)))?;
919        }
920
921        let form = reqwest::multipart::Form::new().part("file", part);
922
923        crate::vk_log!("Uploading file '{}' to {}", filename, upload_url);
924
925        let response = self
926            .client
927            .post(upload_url)
928            .multipart(form)
929            .send()
930            .await
931            .map_err(|e| VkError::NetworkError(format!("Upload request failed: {}", e)))?;
932
933        let status = response.status();
934
935        // Get raw response text
936        let response_text = response
937            .text()
938            .await
939            .map_err(|e| VkError::NetworkError(format!("Failed to read response: {}", e)))?;
940
941        crate::vk_log!("Upload response (status: {}): {}", status, response_text);
942
943        if !status.is_success() {
944            return Err(VkError::NetworkError(format!(
945                "Upload failed with status {}: {}",
946                status, response_text
947            )));
948        }
949
950        // Parse the JSON response
951        let upload_response: UploadResponse =
952            serde_json::from_str(&response_text).map_err(|e| {
953                VkError::InvalidResponse(format!(
954                    "Failed to parse upload response: {}. Raw response: {}",
955                    e, response_text
956                ))
957            })?;
958
959        Ok(upload_response)
960    }
961
962    /// Detect MIME type from filename extension
963    fn detect_content_type(filename: &str) -> Option<String> {
964        let ext = filename.split('.').next_back()?.to_lowercase();
965        match ext.as_str() {
966            "jpg" | "jpeg" => Some("image/jpeg".to_string()),
967            "png" => Some("image/png".to_string()),
968            "gif" => Some("image/gif".to_string()),
969            "pdf" => Some("application/pdf".to_string()),
970            "txt" => Some("text/plain".to_string()),
971            "ogg" => Some("audio/ogg".to_string()),
972            "mp3" => Some("audio/mpeg".to_string()),
973            "mp4" => Some("video/mp4".to_string()),
974            _ => None,
975        }
976    }
977
978    /// Upload and send photo
979    pub async fn send_photo(
980        &self,
981        peer_id: i64,
982        photo_data: Vec<u8>,
983        filename: &str,
984        caption: Option<&str>,
985    ) -> VkResult<i64> {
986        // Step 1: Get upload server
987        let upload_server = self.photos_get_messages_upload_server(peer_id).await?;
988
989        // Step 2: Upload photo
990        let upload_response = self
991            .upload_file(&upload_server.upload_url, photo_data, filename)
992            .await?;
993
994        // Validate upload response for photos
995        if upload_response.server == 0
996            || upload_response.photo.is_empty()
997            || upload_response.hash.is_empty()
998        {
999            return Err(VkError::InvalidResponse(
1000                "Upload response missing photo data. Server, photo, or hash is empty".to_string(),
1001            ));
1002        }
1003
1004        crate::vk_log!(
1005            "Photo upload successful: server={}, photo length={}",
1006            upload_response.server,
1007            upload_response.photo.len()
1008        );
1009
1010        // Step 3: Save photo
1011        let saved_photos = self
1012            .photos_save_messages_photo(
1013                &upload_response.photo,
1014                upload_response.server,
1015                &upload_response.hash,
1016            )
1017            .await?;
1018
1019        // Step 4: Create attachment string
1020        let attachment = saved_photos
1021            .first()
1022            .map(|p| format!("photo{}_{}", p.owner_id, p.id))
1023            .ok_or_else(|| VkError::InvalidResponse("No photo saved".to_string()))?;
1024
1025        // Step 5: Send message with photo
1026        let message = caption.unwrap_or("");
1027        self.messages_send(
1028            peer_id,
1029            message,
1030            None,
1031            Some(&attachment),
1032            None,
1033            None,
1034            None,
1035            false,
1036            false,
1037            None,
1038        )
1039        .await
1040    }
1041
1042    /// Upload and send document
1043    pub async fn send_document(
1044        &self,
1045        peer_id: i64,
1046        file_data: Vec<u8>,
1047        filename: &str,
1048        title: Option<&str>,
1049        caption: Option<&str>,
1050    ) -> VkResult<i64> {
1051        // Step 1: Get upload server
1052        let upload_server = self.docs_get_messages_upload_server(peer_id, None).await?;
1053
1054        // Step 2: Upload file
1055        let upload_response = self
1056            .upload_file(&upload_server.upload_url, file_data, filename)
1057            .await?;
1058
1059        // Validate upload response
1060        if upload_response.file.is_empty() {
1061            return Err(VkError::InvalidResponse(
1062                "Upload response missing file data".to_string(),
1063            ));
1064        }
1065
1066        crate::vk_log!(
1067            "Document upload successful: file field length={}",
1068            upload_response.file.len()
1069        );
1070
1071        // Step 3: Save document
1072        let saved_doc = self
1073            .docs_save(&upload_response.file, title.or(Some(filename)), None)
1074            .await?;
1075
1076        // Step 4: Create attachment string
1077        let doc = saved_doc
1078            .doc
1079            .ok_or_else(|| VkError::InvalidResponse("No document saved".to_string()))?;
1080        let attachment = format!("doc{}_{}", doc.owner_id, doc.id);
1081
1082        // Step 5: Send message with document
1083        let message = caption.unwrap_or("");
1084        self.messages_send(
1085            peer_id,
1086            message,
1087            None,
1088            Some(&attachment),
1089            None,
1090            None,
1091            None,
1092            false,
1093            false,
1094            None,
1095        )
1096        .await
1097    }
1098
1099    /// Upload and send audio message (voice message)
1100    pub async fn send_voice_message(
1101        &self,
1102        peer_id: i64,
1103        audio_data: Vec<u8>,
1104        filename: &str,
1105    ) -> VkResult<i64> {
1106        // Step 1: Get upload server for audio messages
1107        let upload_server = self
1108            .docs_get_messages_upload_server(peer_id, Some("audio_message"))
1109            .await?;
1110
1111        // Step 2: Upload file
1112        let upload_response = self
1113            .upload_file(&upload_server.upload_url, audio_data, filename)
1114            .await?;
1115
1116        // Validate upload response
1117        if upload_response.file.is_empty() {
1118            return Err(VkError::InvalidResponse(
1119                "Upload response missing file data".to_string(),
1120            ));
1121        }
1122
1123        crate::vk_log!(
1124            "Voice message upload successful: file field length={}",
1125            upload_response.file.len()
1126        );
1127
1128        // Step 3: Save document
1129        let saved_doc = self
1130            .docs_save(&upload_response.file, Some(filename), None)
1131            .await?;
1132
1133        // Step 4: Check if audio_message is present
1134        if saved_doc.audio_message.is_none() {
1135            return Err(VkError::InvalidResponse(
1136                "Audio message was not processed".to_string(),
1137            ));
1138        }
1139
1140        // Step 5: Create attachment string from doc
1141        let doc = saved_doc
1142            .doc
1143            .ok_or_else(|| VkError::InvalidResponse("No document saved".to_string()))?;
1144        let attachment = format!("doc{}_{}", doc.owner_id, doc.id);
1145
1146        // Step 6: Send message with audio
1147        self.messages_send(
1148            peer_id,
1149            "",
1150            None,
1151            Some(&attachment),
1152            None,
1153            None,
1154            None,
1155            false,
1156            false,
1157            None,
1158        )
1159        .await
1160    }
1161
1162    // ==================== DOWNLOAD METHODS ====================
1163
1164    /// Download file from URL
1165    #[cfg(feature = "reqwest")]
1166    pub async fn download_file(&self, url: &str) -> VkResult<DownloadedFile> {
1167        let response = self
1168            .client
1169            .get(url)
1170            .send()
1171            .await
1172            .map_err(|e| VkError::NetworkError(e.to_string()))?;
1173
1174        if !response.status().is_success() {
1175            return Err(VkError::NetworkError(format!(
1176                "Download failed: {}",
1177                response.status()
1178            )));
1179        }
1180
1181        let content_type = response
1182            .headers()
1183            .get("content-type")
1184            .and_then(|v| v.to_str().ok())
1185            .map(|s| s.to_string());
1186
1187        let data = response
1188            .bytes()
1189            .await
1190            .map_err(|e| VkError::NetworkError(e.to_string()))?;
1191
1192        Ok(DownloadedFile {
1193            data: data.to_vec(),
1194            content_type,
1195        })
1196    }
1197
1198    /// Download photo by attachment
1199    pub async fn download_photo(&self, photo: &crate::models::Photo) -> VkResult<DownloadedFile> {
1200        // Get the largest available size
1201        let size = photo
1202            .sizes
1203            .iter()
1204            .max_by_key(|s| s.width * s.height)
1205            .ok_or_else(|| VkError::InvalidResponse("Photo has no sizes".to_string()))?;
1206
1207        self.download_file(&size.url).await
1208    }
1209
1210    /// Download document
1211    pub async fn download_document(
1212        &self,
1213        doc: &crate::models::Document,
1214    ) -> VkResult<DownloadedFile> {
1215        self.download_file(&doc.url).await
1216    }
1217
1218    /// Download audio
1219    pub async fn download_audio(&self, audio: &crate::models::Audio) -> VkResult<DownloadedFile> {
1220        let url = audio
1221            .url
1222            .as_ref()
1223            .ok_or_else(|| VkError::InvalidResponse("Audio has no URL".to_string()))?;
1224
1225        self.download_file(url).await
1226    }
1227
1228    /// Download video thumbnail
1229    pub async fn download_video_thumbnail(
1230        &self,
1231        video: &crate::models::Video,
1232    ) -> VkResult<DownloadedFile> {
1233        let image = video
1234            .image
1235            .first()
1236            .ok_or_else(|| VkError::InvalidResponse("Video has no thumbnails".to_string()))?;
1237
1238        self.download_file(&image.url).await
1239    }
1240
1241    /// Download sticker
1242    pub async fn download_sticker(
1243        &self,
1244        sticker: &crate::models::Sticker,
1245    ) -> VkResult<DownloadedFile> {
1246        let image = sticker
1247            .images
1248            .first()
1249            .ok_or_else(|| VkError::InvalidResponse("Sticker has no images".to_string()))?;
1250
1251        self.download_file(&image.url).await
1252    }
1253
1254    /// Download audio message
1255    pub async fn download_audio_message(
1256        &self,
1257        audio_msg: &crate::models::AudioMessage,
1258    ) -> VkResult<DownloadedFile> {
1259        // Prefer MP3 over OGG for better compatibility
1260        self.download_file(&audio_msg.link_mp3).await
1261    }
1262
1263    /// Forward downloaded file back to another peer
1264    pub async fn forward_attachment<A: AsRef<str>>(
1265        &self,
1266        peer_id: i64,
1267        attachment: A,
1268        caption: Option<&str>,
1269    ) -> VkResult<i64> {
1270        self.messages_send(
1271            peer_id,
1272            caption.unwrap_or(""),
1273            None,
1274            Some(attachment.as_ref()),
1275            None,
1276            None,
1277            None,
1278            false,
1279            false,
1280            None,
1281        )
1282        .await
1283    }
1284}
1285
1286/// Long Poll server information
1287#[derive(Debug, Clone)]
1288pub struct LongPollServer {
1289    pub server: String,
1290    pub key: String,
1291    pub ts: String,
1292}
1293
1294/// Upload server information
1295#[derive(Debug, Clone)]
1296pub struct UploadServer {
1297    pub upload_url: String,
1298}
1299
1300/// Upload response from VK
1301///
1302/// Note: VK returns different fields depending on upload type:
1303/// - Photo uploads: server, photo, hash
1304/// - Document uploads: file
1305#[derive(Debug, Clone, Deserialize)]
1306pub struct UploadResponse {
1307    #[serde(default)]
1308    pub server: i64,
1309    #[serde(default)]
1310    pub photo: String,
1311    #[serde(default)]
1312    pub hash: String,
1313    #[serde(default)]
1314    pub file: String,
1315}
1316
1317/// Saved photo information
1318#[derive(Debug, Clone, Deserialize)]
1319pub struct SavedPhoto {
1320    pub id: i64,
1321    pub owner_id: i64,
1322    #[serde(default)]
1323    pub access_key: Option<String>,
1324    #[serde(default)]
1325    pub sizes: Vec<PhotoSizeInfo>,
1326}
1327
1328/// Photo size information
1329#[derive(Debug, Clone, Deserialize)]
1330pub struct PhotoSizeInfo {
1331    #[serde(rename = "type")]
1332    pub size_type: String,
1333    pub url: String,
1334    pub width: i32,
1335    pub height: i32,
1336}
1337
1338/// Document information from save response
1339#[derive(Debug, Clone, Deserialize)]
1340pub struct DocInfo {
1341    pub id: i64,
1342    pub owner_id: i64,
1343    pub title: String,
1344    pub size: i64,
1345    pub ext: String,
1346    pub url: String,
1347    pub date: i64,
1348    #[serde(rename = "type")]
1349    pub doc_type: i32,
1350}
1351
1352/// Audio message information from save response
1353#[derive(Debug, Clone, Deserialize)]
1354pub struct AudioMessageInfo {
1355    pub duration: i32,
1356    #[serde(default)]
1357    pub waveform: Vec<i32>,
1358    pub link_ogg: String,
1359    pub link_mp3: String,
1360}
1361
1362/// Saved document response
1363#[derive(Debug, Clone)]
1364pub struct SavedDocument {
1365    pub doc: Option<DocInfo>,
1366    pub audio_message: Option<AudioMessageInfo>,
1367}
1368
1369/// Response structure from docs.save
1370#[derive(Debug, Clone, Deserialize)]
1371#[allow(dead_code)]
1372struct DocSaveResponse {
1373    #[serde(rename = "type")]
1374    pub doc_type: Option<String>,
1375    pub doc: Option<DocInfo>,
1376    #[serde(rename = "audio_message")]
1377    pub audio_message: Option<AudioMessageInfo>,
1378}
1379
1380/// Downloaded file information
1381#[derive(Debug, Clone)]
1382pub struct DownloadedFile {
1383    pub data: Vec<u8>,
1384    pub content_type: Option<String>,
1385}