Skip to main content

maxbot/api/
messages.rs

1use crate::attachments::{Attachment, AttachmentSource, InlineKeyboard};
2use crate::client::MaxClient;
3use crate::error::Result;
4use crate::utils;
5use serde_json::json;
6use std::time::Duration;
7
8// -----------------------------------------------------------------------------
9// Параметры отправки сообщения (обычная структура)
10// -----------------------------------------------------------------------------
11
12/// Параметры для отправки сообщения.
13///
14/// Содержит все возможные настройки: получатель, текст, форматирование,
15/// вложения, уведомления и т.д. Обычно создаётся через [`SendMessageParamsBuilder`].
16#[derive(Debug, Clone, Default)]
17pub struct SendMessageParams {
18    /// Идентификатор чата (если сообщение в групповой чат).
19    pub chat_id: Option<i64>,
20    /// Идентификатор пользователя (если личное сообщение).
21    pub user_id: Option<i64>,
22    /// Текст сообщения.
23    pub text: String,
24    /// Формат текста: `"markdown"` или `"html"`.
25    pub format: Option<String>,
26    /// Отправлять ли уведомление (по умолчанию `true`).
27    pub notify: Option<bool>,
28    /// Отключить предпросмотр ссылок.
29    pub disable_link_preview: Option<bool>,
30    /// Идентификатор сообщения, на которое нужно ответить (`mid`).
31    pub reply_mid: Option<String>,
32    /// Список вложений.
33    pub attachments: Vec<Attachment>,
34    /// Максимальное количество повторных попыток при ошибках (по умолчанию 3).
35    pub max_retries: usize,
36}
37
38// -----------------------------------------------------------------------------
39// Builder для SendMessageParams
40// -----------------------------------------------------------------------------
41
42/// Построитель параметров для отправки сообщения.
43///
44/// Позволяет удобно задавать параметры цепочкой методов.
45///
46/// # Пример
47///
48/// ```no_run
49/// use maxbot::{SendMessageParamsBuilder, Attachment};
50///
51/// let params = SendMessageParamsBuilder::new()
52///     .text("Привет!")
53///     .chat_id(123456789)
54///     .format_markdown()
55///     .attachment(Attachment::image_local("cat.jpg"))
56///     .build();
57/// ```
58pub struct SendMessageParamsBuilder {
59    chat_id: Option<i64>,
60    user_id: Option<i64>,
61    text: String,
62    format: Option<String>,
63    notify: Option<bool>,
64    disable_link_preview: Option<bool>,
65    reply_mid: Option<String>,
66    attachments: Vec<Attachment>,
67    max_retries: usize,
68}
69
70impl SendMessageParamsBuilder {
71    /// Создаёт новый пустой построитель.
72    pub fn new() -> Self {
73        Self {
74            text: String::new(),
75            chat_id: None,
76            user_id: None,
77            format: None,
78            notify: Some(true),
79            disable_link_preview: None,
80            reply_mid: None,
81            attachments: vec![],
82            max_retries: 3,
83        }
84    }
85
86    /// Устанавливает текст сообщения.
87    pub fn text(mut self, text: impl Into<String>) -> Self {
88        self.text = text.into();
89        self
90    }
91
92    /// Указывает идентификатор чата-получателя.
93    pub fn chat_id(mut self, id: i64) -> Self {
94        self.chat_id = Some(id);
95        self
96    }
97
98    /// Указывает идентификатор пользователя-получателя (для личных сообщений).
99    pub fn user_id(mut self, id: i64) -> Self {
100        self.user_id = Some(id);
101        self
102    }
103
104    /// Включает Markdown-форматирование текста.
105    pub fn format_markdown(mut self) -> Self {
106        self.format = Some("markdown".to_string());
107        self
108    }
109
110    /// Включает HTML-форматирование текста.
111    pub fn format_html(mut self) -> Self {
112        self.format = Some("html".to_string());
113        self
114    }
115
116    /// Отключает звуковое уведомление у получателя.
117    pub fn silent(mut self) -> Self {
118        self.notify = Some(false);
119        self
120    }
121
122    /// Отключает предпросмотр ссылок в сообщении.
123    pub fn disable_preview(mut self) -> Self {
124        self.disable_link_preview = Some(true);
125        self
126    }
127
128    /// Устанавливает сообщение, на которое нужно ответить.
129    ///
130    /// # Аргументы
131    ///
132    /// * `mid` - строковый идентификатор сообщения.
133    pub fn reply_to(mut self, mid: impl Into<String>) -> Self {
134        self.reply_mid = Some(mid.into());
135        self
136    }
137
138    /// Добавляет одно вложение к сообщению.
139    pub fn attachment(mut self, att: Attachment) -> Self {
140        self.attachments.push(att);
141        self
142    }
143
144    /// Заменяет все вложения переданным вектором.
145    pub fn attachments(mut self, atts: Vec<Attachment>) -> Self {
146        self.attachments = atts;
147        self
148    }
149
150    /// Устанавливает максимальное число повторных попыток при ошибках отправки.
151    pub fn max_retries(mut self, retries: usize) -> Self {
152        self.max_retries = retries;
153        self
154    }
155
156    /// Строит финальную структуру [`SendMessageParams`].
157    pub fn build(self) -> SendMessageParams {
158        SendMessageParams {
159            chat_id: self.chat_id,
160            user_id: self.user_id,
161            text: self.text,
162            format: self.format,
163            notify: self.notify,
164            disable_link_preview: self.disable_link_preview,
165            reply_mid: self.reply_mid,
166            attachments: self.attachments,
167            max_retries: self.max_retries,
168        }
169    }
170}
171
172impl Default for SendMessageParamsBuilder {
173    fn default() -> Self {
174        Self::new()
175    }
176}
177
178// -----------------------------------------------------------------------------
179// Параметры для получения списка сообщений
180// -----------------------------------------------------------------------------
181
182/// Параметры для получения списка сообщений чата.
183#[derive(Debug, Clone, Default)]
184pub struct GetMessagesParams {
185    /// Идентификатор чата.
186    pub chat_id: i64,
187    /// Маркер для постраничного вывода (значение `marker` из предыдущего ответа).
188    pub marker: Option<String>,
189    /// Количество сообщений (1–100).
190    pub count: Option<u32>,
191}
192
193// -----------------------------------------------------------------------------
194// Параметры для редактирования сообщения
195// -----------------------------------------------------------------------------
196
197/// Параметры для редактирования сообщения.
198#[derive(Debug, Clone, Default)]
199pub struct EditMessageParams {
200    /// Новый текст сообщения.
201    pub text: Option<String>,
202    /// Формат текста: `"markdown"` или `"html"`.
203    pub format: Option<String>,
204    /// Новый список вложений (полностью заменяет существующие).
205    pub attachments: Vec<Attachment>,
206    /// Новая inline-клавиатура (если требуется заменить).
207    pub inline_keyboard: Option<InlineKeyboard>,
208    /// Отключить предпросмотр ссылок.
209    pub disable_link_preview: Option<bool>,
210}
211
212// -----------------------------------------------------------------------------
213// Вспомогательная функция для подготовки вложений
214// -----------------------------------------------------------------------------
215
216/// Подготавливает вложения для включения в JSON тела запроса.
217/// Для файловых типов:
218/// - Если передан локальный путь — файл загружается и используется токен.
219/// - Если передан токен — используется он.
220/// - Если передан URL и тип изображения — используется поле `url`.
221/// - Если передан URL и тип видео/аудио/файла — файл скачивается и загружается.
222///
223/// Для видео, аудио и файлов при указании URL требуется также `filename` (желаемое имя файла),
224/// которое используется для корректного определения MIME-типа при скачивании.
225///
226/// Возвращает JSON-массив вложений.
227pub(crate) async fn prepare_attachments_json(
228    client: &MaxClient,
229    attachments: &[Attachment],
230) -> Result<serde_json::Value> {
231    let mut attachments_json = Vec::new();
232
233    for att in attachments {
234        match att {
235            Attachment::Image { source, token, url } => {
236                // Изображение: URL поддерживается напрямую
237                if let Some(url_str) = url {
238                    attachments_json.push(serde_json::json!({
239                        "type": "image",
240                        "payload": { "url": url_str }
241                    }));
242                } else {
243                    let final_token = if let Some(tok) = token {
244                        tok.clone()
245                    } else {
246                        match source {
247                            AttachmentSource::LocalFile(path) => {
248                                let typ = att.get_type();
249                                let tok = client.upload_file(path, typ).await?;
250                                tokio::time::sleep(Duration::from_millis(500)).await;
251                                tok
252                            }
253                            AttachmentSource::Token(tok) => tok.clone(),
254                        }
255                    };
256                    attachments_json.push(serde_json::json!({
257                        "type": "image",
258                        "payload": { "token": final_token }
259                    }));
260                }
261            }
262            Attachment::Video { source, token, url, filename } |
263            Attachment::Audio { source, token, url, filename } |
264            Attachment::File  { source, token, url, filename } => {
265                // Видео, аудио, файл: URL не поддерживается, нужно скачать и загрузить
266                if let Some(url_str) = url {
267                    let file_name = filename.as_deref().ok_or_else(|| {
268                        crate::error::Error::InvalidInput("Missing filename for URL attachment".into())
269                    })?;
270                    let temp_path = client.download_to_temp_dir(url_str, file_name).await?;
271                    let typ = att.get_type();
272                    let token = client.upload_file(&temp_path, typ).await?;
273                    tokio::time::sleep(Duration::from_millis(500)).await;
274                    // Удаляем временный файл и директорию
275                    let _ = tokio::fs::remove_file(&temp_path).await;
276                    let _ = tokio::fs::remove_dir(temp_path.parent().unwrap()).await;
277                    attachments_json.push(serde_json::json!({
278                        "type": typ,
279                        "payload": { "token": token }
280                    }));
281                } else {
282                    let final_token = if let Some(tok) = token {
283                        tok.clone()
284                    } else {
285                        match source {
286                            AttachmentSource::LocalFile(path) => {
287                                let typ = att.get_type();
288                                let tok = client.upload_file(path, typ).await?;
289                                tokio::time::sleep(Duration::from_millis(500)).await;
290                                tok
291                            }
292                            AttachmentSource::Token(tok) => tok.clone(),
293                        }
294                    };
295                    attachments_json.push(serde_json::json!({
296                        "type": att.get_type(),
297                        "payload": { "token": final_token }
298                    }));
299                }
300            }
301            Attachment::Sticker { code } => {
302                attachments_json.push(serde_json::json!({
303                    "type": "sticker",
304                    "payload": { "code": code }
305                }));
306            }
307            Attachment::Contact(data) => {
308                let mut payload = serde_json::Map::new();
309                if let Some(name) = &data.name {
310                    payload.insert("name".into(), name.clone().into());
311                }
312                if let Some(contact_id) = data.contact_id {
313                    payload.insert("contact_id".into(), contact_id.into());
314                }
315                if let Some(vcf_info) = &data.vcf_info {
316                    payload.insert("vcf_info".into(), vcf_info.clone().into());
317                }
318                if let Some(vcf_phone) = &data.vcf_phone {
319                    payload.insert("vcf_phone".into(), vcf_phone.clone().into());
320                }
321                attachments_json.push(serde_json::json!({
322                    "type": "contact",
323                    "payload": payload
324                }));
325            }
326            Attachment::InlineKeyboard(keyboard) => {
327                let mut buttons_json = Vec::new();
328                for row in &keyboard.buttons {
329                    let mut row_json = Vec::new();
330                    for btn in row {
331                        let mut btn_json = serde_json::Map::new();
332                        btn_json.insert("type".into(), btn.r#type.clone().into());
333                        btn_json.insert("text".into(), btn.text.clone().into());
334                        if let Some(payload) = &btn.payload {
335                            btn_json.insert("payload".into(), payload.clone().into());
336                        }
337                        if let Some(url) = &btn.url {
338                            btn_json.insert("url".into(), url.clone().into());
339                        }
340                        if let Some(quick) = btn.quick {
341                            btn_json.insert("quick".into(), quick.into());
342                        }
343                        if let Some(web_app) = &btn.web_app {
344                            btn_json.insert("web_app".into(), web_app.clone().into());
345                        }
346                        if let Some(contact_id) = btn.contact_id {
347                            btn_json.insert("contact_id".into(), contact_id.into());
348                        }
349                        if let Some(app_payload) = &btn.app_payload {
350                            btn_json.insert("payload".into(), app_payload.clone().into());
351                        }
352                        if let Some(msg_text) = &btn.message_text {
353                            btn_json.insert("text".into(), msg_text.clone().into());
354                        }
355                        row_json.push(serde_json::Value::Object(btn_json));
356                    }
357                    buttons_json.push(serde_json::Value::Array(row_json));
358                }
359                attachments_json.push(serde_json::json!({
360                    "type": "inline_keyboard",
361                    "payload": { "buttons": buttons_json }
362                }));
363            }
364            Attachment::Location { latitude, longitude } => {
365                attachments_json.push(serde_json::json!({
366                    "type": "location",
367                    "payload": { "latitude": latitude, "longitude": longitude }
368                }));
369            }
370            Attachment::Share(data) => {
371                let mut payload = serde_json::Map::new();
372                if let Some(url) = &data.url {
373                    payload.insert("url".into(), url.clone().into());
374                }
375                if let Some(token) = &data.token {
376                    payload.insert("token".into(), token.clone().into());
377                }
378                attachments_json.push(serde_json::json!({
379                    "type": "share",
380                    "payload": payload
381                }));
382            }
383        }
384    }
385
386    Ok(serde_json::Value::Array(attachments_json))
387}
388
389// -----------------------------------------------------------------------------
390// Реализация методов MaxClient
391// -----------------------------------------------------------------------------
392
393impl MaxClient {
394    /// Отправляет сообщение с разбиением длинного текста, поддержкой вложений и
395    /// автоматическим откатом в случае ошибки.
396    ///
397    /// Если текст превышает [`MAX_MESSAGE_LEN`][crate::utils::MAX_MESSAGE_LEN], он
398    /// разбивается на несколько фрагментов. Вложения (изображения, видео и т.д.)
399    /// прикрепляются только к первому фрагменту, inline‑клавиатура — к последнему.
400    ///
401    /// # Возвращаемое значение
402    ///
403    /// Вектор идентификаторов (`mid`) всех отправленных фрагментов сообщения.
404    ///
405    /// # Ошибки
406    ///
407    /// Если при отправке какого‑либо фрагмента происходит ошибка, все уже отправленные
408    /// фрагменты удаляются, и возвращается ошибка.
409    pub async fn send_message(&self, params: SendMessageParams) -> Result<Vec<String>> {
410        // Проверка получателя
411        if (params.chat_id.is_none() && params.user_id.is_none()) ||
412           (params.chat_id.is_some() && params.user_id.is_some()) {
413            return Err(crate::error::Error::InvalidInput(
414                "Exactly one of chat_id or user_id must be specified".into(),
415            ));
416        }
417
418        // Подготовка вложений
419        let mut ready_attachments = Vec::new();    // (type, token) – файловые
420        let mut content_attachments = Vec::new();  // (type, payload) – sticker, contact, location, share
421        let mut keyboard_attachments = Vec::new(); // (type, payload) – inline_keyboard
422
423        for att in &params.attachments {
424            match att {
425                // Файловые типы
426                Attachment::Image { source, token, url } => {
427                    let final_token = if let Some(tok) = token {
428                        tok.clone()
429                    } else {
430                        match source {
431                            AttachmentSource::LocalFile(path) => {
432                                let file_type = att.get_type();
433                                let tok = self.upload_file(path, file_type).await?;
434                                tokio::time::sleep(Duration::from_millis(500)).await; // пауза для обработки
435                                tok
436                            }
437                            AttachmentSource::Token(tok) => tok.clone(),
438                        }
439                    };
440
441                    // Для изображений: если есть URL – отправляем его напрямую
442                    if let Some(url_str) = url {
443                        content_attachments
444                            .push((att.get_type(), json!({ "payload": { "url": url_str } })));
445                    } else {
446                        ready_attachments.push((att.get_type(), final_token));
447                    }
448                }
449                Attachment::Video { source, token, url, filename } |
450                Attachment::Audio { source, token, url, filename } |
451                Attachment::File  { source, token, url, filename } => {
452                    // Для видео, аудио, файлов: URL не поддерживается, скачиваем и загружаем
453                    if let Some(url_str) = url {
454                        let file_name = filename.as_deref().ok_or_else(|| {
455                            crate::error::Error::InvalidInput("Missing filename for URL attachment".into())
456                        })?;
457                        let temp_path = self.download_to_temp_dir(url_str, file_name).await?;
458                        let typ = att.get_type();
459                        let token = self.upload_file(&temp_path, typ).await?;
460                        tokio::time::sleep(Duration::from_millis(500)).await;
461                        // Удаляем временный файл и директорию
462                        let _ = tokio::fs::remove_file(&temp_path).await;
463                        let _ = tokio::fs::remove_dir(temp_path.parent().unwrap()).await;
464                        ready_attachments.push((typ, token));
465                    } else {
466                        let final_token = if let Some(tok) = token {
467                            tok.clone()
468                        } else {
469                            match source {
470                                AttachmentSource::LocalFile(path) => {
471                                    let typ = att.get_type();
472                                    let tok = self.upload_file(path, typ).await?;
473                                    tokio::time::sleep(Duration::from_millis(500)).await;
474                                    tok
475                                }
476                                AttachmentSource::Token(tok) => tok.clone(),
477                            }
478                        };
479                        ready_attachments.push((att.get_type(), final_token));
480                    }
481                }
482                // Стикер
483                Attachment::Sticker { code } => {
484                    let payload = json!({ "code": code });
485                    content_attachments.push((att.get_type(), json!({ "payload": payload })));
486                }
487                // Контакт
488                Attachment::Contact(data) => {
489                    let mut payload = serde_json::Map::new();
490                    if let Some(name) = &data.name {
491                        payload.insert("name".into(), name.clone().into());
492                    }
493                    if let Some(contact_id) = data.contact_id {
494                        payload.insert("contact_id".into(), contact_id.into());
495                    }
496                    if let Some(vcf_info) = &data.vcf_info {
497                        payload.insert("vcf_info".into(), vcf_info.clone().into());
498                    }
499                    if let Some(vcf_phone) = &data.vcf_phone {
500                        payload.insert("vcf_phone".into(), vcf_phone.clone().into());
501                    }
502                    content_attachments.push((att.get_type(), json!({ "payload": payload })));
503                }
504                // Inline-клавиатура
505                Attachment::InlineKeyboard(keyboard) => {
506                    let mut buttons_json = Vec::new();
507                    for row in &keyboard.buttons {
508                        let mut row_json = Vec::new();
509                        for btn in row {
510                            let mut btn_json = serde_json::Map::new();
511                            btn_json.insert("type".into(), btn.r#type.clone().into());
512                            btn_json.insert("text".into(), btn.text.clone().into());
513                            if let Some(payload) = &btn.payload {
514                                btn_json.insert("payload".into(), payload.clone().into());
515                            }
516                            if let Some(url) = &btn.url {
517                                btn_json.insert("url".into(), url.clone().into());
518                            }
519                            if let Some(quick) = btn.quick {
520                                btn_json.insert("quick".into(), quick.into());
521                            }
522                            if let Some(web_app) = &btn.web_app {
523                                btn_json.insert("web_app".into(), web_app.clone().into());
524                            }
525                            if let Some(contact_id) = btn.contact_id {
526                                btn_json.insert("contact_id".into(), contact_id.into());
527                            }
528                            if let Some(app_payload) = &btn.app_payload {
529                                btn_json.insert("payload".into(), app_payload.clone().into());
530                            }
531                            if let Some(msg_text) = &btn.message_text {
532                                btn_json.insert("text".into(), msg_text.clone().into());
533                            }
534                            row_json.push(serde_json::Value::Object(btn_json));
535                        }
536                        buttons_json.push(serde_json::Value::Array(row_json));
537                    }
538                    let payload = json!({ "buttons": buttons_json });
539                    keyboard_attachments.push((att.get_type(), json!({ "payload": payload })));
540                }
541                // Геолокация
542                Attachment::Location { latitude, longitude } => {
543                    let payload = json!({ "latitude": latitude, "longitude": longitude });
544                    content_attachments.push((att.get_type(), payload));
545                }
546                // Отсылка (share)
547                Attachment::Share(data) => {
548                    let mut payload = serde_json::Map::new();
549                    if let Some(url) = &data.url {
550                        payload.insert("url".into(), url.clone().into());
551                    }
552                    if let Some(token) = &data.token {
553                        payload.insert("token".into(), token.clone().into());
554                    }
555                    content_attachments.push((att.get_type(), json!({ "payload": payload })));
556                }
557            }
558        }
559
560        // Разбивка текста на фрагменты
561        let fragments = utils::split_text(&params.text, params.format.as_deref());
562        let fragments = if fragments.is_empty() &&
563            ready_attachments.is_empty() &&
564            content_attachments.is_empty()
565        {
566            return Err(crate::error::Error::InvalidInput(
567                "Empty message text and no attachments".into(),
568            ));
569        } else if fragments.is_empty() {
570            vec!["".to_string()]
571        } else {
572            fragments
573        };
574
575        // GET-параметры (передаются в URL)
576        let mut get_params = Vec::new();
577        if let Some(disable) = params.disable_link_preview {
578            get_params.push(("disable_link_preview", disable.to_string()));
579        }
580
581        // Формирование link для reply (если задан)
582        let reply_link = params
583            .reply_mid
584            .as_ref()
585            .map(|mid| json!({ "type": "reply", "mid": mid }));
586
587        let mut sent_ids = Vec::new();
588
589        for (i, fragment_text) in fragments.iter().enumerate() {
590            let mut body = json!({
591                "text": fragment_text,
592            });
593            if let Some(fmt) = &params.format {
594                body["format"] = json!(fmt);
595            }
596            if let Some(notify) = params.notify {
597                body["notify"] = json!(notify);
598            }
599
600            // Первый фрагмент: файловые вложения + вложения содержимого + reply
601            if i == 0 {
602                if !body["attachments"].is_array() {
603                    body["attachments"] = serde_json::Value::Array(vec![]);
604                }
605                let attachments_array = body["attachments"].as_array_mut().unwrap();
606                for (att_type, token) in &ready_attachments {
607                    attachments_array.push(json!({
608                        "type": att_type,
609                        "payload": { "token": token }
610                    }));
611                }
612                for (att_type, payload) in &content_attachments {
613                    let mut attachment = json!({ "type": att_type });
614                    if let Some(p) = payload.as_object() {
615                        for (k, v) in p {
616                            attachment[k] = v.clone();
617                        }
618                    } else {
619                        attachment["payload"] = payload.clone();
620                    }
621                    attachments_array.push(attachment);
622                }
623                if let Some(link) = &reply_link {
624                    body["link"] = link.clone();
625                }
626            }
627            // Последний фрагмент: клавиатура (если есть)
628            if i == fragments.len() - 1 && !keyboard_attachments.is_empty() {
629                if !body["attachments"].is_array() {
630                    body["attachments"] = serde_json::Value::Array(vec![]);
631                }
632                let attachments_array = body["attachments"].as_array_mut().unwrap();
633                for (att_type, payload) in &keyboard_attachments {
634                    let mut attachment = json!({ "type": att_type });
635                    if let Some(p) = payload.as_object() {
636                        for (k, v) in p {
637                            attachment[k] = v.clone();
638                        }
639                    } else {
640                        attachment["payload"] = payload.clone();
641                    }
642                    attachments_array.push(attachment);
643                }
644            }
645
646            match self.send_fragment_with_retry(
647                params.chat_id,
648                params.user_id,
649                body,
650                &get_params,
651                i == 0, // is_first
652                params.max_retries,
653            ).await {
654                Ok(mid) => sent_ids.push(mid),
655                Err(e) => {
656                    // Откат: удалить все успешно отправленные фрагменты
657                    if !sent_ids.is_empty() {
658                        let _ = self.delete_messages(&sent_ids).await;
659                    }
660                    return Err(e);
661                }
662            }
663        }
664
665        Ok(sent_ids)
666    }
667
668    /// Удобный метод для отправки сообщения с использованием построителя.
669    ///
670    /// Эквивалентен вызову `send_message(builder.build())`.
671    pub async fn send_message_builder(
672        &self,
673        builder: SendMessageParamsBuilder,
674    ) -> Result<Vec<String>> {
675        self.send_message(builder.build()).await
676    }
677
678    /// Пересылает существующее сообщение в другой чат или пользователю.
679    ///
680    /// # Аргументы
681    ///
682    /// * `chat_id` / `user_id` — получатель (должен быть указан ровно один).
683    /// * `forward_mid` — строковый идентификатор (`mid`) пересылаемого сообщения.
684    /// * `notify` — отправлять ли уведомление (по умолчанию `true`).
685    /// * `disable_link_preview` — отключить предпросмотр ссылок (по умолчанию `false`).
686    ///
687    /// # Возвращаемое значение
688    ///
689    /// Идентификатор (`mid`) нового пересланного сообщения.
690    pub async fn forward_message(
691        &self,
692        chat_id: Option<i64>,
693        user_id: Option<i64>,
694        forward_mid: &str,
695        notify: Option<bool>,
696        disable_link_preview: Option<bool>,
697    ) -> Result<String> {
698        // Проверка получателя
699        if (chat_id.is_none() && user_id.is_none()) ||
700           (chat_id.is_some() && user_id.is_some()) {
701            return Err(crate::error::Error::InvalidInput(
702                "Exactly one of chat_id or user_id must be specified".into(),
703            ));
704        }
705
706        let mut query = Vec::new();
707        if let Some(cid) = chat_id {
708            query.push(("chat_id", cid.to_string()));
709        }
710        if let Some(uid) = user_id {
711            query.push(("user_id", uid.to_string()));
712        }
713        if let Some(disable) = disable_link_preview {
714            query.push(("disable_link_preview", disable.to_string()));
715        }
716        // Обязательный параметр: message_id (строковый mid)
717        query.push(("message_id", forward_mid.to_string()));
718
719        let body = json!({
720            "link": {
721                "type": "forward",
722                "mid": forward_mid
723            },
724            "notify": notify.unwrap_or(true),
725        });
726
727        let resp: serde_json::Value = self.request_with_rate_limit(
728            reqwest::Method::POST,
729            "/messages",
730            &query,
731            Some(body),
732        ).await?;
733
734        let new_mid = resp["message"]["body"]["mid"]
735            .as_str()
736            .ok_or_else(|| crate::error::Error::Api {
737                code: 500,
738                message: "No message ID in forward response".into(),
739            })?;
740        Ok(new_mid.to_string())
741    }
742
743    /// Удаляет одно сообщение по его идентификатору `mid`.
744    pub async fn delete_message(&self, message_id: &str) -> Result<()> {
745        self.request_with_rate_limit::<serde_json::Value>(
746            reqwest::Method::DELETE,
747            "/messages",
748            &[("message_id", message_id.to_string())],
749            None,
750        ).await?;
751        Ok(())
752    }
753
754    /// Удаляет несколько сообщений последовательно.
755    ///
756    /// Ошибки при удалении отдельных сообщений игнорируются (выводится предупреждение в stderr).
757    pub async fn delete_messages(&self, message_ids: &[String]) -> Result<()> {
758        for id in message_ids {
759            if let Err(e) = self.delete_message(id).await {
760                eprintln!("Warning: failed to delete message {}: {}", id, e);
761            }
762        }
763        Ok(())
764    }
765
766    /// Возвращает постраничный список сообщений из указанного чата.
767    ///
768    /// Сообщения возвращаются в обратном хронологическом порядке (от новых к старым).
769    ///
770    /// # Аргументы
771    ///
772    /// * `params` - параметры запроса ([`GetMessagesParams`]).
773    ///
774    /// # Возвращаемое значение
775    ///
776    /// Кортеж `(сообщения, следующий_маркер)`. Если `следующий_маркер` равен `None`,
777    /// достигнут конец истории чата.
778    ///
779    /// # Пример
780    ///
781    /// ```no_run
782    /// # use maxbot::{MaxClient, GetMessagesParams};
783    /// # async fn example(client: &MaxClient) -> Result<(), Box<dyn std::error::Error>> {
784    /// let (messages, next_marker) = client.get_messages(GetMessagesParams {
785    ///     chat_id: 123456789,
786    ///     count: Some(50),
787    ///     ..Default::default()
788    /// }).await?;
789    /// # Ok(())
790    /// # }
791    /// ```
792    pub async fn get_messages(
793        &self,
794        params: GetMessagesParams,
795    ) -> Result<(Vec<crate::types::Message>, Option<String>)> {
796        let mut query = vec![("chat_id", params.chat_id.to_string())];
797        if let Some(m) = &params.marker {
798            query.push(("marker", m.clone()));
799        }
800        if let Some(c) = params.count {
801            query.push(("count", c.to_string()));
802        }
803        let resp: serde_json::Value = self
804            .request_with_rate_limit(reqwest::Method::GET, "/messages", &query, None)
805            .await?;
806        let messages: Vec<crate::types::Message> =
807            serde_json::from_value(resp["messages"].clone())?;
808        let next_marker = resp
809            .get("marker")
810            .and_then(|v| v.as_str())
811            .map(String::from);
812        Ok((messages, next_marker))
813    }
814
815    /// Редактирует ранее отправленное сообщение.
816    ///
817    /// Можно изменить текст, форматирование, вложения и inline‑клавиатуру.
818    /// Поля, оставленные как `None`, не изменяются.
819    ///
820    /// # Аргументы
821    ///
822    /// * `message_mid` - строковый идентификатор (`mid`) редактируемого сообщения.
823    /// * `params` - параметры редактирования ([`EditMessageParams`]).
824    ///
825    /// # Пример
826    ///
827    /// ```no_run
828    /// # use maxbot::{MaxClient, EditMessageParams};
829    /// # async fn example(client: &MaxClient) -> Result<(), Box<dyn std::error::Error>> {
830    /// let params = EditMessageParams {
831    ///     text: Some("Обновлённый текст".to_string()),
832    ///     ..Default::default()
833    /// };
834    /// client.edit_message("abc123mid", params).await?;
835    /// # Ok(())
836    /// # }
837    /// ```
838    pub async fn edit_message(&self, message_mid: &str, params: EditMessageParams) -> Result<()> {
839        let mut body = serde_json::Map::new();
840        if let Some(text) = params.text {
841            body.insert("text".into(), text.into());
842        }
843        if let Some(format) = params.format {
844            body.insert("format".into(), format.into());
845        }
846        if let Some(disable) = params.disable_link_preview {
847            body.insert("disable_link_preview".into(), disable.into());
848        }
849
850        // Собираем все вложения: attachments + inline_keyboard (если есть)
851        let mut all_attachments = params.attachments;
852        if let Some(keyboard) = params.inline_keyboard {
853            all_attachments.push(Attachment::InlineKeyboard(keyboard));
854        }
855
856        if !all_attachments.is_empty() {
857            let attachments_json = prepare_attachments_json(self, &all_attachments).await?;
858            body.insert("attachments".into(), attachments_json);
859        }
860
861        self.request_with_rate_limit::<serde_json::Value>(
862            reqwest::Method::PUT,
863            "/messages",
864            &[("message_id", message_mid.to_string())],
865            Some(serde_json::Value::Object(body)),
866        )
867        .await?;
868        Ok(())
869    }
870
871    /// Редактирует длинное сообщение, представленное несколькими фрагментами.
872    ///
873    /// Новый текст разбивается на фрагменты (как при отправке). Затем:
874    /// - Существующие фрагменты обновляются через `edit_message`.
875    /// - Если старых фрагментов больше, лишние удаляются.
876    /// - Если новых больше, недостающие отправляются через `send_message`.
877    ///
878    /// # Аргументы
879    /// * `chat_id` – идентификатор чата (необходим для отправки новых фрагментов).
880    /// * `old_mids` – список идентификаторов текущих фрагментов (в порядке следования).
881    /// * `new_text` – новый текст сообщения (может быть любой длины).
882    /// * `format` – формат текста (`"markdown"` или `"html"`).
883    /// * `notify` – отправлять ли уведомление.
884    /// * `disable_link_preview` – отключить предпросмотр ссылок.
885    /// * `reply_mid` – идентификатор сообщения, на которое следует ответить (новые фрагменты).
886    /// * `attachments` – вложения (будут прикреплены только к первому фрагменту).
887    /// * `inline_keyboard` – клавиатура (будет прикреплена к последнему фрагменту).
888    ///
889    /// # Возвращает
890    /// Вектор идентификаторов фрагментов после редактирования.
891    ///
892    /// # Пример
893    /// ```no_run
894    /// # use maxbot::MaxClient;
895    /// # async fn example(client: &MaxClient, chat_id: i64, old_mids: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
896    /// let new_ids = client.edit_multimessage(
897    ///     chat_id,
898    ///     &old_mids,
899    ///     "Новый очень длинный текст…",
900    ///     Some("markdown"),
901    ///     None,
902    ///     None,
903    ///     None,
904    ///     vec![],
905    ///     None,
906    /// ).await?;
907    /// # Ok(())
908    /// # }
909    /// ```
910    pub async fn edit_multimessage(
911        &self,
912        chat_id: i64,
913        old_mids: &[String],
914        new_text: &str,
915        format: Option<&str>,
916        notify: Option<bool>,
917        disable_link_preview: Option<bool>,
918        reply_mid: Option<&str>,
919        attachments: Vec<Attachment>,
920        inline_keyboard: Option<InlineKeyboard>,
921    ) -> Result<Vec<String>> {
922        // 1. Разбить новый текст на фрагменты
923        let fragments = utils::split_text(new_text, format);
924        if fragments.is_empty() {
925            return Err(crate::error::Error::InvalidInput(
926                "Empty message text after splitting".into(),
927            ));
928        }
929
930        let old_count = old_mids.len();
931        let new_count = fragments.len();
932
933        // 2. Подготовить вложения для первого фрагмента и клавиатуру для последнего
934        let first_attachments_json = if !attachments.is_empty() {
935            Some(prepare_attachments_json(self, &attachments).await?)
936        } else {
937            None
938        };
939        let keyboard_attachment_json = if let Some(keyboard) = inline_keyboard {
940            let kb_att = Attachment::InlineKeyboard(keyboard);
941            let json_arr = prepare_attachments_json(self, &[kb_att]).await?;
942            // Ожидаем массив с одним элементом
943            if let serde_json::Value::Array(mut arr) = json_arr {
944                arr.pop()
945            } else {
946                None
947            }
948        } else {
949            None
950        };
951
952        let mut new_ids = Vec::with_capacity(new_count);
953
954        // 3. Редактирование существующих фрагментов (до min(old, new))
955        for i in 0..std::cmp::min(old_count, new_count) {
956            let mut body = serde_json::Map::new();
957            body.insert("text".into(), fragments[i].clone().into());
958            if let Some(fmt) = format {
959                body.insert("format".into(), fmt.into());
960            }
961            if let Some(disable) = disable_link_preview {
962                body.insert("disable_link_preview".into(), disable.into());
963            }
964
965            let mut final_attachments = Vec::new();
966            if i == 0 {
967                if let Some(ref arr) = first_attachments_json {
968                    if let serde_json::Value::Array(arr_vec) = arr {
969                        final_attachments.extend(arr_vec.clone());
970                    }
971                }
972            }
973            if i == new_count - 1 {
974                if let Some(kb) = &keyboard_attachment_json {
975                    final_attachments.push(kb.clone());
976                }
977            }
978            if !final_attachments.is_empty() {
979                body.insert("attachments".into(), serde_json::Value::Array(final_attachments));
980            }
981
982            let query = [("message_id", old_mids[i].to_string())];
983            self.request_with_rate_limit::<serde_json::Value>(
984                reqwest::Method::PUT,
985                "/messages",
986                &query,
987                Some(serde_json::Value::Object(body)),
988            )
989            .await?;
990            new_ids.push(old_mids[i].clone());
991        }
992
993        // 4. Удаление лишних старых фрагментов
994        if old_count > new_count {
995            let extra_mids = &old_mids[new_count..];
996            self.delete_messages(extra_mids).await?;
997        }
998
999        // 5. Отправка дополнительных фрагментов (если новых больше)
1000        if new_count > old_count {
1001            let extra_fragments = &fragments[old_count..];
1002            for (i, text) in extra_fragments.iter().enumerate() {
1003                let mut builder = SendMessageParamsBuilder::new()
1004                    .text(text)
1005                    .chat_id(chat_id)
1006                    .max_retries(3);
1007                if let Some(fmt) = format {
1008                    if fmt == "markdown" {
1009                        builder = builder.format_markdown();
1010                    } else if fmt == "html" {
1011                        builder = builder.format_html();
1012                    }
1013                }
1014                if let Some(notify_val) = notify {
1015                    if !notify_val {
1016                        builder = builder.silent();
1017                    }
1018                }
1019                if let Some(disable) = disable_link_preview {
1020                    if disable {
1021                        builder = builder.disable_preview();
1022                    }
1023                }
1024                // Для самого первого из новых фрагментов добавляем reply, если указан
1025                if i == 0 && reply_mid.is_some() {
1026                    builder = builder.reply_to(reply_mid.unwrap());
1027                }
1028                let sent_ids = self.send_message_builder(builder).await?;
1029                new_ids.extend(sent_ids);
1030            }
1031        }
1032
1033        Ok(new_ids)
1034    }
1035
1036    // -------------------------------------------------------------------------
1037    // Внутренние вспомогательные методы
1038    // -------------------------------------------------------------------------
1039
1040    async fn send_fragment_with_retry(
1041        &self,
1042        chat_id: Option<i64>,
1043        user_id: Option<i64>,
1044        body: serde_json::Value,
1045        get_params: &[(&str, String)],
1046        is_first: bool,
1047        max_retries: usize,
1048    ) -> Result<String> {
1049        let mut attempt = 0;
1050        let mut delay_sec = 1;            // для ошибки attachment.not.ready
1051        let mut rate_limit_delay_sec = 5; // для ошибки 429
1052
1053        loop {
1054            match self
1055                .send_single_fragment(chat_id, user_id, body.clone(), get_params)
1056                .await
1057            {
1058                Ok(mid) => return Ok(mid),
1059                Err(e) => {
1060                    if !is_first {
1061                        // Для не-первого фрагмента не повторяем при ошибках вложений
1062                        return Err(e);
1063                    }
1064                    attempt += 1;
1065                    if attempt >= max_retries {
1066                        return Err(e);
1067                    }
1068                    let err_str = e.to_string();
1069                    if err_str.contains("429") || err_str.contains("too.many.requests") {
1070                        tokio::time::sleep(Duration::from_secs(rate_limit_delay_sec)).await;
1071                        rate_limit_delay_sec *= 2;
1072                        continue;
1073                    }
1074                    if err_str.contains("attachment.not.ready") || err_str.contains("not.processed") {
1075                        tokio::time::sleep(Duration::from_secs(delay_sec)).await;
1076                        delay_sec *= 2;
1077                        continue;
1078                    }
1079                    return Err(e);
1080                }
1081            }
1082        }
1083    }
1084
1085    async fn send_single_fragment(
1086        &self,
1087        chat_id: Option<i64>,
1088        user_id: Option<i64>,
1089        body: serde_json::Value,
1090        get_params: &[(&str, String)],
1091    ) -> Result<String> {
1092        let mut query = get_params.to_vec();
1093        if let Some(cid) = chat_id {
1094            query.push(("chat_id", cid.to_string()));
1095        }
1096        if let Some(uid) = user_id {
1097            query.push(("user_id", uid.to_string()));
1098        }
1099
1100        let resp: serde_json::Value = self.request_with_rate_limit(
1101            reqwest::Method::POST,
1102            "/messages",
1103            &query,
1104            Some(body),
1105        ).await?;
1106
1107        let mid = resp["message"]["body"]["mid"]
1108            .as_str()
1109            .ok_or_else(|| crate::error::Error::Api {
1110                code: 500,
1111                message: "No message ID in response (expected message.body.mid)".into(),
1112            })?;
1113        Ok(mid.to_string())
1114    }
1115}